Skip to content

feat(tangle-cloud): shielded payments sidebar — pool, credits, spend auth#3147

Merged
drewstone merged 6 commits into
developfrom
feat/private-cloud-payments
Mar 21, 2026
Merged

feat(tangle-cloud): shielded payments sidebar — pool, credits, spend auth#3147
drewstone merged 6 commits into
developfrom
feat/private-cloud-payments

Conversation

@drewstone
Copy link
Copy Markdown
Contributor

@drewstone drewstone commented Mar 20, 2026

Summary

  • Adds Payments as a first-class sidebar section in tangle-cloud (cloud.tangle.tools) with Shielded Pool and Credits sub-items
  • Implements anonymous payment flows using tnt-core ShieldedGateway + ShieldedCredits contracts (Vitalik "ZK API Usage Credits" pattern)
  • Real EIP-712 spend authorization signing via viem — no mocks
  • Deposit/Withdraw/Fund flows are gated until @tangle-network/shielded-sdk is linked for ZK proof generation

Pages

  • /payments/pool — Deposit/Withdraw from VAnchor shielded pool
  • /payments/credits — Fund credit accounts, sign spend authorizations, view on-chain balances

Security

  • Private keys encrypted at rest (PBKDF2 + AES-256-GCM with per-user random salt)
  • Single wallet signature with domain-separated key derivation (HKDF pattern)
  • Session-locked keypairs requiring explicit unlock
  • No private key material rendered in DOM
  • Input validation on all spend authorization fields (uint64, uint8, expiry)
  • Per-address IndexedDB scoping (no cross-wallet note leakage)
  • No dangling token approvals without atomic deposits

SDK Integration (next PR)

When @tangle-network/shielded-sdk is linked with ethers v6:

  1. Create wagmi-to-ethers adapter
  2. Wire ShieldedGatewayClient.deposit() / buildShieldedWithdrawal() / fundCredits()
  3. Host circuit artifacts (WASM + zkey) on CDN

Test plan

  • yarn nx typecheck tangle-cloud passes
  • yarn nx lint tangle-cloud passes (0 errors, 0 warnings)
  • yarn nx build tangle-cloud succeeds
  • Sidebar shows Payments with Pool/Credits sub-items
  • /payments/pool renders deposit/withdraw tabs
  • /payments/credits renders accounts/fund/authorize tabs
  • Spend authorization signing produces valid EIP-712 structured data

@drewstone drewstone requested a review from AtelyPham as a code owner March 20, 2026 21:27
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 20, 2026

Deploy Preview for tangle-cloud ready!

Name Link
🔨 Latest commit b2ca6bb
🔍 Latest deploy log https://app.netlify.com/projects/tangle-cloud/deploys/69bddc9e5ecbc2000881ee23
😎 Deploy Preview https://deploy-preview-3147--tangle-cloud.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 20, 2026

Deploy Preview for tangle-leaderboard ready!

Name Link
🔨 Latest commit b2ca6bb
🔍 Latest deploy log https://app.netlify.com/projects/tangle-leaderboard/deploys/69bddc9e5ecbc2000881ee27
😎 Deploy Preview https://deploy-preview-3147--tangle-leaderboard.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 20, 2026

Deploy Preview for tangle-dapp ready!

Name Link
🔨 Latest commit b2ca6bb
🔍 Latest deploy log https://app.netlify.com/projects/tangle-dapp/deploys/69bddc9e5dd61f0009d53c9f
😎 Deploy Preview https://deploy-preview-3147--tangle-dapp.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@claude
Copy link
Copy Markdown

claude Bot commented Mar 20, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@drewstone drewstone force-pushed the feat/private-cloud-payments branch from 7d29a42 to 757cd55 Compare March 20, 2026 21:48
Integrate anonymous payment flows into tangle-cloud (app.tangle.tools)
as a new "Payments" sidebar section with Shielded Pool and Credits sub-items.

Architecture:
- ShieldedProvider: keypair derivation (wallet sig → HKDF domain separation),
  per-address IndexedDB note storage, encrypted key-at-rest via AES-256-GCM
  with per-user random PBKDF2 salt
- CreditsProvider: ephemeral credit key generation (crypto.getRandomValues
  salt), IndexedDB persistence, credit account lifecycle
- Real EIP-712 spend authorization signing via viem (concat-based digest,
  account.sign for raw signing, input validation for uint64/uint8/expiry)

Pages:
- /payments/pool — Deposit/Withdraw from VAnchor shielded pool (gated
  until @tangle-network/shielded-sdk is linked for ZK proof generation)
- /payments/credits — Fund credit accounts, sign spend authorizations,
  view on-chain credit balances

Security:
- Private keys encrypted at rest (PBKDF2 + AES-256-GCM)
- Per-user random salt stored alongside ciphertext
- Single wallet signature with domain-separated derivation
- No private key material rendered in DOM
- Session-locked keypairs requiring explicit unlock
- Input validation on all spend authorization fields
- No dangling token approvals without atomic deposits

Consumes contracts from tnt-core: ShieldedGateway, ShieldedCredits,
VAnchor (Vitalik's "ZK API Usage Credits" pattern).
…ixes

Design system:
- All payment containers use Typography, Card, Alert, Button, Input,
  Chip, SkeletonLoader from @tangle-network/ui-components
- Replaced raw HTML (h1, p, button, input) with design system components
- Added ARIA roles (role=tab, aria-selected, role=tabpanel) to payment tabs
- RequireWallet component with ConnectWalletButton for consistent empty states
- PaymentsLayout now includes Header (network selector, wallet connect, tx history)

Architecture:
- SharedPageLayout component replaces 7 duplicate layout files
- Route-level code splitting via React.lazy + Suspense with skeleton fallback
- TANGLE_CLOUD_NETWORKS extracted to shared constant (was defined twice)

Cloud-wide fixes:
- Removed 'use client' directives (Vite SPA, not Next.js)
- Fixed react-router-dom → react-router imports (3 files)
- Fixed hardcoded '/instances' → PagePath.INSTANCES
- Consistent padding across all page layouts (md:px-8 lg:px-10)
P1 fixes:
- EIP-712 prefix uses explicit Uint8Array([0x19, 0x01]) instead of
  toBytes('0x1901') for unambiguous encoding
- addNote/removeNote/importNotes use sequential write queue (promise
  chain) to prevent concurrent persist() from losing notes
- btoa uses chunked Array.from() to avoid spread operator argument limit

P2 fixes:
- AmountInput uses string-based truncation instead of Number().toFixed()
  to preserve precision for large balances
- NoteCard and CreditAccountCard use shortenHex from ui-components
  instead of duplicated truncateHex
- Removed dead onWithdraw prop from CreditAccountCard (no consumer)
- Deleted unused sdkBridge.ts (single 5-line function, no importers)
@drewstone drewstone force-pushed the feat/private-cloud-payments branch from 46fc999 to cc5eeb0 Compare March 20, 2026 22:14
Credit account spending private keys are now encrypted in IndexedDB
using AES-256-GCM, keyed by a domain-separated hash of the shielded
keypair. Keys are decrypted in-memory when the user unlocks their
shielded account.
Tests now use async waitFor() to handle React.lazy + Suspense. Added
test cases for /payments/pool and /payments/credits routes with their
layout and page mocks. All 51 tests pass.
@drewstone
Copy link
Copy Markdown
Contributor Author

drewstone commented Mar 20, 2026

❌ PR Review #6 w/ codex, claude

Don't merge. High: Queued note persists still overwrite each other on back-to-back writes in apps/tangle-cloud/src/app/ShieldedProvider.tsx:100. High: Write queue reads stale notesRef — back-to-back addNote drops notes in apps/tangle-cloud/src/app/ShieldedProvider.tsx:100. High: Credit spending keys permanently lost when generated without shielded keypair in apps/tangle-cloud/src/utils/payments/indexedDbCreditStorage.ts:33. Plus 23 more findings.

Recommendation Needs Work (0/100)
Findings 26 total — 🔴 5 high, 🟠 17 medium, 🟡 4 low
Ensemble 2 reviewers × 4 tracks
Files reviewed 49 files changed
Validator claude
Since last review ✅ 22 fixed, 🆕 26 new
Provenance individual reviewer outputs

✅ Fixed since last review

  • 🔴 Credit accounts are browser-global, so switching wallets exposes and can delete another wallet's records (apps/tangle-cloud/src/utils/payments/indexedDbCreditStorage.ts)
  • 🔴 Pending derive or unlock calls can attach the previous wallet's keypair to the new wallet session (apps/tangle-cloud/src/hooks/payments/useKeypair.ts)
  • 🔴 The note write queue can persist old-wallet notes into the new wallet namespace after an account switch (apps/tangle-cloud/src/app/ShieldedProvider.tsx)
  • 🔴 Credit spending keys stored in plaintext when generated during keypair derivation (apps/tangle-cloud/src/app/CreditsProvider.tsx)
  • 🔴 Credit accounts not scoped by wallet address — cross-wallet metadata leak (apps/tangle-cloud/src/utils/payments/indexedDbCreditStorage.ts)
  • 🔴 Write queue race on wallet switch can persist notes to wrong wallet's storage (apps/tangle-cloud/src/app/ShieldedProvider.tsx)
  • 🟠 Decryption fallback silently returns ciphertext blob as spending private key (apps/tangle-cloud/src/utils/payments/indexedDbCreditStorage.ts)
  • 🟠 Notes and isReady not reset synchronously on wallet switch — stale data window (apps/tangle-cloud/src/app/ShieldedProvider.tsx)
  • 🔴 Spend authorizations are signed in fixed 18-decimal units (apps/tangle-cloud/src/containers/payments/SpendAuthContainer.tsx)
  • 🟠 Failed account reads still produce signatures with nonce 0 (apps/tangle-cloud/src/containers/payments/SpendAuthContainer.tsx)
  • 🟠 Malformed payment contract env vars are treated as live addresses (apps/tangle-cloud/src/data/payments/useDomainSeparator.ts)
  • 🟡 Credit balance parsing masks contract incompatibilities by zero-filling fields (apps/tangle-cloud/src/containers/payments/CreditBalanceContainer.tsx)
  • 🟠 SpendAuthContainer hardcodes 18 decimals for amount parsing, ignoring actual token decimals (apps/tangle-cloud/src/containers/payments/SpendAuthContainer.tsx)
  • 🟠 Nonce silently defaults to 0n when contract read fails, producing invalid but plausible-looking signatures (apps/tangle-cloud/src/containers/payments/SpendAuthContainer.tsx)
  • 🟡 CreditBalanceContainer discards wagmi's typed ABI return via manual Record<string, unknown> coercion (apps/tangle-cloud/src/containers/payments/CreditBalanceContainer.tsx)
  • 🟡 Empty env addresses are cast to Address without runtime validation, relying solely on wagmi's enabled flag (apps/tangle-cloud/src/data/payments/useCreditAccountState.ts)
  • 🟠 Fund Credits shows a completed success path even though no credits are actually funded (apps/tangle-cloud/src/containers/payments/FundCreditsContainer.tsx)
  • 🟡 Top-level suspense drops the route layout and header while lazy pages load (apps/tangle-cloud/src/app/app.tsx)
  • 🟡 PageLayout consolidation changes padding on earnings, instances, and rewards layouts (apps/tangle-cloud/src/components/PageLayout.tsx)
  • 🟠 ShieldedProvider and CreditsProvider run IndexedDB + useAccount hooks on every page load, not just payments (apps/tangle-cloud/src/app/providers.tsx)
  • 🟡 No /payments root redirect — navigating to /payments hits the catch-all 404 (apps/tangle-cloud/src/app/app.tsx)
  • 🟡 Test suite does not cover the /payments 404 gap or a /payments redirect (apps/tangle-cloud/src/app/app.spec.tsx)

🔴 HIGH (5)

  • Queued note persists still overwrite each other on back-to-back writes apps/tangle-cloud/src/app/ShieldedProvider.tsx:100

    persist serializes writes, but each queued callback recomputes from notesRef.current, and that ref is only refreshed on React render. If two note mutations happen before the first setNotes(updated) re-renders, the second callback runs against the pre-first snapshot and writes out a truncated list. Because the IndexedDB save happens before the next render too, this is not just a UI glitch: the earlier note can be dropped from local storage entirely.

  • Write queue reads stale notesRef — back-to-back addNote drops notes apps/tangle-cloud/src/app/ShieldedProvider.tsx:100

    The persist() function chains writes through writeQueueRef.current and reads notesRef.current inside each chained .then(). But notesRef.current is only updated on React re-render (line 64: notesRef.current = notes). When two writes are queued synchronously (or before React re-renders), the second write reads the pre-first-write state and overwrites it.

Scenario: addNote(A) then addNote(B) called rapidly.

  1. Write 1: reads notesRef.current=[], computes [A], saves [A] to IDB, calls setNotes([A]…
  • Credit spending keys permanently lost when generated without shielded keypair apps/tangle-cloud/src/utils/payments/indexedDbCreditStorage.ts:33

    CreditsProvider.generateAndStoreCreditKeys can be called when keypair is null (encryptionKey is undefined). In saveCreditKeys, when encryptionKey is falsy, encryptedPrivateKey is stored as empty string — the private key is not persisted at all. The function returns the keys with the real private key in memory, so the current session works. But on page reload, loadCreditKeysForAddress returns spendingPrivateKey='' for that account. The commitment was potentially submitted on-chain, but the spendi…

  • refetchAccount() can still sign with a stale cached nonce after a failed refresh apps/tangle-cloud/src/containers/payments/SpendAuthContainer.tsx:101

    The code comments say this path should 'fail closed if read fails', but it only checks whether refetchAccount() returned some data. In TanStack Query, data is the last successful result and a refetch can fail while cached data remains available. That means a transient RPC/read failure after an earlier successful fetch will still pass this guard and reuse the old nonce. The user can then share an authorization that looks valid locally but reverts once the operator submits it because the on-…

  • First-time keypair derivation + credit funding race condition silently loses spending private key apps/tangle-cloud/src/containers/payments/FundCreditsContainer.tsx:49

    In FundCreditsContainer.handleFund, when no keypair exists, the code calls await deriveKeypair() then immediately calls generateAndStoreCreditKeys(). The generateAndStoreCreditKeys callback in CreditsProvider closes over encryptionKey, which is derived from keypair via useShieldedContext(). After deriveKeypair() resolves, React flushes the state update to ShieldedProvider, but the generateAndStoreCreditKeys closure captured by the current render of FundCreditsContainer still ha…

🟠 MEDIUM (17)

  • Switching wallets during derive/unlock can leave the hook permanently loading apps/tangle-cloud/src/hooks/payments/useKeypair.ts:33

    Both async flows clear isLoading only when the address that started the request is still current. On an address change, the reset effect clears keypair/error/hasStoredKeypair but never resets isLoading. If the user changes accounts while the signature prompt is open, the old request returns through the guarded finally path and skips setIsLoading(false), so the new account can stay stuck in a loading state until another successful action or a reload.

  • Credit-account reload is missing a stale-response guard across wallet/key changes apps/tangle-cloud/src/app/CreditsProvider.tsx:55

    CreditsProvider starts an async loadCreditKeysForAddress(currentAddress, encryptionKey) on every address/encryption-key change, but unlike ShieldedProvider it never verifies that the response still belongs to the active wallet/key before calling setCreditAccounts. If wallet A is unlocked and the user switches to wallet B while A's load is still in flight, A's decrypted credit keys can be written into B's session state until the later load wins. That breaks the per-owner isolation this st…

  • Locked or undecryptable credit accounts are exposed as usable accounts with empty private keys apps/tangle-cloud/src/app/CreditsProvider.tsx:57

    When the shielded keypair is absent or still locked after reload, CreditsProvider still loads credit accounts with encryptionKey undefined. The storage layer then returns records with spendingPrivateKey: '' whenever it cannot decrypt, but there is no locked/error marker on those accounts. In the normal spend flow, those accounts are still selectable and only fail later when code tries to sign with the empty key, so users get a broken authorization flow instead of an explicit unlock require…

  • deleteCreditKeys has no owner scoping — any wallet can delete any account's keys apps/tangle-cloud/src/utils/payments/indexedDbCreditStorage.ts:100

    deleteCreditKeys takes only a commitment string and deletes the record unconditionally. Unlike loadCreditKeysForAddress which filters by owner, delete performs no ownership check. If wallet A knows the commitment of wallet B's credit account (commitments are public on-chain), wallet A can call removeCreditAccount and delete wallet B's local keys. Since this is client-side, the practical risk is limited to shared browsers, but it violates the owner-scoping pattern used by the rest of the credit s…

  • No onversionchange handler — long-lived tabs will use stale DB connections after schema upgrade apps/tangle-cloud/src/utils/payments/db.ts:35

    openDb caches the IDBDatabase instance in dbPromise and handles onclose, but does not register a db.onversionchange handler. If a future release bumps DB_VERSION, other open tabs won't be notified that their connection is being obsoleted. The open request in the new tab will fire 'blocked' (handled), but the old tab's connection silently becomes invalid. Transactions on the old connection will fail with opaque errors.

The standard fix is to add `db.onversionchange = () => { db.close(); dbPromis…

  • Payments contract addresses are global, not chain-scoped, so wrong-network users can sign against the wrong domain separator apps/tangle-cloud/src/constants/payments.ts:2

    The payments layer reads SHIELDED_CREDITS_ADDRESS/SHIELDED_GATEWAY_ADDRESS/WRAPPED_TOKEN_ADDRESS from single global env vars and then feeds those addresses directly into useReadContract without any chain guard or per-chain mapping. If the wallet is connected to a different network than the deployment those env vars were set for, the app will still try to read DOMAIN_SEPARATOR and getAccount at that raw address on the active chain. Best case, the reads fail and the UI stalls; worse, t…

  • SpendAuthContainer signs with empty private key when shielded keypair is locked apps/tangle-cloud/src/containers/payments/SpendAuthContainer.tsx:153

    When credit accounts are loaded from IndexedDB without the shielded keypair unlocked (encryption key unavailable), spendingPrivateKey is set to '' (indexedDbCreditStorage.ts line 74-89). The SpendAuthContainer still shows these accounts as selectable in the dropdown and allows the user to fill in all fields and click 'Sign Authorization'. At line 153, privateKeyToAccount(selectedAccount.spendingPrivateKey as Hex) is called with '' as Hex, which throws a viem error about invalid private k…

  • Lazy route imports have no error boundary, so chunk-load failures now white-screen the app shell apps/tangle-cloud/src/app/app.tsx:18

    app.tsx changed every routed page from an eager import to React.lazy(...), but the new helper only wraps those components in Suspense. Suspense handles pending imports, not rejected ones. If a page chunk 404s during deploy skew, the network flakes, or the browser blocks the fetch, the rejection will escape the route tree and crash the whole cloud app instead of leaving the layout/header mounted with an in-app error state. This is a new production failure mode introduced by the routing re…

  • Mounting payment providers globally makes storage initialization failures break every cloud page apps/tangle-cloud/src/app/providers.tsx:36

    providers.tsx now wraps the entire app in ShieldedProvider and CreditsProvider, so their browser-storage initialization runs on every route. ShieldedProvider pulls in useKeypair, whose address-change effect does an unguarded localStorage.getItem(...). In storage-restricted environments (private browsing, embedded webviews, enterprise policies), that read can throw synchronously. Before this PR, payments storage code was not part of the global shell; after this change, a connected use…

  • Layout consolidation changes padding for earnings, instances, and rewards pages apps/tangle-cloud/src/components/PageLayout.tsx:13

    The old earnings/layout.tsx, instances/layout.tsx, and rewards/layout.tsx used md:px-5 for medium-breakpoint padding. The new PageLayout uses md:px-8 lg:px-10. This is a visible spacing regression on medium-width screens for these three route groups — content will have noticeably more horizontal padding than before.

  • Layout consolidation drops relative positioning from blueprints and operators layouts apps/tangle-cloud/src/components/PageLayout.tsx:13

    The old blueprints/layout.tsx, operators/layout.tsx, and operators/manage/layout.tsx had relative in their container class. PageLayout does not include relative. Any absolutely-positioned children (dropdowns, tooltips, overlays) in those pages that relied on the layout div as their positioning context will now escape to the next positioned ancestor, potentially rendering in the wrong location.

  • ShieldedProvider and CreditsProvider run for all routes, triggering IndexedDB + wagmi reads on every page apps/tangle-cloud/src/app/providers.tsx:39

    Both providers are placed inside the global Providers wrapper (providers.tsx:39-42), meaning they initialize for every route — including /instances, /blueprints, /operators, etc. ShieldedProvider instantiates IndexedDbNoteStorage and loads notes from IndexedDB on every address change. CreditsProvider calls loadCreditKeysForAddress (another IndexedDB read) and depends on ShieldedProvider's keypair for its encryption key derivation. For users who never visit payments pages, thi…

  • Fund tab reports success without funding the requested amount apps/tangle-cloud/src/containers/payments/FundCreditsContainer.tsx:25

    FundCreditsContainer treats any non-empty amount as enough to proceed, but the handler never parses that amount, checks it against the shielded balance, or performs any funding call. The only real side effect is generating/storing local credit keys, after which the UI moves to DONE and shows a success alert with a commitment. A user can therefore enter an arbitrary value and get a successful 'Fund Credit Account' flow even though the on-chain credit account is still unfunded.

  • Credits page exposes Authorize while the underlying credit keys may still be locked apps/tangle-cloud/src/pages/payments/credits.tsx:28

    The credits route only requires a connected wallet before rendering the Accounts/Fund/Authorize tabs. Returning users can therefore open the Authorize flow before unlocking their shielded state. In that locked state, stored credit accounts are still listed, but loadCreditKeysForAddress leaves spendingPrivateKey empty when decryption cannot run, and the authorize flow later tries to sign with that empty key. The result is a late runtime failure instead of an upfront unlock prerequisite, which…

  • Withdraw form shows total shielded balance even though it only considers confirmed notes spendable apps/tangle-cloud/src/containers/payments/WithdrawContainer.tsx:71

    WithdrawContainer correctly derives confirmedNotes by filtering out notes without an index, but the amount input still shows shieldedBalance from context. Because shieldedBalance is the sum of all notes, the withdraw surface can advertise more balance than the 'Available Notes' section can actually spend. That becomes user-visible whenever there are pending/unconfirmed notes: the page says the balance is there, while the selectable note set says otherwise.

  • FundCreditsContainer shows misleading proof progress for a local-only operation apps/tangle-cloud/src/containers/payments/FundCreditsContainer.tsx:37

    handleFund generates local credit keys (a synchronous-ish operation) but wraps it in ProofProgressIndicator with stage FETCHING_ARTIFACTS, then transitions to DONE. The 5-stage progress bar (fetching artifacts → syncing leaves → building witness → generating proof → sending tx) renders with the first segment active, implying a ZK proof pipeline is running. In reality, no proof is generated, no transaction is sent, and no on-chain funding occurs. The DONE message clarifies this in text, but the v…

  • FundCreditsContainer doesn't validate amount against shieldedBalance before proceeding apps/tangle-cloud/src/containers/payments/FundCreditsContainer.tsx:28

    The button is disabled when shieldedBalance === 0n, but when shieldedBalance > 0n the user can enter any amount (including one exceeding their balance) and click 'Generate Credit Keys'. handleFund only checks !amount (empty string), never parses or compares the amount to shieldedBalance. Since no on-chain tx happens yet this isn't exploitable, but once the SDK integration lands, this missing validation will silently carry forward. More immediately, a user entering 1000 with a shieldedBalance o…

🟡 LOW (4)

  • Contract hooks pass empty string as Address when env vars are unset apps/tangle-cloud/src/data/payments/useCreditAccountState.ts:8

    In useCreditAccountState.ts and useDomainSeparator.ts, SHIELDED_CREDITS_ADDRESS as Address is passed to useReadContract even when the env var is unset and the constant is ''. The enabled: Boolean(SHIELDED_CREDITS_ADDRESS) guard prevents the query from firing, so this is safe today. However, it's a fragile pattern — if someone adds refetch() calls (as SpendAuthContainer does at line 106) without checking the enabled state, or if wagmi's behavior around disabled queries changes, RPC call…

  • Tests mock all providers, so lazy-loading error boundaries and provider initialization regressions are untested apps/tangle-cloud/src/app/app.spec.tsx:12

    The test suite mocks ./providers to a passthrough (({ children }) => children), which means none of the provider nesting (WagmiProvider, ShieldedProvider, CreditsProvider, etc.) is exercised. Combined with mocking all page modules (which defeats lazy-loading), the tests only verify that route patterns match test IDs. A Suspense boundary failure, a provider throwing during initialization, or a lazy import failing would not be caught. The waitFor addition is correct for the lazy-loading chan…

  • AmountInput MAX button is a no-op when balance is 0n apps/tangle-cloud/src/components/payments/AmountInput.tsx:27

    handleMax fires when formattedBalance is truthy. formatUnits(0n, 18) returns '0', which is falsy in JS. So when balance is exactly zero, clicking MAX does nothing — no feedback, no '0' inserted. This is a minor edge case but breaks the principle of least surprise: the MAX button appears clickable (not disabled) but silently does nothing.

  • Deposit and Withdraw allow form input that can never be submitted apps/tangle-cloud/src/containers/payments/DepositContainer.tsx:40

    Both DepositContainer and WithdrawContainer pass disabled to AmountInput (preventing typing) AND permanently disable their submit buttons. However, DepositContainer still shows the keypair derive/unlock flow as an actionable prerequisite, implying the user should complete it before depositing. A user who unlocks their keypair and then discovers the deposit button is permanently disabled gets a confusing experience. The withdraw container similarly allows typing a recipient address (Input is no…


🎯 What would get this approved

  • Fix [high] Queued note persists still overwrite each other on back-to-back writes in apps/tangle-cloud/src/app/ShieldedProvider.tsx
  • Fix [high] Write queue reads stale notesRef — back-to-back addNote drops notes in apps/tangle-cloud/src/app/ShieldedProvider.tsx
  • Fix [high] Credit spending keys permanently lost when generated without shielded keypair in apps/tangle-cloud/src/utils/payments/indexedDbCreditStorage.ts
  • Fix [high] refetchAccount() can still sign with a stale cached nonce after a failed refresh in apps/tangle-cloud/src/containers/payments/SpendAuthContainer.tsx
  • Fix [high] First-time keypair derivation + credit funding race condition silently loses spending private key in apps/tangle-cloud/src/containers/payments/FundCreditsContainer.tsx

pr-reviewer v0.5.0 · review #6 · 2026-03-21T00:25:01.052228+00:00

Wallet-switch safety:
- Credit storage scoped by owner address (owner field + filtered loads)
- useKeypair guards async completions with addressRef to prevent stale
  wallet's keypair from being set after account switch
- ShieldedProvider resets notes synchronously and flushes write queue
  on address change to prevent cross-wallet note leakage
- Suspense moved inside each route's layout so Header stays visible
  during lazy page loads

Security:
- Credit keys not stored when encryption key is unavailable (empty
  string instead of plaintext fallback)
- Decryption failure returns empty private key, not ciphertext blob
- Nonce read failure aborts signing (fail-closed, no 0n fallback)

Correctness:
- SpendAuth uses TOKEN_DECIMALS constant instead of hardcoded 18
- /payments redirects to /payments/pool (was 404)
- Test for /payments redirect added (52 tests pass)
@drewstone drewstone merged commit 218e39c into develop Mar 21, 2026
19 of 20 checks passed
@drewstone drewstone deleted the feat/private-cloud-payments branch March 21, 2026 00:39
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.

1 participant