Skip to content

feat(mobile): Freighter signing for admin contract actions, TradeDetail/WalletConnect tests, frontend docs audit (#276 #281 #283 #275) - #375

Open
lockoabosede8-byte wants to merge 28 commits into
Elite-Work:mainfrom
lockoabosede8-byte:feat/mobile-freighter-tests-docs-audit
Open

lockoabosede8-byte wants to merge 28 commits into
Elite-Work:mainfrom
lockoabosede8-byte:feat/mobile-freighter-tests-docs-audit

Conversation

@lockoabosede8-byte

Copy link
Copy Markdown
Contributor

Description

Delivers four independent pieces of work in one reviewable PR (each issue was scoped to "a single reviewable PR", so they are shipped together on one branch as four commits, one per issue, which can be reviewed or reverted separately):

Issue Area Change
#281 mobile / tests Tests for TradeDetailScreen
#283 mobile / tests Tests for WalletConnectScreen
#276 mobile / feature Freighter sign-and-submit flow for admin contract actions
#275 frontend / docs Staleness audit of frontend/docs/ against src/app/

pnpm type-check and pnpm lint pass in mobile/ (0 errors; the 2 remaining warnings already exist on main: unused MOCK_WALLET_ADDRESS in e2e/smoke.e2e.ts and unused styles.center in VaultDashboard.tsx). Full mobile suite: 30 suites / 341 tests, all passing.

Related Issues


#276 — Freighter signing for admin contract actions

AdminContractScreen had // TODO: sign unsignedXdr with @stellar/freighter-api and submit it. and left every mediator/fee transaction unsigned. It now has a working flow.

What it does. Under each unsignedXdr card (add-mediator and update-fee) there is a Sign & submit with Freighter button:

  1. Probe Freighter (isConnected) with a 3 s timeout.
  2. signTransaction(unsignedXdr, { networkPassphrase, address }) with the connected admin wallet as the address hint (2 min ceiling for the user to approve).
  3. Submit the signed XDR with the Soroban RPC sendTransaction JSON-RPC call — the same call frontend/src/app/vault/page.tsx already makes — with a 20 s abort timeout.
  4. Show the transaction hash on success (PENDING and DUPLICATE both mean the network has the transaction, so both resolve with its hash).

Design decisions worth reviewing

  • Freighter is a browser extension. @stellar/freighter-api only works where the extension is reachable (e.g. the Expo web target in a browser with Freighter installed). On a native device it cannot work, so the service reports a typed WALLET_UNAVAILABLE error and the screen keeps the existing selectable/copyable XDR so an admin can still sign externally. I verified against the real, un-mocked package in a React Native-like runtime (window defined): it throws internally, and the probe resolves to "unavailable" instead of hanging or crashing the app.
  • Typed, retryable errors (TransactionSigningError.code): WALLET_UNAVAILABLE, USER_REJECTED, SIGNING_FAILED, SUBMISSION_FAILED. They render inline under the XDR card (not as AdminErrorBanner, because they are wallet/network problems, not backend admin errors) and the button changes to "Try signing again". After success the button is removed so a transaction cannot be double-submitted; building a new XDR resets the control.
  • Lazy require of @stellar/freighter-api inside the service so merely importing the screen never initialises the extension bridge, and a package that fails to load degrades to WALLET_UNAVAILABLE. (A native import() is not supported by the Jest/babel setup, which is why this is a lazy require.)
  • Network config lives in src/constants/stellar.ts. It reuses the existing EXPO_PUBLIC_STELLAR_NETWORK (testnet | public; a full passphrase is also accepted to stay interchangeable with the frontend) and adds an optional EXPO_PUBLIC_STELLAR_RPC_URL. Defaults are unchanged for current users (testnet).
  • User rejection is detected from the wallet's error message (/reject|declin|denied|cancel/i), because the package does not export a stable declined-error code.

Files: src/services/freighterSigner.ts, src/components/SignAndSubmitXdr.tsx, src/constants/stellar.ts, src/screens/AdminContractScreen.tsx (TODO replaced, header/hint copy updated), src/types/env.d.ts, .env.example, .env.mobile.example, README.md, docs/admin-errors.md, src/screens/ADMIN_SCREENS.md.

Lint config. Declared AbortController (runtime global, like the already-listed fetch/URL) and global (test files only) in eslint.config.mjs.

Tests (45 new):

  • freighterSigner.test.ts (30): availability (connected / not connected / wallet error / throws / never answers / package fails to load); signing (success + network passphrase + address, unavailable without prompting, four rejection phrasings, other wallet errors, empty XDR, thrown error, user never answers); submission (JSON-RPC request shape, DUPLICATE, TRY_AGAIN_LATER, ERROR, PENDING without hash, RPC error, non-2xx, network failure, abort timeout); orchestration (no submit when signing fails).
  • SignAndSubmitXdr.test.tsx (9): idle, success + hash + onSubmitted, busy state and re-entry guard, all four error codes with retry, wrapping of unexpected errors.
  • AdminContractScreen.test.tsx (+6): no sign action before an XDR exists, signing for both sections with the admin wallet, section independence, inline error that keeps the XDR and raises no banner, result reset when a new XDR is built.

Not verified: I could not run this against a real Freighter extension or a live Stellar RPC in my environment. The Freighter API and fetch are mocked in the unit tests; the only unmocked check is the graceful-degradation path above. Reviewers with the extension installed can try it with pnpm web on testnet.


#281 — TradeDetailScreen tests (40 tests)

src/screens/TradeDetailScreen.test.tsx renders the real screen and the real AdminErrorBanner, mocking only the stores and safe-area insets (same pattern as TradeListScreen.test.tsx).

  • Load states: fetch on mount for the route param; spinner on first load; the trade stays visible during a refresh; synthetic "Trade not found" banner; store error with retry; sign-out from the banner; header back button.
  • Loaded content: status card, trade info, contract defaults vs. explicit loss ratio, optional commodity/quantity/updated rows, label for every TradeStatus, terminal timeline steps for DISPUTED/REFUNDED.
  • Actions per status (table-driven, asserts exactly which of Deposit / Confirm / Release / Dispute / Upload Evidence / "No actions" are shown for all 7 statuses).
  • Interactions: deposit, confirm delivery and release funds only fire after the confirmation Alert is accepted (release is a destructive button; cancel does nothing); the action button swaps for a spinner while running; evidence navigation; the dispute modal (hidden until opened, blank reason rejected with an alert and no store call, trimmed reason submitted then modal closes and resets, cancel discards the draft).
  • Errors: load error offers retry; a mutation error wins over a stale load error and does not offer "Try again" (only Contact support); go-back clears the error; sign-out; support mailto: carries the screen name, error code and request id.

#283 — WalletConnectScreen tests (22 tests)

src/screens/WalletConnectScreen.test.tsx mocks the auth store, authApi and the biometrics util, and spies on Alert.prompt / Alert.alert.

  • Render states: first-time user; returning user with biometric unlock (label, truncated address, "Use a different wallet"); hidden when the device has no usable biometrics or there is no remembered wallet; no state update after unmount mid-lookup.
  • Manual connect: prompt copy; a valid address runs challenge → verify → setToken → setWalletAddress → navigation.replace('TradeList') (token persisted before the wallet is marked connected); invalid input (wrong prefix, <56 chars, empty, cancelled prompt) alerts and never calls the API; challenge failure, verify failure and a message-less failure show Connection failed; busy/disabled button while the prompt is open; recovery if Alert.prompt throws.
  • Biometric unlock: success re-authenticates the remembered wallet; user_cancel is silent; other failures alert "Unlock failed"; an API failure after a successful biometric check alerts and re-enables the button; both buttons disabled while unlocking; a returning user can still connect a different wallet.

#275 — frontend/docs/ staleness audit

I checked every claim in all 8 docs against frontend/src/app/ (25 routes) and the files, symbols, CI steps and configs they reference. Docs-only; no code changed. Corrections:

Doc What was stale Fix
loading-states.md Inventory claimed route loading.tsx for /dashboard, /trades/[id], /assets*; only app/vault/loading.tsx exists. Also wrong: /trades/[id] uses LoadingState (it uses a bare spinner), /vault/manage and the wallet-balance widget have skeletons (they don't / don't exist), /trades/create totals skeleton (none), admin lists use LoadingState (they use SkeletonList), 3× vs 4× SkeletonCard. Missing routes. CI snippet pinned @lhci/cli@0.13.x although CI already runs lighthouse.yml with per-page configs on @lhci/cli@0.15.1. Token table listed dark values only. Rewrote the inventory from the code, added the missing routes, flagged the 3 real gaps (/trades/[id], /vault/manage, /mediator/disputes/[id]), replaced the CLS/CI section with the real configs and workflow, added light-theme token values.
accessibility-report.md "Baseline — all routes" is produced by routes.axe.test.tsx, which renders a minimal <h1> shell, not the real pages; 8 routes have no row; claims window.prompt was removed but /trades/[id] still calls it (page.tsx); points to ci.yml:98 (the step is now elsewhere); references frontend/tests/a11y-keyboard-walkthrough.md, which does not exist. Added a dated coverage-notes block, softened the two prompt claims to the create→deposit→release path, referenced the CI step by name, stated that the walkthrough file does not exist.
offline-resilience.md Said the replay executor treats a 409 as success (it doesn't — any error leaves the action queued); said fetchTrades runs after replay (only retryOnline() does); PendingBadge is not its own file; idempotency key/lock names imprecise. Described the actual behaviour and the real backend file/keys.
offline-cache-conflict-matrix.md Said replay writes the server response into the cache and a 409 evicts the entry via cacheInvalidate; neither is implemented (the queue does not touch the cache). Still referred to the queue as a future "issue #74". Matrix rows and prose now match the code; points to offlineQueueStore / offline-resilience.md.
wallet-states.md Reads as if the state machine is live, but WalletStateCallout is not rendered by any route and nothing else imports the state machine. Added an "Integration status" section.
i18n.md Said react/jsx-no-literals flags raw literals and the checklist said "lint-enforced"; the rule is not enabled in eslint.config.mjs. Stated that it isn't enabled and that review is manual until it is.
admin-route-guard.md Route list omitted /admin/streams/[id]; said every admin page renders ForbiddenState (only the two list pages do). Corrected both; noted adminAccess.ts.
optimistic-updates.md Ambiguous test path. Pointed to src/__tests__/tradeStore.test.ts.

Everything else (store/toast/dedup APIs, TTLs and quota in offlineCache.ts, wallet state list and telemetry, i18n module layout, fr catalog, LocaleSwitcher, backend idempotency semantics) was checked and is accurate, so it is left untouched. I did not fix any of the code gaps the audit surfaced (e.g. the window.prompt, the missing skeletons, the unwired wallet callout, the unenforced lint rule) — they are now documented honestly and can be filed as follow-ups.


Type of Change

Verification

Run from mobile/:

pnpm type-check   # tsc --noEmit → 0 errors
pnpm lint         # eslint .     → 0 errors, 2 pre-existing warnings
pnpm jest         # 30 suites, 341 tests, all passing

🔒 Required CI Gates

  • Mobile Required Gate: type-check and lint in mobile/ pass
  • Frontend gate: docs-only change under frontend/docs/ (no source touched)
  • Backend / Contracts: not touched

PR Checklist

  • Code builds and passes all unit tests locally
  • Documentation updated (mobile/README.md, .env.example, mobile/docs/admin-errors.md, ADMIN_SCREENS.md, and the frontend/docs/ audit)
  • Secret Management: no secrets added; the RPC URL and network are public configuration via EXPO_PUBLIC_*
  • No backend/admin routes touched, so no admin regression tests are required

@drips-wave

drips-wave Bot commented Oct 22, 2026

Copy link
Copy Markdown

@lockoabosede8-byte 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

lockoabosede8-byte and others added 18 commits October 27, 2026 18:33
Cover loading/not-found/loaded states, per-status actions, the
confirmation alerts for deposit/confirm/release, the dispute modal,
evidence navigation and the error banner actions.
Cover the first-time and returning-user render states, the manual address
prompt and validation, the challenge/verify exchange, biometric unlock
(success, cancel, failure) and busy-state handling.
…lite-Work#276)

Replace the TODO in AdminContractScreen with a working sign-and-submit
flow. A new freighterSigner service asks Freighter to sign the
backend-built unsignedXdr for the configured network and submits the
signed transaction via the Soroban RPC sendTransaction method (the same
call the web frontend makes). SignAndSubmitXdr renders the action and its
result under each XDR card.

Freighter is a browser extension, so the service probes it first and
degrades to a WALLET_UNAVAILABLE error with the XDR still copyable for
external signing. Declined signatures, signing failures and submission
failures are reported inline and can be retried.

Adds EXPO_PUBLIC_STELLAR_RPC_URL (optional) and unit tests for the
service, the component and the screen integration.
…ite-Work#275)

Check every doc against src/app and the code it references and correct
the stale parts: routes missing from the loading-state inventory and the
accessibility baseline, loading.tsx/lhci claims that no longer match the
repo, the removed-window.prompt claim, a missing walkthrough file, the
unenforced jsx-no-literals rule, replay/409 and cache behaviour that the
code does not implement, the unwired wallet state machine and the admin
guard's route list.
…ile E2E)

- test:scripts: run jest via node_modules/jest/bin/jest.js; pnpm's
  node_modules/.bin/jest is a shell wrapper that node cannot parse.
- Feature Flag Catalog Drift: pin global typescript@5 (ts-node cannot load
  the native TypeScript 7) and compile the script as CommonJS with
  --transpile-only, since Node 22 otherwise loads it as ESM where
  __dirname is undefined.
- Backend Required Gate: give 'prisma migrate deploy' the DATABASE_URL of
  the job's postgres service.
- Mobile E2E iOS: drop the invalid getDefaultConfig spread from
  app.config.ts (not exported by expo/config), which made
  'expo prebuild' fail to read the config.
- Mobile E2E Android: install only platform-tools in setup-android; the
  default 'tools' package no longer exists and made sdkmanager exit 1.
@lockoabosede8-byte
lockoabosede8-byte force-pushed the feat/mobile-freighter-tests-docs-audit branch from 47845cf to b11c3d2 Compare October 27, 2026 17:35
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