Skip to content

Add mobile vault screen for deposit, withdraw, and escrow management - #521

Closed
BigManly4 wants to merge 1 commit into
Miracle656:mainfrom
BigManly4:mobile/vault-screen-477
Closed

Add mobile vault screen for deposit, withdraw, and escrow management#521
BigManly4 wants to merge 1 commit into
Miracle656:mainfrom
BigManly4:mobile/vault-screen-477

Conversation

@BigManly4

Copy link
Copy Markdown
Contributor

Summary

Ports the vault sub-account feature from the Next.js web wallet to the Expo mobile app, following the conventions already established by the bulk-payout and swap mobile ports in this repo.

What was ported

  • frontend/mobile/lib/vault.ts — typed vault domain logic ported from frontend/wallet/lib/vault.ts: XLM/stroops conversion, address validation, delay parsing, and the delay/countdown/status formatting helpers used to render the withdrawal queue. Deploy, deposit, queue withdrawal, cancel withdrawal, and execute withdrawal are all present as typed functions with the same shapes (VaultDetails, VaultWithdrawal, VaultConfig) as the wallet lib.
  • frontend/mobile/lib/escrow.ts — claimable-balance escrow helpers (createEscrow, claimEscrow, reclaimEscrow, buildEscrowClaimants, buildClaimLink). The wallet's frontend/wallet/lib/escrow.ts is a one-line re-export of sdk/src/claimableBalance.ts. That same relative-path re-export does not typecheck from frontend/mobile/lib, because the mobile TypeScript program only resolves @stellar/stellar-sdk from frontend/mobile/node_modules and this repo has no hoisted root node_modules — pulling in the sdk file via export * makes tsc unable to find @stellar/stellar-sdk from that file's location. Since mobile already depends on @stellar/stellar-sdk directly, this file mirrors the sdk implementation locally instead of reaching outside the mobile package.
  • frontend/mobile/app/vault.tsx — a React Native screen covering: create/attach a vault, view balance (total, available, queued, delay), a deposit form, and the full withdrawal lifecycle (queue with a delay, live countdown to unlock, execute once ready, cancel while pending). Styled to match the existing dark-theme mobile screens (#0B0B0F background, #1e293b cards, #6366f1 accent, #f87171 error).

How signing is stubbed, and why

Mobile does not yet have the passkey/session-based signing infrastructure the web wallet uses (frontend/wallet/lib/passkeyAuth, sessionStorage-backed signer keys). Rather than inventing a signing flow, the actual chain submission and reads are behind injectable async parameters — VaultSubmit and VaultFetch in lib/vault.ts — the same style as executeBulkPayout(rows, submitBatch) in the existing lib/bulkPayout.ts. The screen supplies stubSubmitVaultTx and stubFetchVaultDetails today so the full deposit/withdraw/queue/execute/cancel flow is exercisable end to end in the UI, and a real Soroban RPC-backed implementation (mirroring frontend/wallet/lib/vault.ts's submitOperation/simulateCall) can be swapped in later without touching the screen or the typed domain logic.

What was verified

  • npm install && npm run typecheck passes cleanly with no errors.
  • npx expo export --platform web produces a working static bundle that includes /vault as a route alongside the existing /swap and /bulk-payout routes, with no bundling errors.
  • npm run lint could not be run: this repo has no ESLint config yet, and expo lint's auto-setup wanted to add new devDependencies and an eslint.config.js outside the scope of this change, so that was not pursued or committed.
  • Not verified: an interactive simulator/device render (no simulator available in this environment). The static web export bundling successfully is the closest available signal that the screen renders without crashing.

closes #477

Port the vault sub-account flow from the Next.js wallet (frontend/wallet/app/vault/page.tsx
and frontend/wallet/lib/vault.ts) to Expo/React Native, following the conventions already
established by the bulk-payout and swap mobile ports.

- frontend/mobile/lib/vault.ts: typed vault domain logic ported from the wallet lib
  (amount/delay parsing and formatting, address validation, withdrawal status/countdown
  helpers). Chain submission and reads are behind injectable VaultSubmit/VaultFetch
  parameters, matching the executeBulkPayout(rows, submitBatch) pattern already used in
  lib/bulkPayout.ts, since mobile has no passkey/session signing infrastructure ported yet.

- frontend/mobile/lib/escrow.ts: claimable-balance escrow helpers (createEscrow, claimEscrow,
  reclaimEscrow, buildEscrowClaimants). The wallet's version is a one-line re-export of
  sdk/src/claimableBalance.ts; that re-export does not typecheck from frontend/mobile because
  the mobile TS program only resolves @stellar/stellar-sdk from frontend/mobile/node_modules
  and there is no hoisted root node_modules, so this file mirrors that implementation locally
  using the @stellar/stellar-sdk dependency mobile already has.

- frontend/mobile/app/vault.tsx: React Native screen covering vault creation/attach, balance
  display, deposit, and the full withdrawal lifecycle (queue with delay, live countdown,
  execute once ready, cancel while pending). Styled to match the existing dark-theme mobile
  screens. Signing/submission and vault reads go through stub functions declared at the top
  of the screen (stubSubmitVaultTx, stubFetchVaultDetails) so the flow is exercisable today
  and can be swapped for real Soroban RPC calls once mobile signing lands.

Verified: npm run typecheck is clean, and npx expo export --platform web bundles /vault as a
static route with no errors. Not verified: an interactive simulator/device render.
@BigManly4
BigManly4 requested a review from Miracle656 as a code owner July 28, 2026 00:37
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@BigManly4 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@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.

The domain layer here is genuinely well done. lib/vault.ts splits cleanly into pure, testable helpers — xlmToStroops, stroopsToXlm, formatCountdown, withdrawalStatus, isWithdrawalReady — and the injectable VaultSubmit / VaultFetch seams are the right pattern for a port with no signing infra yet. It matches how lib/sep24.ts and lib/bulkPayout.ts are structured, which is what I want.

The doc comment in escrow.ts about claimable-balance semantics is also exactly the kind of thing worth writing down:

A Stellar claimable balance is consumed by the first successful claim. Once either party claims, the balance is gone — the other party's subsequent claim will be rejected by the Stellar network.

Two blockers though.

🚨 1. The screen is fully stubbed, and the UI doesn't say so

Every operation goes through a stub:

const stubSubmitVaultTx: VaultSubmit = async (call) => {
  return { hash: `stub-${call.method}-${Date.now().toString(36)}` }
}
...
const address = `C-stub-${Date.now().toString(36)}`

Nothing is deployed, nothing is submitted, and the vault details are fabricated. That's a defensible staging decision on its own — but I searched the screen for any user-facing indication and there is none. No demo banner, no disabled state, no "not yet functional" copy. The word "stub" appears only in variable names.

So the user flow is: open Vault → enter an amount → tap Deposit → see a success state with transaction hash stub-deposit-mf3k2j → believe funds moved into a time-locked vault.

In a wallet, showing a fabricated success for a financial operation is the most damaging version of this bug. Someone will believe they've locked funds they haven't.

Either of these is fine:

  • Gate it. Render the screen behind a dev flag (process.env.EXPO_PUBLIC_VAULT_DEMO === '1'), so it isn't reachable in a normal build.
  • Label it. A persistent, unmissable banner — "Demo mode — no transactions are submitted" — plus replacing the fake hash with something that can't be mistaken for a real one.

I'd prefer the first. This is the same class of issue I flagged on #515, where the NFT gallery shows fabricated holdings, so it's worth stating the general rule: never render fabricated financial state to a user without an unmistakable marker.

🚨 2. lib/escrow.ts is dead code

183 lines, and app/vault.tsx references it zero times. It isn't imported anywhere in the PR.

It's also structurally different from vault.ts — it takes raw Keypair objects (senderKeypair, claimantKeypair) and calls server.loadAccount() directly, rather than using an injectable signer seam. So it can't be wired into the screen as-is without first deciding where those keypairs come from, and that decision is exactly the one still open on #512 (which currently pulls a secret key out of unencrypted AsyncStorage — please don't follow that pattern here).

Please either wire it up in this PR or split it into its own, once the signing story is settled. Merging 183 lines of unreferenced key-handling code isn't something I want to do on the way past.

Smaller

buildClaimLink hardcodes a domain:

return `https://app.veil.xyz/claim/${balanceId}`

Worth knowing there are currently three different app identities proposed across open PRs. Yours matches #508's (app.veil.xyz) — noting it as a data point. I'm settling the canonical value and will confirm; don't change it yet, but it should come from shared config rather than a literal once it's fixed.

Collision with #507 — the navigation shell adds a vault route stub. Yours supersedes it; just rebase once #507 lands.

TestsxlmToStroops / stroopsToXlm / formatCountdown / withdrawalStatus are pure and are the obvious first tests once a runner exists (#524/#525 add jest.config.js). Rounding on the stroops conversion in particular is worth pinning down.


The domain modelling is the hard part and you've done it well. Gate the demo state and resolve the escrow file and this gets much closer.

@Miracle656

Copy link
Copy Markdown
Owner

Thanks — lib/vault.ts and lib/escrow.ts are well put together, and injecting the IO through VaultSubmit / VaultFetch rather than reaching for a client inline was the right instinct. I still have to close this, because in its current form the screen reports success for transactions that never happen.

The blocking problem

Both IO ends are stubs:

const stubSubmitVaultTx: VaultSubmit = async (call) => {
  await new Promise((resolve) => setTimeout(resolve, 400));
  return { hash: `stub-${call.method}-${Date.now().toString(36)}` }
};

and the screen renders whatever comes back as a confirmation:

const result = await operation();
const hash = typeof result === 'string' ? result : result.hash;
setNotice(`Transaction submitted: ${hash.slice(0, 16)}...`);

So a user taps Deposit, waits 400 ms, and is told "Transaction submitted: stub-deposit-mf…". No transaction was built, signed or submitted, and no funds moved. The comments in the source are honest about this, but the comments aren't what the user sees — the UI is, and the UI says it worked.

For a screen that moves money that's a correctness problem rather than a rough edge, and it's the same class of issue as #515 (closed for showing NFTs a wallet doesn't hold). A placeholder that fails loudly would be fine; one that succeeds falsely isn't.

Against #477's acceptance — vault balances render; deposit/withdraw sign + submit — the balances come from stubFetchVaultDetails, which always returns an empty vault, and nothing signs or submits.

This is more buildable than the comments suggest

The stub comment says mobile "does not yet have the passkey/session signing infrastructure". That was true when you opened this, but has since changed:

  • contracts/vault exists in the repo.
  • frontend/wallet/lib/vault.ts is a real implementation — SorobanRpc.Server, simulateTransaction, the lot — and is the direct port target 49. Vault screen #477 names.
  • frontend/mobile/lib/network.ts resolves rpcUrl / networkPassphrase from EXPO_PUBLIC_*.
  • frontend/mobile/lib/passkey.ts (feat(mobile): dApp approval modal signing via device passkey #544) drives react-native-passkeys, passing the Soroban authorization-entry hash as the WebAuthn challenge — that's the signer.
  • frontend/mobile/lib/walletStore.ts holds the address, passkey id and signer secret.

So the two stubs can be replaced with real implementations rather than deferred.

What I'd keep

lib/vault.ts and lib/escrow.ts are the valuable parts and mostly survive as-is — the stroop/XLM conversion, the queued-withdrawal model with unlockAt / cancelled / executed, and the reserved-vs-available split are all sound, and being pure makes them testable. There are no tests in this branch; with the IO injected they'd be easy to add, and worth having.

If you'd like to resubmit: branch off current main, port fetchVaultDetails against SorobanRpc.Server using lib/network.ts, wire the submitter through lib/passkey.ts, and add tests for the pure helpers. If any part genuinely can't be wired yet, have it throw and let the screen show the error state you already built — just don't let it return a hash. Happy to review that.

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.

49. Vault screen

2 participants