feat(examples): react native (webview) example with an eip-1193/6963 wallet bridge - #845
Conversation
…bridge The widget in a react-native-webview, with the host app's wallet serving it through an EIP-1193 provider injected before page load and announced via EIP-6963 - the widget's wallet menu lists the RN app like any injected wallet, and every request() round-trips over postMessage with id correlation, 4001 rejection fidelity and chainChanged/accountsChanged pushed page-ward. Zero widget internals touched. Two packages per the examples/* workspace glob: the vite page rendering the widget, and the Expo host with the bridge (injected provider, viem wallet host, Alert-based approvals). Note: WebView<object> in the app is a workaround for react-native-webview declaring WebView<P = undefined> extends Component<WebViewProps & P>, which TS6 collapses to never the moment ref forces inference of P. Candidate upstream fix on their side is P = object (or splitting the default), tracked separately.
- page passes providers: [EthereumProvider()] - without it the v4 widget
mounts no wallet stack at all ('No widget providers specified') and the
wallet menu is permanently empty. This was the missing piece between
'bridge announces correctly' and 'widget lists the wallet'.
- react-native-get-random-values first at app entry: Hermes has no
crypto.getRandomValues and viem dies without it
- WebView<object> generic pin (react-native-webview P=undefined collapses
to never under TS6 when ref forces inference)
- vite on 5174 (5173 squatted by a local ssh forward), LAN ip for device
- TEMP: debug console-forwarding + probes in the injected script and
App.tsx, stripped before the PR
Verified on iOS sim end to end: widget renders, wallet menu lists
'LI.FI RN Example (Installed)' via the injected 6963 announcement,
connect round-trips through the native approval alert, address chip
shows, live quote WETH(mainnet)->ETH(Boba) at 0.1 WETH with gas/ETA,
balance check correctly flags insufficient funds, disconnect works.
… sim WKWebView content is invisible to the iOS a11y tree, so coordinate taps were the only way to drive the widget from test tooling - flaky and unscriptable. Dev builds now poll a host-side command server and run injected JS in the WebView, so tests click widget elements by selector/text and read DOM state deterministically. threading fix: the command server must be ThreadingHTTPServer - a single-threaded one deadlocks (await blocks the loop, app can't poll). Verified: scripted connect flow (click 'Connect wallet' -> click wallet row -> native alert), and the reject path - after Reject the widget recovers from Connecting back to the wallet list, proving the 4001 error shape round-trips.
…o real funds HARNESS mode: wallet becomes anvil account #0, all chain-1 traffic (the app's viem clients AND the widget's SDK reads via sdkConfig.rpcUrls query config) goes to a local mainnet fork. Same-chain swaps execute for real against forked DEX state - signed by the bridge, broadcast, mined - so every execution state is reachable and receipted without a cent moving. Verified end to end on the sim: connect as 0xf39F..., widget reads the fork's 10,000 ETH balance, live quote 0.5 ETH -> 936.94 USDC, Review -> Sign -> broadcast (anvil log: eth_sendRawTransaction) -> mined -> 'Swap successful'. Fork balance after: 936.544051 USDC - real slippage vs the quote, proving real contract execution. Two harness facts documented for the matrix run: - old anvil needs a basefee nudge (anvil_setNextBlockBaseFeePerGas + evm_mine) or txs sit pending; run with --base-fee 1 to avoid - the widget's terminal status comes from li.quest /v1/status, which cannot know fork txs; the harness mocks exactly that one endpoint in-page (fetch wrapper) and nothing else
The ios/ dir is prebuild output (Podfile.lock, Xcode project) - regenerated by expo run:ios. Every other example in the repo tracks only source.
Strip the receipt-driving scaffolding and conform to house style so the example reads like the other examples/ entries, not a WIP branch: - remove the DOM automation rig (scripts/automation-server.py + devAutomation.ts) and the onLoadEnd/console-forwarding debug probes - default the anvil swap harness OFF (ephemeral wallet out of the box); document it as opt-in in the README - add a README matching the other examples' structure - use the exported BRIDGE_SOURCE const instead of a duplicated string literal - biome clean, both packages typecheck green - untrack .expo/ cache that had leaked in Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| const ANVIL_ACCOUNT_0_KEY = | ||
| '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' as const |
There was a problem hiding this comment.
Exposed secret in examples/react-native-webview/App.tsx - high severity
Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More Info
There was a problem hiding this comment.
Good catch in principle, but no real secret here: that was anvil's well-known account #0, a public test account that only ever holds funds on a local fork. A raw 32-byte key literal trips secret scanners regardless, though, so I've removed it in fcabf2f - the key is now derived from anvil's public dev mnemonic at runtime, so no key literal ships in the example. The derived account is still anvil #0 (0xf39Fd6…2266).
…eral Aikido flagged the hardcoded anvil account #0 private key as an exposed secret. It's anvil's public test account (holds nothing outside a fork), but a raw 32-byte key literal trips every secret scanner regardless. Derive it from anvil's well-known dev mnemonic at runtime instead, so no key literal ships in the example. Verified the derived account is still anvil #0 (0xf39Fd6…2266). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WebView<object> workaround and its TypeScript-6 comment were unnecessary on react-native-webview 14 - a plain <WebView> with useRef<ComponentRef<typeof WebView>> typechecks cleanly (verified: postMessage on the ref resolves, tsc --noEmit green). Drops a confusing artifact and a claim that doesn't reproduce on the pinned version. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
issue
closes #565
tl;dr
this adds a react-native example under
examples/(nothing inpackages/widgetchanges). It runs the widget inside areact-native-webview, and the host RN app's wallet serves it over an EIP-1193 provider that's injected before page load and announced via EIP-6963. From the widget's side the RN app is just another injected wallet: it shows up in the wallet list, and connect/sign round-trip overpostMessage. The widget itself is untouched, that's the point of doing it this way.Went the webview route on purpose (note below on why not a native port). @chybisov this is the "use it in a webview" path you suggested on #565, with the wallet plumbing done end to end so it works out of the box.
how it works
examples/react-native-webview-widget-page/: tiny vite page that renders<LiFiWidget>withproviders: [EthereumProvider()]. Served from a real origin, neverfile://(opaque origin breaks localStorage on Android).examples/react-native-webview/: Expo host.injectedJavaScriptBeforeContentLoadedmaterialises an EIP-1193 provider in the page, announces it over 6963 with the host app's name/icon, and forwards everyrequest()to the RN side overpostMessagewith id-correlated responses. RN routes to viem/wagmi (WalletConnect works identically, it's just another EIP-1193 source on the host).chainChanged/accountsChangedare pushed back into the page. Without those the route execution machine hangs, or wagmi desyncs from the host chain.EVM only here. Solana/Sui/Tron bridges are follow-ups (each is another provider family, keeps this reviewable).
receipts (iOS sim)
Drove the whole thing end to end.
1. Widget lists the RN app as a wallet. This is the money shot, it proves the 6963 announce lands:

2. Connect crosses the bridge into a native RN approval (the request left the webview):

3. Connected, address chip shows:

4. Live quote WETH to ETH, real li.quest routing through the bridged provider:

5. Reject path. Hit Reject on the native alert, widget recovers cleanly from "Connecting" back to the wallet list, which proves the 4001 round-trips right instead of hanging:

full swap, executed on an anvil mainnet fork, zero real funds
Wired an
anvil --fork-url <mainnet>harness (dev-only,HARNESSflag). Wallet becomes anvil acct #0, and both the app's viem clients and the widget's own SDK reads (viasdkConfig.rpcUrls) point at the fork, so a same-chain swap executes for real against forked DEX state.6. Widget reads the fork's balance (10,000 ETH, "/ 10,000"):

7. Sign, broadcast, pending (anvil log shows the

eth_sendRawTransaction):8. Swap successful, 0.5 ETH to 936.94 USDC. Fork USDC balance after: 936.544051, real slippage vs the quote, so real contract execution rather than a painted screen:

honest caveats
/v1/status, which can't know about fork txs, so the harness mocks only that one endpoint in-page. Everything else is real.state of the diff
16 files, no scaffolding: the receipt-driving automation rig and debug probes are stripped, the anvil harness ships off by default (ephemeral wallet out of the box, opt-in for local swaps), there's a README matching the other
examples/entries, biome is clean and both packages typecheck green.