feat(onchain): add native contract-hosted apps - #192
Conversation
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>
A Safe account has no key of its own: its owners are existing wallet records, and a SafeTx is EIP-712 typed data, so every signer backend (vault, Ledger, phone) co-signs through the ordinary getSigner seam. src/main/wallet/safe/safe-executor.js orchestrates the rest over @safe-global/protocol-kit (local SDK only — never the hosted Safe Transaction Service): counterfactual address prediction, deployment through the canonical safe-deployments factory (asserted, never a user-supplied one), SafeTx build/hash returning plain-JSON shapes, sequential recover-verified owner-signature collection, and execTransaction submitted by an executor EOA that pays the gas. The record's original init params (owners, threshold, saltNonce) are the reproducibility anchor: buildSafeTransaction re-derives the CREATE2 address from them and refuses on mismatch, and the anvil-fork integration test (skips without anvil/network) proves the same params deploy to the same address on forked Gnosis AND Base, with retroactive deployment claiming funds sent to the address before it existed. getSigner(safeIndex) now throws — a Safe is an account, not a signer. provider-manager grows getEip1193Provider(chainId) (protocol-kit needs a raw request interface FallbackProvider cannot give) sharing the RPC pool and cache invalidation; transaction-service grows toFeeFields so callers stop re-deriving the eip1559/legacy branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Safe is now a first-class wallet record (type 'safe') whose owners are existing accounts, created from the account dropdown with the two shipped presets — backup (1 of 2) and resilient (2 of 3); 2-of-2 is deliberately not offered since losing either device would brick the funds. The record freezes the init params (owners, threshold, saltNonce) that make the CREATE2 address reproducible; identity-manager enforces the presets, refuses Safes owning Safes, and blocks deleting an account while a Safe references it as owner. Creation is free: safe-service predicts the counterfactual address and stores the record, so the account can receive immediately. "Needs funds" is a first-class blocking state, not an error — the status card under Send/Receive quotes the one-time Gnosis activation (deployment tx built once, gas + executor balance checked in parallel) and either offers Activate, or blocks with "fund <executor> with ≥ X xDAI", or explains that no owner can pay. Activation reuses the quoted deployment tx, waits for confirmation, and only then marks the record deployed (chain state also self-heals the record). Until the multi-owner signing flow ships, Safe accounts are receive-only: Send is disabled with an explanation and dApp connect skips them (EIP-1271 comes later). The shared account picker grew label/multi-select support so the owner list, Ledger, and phone screens render from one component. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ence A deployed Safe can now send: the send flow branches into a Safe path where main builds the SafeTx, collects owner signatures one device at a time (vault instantly, Ledger tap, phone QR — all through the existing signer seam), and submits execTransaction through the executor EOA. The pending view shows a per-owner signature checklist driven by progress events streamed from main; the review screen names who pays the fee instead of quoting gas that can only be known after signing. Every collected signature is persisted the moment it exists (interim JSON, one pending SafeTx per Safe — a single slot sidesteps the nonce replacement swamp). A rejection, an unreachable phone, a failed broadcast, or an app restart never loses signatures: the status card shows "transaction awaiting signatures (1 of 2)" with continue/discard, and resume skips owners who already signed. Safe transactions land in payment history: safe-send rows say from = safe address with the executor and safeTxHash in metadata (tx-recorder gained a fromAddress override), and activation deploys record as safe-deploy. The payments page knows both kinds. Also fixes a stale-capability bug: the renderer's record snapshot now syncs deployment truth from main when the status card refreshes, so the Send button enables right after activation instead of after a restart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
startSafeSend passed the safe record straight into the executor layer,
so protocol-kit received the owners as wallet INDEXES and viem died on
getAddress(0) ("Address \"0\" is invalid") the moment a safe send was
confirmed. The activation path resolved them correctly; the send
orchestrator now does the same, and initPredictedKit fails loudly on
non-address owners so the index/address mixup can never reach the
address parser again.
The unit test had asserted the wrong shape against a mocked builder —
fixed, and the anvil-fork suite gained an un-mocked regression test
that walks the real record → build → collect(2/3) → execTransaction
path and checks the funds arrive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New Playwright spec drives the actual app end to end: create a 1-of-2 Safe in the wizard, hit the needs-funds blocking state, fund the executor on an anvil fork of Gnosis (the registry's user config points every Gnosis RPC at it, builtin endpoints removed), activate through the canonical factory, watch Send enable, and send 0.5 xDAI out of the Safe — recipient balance verified on-chain. Skips cleanly without anvil/network, like the jest fork suite. The fork runs with --block-time so confirmation behaves like the real chain. Two findings it caught immediately, both fixed: - activating with a locked vault failed silently to the console and reset the button — the status card now surfaces the error inline; - the Safe send path skipped gas estimation and with it the only check that an asset was selected — validateAmount now owns that check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field testing a 2/3 Safe showed the flaw: signature collection ran as a
pipeline that marched through owners in record order and declared
"Transaction Failed" the moment the Ledger wasn't plugged in — for a
task that is fundamentally asynchronous and belongs to the user, not
the app.
Collecting signatures is now a board the user drives. Confirming a send
creates and persists the SafeTx, silently adds the free signatures
(mnemonic owners, vault unlocked — kept, that's what made 1-of-2 feel
instant), and opens a dedicated "Collect signatures" subscreen: what is
being sent and to whom, "1 of 2 signatures — any 2 of the 3 owners can
sign", when it started, and one row per owner with its own action
(Sign with Ledger / Show QR code) that the user taps when the device is
actually in hand. A failed attempt is a row state with main's error
message; a rejection is a decision — the row quietly returns to
waiting. The board is leaveable: the account card shows what's waiting
("Sending 0.5 xDAI to 0x12…cd — 1 of 2 signatures, started 2 days
ago") and re-opens it. While the board is open, an unplugged Ledger row
flips to "Ledger detected — sign now" the moment it's connected. The
phone QR overlay now says it's approving your own multi-owner
transaction instead of dApp copy.
Main's API turned granular and defensive: start (build + free
signatures only — devices are never cold-called), sign-one-owner
(per-safe mutual exclusion; a signature landing after its transaction
was discarded is dropped by a safeTxHash identity check), and execute
as its own idempotent step — auto-run by the board at threshold,
nonce-guarded so a broadcast that secretly landed (or an
app.safe.global execution) flips the transaction to a truthful terminal
"superseded" state instead of retry-looping, with an executor-can't-pay
banner and signatures that survive every failure. The progress event
stream is gone; the board renders from returned state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pending SafeTxs move out of the account status card into a wallet-wide
"Unfinished transactions" row above Recent payments, with a count badge
across ALL Safe accounts. It opens an overview subscreen (one entry per
waiting transaction: summary, which Safe, signature progress, age) and
each entry opens its signing board — with a single pending transaction
the row jumps straight to the board. The status card is back to
activation states only.
Also fixes the raw-wei summary ("Sending 2000000000000000 to …") that
pending transactions created before the presentation fields existed
rendered with: the summary now formats atomic amounts with the stored
decimals and falls back to xDAI for native sends.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field report: tapping "Show QR code" on the signing board failed within a split second with "Phone signing failed. Try again." A new E2E — a phone owner co-signing a 2/3 SafeTx through the real bridge page over a local relay — passes, which isolated the failure to the environment: the default signaling relay (test.mosquitto.org's public TEST broker) currently accepts TLS and instantly hangs up the MQTT websocket, reproducible with a raw probe. The session broker now resolves its relay per session: an explicit override (env) is used as-is, otherwise the public candidates (mosquitto, EMQX, HiveMQ) are probed with a single websocket handshake and the first reachable one wins (cached for a minute). The chosen relay rides inside the QR, so the phone always joins the same one — and relays only ever carry ciphertext. Non-Error job failures also stop collapsing into the generic registry message, so the next environmental failure names itself. The new safe-phone E2E stays: it covers the signing board's QR row end to end (vault free signature + phone co-signature + execution, funds verified on the fork). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field report: after collecting both signatures the board said "Vault is locked / The signatures are kept — you can try again" — the vault had auto-locked while the phone signature was being gathered, and executing needs the executor's vault key. A locked vault is a step in the flow, not an error: the safe IPC handlers now tag vault-locked failures with a stable code, and the signing board (execute AND per-owner sign) plus the activation card respond by opening the standard vault-unlock screen and retrying the step after a successful unlock. Cancelling the unlock leaves an actionable notice instead of a dead end. The safe-accounts E2E now exercises this path for real: it clicks Activate with a locked vault, unlocks through the UI, and waits on the Send button (the deployed-truth barrier — the status card sits inside the identity view, which the unlock screen hides, so a card-hidden assertion passes prematurely). The phone E2E tolerates the card quoting before or after the executor funding lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A dApp's personal_sign / eth_signTypedData_v4 against a Safe account is answered with owner signatures over the SafeMessage EIP-712 envelope; completeSafeMessage returns the sorted concatenated bytes the dApp verifies via isValidSignature on the Safe. - safe/safe-messages.js: in-memory sessions (a dApp request is a live promise that dies with its page); same-hash restart resumes with the collected signatures, a different hash replaces the dead session. Digests are computed with ethers over normalized input — protocol-kit's hashSafeMessage would UTF-8-hash hex personal_sign payloads, diverging from what EOA signers and verifying dApps compute. - safe/signature-collection.js: the owner-collection machinery (in-flight lock, free-signature sweep, per-owner ceremony with identity re-check) extracted from safe-transactions over a store adapter; sends and message sessions share one lock per Safe. - safe/errors.js: the SAFE_* code registry. - IPC wallet:safe-message-start/-sign/-state/-cancel/-complete (lazy, VAULT_LOCKED-tagged) + window.wallet.safeMessage*. - Fork test: collected signatures pass isValidSignature(bytes32,bytes) on the real deployed contract for personal_sign AND typed data; a tampered digest is refused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- dapp-connect lists DEPLOYED safes (receive-only stay hidden), defaults to the active account, and says upfront that a multi-owner account signs as a smart contract some apps can't verify (same caveat on the sign approval screen). - Message signing routes through the signing board's new 'message' mode: free vault signatures that meet the threshold answer instantly (no board), otherwise the board collects per-owner and completion hands the combined signature back and closes — closing/cancelling rejects the dApp with 4001. Opening the board over a live message session settles that session first. - dApp eth_sendTransaction starts a pending SafeTx and hands over to the board; the fee row names who pays (getSafeStatus executor — also used by the send review now); the dApp promise resolves with the execution hash via wallet:safe-executed and rejects on wallet:safe-discarded. Parking the board keeps the dApp waiting — its transaction IS pending. - Gnosis-only guards on both paths (an unverifiable signature or a never-executable SafeTx beats a confusing failure later); safes are never auto-approved for transactions. - Board openers accept the caller's fresh state, dropping the redundant re-fetch + free-sweep IPC on every dApp interaction. - SafeMessage phone signatures get their own QR copy (context 'safe-message'). - NEW E2E safe-dapp.spec.js: a real bzz:// dApp page connects the Safe through the webview provider bridge, personal_sign verifies via isValidSignature on the anvil Gnosis fork, and a send comes back as an execution hash with the balance moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without an injected signaling override the broker probes the public MQTT brokers with a real WebSocket — the unit suite silently depended on broker reachability. The harness now injects a fixed relay. 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>
A SafeMessage session was keyed only by Safe index and hash: a second
dApp tab requesting the identical digest resumed (and could complete)
another site's session, and a different digest silently replaced a
still-live request while its board and promise stayed attached to the
old ceremony.
Sessions now carry an unguessable token (crypto.randomUUID) plus the
requester identity {origin, webContentsId}, threaded from the dApp
provider through preload/IPC:
- sign/complete/cancel require the token; state queries with a foreign
token render as "nothing open" for that caller
- the identical request only resumes for the SAME page (origin AND
webContents); any other request is refused with SAFE_MESSAGE_EXISTS
while a live session exists — no silent replacement
- sessions are dropped when the requesting webContents navigates or is
destroyed (a dead page's leftover no longer blocks, and its
signatures cannot linger for whatever loads next)
- the session slots live in a new dependency-light message-sessions
module so lifecycle owners can force-drop without the signing stack
Fixes PR #160 review finding P1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
safe-pending.json entries and live SafeMessage sessions are keyed by wallet index. Deleting a Safe left them behind; since new wallet indexes are assigned max+1, deleting the highest-index Safe let the next account inherit — or be blocked by — the dead Safe's half-signed SafeTx and session. deleteDerivedWallet now discards the pending SafeTx entry and force-drops the message session (unhooking its webContents listeners) before the record is removed, via two dependency-light lazy requires. The renderer's delete confirm warns when a waiting transaction and its collected signatures are about to be discarded. Fixes PR #160 review finding P2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retry data-less Colibri transport failures once with a fresh client and log bounded structured diagnostics. Pin ephemeral quorum providers to Ethereum Mainnet so ethers does not flood logs with network-detection retries.
Publish bidirectional Myotis availability epochs, reject reads that overlap shutdown, and invalidate cached or in-flight policy decisions on lifecycle changes. Add immediate fallback diagnostics and poll availability every second so startup and shutdown transitions are visible promptly.
Pin the verified v0.1.4 release and checksum manifest, adopt engine ABI 22, and isolate CI sync caches for the new client version. The release includes the Gnosis light-client catch-up fixes.
Pin the verified v0.1.5 release and refresh the runtime/UI version. Move CI to fresh v0.1.5 sync-cache namespaces so all three shipping platforms rerun the Ethereum and Gnosis native smoke tests from clean profiles.
Pin the published v0.1.6 checksum manifest and refresh the Myotis CI cache namespace so the three-platform live smoke suite exercises the new Roost discovery path from fresh profiles.
Myotis answers account reads from its verified head state and takes no block parameter, so requestMyotis silently dropped the tag: a `pending` nonce request (every signAndSendTransaction, and dapps via wallet:chain-request) was answered from stale verified state instead of falling through to a source that honours the tag, so two transactions in a row could be signed with the same nonce. Fall through to the next source for any explicit tag other than `latest`, on the account reads and on the identical eth_estimateGas site. feeQuote also set maxFeePerGas to the raw eth_gasPrice, leaving zero headroom for a base fee that can rise 12.5% per block between quoting and inclusion. Restore the wallet's long-standing market preset of 2x base fee + priority fee; effectiveGasPrice (what the UI displays) is unchanged.
Wallet gas estimates carry a decimal wei value (ethers parseUnits output). The router serialized it verbatim into the JSON-RPC params, so spec-compliant nodes answered -32602 and the estimate silently degraded to whichever lenient endpoint replied via the unverified `direct` tier — or failed outright against a strict user-configured node. Normalise the QUANTITY fields of a call object once, at the boundary every source shares, so eth_call/eth_estimateGas reach the quorum tier and all endpoints compare a byte-identical body. Myotis is unaffected: it re-decodes the value through BigInt either way.
The engine executes a call against its verified head with only from/to/data/value: the block argument is discarded (`_block`, never read — the servable-window gate is a host obligation) and the addon exposes no state-override entry point. Forwarding the tag therefore only looked like it honoured it: an eth_call at an explicit historical block was answered from head state and labelled `verified: true`, and because it succeeded the router never fell through to colibri/quorum/direct, which honour (or correctly reject) the tag. State overrides in `params[2]` and call-object `gas`/fee/`nonce`/`accessList` fields were dropped the same way, so simulations ran without their overrides. Apply the ffd2ec8 pattern to `eth_call`: gate on the block tag, on `params[2]`, and on the call fields the engine cannot take, raising SourceUnavailableError so the read continues down the source order. The same gate now covers the structurally identical `eth_estimateGas` branch, which dropped the extra call fields and overrides too. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Call objects carry calldata as either `data` (legacy) or `input` (the standardized field web3.js v4 and friends send). Every source read only `data`, so an `input`-only eth_call executed against Myotis' verified head with empty calldata and returned that answer as verified, and an eth_estimateGas came back as a plain-transfer 21000 — enough gas for the resulting transaction to revert out-of-gas on chain. Canonicalise the alias into `data` in normalizeParams, the boundary every tier shares, so Myotis, quorum and direct all execute the same calldata. A call carrying both aliases with different payloads is ambiguous — strict nodes reject the pair — so Myotis declines it and the read falls through rather than executing one side and calling it verified.
Merge current main while preserving the Myotis integration and newer browser features. Pin the Nethermind-compatible release, harden verified-source routing and downloads, and expand addon/live CI coverage across shipped operating systems.
1. ENS-page prover upsert now includes `name` so clearing the field
collapses the override back to the builtin (matches the Chains-page
path) instead of pinning builtin URLs in the user layer forever.
2. colibri-resolver: close the acquire→use use-after-destroy window —
acquireClient retains optimistically then re-validates the client is
still cached (else releases + re-acquires), and the provider is
co-located in the clients entry so the {client, provider} pair is
captured atomically. Replaces useClient; withColibriClientRetry now
owns the acquire/release lifecycle.
3. reorderAccess persists only the changed key ({ access: { [key]: order } })
so a drag no longer freezes merged builtin defaults into the user config.
4. custom-chain default read order in the UI aligned to the router's
['colibri','quorum','direct'] so the UI matches routing and the first
drag can't drop colibri.
5. prover-role coverage URLs now get the same https-or-loopback SSRF
validation as rpc URLs (both are main-process fetched); +tests.
6. endpoint Save has an in-flight guard so a double-click can't mint two
'user-<Date.now()>' duplicate sources.
7. broadcast + fee-quote loops preserve the last real node error (code/data)
instead of only the stringified aggregate, matching request().
Preserve Myotis chain routing and resolution behavior while integrating the current main branch's Ledger signer, adblock, shortcuts, and Swarm changes.
Bring in the Settings navigation repair and shared Linux native dependency action, then apply that setup to the Myotis and settings E2E jobs.
Bring in the reviewed Settings E2E job from main while retaining Myotis policy coverage and the shared Linux native prerequisites.
Run verified Ethereum, Gnosis, resolver, and UI checks through one production-app Myotis lifecycle. This avoids persisting peer backoff in a raw smoke and immediately restarting the same client for Playwright while retaining strict method and verification assertions.
Require every ENS and NameNFT assertion to be served by Myotis, but retry after transient peer misses that production correctly routes to Colibri. Record readiness, peer state, policy, and fallback results for actionable CI failures.
Integrate the current remote-signing branch as a prerequisite snapshot for native onchain app development. # Conflicts: # scripts/build.js # src/main/identity-manager.js # src/main/identity-manager.test.js # src/main/preload.test.js # src/main/wallet/signers.js # src/main/wallet/signers.test.js # src/main/wallet/transaction-service.js # src/main/wallet/vault-access.js # src/renderer/lib/wallet/connect-ledger.js # src/renderer/lib/wallet/send.js
Integrate the current Safe smart-account branch on top of the OpenLV prerequisite snapshot for native onchain app development. # Conflicts: # package-lock.json # src/main/preload.js # src/main/wallet/signers.js # src/main/wallet/signers.test.js # src/renderer/lib/wallet/dapp-connect.js # src/renderer/lib/wallet/dapp-sign.js # src/renderer/lib/wallet/dapp-tx.js # src/renderer/lib/wallet/send.js
Integrate the current verified Ethereum light-client and chain-data routing branch on top of the composed OpenLV and Safe wallet foundation. # Conflicts: # README.md # scripts/check-binaries.js # src/main/ens-resolver.js # src/main/ens-resolver.test.js # src/main/index.js # src/main/ipc-handlers.test.js # src/main/preload.js # src/main/preload.test.js # src/main/profile-catalog.js # src/main/profile-catalog.test.js # src/main/profile-paths.js # src/main/profile-paths.test.js # src/main/service-registry.js # src/main/settings-store.js # src/main/wallet/wallet-ipc.js # src/renderer/pages/settings.html
Resolve draft ERC-8244 html() documents through Freedom's chain-data router under contract-and-chain-scoped web3 origins. Pin the EIP-1193 provider to the app chain, enforce isolated response policy, and add navigation, history, documentation, unit coverage, and an Electron smoke test.
Bring the isolated E2E log path fix into the onchain-app preview branch so the private-window privacy assertions run against their per-test data directory.
Reuse the existing address-bar trust shield for web3 applications and show retrieval evidence, network, contract, and HTML hash. Keep provenance host-owned and scoped to the committed guest navigation so pages and stale tabs cannot spoof the modal.
Keep Chromium's chain-scoped synthetic hostname as internal plumbing while reverse-mapping browser chrome, history, bookmarks, copying, trust details, and permission prompts to the standard web3 form.
Update: provenance UI and standards-aligned URLsTwo browser-integration refinements have landed since this draft was first shared. Onchain trust detailsThe address-bar trust shield is now active for contract-hosted applications. Its browser-owned popover shows the retrieval result and the identity of the document Freedom actually loaded:
This deliberately reports verifiable provenance rather than making a broader immutability claim: the strong statement is that these exact bytes were retrieved from this contract through this chain-data result. A contract's URL scheme research and decisionWe are keeping
We considered prettier chain-specific schemes such as The standard URL is therefore what users now see and share: Chromium cannot use that exact text as the guest's physical origin. A bare all-hex The Implementation commits:
Validation for the URL refinement: ESLint passed, 533 focused unit tests passed, and the Electron onchain-app smoke test passed with an assertion that history persists the standard URL rather than the internal hostname. |
Keep read-heavy dapps responsive without changing their configured source order. Apply two-second Colibri and quorum deadlines, settle quorum as soon as its result is known, and remember scoped timeouts or deterministic execution ceilings before falling through to the next configured source. Bound non-cancellable Colibri work and carry the canonical app origin through the existing provider request path.
Carry a usable RPC response from a failed quorum into an immediately following Direct tier instead of issuing the same request again. Keep pending members alive under Direct's existing compatibility budget, skip members that already failed, and preserve the failed-quorum evidence in trust metadata.
Install Freedom's EIP-1193 provider synchronously in the page main world so eager dapps can detect it during HTML parsing. Keep an idempotent DOM fallback and cover parser-time detection, fallback behavior, and private-mode isolation.
Update: eager wallet detection fixedTesting another contract-hosted app exposed a timing incompatibility in Freedom wallet injection. The app captures Commit Validated with:
This fixes both the explicit “No Ethereum wallet found” case and the broader flaky-connect class caused by dapps performing eager provider detection. |
Update: adaptive routing for read-heavy onchain appsLive zSwap testing showed that quote-heavy, block-pinned multicalls expose compatibility limits across the verified read sources:
Freedom now handles this automatically while preserving the configured order (
The result is that zSwap works with Freedom’s default routing order—users no longer need to move Direct RPC to the top. Implemented in |
Preview status
Important
This is a shareable draft preview, not the eventual merge branch.
It is stacked on the pending foundation work in #159 (
feature/openlv), #160 (feature/safe-accounts), and #181 (feat/myotis-spike). Those branches may still change during review. After they merge, the onchain-app commit will be transplanted onto freshmainso the final PR contains only the focused feature diff.What this adds
Freedom can now load draft ERC-8244 contract-hosted applications directly from Ethereum-compatible chains.
Users can enter the friendly form:
The chain ID is optional and defaults to Ethereum mainnet. Freedom canonicalizes the address to a Chromium-safe, chain-scoped origin:
The main process calls the contract's read-only
html()function (0x33c34ac3) through Freedom's chain-data router, ABI-decodes the returned UTF-8 document, and serves it under that stableweb3:origin. The app therefore does not depend on an HTTP gateway or a page-selected RPC endpoint.zSwap validation
This was tested against the live zSwap v0.1 root:
Its live
html()result decoded to a 240,945-byte HTML document and rendered successfully in Freedom. The current zSwap document uses inline application code and no remote scripts, fetch/XHR calls, or WebSockets, so it is compatible with Freedom's isolated onchain-app policy.Security and wallet behavior
wallet_switchEthereumChaincannot move an onchain app away from its declared chain.chainChangedevents into pinned apps.Browser integration
web3:protocol registration in regular and private sessionsCurrent scope
This first slice resolves direct contracts exposing
html(). Upgrade registries and resolver discovery are intentionally deferred; they can be added later without changing the origin or security model.ERC-8244 is currently a draft proposal:
Validation
npm run lint— passednpm test— 177 suites passed, 3 skipped; 3,387 tests passed, 10 skippednpm run test:e2e— 69 passed, 2 platform-specific skipped; 0 failednpx playwright test --project=harness test-e2e/onchain-apps.spec.js— passedhtml()read and document inspection — passedgit diff --check— passedThe earlier macOS
main.logtest-path failures were fixed onmainbyb0dbfa8eand merged into this preview branch. The full Electron harness is now green.Developer impact
The implementation deliberately builds on the pending Myotis, Safe Accounts, and OpenLV foundations instead of duplicating their chain routing, account, or signing work. The final cleanup after those PRs merge should be a transplant/rebase operation, not a redesign of this feature.