Skip to content

feat(wallet): phone signing (OpenLV remote signing) — re-targeted to main - #196

Merged
meinharrd merged 19 commits into
mainfrom
openlv-remote-signing
Aug 22, 2026
Merged

feat(wallet): phone signing (OpenLV remote signing) — re-targeted to main#196
meinharrd merged 19 commits into
mainfrom
openlv-remote-signing

Conversation

@meinharrd

Copy link
Copy Markdown
Contributor

Phone signing (OpenLV remote signing) — re-targeted to main

Lands the remote (phone) signing feature originally in #159. That PR was stacked on #149 (feature/hardware-wallet) and its base was never re-targeted to main, so merging it landed the feature on the (now-stale) feature/hardware-wallet branch rather than main. This PR brings the exact same feature onto main.

Since #149 is already in main and the branch had main merged in, the diff here is only the openlv/remote-signing feature (47 files) — no #149 re-introductions.

Production bridge is deployed and pinned: BRIDGE_ORIGIN = https://bridge.freedom.baby (nginx + Let's Encrypt on the release host), serving the audited freedom-bridge artifact (commit 0a34fe0). This clears the dist build guard from #159.

Full feature description, testing (60+ unit, real-openlv integration, offline E2E, real-device smoke tests), and design notes are in #159. CI re-runs here against main.

flotob and others added 19 commits July 8, 2026 23:08
Sign with a wallet on your phone over the Open Lavatory protocol
(openlv.sh): main publishes signing jobs to a renderer-hosted openlv
session (P2P WebRTC, end-to-end encrypted, per-request QR); the phone's
answer is verified against the account record before use.

- remote signer backend behind the getSigner seam (type 'remote'):
  personal_sign / eth_signTypedData_v4 with recover-and-compare
  verification (REMOTE_WRONG_ACCOUNT), stable REMOTE_* error codes
  mirroring the LEDGER_* registry, EIP-1193 rejection mapping
- optional Signer.sendTransaction capability: phones broadcast
  themselves; transaction-service prefers it and best-effort verifies
  the reported tx's sender on-chain; coded device errors now pass
  through its catch unwrapped
- renderer session broker (one openlv session per job, dual-purpose QR
  URL with the session secret in the fragment) + preload IPC bridge
  that only accepts responses from the chrome renderer
- shared signing-utils for EIP-712 wire-payload construction (was
  duplicated across vault/ledger, now also used by remote)
- vendored @openlv bundle (LGPL-3.0, regenerable via npm run
  vendor:openlv), lazy-loaded off the renderer boot path
- integration test: real @openlv stack, both roles, against an in-test
  MQTT broker — handshake, encryption, signing round-trips, and a
  relay-sees-only-ciphertext assertion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add remote (phone) accounts to the wallet list and the sidebar flow to
connect one: show an openlv QR, receive the phone's accounts over the
encrypted P2P session (eth_requestAccounts), pick and name one, and
switch to it. Works with the vault locked — the key stays on the phone.

- identity-manager: addDeviceWallet generalizes the Ledger add path
  (shared validation/dedup/auto-naming via DEVICE_LABELS);
  addRemoteWallet + wallet:add-remote-wallet IPC; device-neutral
  key-guard messages
- broker connectPhone(): local discovery job reusing the session/
  teardown/cancel plumbing via a per-job respond seam
- Connect Phone subscreen with QR/status/retry, account picker, and
  success step; wallet selector gains the entry point and per-type
  badges (Ledger/Phone)
- shared device-account picker + inline-error helpers +
  activateAddedWallet extracted; connect-ledger migrated to them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a phone account signs (dapp tx/sign, manual send, x402 payment),
a QR panel overlays the approval flow: scan with the phone, live
connection status, cancel, and a fresh-QR retry that supersedes the
session without dropping the pending request.

- wallet-utils: accountType/isDeviceAccount/deviceLabel generalize the
  ledger-only helpers; signingButtonLabel and the unlock-gate bypass
  (renamed bypassUnlockGateForDevice) now cover phone accounts, so all
  four approval flows skip the vault gate and label buttons per device
- broker: job events carry kind ('signing'|'connect'); retryJob mints a
  new QR for a live job with staleness-guarded attempts; aborted
  attempts release their session immediately (Promise.race) instead of
  parking on the SDK's long response timeout
- send/x402 copy adapts to the device type (Ledger vs phone)
- shared PHASE_STATUS_TEXT + generateScannableQr used by both QR UIs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The other half of the dual-purpose QR: phones without an openlv-native
wallet open the bridge page (static, in-repo under bridge/, no backend)
inside their wallet app's browser. It joins the session from the URL
fragment — never sent to any server — and forwards freedom's requests
to the wallet's window.ethereum behind a method allowlist, so the
wallet's own confirmation UI still gates every signature.

- bridge/: self-contained page + the same openlv bundle the renderer
  vendors (built once, copied; npm run bridge:serve for local dev)
- FREEDOM_OPENLV_SIGNALING env override (preload nodeConfig → broker)
  so tests pin the signaling relay to a local broker
- Playwright E2E: real Electron app with a pre-seeded locked vault,
  bridge page in a plain Chromium as the "phone" (fake window.ethereum
  signing with a test key in Node) — connects a phone account through
  the sidebar UI and round-trips a verified personal_sign through the
  remote signer, QR panel included, all offline (local MQTT + local
  WebRTC)
- shared test/helpers/local-mqtt-broker.js for the jest protocol test
  and the E2E fixture

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
freedom.florianglatz.eth.limo hosts the bridge page for smoke testing;
final hostname still TBD.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phone-side dapp sessions default to Ethereum mainnet regardless of the
wallet's selected network, so remote sendTransaction landed on the
wrong chain (found in the Gnosis smoke test). The chainId field inside
eth_sendTransaction params is ignored by wallets — the chain is session
state, switched explicitly like any dapp does.

- remote signer ships an EIP-3085 chain descriptor with each
  eth_sendTransaction job (name/currency/explorer from the chain
  registry; public https RPC endpoints only — a user's local node
  config never leaves the machine)
- broker pre-flights wallet_switchEthereumChain over the same session
  (same QR); unknown chain (4902) → wallet_addEthereumChain with the
  descriptor, then switch again; a declined switch fails the job before
  the tx is ever sent
- QR panel shows a "confirm the network switch" status; bridge page
  allowlists the two wallet_* methods

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The freedom mobile wallet endpoint (swarm-mobile-ios, WP-R4a) runs the
vendored openlv SDK inside a hidden WKWebView, and WKWebView refuses
ES-module imports from file:// pages while the SDK hard-requires a
secure context for crypto.subtle — so bundle-openlv.js now also emits
bridge/openlv.iife.js (window.OpenLV, classic script) and refreshes the
sibling iOS checkout's vendored copy when present.

npm run openlv:ios-harness plays the desktop host role for the iOS
XCTest suite: local aedes MQTT broker + a headless Chromium page
running the same SDK (Node has no WebRTC) + an HTTP control surface on
127.0.0.1:8798, sending the signing-job sequence (eth_requestAccounts,
wallet_switchEthereumChain pre-flight, personal_sign) and recording the
responses. The Chromium mDNS ICE workaround moves to
test/helpers/webrtc.js, shared with the remote-signing E2E fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each XCTest consumes one host session; GET /reset reloads the Chromium
host page so the next test gets a fresh session and a clean exchange
log instead of a consumed one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two findings from the freedom-mobile smoke test:

Freedom's signing jobs never send eth_requestAccounts (the account was
captured in an earlier connect session), and strict wallets — freedom
mobile's own in-tab provider included — reject signing from an origin
that never connected ("Connect first … isn't authorized"). The bridge
now connects lazily before the first signing request; lenient wallets
treat the extra connect as a no-op.

The page also offers "Open in Freedom app" via the freedom:// custom
scheme, so scanning the QR with the plain camera can hand the session
to the native endpoint today, before the universal-link claim on this
origin is deployed. On phones without the app the tap is a no-op and
every other path stays available.

Redeploy bridge/ to Swarm to take effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/reset?mode=tx makes the host send the desktop signing-job shape for a
transaction (eth_requestAccounts → chain-switch pre-flight →
eth_sendTransaction) instead of personal_sign, so the iOS suite can
prove the wallet endpoint's self-built transaction actually mines on a
local anvil chain — not just that it was signed and broadcast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bridge page is a standalone static site deployed independently to
Swarm on its own cadence — it now lives in
github.com/solardev-xyz/freedom-bridge. Everything here that touched
bridge/ points at a sibling checkout instead: serve-bridge.js (and
through it the remote-signing E2E) resolves ../freedom-bridge with a
FREEDOM_BRIDGE_DIR override, the iOS harness reads the esm bundle from
the renderer vendor copy (byte-identical), and bundle-openlv.js syncs
both sibling repos' vendored bundles from its one build so nothing can
drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serve-bridge.js threw at require time when the sibling checkout was
missing, which failed the whole `npm run test:e2e` sweep for anyone
without it. The availability check moves behind bridgeAvailable() and
the spec skips itself with a clone hint instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The require was one directory short (resolved to src/test/... instead
of the repo-root test/helpers/), so the suite failed to load — the CI
red on PR #159. Six directories up, six dots of shame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The openlv mqtt signaling layer uses the browser WebSocket global,
which Node only ships from v22 — CI runs Node 20, so the suite failed
with "WebSocket is not defined" there while passing on dev machines.
Polyfill from `ws`, which the local broker helper already depends on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Distributable builds now fail while BRIDGE_ORIGIN still points at the
interim test deployment, so the pre-merge checklist item on #159 is
enforced rather than remembered. FREEDOM_ALLOW_INTERIM_BRIDGE=1
overrides for local experiments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve wallet, signing, preload, and build conflicts while preserving OpenLV remote-device behavior alongside main's current safety checks and chain-data routing.
Replaces the interim test deployment (freedom.florianglatz.eth.limo) with the
production bridge origin https://bridge.freedom.baby, where the audited
freedom-bridge artifact (repo commit 0a34fe0) is now deployed over HTTPS.

This is the maintainer pre-merge checklist item from #159; it also satisfies the
dist build guard in scripts/build.js (which refuses to ship while the origin is
the interim host).

Deployed artifact sha256:
  index.html     d3ec3bb67c22efe5b36f5e73861e476c48494189adfa1d74ff925f77ad82516b
  bridge.js      e24b932e65d774d35e21b41b0f08f2c61ecf4c33d0aff507f646ed6cf78bb66c
  openlv.esm.js  48bfb8411dc8b1a880154d03806f3c779f1bca8c72d31dbd5efcbc56de8798c7
Sign with your phone: OpenLV remote signing
@meinharrd
meinharrd merged commit 8612be5 into main Aug 22, 2026
77 checks passed
@meinharrd
meinharrd deleted the openlv-remote-signing branch August 22, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants