Skip to content

Latest commit

 

History

History
417 lines (362 loc) · 23 KB

File metadata and controls

417 lines (362 loc) · 23 KB

Runtime QA: Base mainnet (real money)

The Base counterpart to runtime-qa.md. Same discipline: top to bottom, each step depends on the last, record everything in the Results table. But this pass spends real money, so every step names its budget and where the money goes. Total budget: ~$25 of ETH on Base, most of it recovered at the end.

Run runtime-qa.md on Sepolia first. It is free, and anything it catches is a bug you did not pay mainnet gas to find. This pass exists for what Sepolia cannot prove: OP-stack fee math against the real GasPriceOracle, the canonical mainnet contracts, and real-value UX honesty.

Gate: is the SDK Base-aware yet?

Stealth features on Base (meta-address, scanning, receive ladder, stealth sends, sweeps) require an @opaquecash/deployments that knows chain 8453. The currently pinned releases do (the repo repinned to the Base-aware SDK), but a future repin or a stale node_modules can regress it silently, so check before starting:

node --input-type=module -e "import * as d from '@opaquecash/deployments'; console.log(d.getEvmChainIds())"
  • [ 11155111 ]NOT repinned. Sections 1–5 still apply in full; in sections 6–8 the expected-failure shape is the test: unlocking on Base must surface the deployments error as readable meta-address / scan error text (not a blank popup or a spinner), while plain per-site wallet sends and dApp signing keep working.
  • Includes 8453 → full pass. Also re-verify the announcer below.

Contracts under test (Base mainnet)

Contract Address Basescan
ERC-5564 announcer (canonical singleton) 0x55649E01B5Df198D18D95b5cc5051630cfD45564 link
ERC-6538 registry (canonical singleton) 0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538 link
GasPriceOracle (OP-stack predeploy) 0x420000000000000000000000000000000000000F link

announceGas for Base (60,000 in src/lib/config.ts) was measured against the canonical announcer. If the repinned SDK targets a different announcer or registry on Base, update this table and re-measure: the 2× headroom claim is only as good as the contract it was measured on.

Before you start

  • Chrome with MetaMask funded with ~$30 of ETH on Base (bridge via bridge.base.org or withdraw from an exchange directly to Base), plus ~$2 of USDC on Base for the token-detection steps (swap dust for it on any Base DEX).
  • An Alchemy/Infura Base key ready in case the default endpoint (mainnet.base.org) rate-limits scans (Settings → Advanced → RPC; note this now exercises the runtime permission prompt, step 9). Do not fall back to base-rpc.publicnode.com: it 403s eth_getTransactionReceipt, which is the reason it is no longer the default.
  • npm run release: QA the unzipped release artifact, not dist/, so the pass covers exactly what users get.
  • The bundled test bench (npm run demo:dev, or the hosted copy) has a Base mainnet mode: the network picker at the top of the page. Use it as the dApp and the stealth counterparty for the steps below. Its Base defaults are cent-scale on purpose (a 0.00003 ETH stealth send, a 0.01 USDC transfer to yourself, a 0.10 USDC approve at a burn-style spender), so the provider and token-prompt steps can be exercised for well under a dollar even before committing this runbook's full budget. Deployment, minting, the permit battery, and the adversarial payloads are Sepolia-only by design.

1 · It loads, and the hardened manifest holds (first-ever browser run)

The re-bundled IIFE content scripts and the stripped web_accessible_resources have never executed in a browser. This is the highest-risk step in the whole pass; nothing after it means anything if injection is broken. $0.

  • Unzip the release zip, load unpacked. Extension ID reads fcdigoihpflbghifhmpecihhfjkhabji (the pinned key works).
  • The install prompt/details show host access for only the three built-in RPC endpoints, no "read data on all websites".
  • Service-worker console clean; popup boots.
  • On a real page (any https site), the EIP-6963 provider announces at document_start: Kivuli appears in a wallet-picker page alongside MetaMask, and window.postMessage bridge traffic flows (connect works).
  • The loaded manifest.json (extension Details → view source, or the unzipped folder) has no web_accessible_resources.
  • From a normal web page's console, fetch('chrome-extension://fcdigoihpflbghifhmpecihhfjkhabji/assets/…') for a chunk named in the folder fails: no probeable fingerprint.

2 · Onboarding, verified backup, About

$0. New since the Sepolia runbook: the backup ceremony now challenges you.

  • Passcode → 12 words → the 3-word verification appears; a wrong pick regenerates fresh challenges; passing activates the wallet.
  • Close/reopen mid-backup resumes at the words, and finishing still works.
  • Chip layout survives the 360px popup with long words.
  • Settings → About shows the real version from the manifest (matches the zip name), not a placeholder.

3 · Base is the default, and says so honestly

$0 until noted.

  • Fresh wallet lands on Base (network surface in the popup says Base; no Sepolia anywhere without the developer opt-in).
  • Connect Kivuli to a dApp (or the bench in Base mode): eth_chainId0x2105, net_version8453.
  • If not repinned: unlock surfaces the deployments-registry failure as readable error text in the meta-address/scan surfaces, not a blank popup, not an eternal spinner. Plain per-site signing still works.
  • A dApp wallet_switchEthereumChain to Sepolia (or anything else) is refused with 4902: switching is user-only.
  • No fiat amounts appear anywhere on mainnet, and every fee shown is a real quote in ETH.
  • Force a failure (RPC blocked, or send more than a source can cover): the message on screen is a plain sentence. No "Version: viem@", no "Request Arguments", no multi-line dumps, and any endpoint URL appears as its host only.
  • Settings → Advanced → Developer options → "Show technical error details": with it ON, the send failure screen offers a collapsible "Technical details" block with copy to clipboard; with it OFF, the block renders nowhere.

4 · Receive

$0.

  • Receive shows a plain 0x… address + QR; "New address" rotates it; meta-address copy available below (once SDK-gated features work).
  • With the SW console open, minting or showing an address fires one targeted eth_getBalance right away (the address is watched from the moment it exists, not from the next 2-minute tick).
  • Tokens tab shows a USDC row the moment any account holds USDC, with no manual import: the registry's curated Base tokens are tracked by default. Zero balances stay hidden like any other token.

5 · Per-dApp wallet on a real Base dApp

Budget: $2 (gas dust to the per-site address).

  • Connect Kivuli on a real Base dApp (Uniswap on Base, Aerodrome, anything with a connect button). Approval names the origin; address is unique to the origin.
  • personal_sign / typed-data prompts raise and return.
  • Affordability banner. While the per-site wallet is still empty, submit any transaction from the dApp/bench: the approval prompt shows the danger banner "This wallet can't pay the network fee", names the wallet's balance and the approximate need in ETH (plain decimals, no exponents), and Approve stays disabled until the "Approve anyway" override is ticked. After funding (next step), the same submission shows no banner.
  • Send ~$2 of ETH from MetaMask to the per-site address, then submit a 0-value self-tx through the dApp/bench. The approval prompt's fee is a real quote (execution + L1 data fee), and the tx lands on basescan with an actual fee at or under the quote.
  • Receipt polling works on the default config. After the dApp/bench submits, its own eth_getTransactionReceipt polling resolves (the bench or dApp reports the confirmation itself, no endless "pending"), and the wallet's activity row flips to Confirmed, all without a custom RPC. This is what the mainnet.base.org default exists for; a 403 here means the endpoint regressed.
  • Switch networks in the popup while a dApp approval prompt is open: the approval must sign on the network the prompt showed, or fail closed, never sign silently on the new one.
  • Connected sites shows the wallet's address. Settings → Connected sites: the connected origin's row shows its per-site address in monospace, middle-truncated; tapping it copies the full address with an inline confirmation, and "View on Basescan" opens basescan.org/address/<that address> in a new tab (on Sepolia the same control reads "View on Etherscan" and targets sepolia.etherscan.io).

6 · Stealth payment lifecycle (repinned SDK only)

Budget: $12: two $5 sends recovered by sweeping, plus ~$2 of USDC that step 6b sends back out.

  • Live arrival, Receive screen open. Keep Receive open and send $5 from MetaMask to the shown address. Within ~10 seconds the screen shows the "Payment received" banner and rotates to a fresh address. No browser restart, no "Check now", no popup reopen. Home's quarantine count and balance update on their own while the popup stays open.
  • Arrival while closed. Close the popup, send a small second transfer to another receive address, wait out one scan interval, then just open the popup: the note is there (the alarm found it), or appears within a few seconds of opening (opening the wallet runs a check when the last one is stale). A browser restart must never be the thing that surfaces money.
  • USDC auto-detection. Send ~$2 of USDC to a fresh receive address with no token imported. The note appears in quarantine showing the USDC amount with the "Can't move yet" badge (its subtitle says the address needs ETH for the network fee, not that a future version is required), and Home's token list shows the USDC row. Removing the USDC row and re-adding it by address behaves as hide/unhide, per network.
  • Honest failure, no wedge. Point the RPC at a dead host (Settings → Advanced) and press "Check for money now": it fails with a readable error within ~20 seconds instead of hanging. Restore the endpoint and check again: it works without reloading the extension.
  • Send $5 via the meta-address path (transfer + announce: two transactions). Verify the announce on basescan against the announcer contract above, and that its gas used is ≤ 60,000 (the configured announceGas); record the actual number.
  • Sweep both ETH notes to a per-site wallet. Fees quoted before, actuals on basescan after: quote ≥ actual, and by a sane margin (not 3×). The USDC note stays visible as "Can't move yet" after the ETH sweeps (a sweep moves native only; the tokens leave in step 6b).
  • With the popup open and in use past the auto-lock timeout (set it low first), the wallet does not lock mid-session; closed and idle, it still locks on schedule.

6b · Token sends: USDC out under the gas rule

Budget: reuses step 6's ~$2 of USDC plus cents of ETH for fees. The rule under test: any source whose own ETH covers the quoted gas can send its tokens; a source with tokens but no ETH is refused with "add ETH" copy, and the fee is never taken in the token. Run the same four send checks twice: once here on Base mainnet, and once on the free network pass (Sepolia today, Base Sepolia once the repinned SDK lands there): Sepolia's curated USDC/USDT make the free twin a faucet-only exercise.

  • Gas rule, refused honestly. While the USDC note from step 6 holds no ETH, open Send → pick its USDC row. The quote shows the fee in ETH and the blocker "This wallet holds no ETH for the network fee. Add ETH to it first.", with no version-limit copy anywhere. The Review button never submits.
  • Plain-recipient USDC send. Fund the note's address with a little ETH (or pick a source that already holds both). Send half the USDC to a plain 0x address you control (MetaMask). Amount entry is in USDC at 6 decimals, the fee row stays in ETH, Review names "USDC" and the amount plainly, and the ERC-20 transfer lands on basescan from the source address. Activity shows the entry as USDC with the right amount.
  • Meta-address USDC send. Send the rest by meta-address (use the bench wallet as recipient). Two transactions: the token transfer to a fresh one-time address, then the announce against the canonical announcer. The recipient's scan finds the note showing USDC. Verify on basescan that the announce's stealth address equals the transfer's recipient: the two halves must agree.
  • MAX means the whole token balance. On any USDC source, tap MAX: the field shows the full balance to the last unit (gas is ETH-side, nothing subtracted), and the send leaves the source's USDC at exactly zero with the ETH reduced only by the fee.

7 · OP-stack fee correctness: the MAX send

Budget: consumed from step 6's swept funds. This is the check Sepolia structurally cannot do: on Base every transaction also pays an L1 data fee, and a MAX send that ignores it reverts or strands dust.

  • MAX-send the entire balance of one address. The quote visibly includes the L1 data fee; the transaction lands (no revert, no insufficient-funds), and the source is left with zero (or only the quoted-vs-actual margin).
  • The SW reached the GasPriceOracle over the narrow host permissions (no CORS/offline errors in the SW console during quoting).
  • Kill the RPC (airplane mode / block the host) and request a quote: the failure is an honest error, not a silently L1-fee-free (under)quote.
  • Stale-quote MAX: open the amount step, tap MAX, then wait two or more minutes on Review before pressing Send. The send still lands: submit re-reads balance and fees, pins them onto the transaction, and re-clamps the MAX amount downward; a pre-broadcast insufficient-funds rejection is retried once with refreshed numbers. The Sent screen echoes the amount that actually moved, never more than the reviewed figure.
  • Review shows the "Max send" note under the amount (the final amount is set at broadcast).
  • The Choose-what-to-send sheet shows the "N sources" aggregate line, and with more than about 20 rows a filter box appears; scrolling stays smooth however many sources exist (rows are windowed).

8 · Network switch Base ↔ Sepolia

$0 (Sepolia is free; Base side reuses existing funds).

  • Settings → Advanced → Developer options → show test networks; switch to Sepolia via the confirm dialog. With a page connected: chainChanged fires (0x21050xaa36a7), eth_chainId and net_version flip.
  • History/inbox show only the active network's entries; explorer links go to sepolia.etherscan.io vs basescan.org respectively, never crossed.
  • On Sepolia, ladder-derived notes from the earlier Sepolia QA pass re-appear after a scan (re-discovery through the receive ladder). Announced-only notes older than the ~20k-block scan window may not; that is a known limit, not a bug.
  • The RPC override field edits per network: set an override on Sepolia, switch to Base, confirm Base still uses its own endpoint.
  • The test-networks toggle refuses to turn off while Sepolia is active.
  • Connected dApp on the wrong network gets the switch prompt. While on Sepolia, open a previously connected Base site (basescan.org's connect flow, or the bench pointed at Base) and let it request Base (wallet_switchEthereumChain to 0x2105; sites that fall back to wallet_addEthereumChain for a built-in chain must land in the same prompt). The dedicated approval window opens naming the origin and showing Sepolia → Base. Approve: the wallet switches (header pill, inbox, history all follow), chainChanged fires on every connected page, and the site unstalls and carries on on Base. Reject (or close the window): the site gets a clean 4001 user-rejection, no switch, no hang. Asking again for the network the wallet is already on resolves silently with no prompt.
  • Testnet targets stay gated. With test networks OFF in Settings, a page requesting Sepolia (0xaa36a7) gets 4902 with copy pointing at Settings → Advanced → Developer options, and no prompt opens. Unknown chains (e.g. Polygon 0x89) get 4902 naming the supported networks.
  • The connect approval names the active network in the wallet card (e.g. "Base", or "Ethereum Sepolia" while on the testnet), so what the site will see is stated before consenting.
  • Switch back to Base: Base notes and history return intact.
  • Base Sepolia (84532) appears behind the same toggle. Switch to it: chainChanged reports 0x14a34, explorer links go to sepolia.basescan.org, and plain per-site wallet sends and dApp signing work over sepolia.base.org. Until the Base-Sepolia-aware SDK is repinned, unlocking there must surface the deployments-registry error as readable meta-address / scan error text (the Receive screen and scan status say what broke), never a crash, a blank popup, or a spinner.

9 · Custom RPC permission grant

$0. chrome.permissions.request requires a user gesture, and this flow has never run.

  • Enter an Alchemy/Infura Base URL in Settings → Advanced → RPC: the Chrome permission prompt appears on save.
  • Grant → scans/quotes work over the custom host (no CORS "offline").
  • Re-enter and decline → the save is blocked with honest copy and the previous endpoint stays in use.
  • http://localhost / 127.0.0.1 URLs skip the prompt.

10 · Side panel

$0. The same wallet in Chrome's side panel (chrome.sidePanel, within the manifest's 116 floor). This surface has never run in a browser either.

  • Menu → Open in side panel: the panel opens with the full wallet and the popup closes. The panel fills its height and stays usable resized down to ~360px.

  • Settings → Advanced → Open in side panel from the toolbar: ON → the toolbar icon opens the panel instead of the popup; OFF → the popup is back. Check both directions.

  • With the toggle ON, let the service worker die (chrome://extensions → Kivuli; the "service worker" link goes inactive after ~30 s idle), then click the toolbar icon: still the panel. The mode is runtime state the background re-applies at SW startup; Chrome does not keep it.

  • With the toggle ON, trigger a dApp approval on a connected site: it still opens as its own window and resolves normally: approvals ride dedicated windows, never the action popup.

  • Panel + expanded tab + (toggle OFF) popup open at once: one wallet, three views: changes in one show in the others.

  • The release artifact still verifies: src/ui/sidepanel.html in the zip, no web_accessible_resources, extension ID unchanged.

  • Bottom sheets anchor to the panel's bottom edge: open the send flow's Choose-what-to-send sheet and an inbox note sheet in the side panel. The sheet sits on the true bottom of the panel with the scrim covering the full height, no dead space below; repeat once in the popup and the expanded tab, and resize the panel while a sheet is open.

11 · The stuck announcement

Budget: $2 (one small stealth send; funds recovered).

  • Start a stealth send and kill connectivity between the transfer landing and the announce (disconnect at the right moment, or point the RPC at a dead host after the transfer confirms).
  • Activity shows the Delivering badge; the detail sheet explains the money has left and must not be re-sent.
  • Restore connectivity → Retry now clears it with a successful announce, without a second transfer. Confirm on basescan: one transfer, one announce.

12 · Recovery: last, with real money on the line

$0 new money. This erases the wallet; that is the point.

  • Reveal the phrase; erase; restore from the 12 words.
  • The same meta-address returns; ladder addresses re-derive; the Base funds from steps 5–7 re-appear after the first full scan, the USDC note included (curated-token detection needs no import after a restore).
  • The restored wallet's scan cache and origin map are fresh (encrypted stores fenced to the new identity: no stale connections, no ghost notes from before the erase).

Results

# Step Result Notes ($ spent)
1 Loads + hardened manifest
2 Onboarding + verified backup
3 Base default + honest failure
4 Receive
5 Per-dApp wallet on real dApp
6 Stealth lifecycle
6b USDC sends (gas rule)
7 MAX send / L1 fee
8 Network switch
9 RPC permission grant
10 Side panel
11 Stuck announcement
12 Recovery

Known gaps (do not file)

  • The pinned @opaquecash SDK is Base-aware, but the mainnet stealth flows have not passed this runbook yet; until they do, treat them as unverified rather than broken (and re-run the Gate check after any repin).
  • Base Sepolia (84532) ships in the network table, but the pinned @opaquecash/deployments has no 84532 row yet: stealth flows there surface the registry error (readable, via metaAddressError / scan errors) until the next SDK release is repinned. Plain sends, dApp signing, and token transfers on 84532 are unaffected.
  • Token sends require the source to hold ETH for the fee (the gas rule); there is no token-denominated fee and no gasless rail yet. Sweeps remain native-only: a token-only note is sendable only after its address holds ETH.
  • The Receive live check and curated-token detection cover the active EVM network's registry tokens; arbitrary imported tokens are read for account balances but do not create inbox notes on their own.
  • Scan window is ~20k blocks for announced-only notes; ladder notes are always re-discoverable.
  • No Solana, no ENS, no custom networks.
  • Sepolia's endpoint keeps an install-time host permission on purpose: the free QA path must work without a grant step.