Skip to content

Add matured lock withdrawal flow#289

Merged
El-swaggerito merged 1 commit into
Axionvera:mainfrom
Adeolu01:feat/matured-lock-withdrawal-264
Jul 23, 2026
Merged

Add matured lock withdrawal flow#289
El-swaggerito merged 1 commit into
Axionvera:mainfrom
Adeolu01:feat/matured-lock-withdrawal-264

Conversation

@Adeolu01

Copy link
Copy Markdown
Contributor

closes #264

Summary

Matured vault locks had a "Withdraw Funds" button whose confirmation state was set but never rendered — the confirm handler that fed it was unreachable, so the button did nothing beyond flipping a boolean. This PR gives the lock a real withdrawal path with eligibility, an explicit confirmation, a loading state, and distinct success and failure states, structured so the SDK call is the only thing left to swap.

Design

The flow is split into three layers so each part is testable on its own.

src/features/vault/maturedLockWithdrawal.ts — pure rules.

  • evaluateWithdrawalEligibility(lock, { publicKey, now }) derives maturity from the lock's unlockDate rather than its stored status. status is only recomputed when locks are loaded, so a lock that matures while the detail screen is open would otherwise keep reporting itself as locked. It also blocks withdrawal when no wallet is loaded or the amount is not positive, and returns the plain-language reason to render.
  • Failures are tagged with a code (network, secret-unavailable, not-matured, lock-not-found, …) and mapped to fixed copy by describeWithdrawalError, so raw Soroban/RPC strings never reach the UI. Each mapping declares whether retry can help.
  • The tagged error is a plain Error with a code property rather than an Error subclass, because subclassed built-ins do not survive transpilation reliably on the Babel/Hermes targets this app builds for.

vaultStore.withdrawMaturedLock(lockId, { publicKey, getSecretKey }) — the transfer.

  • Re-checks eligibility at submission time, so a stale screen cannot submit an immature lock.
  • Reads the secret key only when a contract is configured, and only at the moment of signing.
  • Removes the lock from storage after the transfer resolves, never before, so a failed withdrawal leaves the vault untouched.
  • Returns { amount, hash, isPreview }.

useMaturedLockWithdrawal + MaturedLockWithdrawalModal — the flow.

  • A state machine over idle → confirming → submitting → success | failed, with cancel, retry, and a guard against a double tap on confirm.
  • The modal renders one step at a time: confirmation with amount, maturity date, network, and contract; a loading state; a success state with the transaction hash; and a failure state with mapped copy and a retry action where retry is meaningful.
  • On success the detail screen navigates back, since the lock no longer exists.

Not implying a live withdrawal

When EXPO_PUBLIC_VAULT_CONTRACT_ID is unset the flow runs through mockWithdrawFromVault, and the confirmation and success states say directly that no vault contract is configured and no funds move on the network. No transaction hash is shown in that mode. With a contract configured, the flow goes through the existing withdrawFromVault service unchanged.

Documentation

docs/vault-integration-assumptions.md gains a "Matured Lock Withdrawal" section covering the assumptions the flow encodes (whole-lock withdrawal, the withdraw(to, amount) call shape, the contract as the real maturity authority, lock removal ordering, SUCCESS as terminal, mapped errors), what each would cost if it turns out wrong, the placeholder behaviour, and the eligibility rules.

Incidental fixes

app/(tabs)/vault.tsx declared const router = useRouter() twice, which made the screen fail to parse — the vault entry point to this flow did not build on main, and __tests__/vault.test.tsx had been failing to run at all because of it. Removing the duplicate unmasked that suite, which then failed on a store mock missing locks/loadLocks and on copy assertions that had drifted from the screen (Lock Funds (30 days) vs Set Aside for 30 Days, the lock action now recording a lock instead of alerting "not yet implemented"). Those 20 tests are repaired here.

Acceptance criteria

  • Matured withdrawal flow is added — confirm, submit, resolve, from the lock detail screen.
  • Eligibility is displayed — maturity, unlock date, and the blocking reason when a matured lock still cannot be withdrawn.
  • Confirmation step is included — nothing is submitted without an explicit confirm.
  • Loading and failure states are handled — dedicated states, mapped failure copy, retry where it can help, and the lock retained on failure.
  • SDK assumptions are documented — see above.

Testing

npx jest __tests__/maturedLockWithdrawal.test.ts __tests__/vaultStore.withdrawMaturedLock.test.ts __tests__/maturedLockWithdrawalFlow.test.tsx __tests__/vault.test.tsx

49 tests, all passing, covering eligibility rules and error classification; the store action across configured and placeholder modes, including that the lock survives a network failure and an unreadable key; and the full UI flow including confirmation, loading, success, cancel, failure with retry, and a non-retryable failure.

Repository-wide, main fails 25 tests across 5 suites; this branch fails 15 across 4. The remaining failures (send, paymentSuccess, home.pullToRefresh, WalletResetConfirmModal) are pre-existing and untouched here. Typechecking reports no new errors.

Follow-up, not included

app/vault-lock/[id].tsx is a separate legacy screen built on hardcoded mock data, reachable from the "Mock Active Locks" demo section of the vault tab. Its withdraw button still raises "Withdrawal not implemented for mock data." Its lock does not exist in the store, so it cannot use this flow as-is; removing or re-pointing that demo route is worth a follow-up.

Give matured vault locks a real withdrawal path: eligibility, an explicit
confirmation, a loading state, and distinct success and failure states.

- Add `evaluateWithdrawalEligibility`, which derives maturity from the
  lock's unlock date rather than its stored status, so a lock that
  matures while the screen is open is not reported as locked.
- Add `vaultStore.withdrawMaturedLock`, which re-checks eligibility at
  submission time, submits through the vault service when a contract is
  configured, and removes the lock only after the transfer resolves so a
  failure leaves the vault untouched.
- Add `useMaturedLockWithdrawal`, a small state machine over
  idle / confirming / submitting / success / failed with cancel and
  retry.
- Add `MaturedLockWithdrawalModal` rendering each of those steps, and
  wire it into the lock detail screen in place of the confirmation state
  that was set but never rendered.
- Map failures to codes and fixed copy rather than raw RPC errors, and
  offer retry only where a second attempt can succeed.
- State plainly in the confirmation and success states when no vault
  contract is configured and nothing moves on the network.
- Document the flow's SDK assumptions, placeholder behaviour, and
  eligibility rules in docs/vault-integration-assumptions.md.

Also fixes a duplicate `router` declaration in the vault tab that made
the screen fail to parse, and repairs the vault screen test suite that
the parse error had been masking.
@El-swaggerito
El-swaggerito merged commit 840f01e into Axionvera:main Jul 23, 2026
1 check passed
@grantfox-oss grantfox-oss Bot mentioned this pull request Jul 23, 2026
5 tasks
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.

Add matured lock withdrawal flow

2 participants