Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion wallets/rn_cli_wallet/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ EXPO_PUBLIC_PAY_API_BASE_URL=''
SENTRY_DISABLE_AUTO_UPLOAD=true

# Sentry auth token
SENTRY_AUTH_TOKEN=""
SENTRY_AUTH_TOKEN=""
# Dapp Picker POC (H2b) — all optional, defaults are hardcoded demo values
# Fee recipients declared in wc_feeTerms at session approval:
EXPO_PUBLIC_FEE_RECIPIENT_SOLANA=
EXPO_PUBLIC_FEE_RECIPIENT_EVM=
EXPO_PUBLIC_FEE_BPS=
# Base URL of the fee-demo dapp opened by Explore tiles:
EXPO_PUBLIC_PICKER_DAPP_URL=
EXPO_PUBLIC_STAKE_DAPP_URL=
128 changes: 128 additions & 0 deletions wallets/rn_cli_wallet/DAPP-PICKER-POC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Dapp Picker POC (H2b) — wallet side

The wallet ships an **Explore** tab: a curated directory of fee-honoring
dapps. Tapping a tile opens the dapp in a webview with a **monetized
WalletConnect session pre-established** — the user lands already connected,
`wc_feeTerms` attached, and every swap pays the wallet via the session-fees
mechanism (see `web-examples` branch `session-fees-poc`, PR #1042).

## How it works

1. **Explore tab** (`src/screens/Explore`) — four tiles (Jupiter, 1inch,
KyberSwap, Uniswap). All open the same Session Fees POC dapp with a
different `?aggregator=` default (the "picker illusion"); tile data is
shaped like a future registry entry (`PICKER_DAPPS` in
`src/utils/PickerUtil.ts`). First tap shows a **one-time consent alert**
("Auto-connect to dapps opened from Explore?"), persisted in MMKV.
2. **Webview host** (`src/screens/DappBrowser`) — the dapp page acquires a
WC pairing URI and posts `{type:'wc_session_offer', uri}` via
`window.ReactNativeWebView.postMessage`; a `wc:` navigation intercept
(`onShouldStartLoadWithRequest`) is the fallback. The host registers the
pairing topic as picker-initiated and calls `walletKit.pair({uri})`.
3. **Auto-approve** (`src/hooks/useWalletKitEventsManager.ts`) — proposals
whose `pairingTopic` was registered by the webview are approved directly
(full supported namespaces + sessionProperties) when consent is granted;
any failure falls back to the normal proposal modal. **All other
proposals are untouched** — the normal modal flow applies.
4. **Fee terms** (`src/utils/PickerUtil.ts`) — every approval (modal or
auto) now carries
`wc_feeTerms = {"version":1,"feeRecipient":"9zYt…","feeRecipientEip155":"0x879d…","feeBps":50}`
— the same demo recipients the Session Fees POC collects on (Solana USDC
ATA already initialized; EVM EOA holds the KyberSwap/Uniswap fees).
5. **Connect-variant toggle** — Settings → "Explore: headless connect"
switches tiles between `?connect=headless` (AppKit Headless) and
`?connect=provider` (provider-direct). Default: headless.

## Setup / run

```bash
cd wallets/rn_cli_wallet
cp .env.example .env # set EXPO_PUBLIC_PROJECT_ID
yarn install
yarn ios # or: yarn android
```

Env (all optional, hardcoded demo defaults): `EXPO_PUBLIC_FEE_RECIPIENT_SOLANA`,
`EXPO_PUBLIC_FEE_RECIPIENT_EVM`, `EXPO_PUBLIC_FEE_BPS`,
`EXPO_PUBLIC_PICKER_DAPP_URL` (defaults to the `session-fees-poc` Vercel
preview of react-dapp-v2).

Machine gotchas found while building this POC:
- **iOS**: if the Android NDK toolchain is in your `PATH`, its `clang`
shadows Apple's and an Expo pod script fails with
`ld64.lld: error: library not found for -lSystem`. Strip it for iOS builds:
`PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "ndk/" | paste -sd: -) yarn ios`.
- Run `pod install` (`ios/`) if xcodebuild complains the sandbox is out of
sync with Podfile.lock.

## Demo script (~60 s)

1. Open the wallet → **Explore** tab: four fee-sharing dapps.
2. Tap **KyberSwap** (or Jupiter for Solana) → consent alert (first time
only) → **Allow**.
3. The dapp loads **already connected**: "✓ Connected via rn_cli_wallet —
fee sharing active", aggregator preselected, fee terms card showing
0.50% / recipient.
4. Enter an amount → Swap → the wallet's normal sign sheet appears over the
webview → sign once.
5. Fee lands on-chain in the same transaction (Solscan ATA / Arbiscan EOA
linked from the dapp's live fee-balance card).

Tap count to connected: **1 tap** (tile) after the one-time consent —
the H2b claim holds.

## Integration steps — for a wallet adopting this

Generalized from this POC (≈300 lines in rn_cli_wallet; the five touched
files below are the map):

1. **Attach fee terms at session approval** — add `wc_feeTerms` (JSON
string: `{version, feeRecipient, feeRecipientEip155, feeBps}`) to
`sessionProperties` in your `approveSession` call, for every session.
(Target architecture: skip this — terms live in the WCN registry, zero
wallet code; see web-examples `docs/session-fees/fee-splitter.md`.)
*Here: `src/utils/PickerUtil.ts` (`buildSessionProperties`) +
`src/modals/SessionProposalModal.tsx`.*
2. **Ship the directory UI** — a screen listing fee-honoring dapps. Each
entry needs only `{name, icon, url}` where `url` carries the contract
(`?wc_auto=1&connect=…` + dapp-specific presets). Hardcode for a pilot;
fetch from a registry later — the entry shape is registry-ready.
*Here: `src/screens/Explore/index.tsx`.*
3. **Webview host** — open the tile URL in a webview with two hooks:
`onMessage` parsing `{type:'wc_session_offer', uri}`, and
`onShouldStartLoadWithRequest` intercepting `wc:` URLs (return false) as
the fallback channel. On either: **record the pairing topic** (parse
`wc:<topic>@`) as picker-initiated, then `walletKit.pair({ uri })`
silently. *Here: `src/screens/DappBrowser/index.tsx`.*
4. **Scoped auto-approval** — in your `session_proposal` handler, if
`proposal.params.pairingTopic` is in the picker set AND the user granted
consent: approve directly with your full supported namespaces + the fee
terms; on any error fall back to your normal approval modal. **Never
auto-approve proposals from other sources** (QR, deep links). *Here:
`src/hooks/useWalletKitEventsManager.ts` + `PickerUtil.ts`
(`autoApprovePickerProposal`, `isPickerPairing`).*
5. **Consent UX** — a one-time "Auto-connect to dapps opened from Explore?"
prompt before the first auto-approval, persisted only when granted
("Not now" re-asks next app start), plus a settings switch to
grant/revoke any time. Transactions keep the normal sign flow — consent
covers connections only. *Here: `Explore/index.tsx` (alert),
`src/store/SettingsStore.ts`, `src/screens/Settings/index.tsx`.*
6. **Leave signing untouched** — session requests arrive on the same
session events as always; your existing sign sheets render over the
webview.

Security invariants to preserve when adapting: auto-approval keyed strictly
to pairing topics your own webview created; consent explicit and revocable;
fallback to the manual modal on every error path.

## Corners cut (POC)

- Explore tiles are a hardcoded array, not a registry; icons are colored
glyphs, not brand assets; Android tab icon reuses the connections svg.
- Auto-approval trusts the pairing topic registered by the webview — no
origin verification of the page inside the webview beyond the tile URL.
- One-time consent is an OS alert, not a designed sheet.
- Fee recipients are hardcoded demo addresses (env-overridable).
- The modal's supportedNamespaces map is duplicated in PickerUtil (kept in
sync manually) rather than refactored to one source.
- Web build (`HomeTabNavigator.web.tsx`) does not get the Explore tab.
23 changes: 23 additions & 0 deletions wallets/rn_cli_wallet/src/hooks/useWalletKitEventsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { TON_SIGNING_METHODS } from '@/constants/Ton';
import { CANTON_SIGNING_METHODS } from '@/constants/Canton';
import { approveCantonRequest } from '@/utils/CantonRequestHandlerUtil';
import { getRequestConfig } from '@/modals/requestConfig';
import {
autoApprovePickerProposal,
isPickerPairing,
} from '@/utils/PickerUtil';

export default function useWalletKitEventsManager(initialized: boolean) {
/******************************************************************************
Expand All @@ -33,6 +37,25 @@ export default function useWalletKitEventsManager(initialized: boolean) {
// set the verify context so it can be displayed in the projectInfoCard
SettingsStore.setCurrentRequestVerifyContext(proposal.verifyContext);

// Dapp Picker POC: proposals arriving on a pairing initiated from the
// Explore webview are auto-approved (with wc_feeTerms) when the user
// has granted the one-time consent. Everything else keeps the modal.
if (
isPickerPairing(proposal.params.pairingTopic) &&
SettingsStore.state.pickerAutoConnect
) {
autoApprovePickerProposal(proposal).catch(e => {
LogStore.error(
(e as Error).message,
'WalletKitEvents',
'pickerAutoApprove',
);
// Fall back to the normal consent modal.
ModalStore.open('SessionProposalModal', { proposal });
});
return;
}

const chains = getSupportedChains(
proposal.params.requiredNamespaces,
proposal.params.optionalNamespaces,
Expand Down
18 changes: 5 additions & 13 deletions wallets/rn_cli_wallet/src/modals/SessionProposalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import { handleRedirect } from '@/utils/LinkingUtils';
import { RequestModal } from './RequestModal';
import { getSupportedChains } from '@/utils/HelperUtil';
import { suiAddresses } from '@/utils/SuiWalletUtil';
import { buildSessionProperties } from '@/utils/PickerUtil';
import { EIP155_CHAINS, EIP155_SIGNING_METHODS } from '@/constants/Eip155';
import { SUI_CHAINS, SUI_EVENTS, SUI_SIGNING_METHODS } from '@/constants/Sui';
import { TON_CHAINS, TON_SIGNING_METHODS } from '@/constants/Ton';
import { getWallet, tonAddresses } from '@/utils/TonWalletUtil';
import { tonAddresses } from '@/utils/TonWalletUtil';
import { tronAddresses } from '@/utils/TronWalletUtil';
import { TRON_CHAINS, TRON_SIGNING_METHODS } from '@/constants/Tron';
import {
Expand Down Expand Up @@ -240,22 +241,13 @@ export default function SessionProposalModal() {
});

try {
// Build session properties for TON
const sessionProperties: Record<string, string> = {};

if (namespaces.ton) {
const tonWallet = await getWallet();
sessionProperties.ton_getPublicKey = tonWallet.getPublicKey();
sessionProperties.ton_getStateInit = tonWallet.getStateInit();
}
// TON props + wc_feeTerms (Session Fees / Dapp Picker POC)
const sessionProperties = await buildSessionProperties(namespaces);

const session = await walletKit.approveSession({
id: proposal.id,
namespaces,
sessionProperties:
Object.keys(sessionProperties).length > 0
? sessionProperties
: undefined,
sessionProperties,
});
haptics.requestResponse();
SettingsStore.setSessions(Object.values(walletKit.getActiveSessions()));
Expand Down
16 changes: 16 additions & 0 deletions wallets/rn_cli_wallet/src/navigators/HomeTabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Platform } from 'react-native';
import { HomeTabParamList } from '@/utils/TypesUtil';
import Wallets from '@/screens/Wallets';
import Connections from '@/screens/Connections';
import Explore from '@/screens/Explore';
import Settings from '@/screens/Settings';
import { useTheme } from '@/hooks/useTheme';

Expand All @@ -19,6 +20,12 @@ const tabConnectionsIcon = Platform.select({
default: require('@/assets/icons/tab-connections.svg'),
});

const tabExploreIcon = Platform.select({
ios: { sfSymbol: 'safari.fill' },
// POC corner cut: reuse the connections svg on Android
default: require('@/assets/icons/tab-connections.svg'),
});

const tabSettingsIcon = Platform.select({
ios: { sfSymbol: 'gearshape.fill' },
default: require('@/assets/icons/tab-settings.svg'),
Expand Down Expand Up @@ -62,6 +69,15 @@ export function HomeTabNavigator() {
sceneStyle,
}}
/>
<TabNav.Screen
name="Explore"
component={Explore}
options={{
tabBarLabel: 'Explore',
tabBarIcon: () => tabExploreIcon,
sceneStyle,
}}
/>
<TabNav.Screen
name="Settings"
component={Settings}
Expand Down
15 changes: 15 additions & 0 deletions wallets/rn_cli_wallet/src/navigators/RootStackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HomeTabNavigator } from './HomeTabNavigator';
import Scan from '@/screens/Scan';
import { useTheme } from '@/hooks/useTheme';
import { LogList } from '@/screens/LogList';
import DappBrowser from '@/screens/DappBrowser';
import SecretPhrase from '@/screens/SecretPhrase';
import { useLogs } from '@/hooks/useLogs';
import { FontFamily } from '@/utils/ThemeUtil';
Expand Down Expand Up @@ -47,6 +48,20 @@ export function RootStackNavigator() {
component={Scan}
options={{ headerShown: false }}
/>
<StackNavigator.Screen
name="DappBrowser"
component={DappBrowser}
options={({ route }) => ({
headerShown: true,
title: route.params.name,
headerBackButtonDisplayMode: 'minimal',
headerTintColor: Theme['text-primary'],
headerTitleStyle: {
...headerTitleStyle,
fontWeight: '400',
},
})}
/>
<StackNavigator.Screen
name="Logs"
component={LogList}
Expand Down
Loading
Loading