Skip to content

Implement privacy cost, mobile prover, bootnode, and WalletKit issues - #941

Merged
Miracle656 merged 10 commits into
Miracle656:mainfrom
Stanley471:fix/batch-701-719-720-725
Oct 2, 2026
Merged

Miracle656 merged 10 commits into
Miracle656:mainfrom
Stanley471:fix/batch-701-719-720-725

Conversation

@Stanley471

@Stanley471 Stanley471 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #725
Closes #720
Closes #719
Closes #701

@vercel

vercel Bot commented Sep 30, 2026

Copy link
Copy Markdown

@Stanley471 is attempting to deploy a commit to the miracle656's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of real work here — a whole services/spp-bootnode/, a prover WebView, an ADR comparing WebView against native. I'm asking for a split before review rather than after, and I want to be straight about why, because unlike some bundled PRs this one does declare all four issues, so you are not under-credited.

The problem is coupling, not credit.

This closes #701 (200), #719 (200), #720 (200) and #725 (100) — 700 points across four unrelated areas: a WalletConnect library migration, a history/indexing service, a mobile prover spike, and fee measurement. As one PR:

  • Any one of the four being wrong blocks the other three. If the bootnode needs another round but the prover spike is perfect, the spike waits too. Split, the three land this week.
  • It is CONFLICTING against main, and resolving 46 files spanning four domains in one pass is where mistakes get made. Four smaller rebases are each almost trivial.
  • Honestly: I cannot review this well as a unit. Judging a WalletConnect migration and a zero-knowledge prover spike in one sitting means one of them gets the attention it deserves and the other gets a skim. You deserve better than a skim on work this size.

Suggested order, easiest first:

  1. #720 (prover spike) — self-contained in frontend/mobile/lib/prover/ plus the ADR. The ADR is the deliverable; it'll merge fast.
  2. #725 (fee sponsorship and measurement) — smallest.
  3. #719 (history past the 7-day RPC window) — this is services/spp-bootnode/ and deserves review on its own. A new service is a new operational commitment: who runs it, what happens when it is down, what the wallet does without it.
  4. #701 (WalletConnect → @reown/walletkit) — leave this for last, and please read the note below first.

Two things specific to #701 before you rebase it:

  • main's frontend/mobile lockfile was repaired a few hours ago. @walletconnect/keyvaluestorage declares a peer on @react-native-async-storage/async-storage@1.x and is vendored three times, so three nested 1.24.0 installs have to be recorded or npm ci refuses outright. A migration off @walletconnect/* will change that graph. Regenerate with plain npm install in frontend/mobile — never --legacy-peer-deps, which corrupts this lockfile — and check npm ci succeeds from clean before pushing. Your current diff touches both lockfiles and was written against the broken state.
  • #701's acceptance criterion is a testnet round-trip, and the PR body doesn't mention one. If you've done it, add the transaction hash; if not, that's the remaining work. Nothing may reimplement the signing ceremony — frontend/mobile/lib/walletConnect.ts signXdrPayload() already does the host function, low-S normalisation, expiration ledger, footprint via re-simulation, sequence and the 5-element signature vector, and all six are needed together. A migration must call into it, not around it.

One correction worth making now, whichever PR carries it: there is no EURC pool. Both upstream SPP testnet pools are native XLM. We had that wrong in our own docs and fixed it, so if anything here assumes otherwise it inherited our error, not yours.

Open the first split PR whenever you like and I'll review it the same day. Nothing here is a criticism of the work — it's about being able to land it.

@Miracle656

Copy link
Copy Markdown
Owner

Reviewing this to land, not to bounce. Taking the whole thing — all four Closes lines stay, so the full 700 points land together. The split request is withdrawn; it was about my reading capacity and that's my problem to solve, not yours.

Progress so far, posting as I go rather than at the end.

Six conflicts resolved. Five in favour of main, one differently — and that one is worth explaining.

frontend/wallet/lib/privacy/client.ts came through as an add/add, and it turned out not to be a real collision of intent at all: main's file (from #774) is the SPP client wrapper — sync / privateBalance / shield / privateSend / unshield. Yours is a sponsored fee-bump submit helper plus opt-in cost telemetry, which is #725. Two unrelated things that happened to pick the same filename. So rather than discard either, I kept main's client.ts byte-for-byte and moved your #725 helper to frontend/wallet/lib/privacy/sponsoredSubmit.ts. Nothing imported it yet, so no call sites changed, and #725's substance lands instead of being thrown away as a conflict casualty.

The other five:

  • mobile-e2e.yml — kept main's setup-android@v4.0.4. You and main independently fixed the same Failed to find package 'tools' break; yours pins v3.2.2 and passes packages: platform-tools, main's upgrades to v4, which defaults to platform-tools and needs no input. Same outcome, but v4 doesn't hold the action back. Your diagnosis was right either way.
  • mobile/app/swap.tsx — main's registry-driven tokensFor. feat: USDY/USDC buy & sell with honest spread + price-impact disclosure #877 rewrote this after you branched; the TOKENS array your side restores would reintroduce a per-module issuer table that feat: USDY/USDC buy & sell with honest spread + price-impact disclosure #877 deleted for carrying a bad-checksum mainnet USDY address.
  • wallet/app/layout.tsx — kept main's formatting and the BootnodeBanner import. security: tighten frontend CSP headers #759's CSP nonce threading ((await headers()).get("x-nonce"), nonce={nonce}) was outside the conflict hunk and survived; I checked rather than assumed, because losing it blocks the pre-paint theme script and the wallet renders unstyled.
  • mobile/package.json — kept react-native-webview@~13.15.0 over ^14.0.1. 13.15 is what Expo SDK 54 pins; the prover WebView only uses injectedJavaScript / onMessage, stable across both.
  • mobile/package-lock.json — reset to main's repaired version, then plain npm install. All three nested @react-native-async-storage/async-storage@1.24.0 entries are intact, now under @reown/walletkit, @walletconnect/core and @walletconnect/utils. @walletconnect/web3wallet is gone from the tree entirely.

One thing fell out of that lockfile regeneration that's worth knowing. Your core: new Core({ projectId }) as any in mobile/lib/walletConnect.ts — I removed the cast and the typecheck is still clean. The incompatibility it was suppressing was an artifact of the stale lockfile, not a real mismatch between @walletconnect/core and WalletKit. Worth remembering as a pattern: when a cast is needed on one platform and not the other, the asymmetry is usually the dependency graph talking.

On #701 specifically: the migration is exactly right. It's a type-level swap — IWeb3Wallet → IWalletKit, Web3Wallet.init → WalletKit.init, and nothing else. signXdrPayload() is untouched, so the host function, low-S normalisation, expiration ledger, footprint re-simulation, sequence and the 5-element signature vector all stay exactly where they were. That was the thing I was most worried about and you got it right by not touching it, which is the correct instinct.

Mobile typecheck is clean and the EURC removal is correct — both upstream SPP testnet pools really are native XLM.

Still running: mobile/wallet/sdk test suites, npm ci from clean, and the web build. Two findings on the bootnode I'm fixing myself — detail in the next comment. Nothing here needs anything from you.

@Miracle656

Copy link
Copy Markdown
Owner

Four bugs found and fixed on my side. Two are the kind that only show up when you go looking, so here's the reasoning rather than just the diff.

1. The default SPP pool address is the SDF test anchor's SEP-45 web-auth contract.

CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX appears as the pool id in six places — indexer.ts (the DEFAULT_POOL_ADDRESSES const and the comment above it), .env.example, the README twice, prover-spike.tsx, and ProverWebView.tsx. It is a valid contract id, it passes StrKey.isValidContract, and it is real. It is just not an SPP pool: it is the SEP-45 web-auth contract from docs/SEP45_SPIKE.md and docs/NGN_RAILS.md. I'd bet it came from grepping our own repo for a plausible C…, which is a completely reasonable thing to do and is why I'm not treating it as carelessness — we have a history of this exact failure caused by our own docs.

What makes it nasty is the failure mode. A bootnode pointed at the wrong contract doesn't error. getEvents succeeds, returns nothing, the indexer writes zero rows, and /events serves an empty history — which is indistinguishable from "your notes are gone." That is precisely the symptom #719 exists to eliminate, so the bug would have presented as the feature not working at all, with no diagnostic pointing at the cause.

Corrected all six to CBEDPYMAEPQ6JR7WKWXRM6CFHHJLKA5RHPRRLSD4UZXZRGNMBXOT2GOT, the block-list-policy XLM pool already recorded in frontend/*/lib/privacy/config.ts from upstream's deployments/testnet.

I also added a startup guard in indexerConfigFromEnv() that throws on any POOL_ADDRESSES entry failing StrKey.isValidContract. It's an honest half-measure and the comment says so — a valid id for a non-pool contract still passes, because only a liveness probe could catch that. It converts the malformed case from silent to loud; the wrong-but-valid case stays a configuration discipline problem.

2. BigInt('100_000_000') throws at module load.

In prover-spike.tsx's BENCH_TX. BigInt() parses a string with the numeric literal grammar, which does not include separators — so this is SyntaxError: Cannot convert 100_000_000 to a BigInt, not 100000000n. It's at module scope, so importing the route throws and the Prover Spike tab cannot open at all. The test suite never caught it because nothing imports that screen. Changed to the literal 100_000_000n, where separators are legal.

3. The bootnode fallback was wired to the banner but not to the client.

This is the subtle one, and it's a design-seam bug rather than a mistake. config.ts sets bootnodeUrl to Veil's URL unconditionally when the env var is set — no probe. bootnode.ts's resolveBootnodeWithFallback is excellent and does exactly what SPP_BOOTNODE.md describes, but its only caller was BootnodeBanner.

So with NEXT_PUBLIC_SPP_BOOTNODE_URL set and our bootnode down: the banner probes, fails, and announces "Using Nethermind's public bootnode." Meanwhile client.ts is still constructing the SPP client against the dead URL. The UI states a fallback that did not happen, which is worse than no banner — a user reading it would conclude note discovery is fine when it is broken, and your own acceptance checklist item ("App falls back... and says so") would read as satisfied while only the "says so" half was true.

Fixed by resolving through the same cached probe in initClient(), so the banner now reports an outcome the client actually took. To make that work cleanly I added getConfiguredBootnodeUrl() to both config.ts files, returning null when unset — otherwise the primary defaults to Nethermind's URL and we'd health-check Nethermind in order to "fall back" to Nethermind, warning spuriously every time. Both banners now use that helper instead of reading process.env directly.

4. The banner re-probed every 5 seconds. Looks like a leftover debug value; the result is cached for 5 minutes so 59 of every 60 calls were no-ops, but it was a permanent timer on every page since the banner sits in the root layout. Raised to 60 s — one probe per cache window is the most that can ever do anything.

One doc correction. The #725 line in PRIVACY_COST.md said opted-in diagnostics "can also report" fee and CPU instructions, present tense. Nothing imports the helper — the SPP SDK submits shield/send itself — so no samples are being collected. I rewrote it to say what's true: the helper exists, here is what it would record and what it deliberately omits, and wiring it into the flow is follow-on work. The privacy properties you described are accurate and I kept them; it was only the "this is happening now" implication that needed softening.


On the parts I'm not changing, because they're right.

The #720 ADR is the best thing in this PR, and I want to be specific about why, because it would have been easy to do the opposite. You had a stub returning setTimeout values and you labelled it — in the status field, in a blockquote above the results table, and again in the methodology note — as synthetic placeholders that must not be used as benchmark evidence. You put the inferred numbers in a separate table named "Pre-analysis estimates (not device measurements)" and marked the recommendation preliminary pending a real device run. Plenty of spikes launder estimates into a results table and call the decision made. This one is auditable by someone who wasn't in the room, which is the whole point of an ADR. The reasoning holds too: proof frequency is low, the 12 MB circuit download dominates either way, and the ProverWebViewHandle seam makes the native swap cheap later. "Rejected: run the prover server-side — the note commitment leaves the device, defeats the privacy model" is the correct call and you gave the correct reason.

#719's operational answers are all present, which is what I asked for and rarely get: Fly.io shared-cpu-1x at ~$2–3/month with a named reason (avoiding the Render instance-hour contention from the August outage, cited to MONITORING.md §1), measured storage of ~3.88 MB per 10k events, degraded-not-blocked behaviour when it's down, and the service is still undeployed so nothing regresses by merging. Noting for the record that the Nethermind fallback is testnet-only — fine, since SPP is testnet-only and mainnet stays locked out.

#701 I covered above: untouched signXdrPayload(), type-level swap only. Correct.

One genuinely open item, which I am not holding the merge for: #701's acceptance criterion is a testnet round-trip, and there is no transaction hash in this PR. I looked and I'm not going to invent one. The migration is small and provably doesn't touch the signing path, so the risk of landing it unverified is low — but "WalletKit completes a real dApp session end-to-end" is still unproven, and I'd rather record that honestly than quietly mark it done. If you run one, post the hash here and I'll note it against #701. If something blocks you, say what and I'll run it myself.

@Miracle656
Miracle656 merged commit c1bfba0 into Miracle656:main Oct 2, 2026
1 of 4 checks passed
@Miracle656

Copy link
Copy Markdown
Owner

Merged. All four issues closed by the merge: #701, #719, #720, #725 — 700 points. Landed as ea63384a on main; your commits are ancestors, so GitHub marked this MERGED rather than closed. Three prior attempts bounced; this one is in.

main moved three times under me while I worked (#908 bulk payout, #756 web private send, #898 danger zone, #890 SPP drift detection), so this went in as a local 3-way merge with three follow-up merges of main. Two of those produced new conflicts, both resolved toward main:

Confirmed surviving the resolution, since both were explicit risks: #774's typed privacy/client.ts (privateSend typed, not any) and #759's CSP nonce threading in app/layout.tsx ((await headers()).get("x-nonce"), nonce={nonce}). Your BootnodeBanner import sits alongside them.

Verified on the final tree, all counts up from baseline, no drops:

suites tests
mobile 90 1061 typecheck clean; npm ci from clean ✅
wallet 51 (+1 skipped) 639 typecheck clean; next build --webpack ✅
sdk 28 344 typecheck clean
spp-bootnode 2 8 typecheck clean

Your three nested @react-native-async-storage/async-storage@1.24.0 entries are intact under @reown/walletkit, @walletconnect/core and @walletconnect/utils; @walletconnect/web3wallet is gone from the tree. GitGuardian passed. The three Vercel Authorization required to deploy failures are the usual fork false red.

One nice confirmation: #890's SPP drift detector landed on main mid-review and its tests pass against the corrected pool id. If the wrong CD3LA6RK… had gone in, that detector would have been checking the SEP-45 auth contract for pool drift.


Two things for you, neither blocking.

#701's testnet round-trip is still unproven. No transaction hash, and I didn't invent one. The migration is provably safe — type-level swap, signXdrPayload() untouched — so merging it unverified was the right call, but "WalletKit completes a real dApp session end-to-end" remains unconfirmed. Post the hash here when you run one and I'll record it against #701. If something blocks you, say what and I'll run it.

The bootnode is undeployed. Nothing regresses, because the fallback means the wallet uses Nethermind's archive, which is also what it used before. But the >7-day history that #719 is actually about only exists once something is running at NEXT_PUBLIC_SPP_BOOTNODE_URL. Your SPP_BOOTNODE.md deployment checklist is good and the Dockerfile now has a lockfile to npm ci from, so it should be a clean Fly.io deploy when you or I get to it.


On the review process, since this took four attempts.

I asked you to split this twice and you didn't, and you were right not to. The split would have cost you two more weeks and the four parts turned out to be genuinely independent — resolving all six conflicts took one pass, not four. My reason for asking was that I didn't think I could review 56 files across four domains well. That was a real limitation but it was mine to solve, and "I find this hard to review" is not a defect in your work. What actually found the bugs was reading the code, which I could have done the first time.

Worth saying what you did right, because it's most of this. You acted on every substantive note without being asked twice: the EURC removal is correct and you went further than the feedback did by removing it from the app entirely rather than patching the pool id. You didn't touch signXdrPayload() — the single highest-risk thing in the repo — and that restraint is why #701 was safe to land on sight. And the ADR labels its own placeholder numbers as unusable three separate times, which is the opposite of what most spikes do.

The four bugs I fixed are the kind that survive review and tests: a valid-but-wrong contract id, a BigInt() string that only throws at runtime on a screen no test imports, a guard whose comment and implementation disagreed, and a fallback wired to the display instead of the behaviour. None is a sign of carelessness; all four are what happens at this volume. The pattern worth carrying forward is the one in the pool id — when you need a C…, derive it or copy it from lib/privacy/config.ts, never from a grep, because every wrong one still passes isValidContract.

Full detail of every change is in the merge commit message on ea63384a. Thanks for sticking with it through three closes.

Miracle656 added a commit that referenced this pull request Oct 2, 2026
Correcting my own slip while landing #941. The PR added
`npm ci || npm install` to the mobile job; my revert replaced the *first*
occurrence in the file instead, which was the wallet job's — a pre-existing
pattern on main that #941 never touched and that I had not verified.

So:
  * wallet job  — restored to main's pre-existing fallback. Out of scope for
    #941 and I never ran `npm ci` from clean in frontend/wallet, so flipping it
    to strict was an unverified change to unrelated CI.
  * mobile job  — now plain `npm ci`. This one is in scope: the fallback was a
    workaround for the desynced lockfile on #941's stale base, that lockfile is
    repaired, and `npm ci` from a wiped node_modules was verified locally before
    the merge.

The fallback turns a desynced lockfile into a warning rather than a failure,
which is the failure class this repo keeps paying for. Four of these remain on
main (sdk, playwright, agent, lighthouse); leaving them alone here, since
tightening them belongs in its own change with each one's `npm ci` actually
verified first. The dedicated "Lockfiles — prove npm ci works for every
workspace (#823)" job already uses strict `npm ci`, so integrity is still
enforced repo-wide.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
@Miracle656

Copy link
Copy Markdown
Owner

One correction to my own work, since I got a detail wrong and would rather say so than leave it.

In the findings comment I said I'd reverted npm ci || npm install "in ci.yml (both wallet and mobile jobs)". That was wrong in a way that mattered. The fallback is a pre-existing pattern across main — it's on five jobs (sdk, playwright, agent, mobile, lighthouse) — and your PR added a sixth on the mobile job. My edit replaced the first occurrence in the file, which was the wallet job's: pre-existing, untouched by #941, and not something I had verified. The one you added on mobile stayed in.

Fixed in 8115c274:

  • wallet job — restored to main's pre-existing fallback. I never ran npm ci from clean in frontend/wallet, so flipping that to strict was an unverified change to CI unrelated to this PR.
  • mobile job — now plain npm ci. That one is in scope: the fallback was a workaround for the desynced lockfile on your stale base, the lockfile is repaired, and I did verify npm ci from a wiped node_modules in frontend/mobile before merging.

The four remaining fallbacks on main I've left alone. Tightening them belongs in its own change with each one's npm ci actually proven first, rather than as a side effect of landing your PR — which is exactly the mistake I just made in miniature. The dedicated "Lockfiles — prove npm ci works for every workspace (#823)" job already runs strict npm ci, so lockfile integrity is enforced repo-wide regardless.

Nothing about the merge changes; ea63384a stands and all four issues remain closed. This was mine, not yours.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants