Skip to content

The practice sandbox at the bridge; the guided tour covers the online beta - #1097

Merged
cryptskii merged 7 commits into
mainfrom
feat/practice-containment-and-tour-expansion
Oct 2, 2026
Merged

cryptskii merged 7 commits into
mainfrom
feat/practice-containment-and-tour-expansion

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

During the guided tour, practice mode kept only the dsmClient object in practice. Every screen that imported a write directly reached the real wallet:

  • token creation, with a real token created and real ERA burned as the fee;
  • burn, forget and add-by-anchor;
  • every SoFi action;
  • the lock;
  • NFC backup and recovery.

This branch moves the sandbox to the bridge, where every call to native code passes, and expands the tour to cover the online beta. Offline sending and Bitcoin are left out.

Owner rulings (2026-10-01)

  • "make the bridge-level practice gate the authoritative safety boundary, and treat the current dsmClient interception as convenience behavior only. … While practice mode is active, no state-changing Rust call reaches the real wallet unless it is explicitly designated as a practice-safe simulated action." Deny by default, with an explicit read allowlist; everything else gets "a deterministic 'blocked in practice mode' result without touching Rust state."
  • "do not fake monetary/state-machine behavior in TypeScript". TRADE, token creation, burn/forget, lock setup and NFC backup/recovery are walkthroughs that highlight the real UI with the action blocked.
  • "PracticeMode is missing… needs to be expanded… cover everything… Except for Bitcoin for now, and the offline for now". Also: remove the offline steps and the Bitcoin references, fix the lock and storage text, make add-contact-by-code hands-on, and fix the practice Add Contact response shape.

Changes

The sandbox at the bridge (bridge/practiceGate.ts, bridge/BridgeRegistry.ts). While practice mode is on, getBridgeInstance() returns the practice view. Every path to native code takes its bridge from there: callBin, the router ingress and host requests. Each request is read before it leaves, and only named reads cross:

  • reading bridge methods;
  • reading router routes, including sofi.findRoute and sofi.vaults, which the router takes as invokes;
  • the QR camera;
  • the display preferences ui_theme and sfx_enabled.

Everything else gets "blocked in practice mode" in that channel's own wire shape and never reaches Rust or the host. That includes an unreadable request, and a raw sendMessageBin frame carrying an ingress or host request. Practice mode's verb regex and its refusals are deleted; its dsmClient overrides now only simulate.

Practice Add Contact answered { ok: true }, which the contacts store read as a refusal, and it stored one fixed practice id for every device. It now answers AddContactResult with the card's real ids. A contact added with no alias gets the name Rust gives it.

The tour, 79 steps (up from 57):

  • Removed: the five offline-send steps and the Bitcoin mention.
  • Fixed: the lock step no longer mentions a fingerprint unlock; storage has three tabs, not two; burn/forget are no longer described on a card that has neither.
  • Wallet: recent activity, wallet identity, the History tab, and the inbox (your real one, read-only).
  • Tokens: the creation wizard (open, walk through, close), Add Token by anchor, burn and forget, sharing a token by its anchor and QR.
  • Trade: what you pay and get, the quote and the trade; a liquidity vault's reserves and fee, closing a vault, relaying.
  • Contacts: hands-on add-by-code with a practice contact, bob, plus opening a contact and its facts.
  • Storage: the DLVs tab.
  • Settings: lock setup, ring backup, recover.

Bob's code comes from Rust. The dsm_sdk test the_tour_practice_contact_code_is_one_rust_reads builds his card on the beta network and requires read_contact_code to read it back. It also keeps components/tour/practiceContact.ts equal to Rust's encoding (DSM_WRITE_FRONTEND_FIXTURES=1 rewrites it). In the tour, pasting the code runs the real contacts.readContactCode, a read the sandbox lets through; adding bob is practice mode's job. A tour step can now carry text, shown selectable with a Copy button.

Rebased onto #1096. Practice sends name their recipient by device id, as the real sendOnlineTransferSmart now does: the simulated send takes PRACTICE_CONTACT_DEVICE_ID, and recordSend matches on deviceId only. This was agreed with the SoFi/#1096 session.

Tests

  • bridge/__tests__/practiceGate.test.ts covers the acceptance list:
    • practice on: the direct imports burnToken, sofi.trade and sofi.resolve are blocked, as is a raw ingress frame; the bridge receives nothing;
    • dsmClient.burnToken and forgetToken are blocked; a practice send crosses only wallet.amount reads;
    • a read crosses and Rust answers it;
    • ui_theme crosses, while the lock and diagnostics preferences don't; the camera crosses, an NFC write doesn't;
    • practice off: the same writes reach the bridge;
    • leave() ends the sandbox.
  • tour.test.tsx:
    • the data-tour anchors are exactly fourteen, and each exists in the app;
    • every target, wait and back-out selector names only attribute values, classes and ids that the screens or the shell render. This is a static check against the source, not a DOM render;
    • no step teaches offline sending or Bitcoin.
  • practiceMode.test.ts: the Add Contact shape and ids, and the no-alias name.
  • Mutations, each reverted after the run:
    • no gate in the registry turns 5 named tests red;
    • token.burn on the read list turns 2 red;
    • renaming a targeted class turns 1 red (step wallet-identity: …);
    • Bluetooth back in a step turns no step teaches offline sending or Bitcoin red.

Verification (local, combined head f92a91a on e736dbc)

  • Frontend: tsc 0, lint 0, jest 125 suites / 906 tests, 0 failed.
  • dsm_sdk, release: the identity routes, wallet_amount and ingress record tests pass, 7/0. Both frontend records still match live Rust after security: beta pre-audit fixes (combined CORE, STORAGE, SOFI) #1096's proto change.
  • clippy -p dsm_sdk --lib --tests -D warnings: 0. rustfmt check: 0.

For the owner: one more baseline entry

This branch adds one guard line: the real AddContactResult shape in the practice Add Contact.

line rule hash text
components/tour/practiceMode.ts:263 bool-literal 55012bf03379d80f return { accepted: true, contactId, alias };

Path prefix: dsm_client/frontend/src/. Main's frontend still reports #1095's 21 lines until main records them. Three of those are in this file (now lines 93, 215 and 227), with unchanged hashes.

Notes

  • Code map / Pin evidence: the branch touches identity_routes.rs and screen files, so pins over those may read stale. I'll refresh them from this PR's CI map.
  • During the tour, the TRADE pickers are empty because the practice tokens carry no policy anchor, so the TRADE steps explain rather than trade.

cryptskii and others added 7 commits October 1, 2026 23:03
…names

Practice mode swapped dsmClient's properties and refused the ones whose names
matched a list of verbs. A screen that imported a write directly never saw the
swap: TokenCreationDialog's createToken (a real token, the real ERA fee
burned), AccountsScreen's burnToken / forgetToken / addTokenByAnchor, every
SoFi action (trade, route, createVault, close, resolve, relay), the lock's
configure_lock and the NFC and recovery services all reached the real wallet
while the tour ran. The verb list also missed trade, route, resolve, relay,
enable, activate and complete.

Owner ruling 2026-10-01: "make the bridge-level practice gate the authoritative
safety boundary, and treat the current dsmClient interception as convenience
behavior only… While practice mode is active, no state-changing Rust call
reaches the real wallet unless it is explicitly designated as a practice-safe
simulated action."

- bridge/practiceGate.ts: while the sandbox is on, every request is read
  before it leaves. Only named reads cross: bridge methods, router routes
  (queries and the two SoFi reads the router takes as invokes), the camera,
  and the display preferences ui_theme / sfx_enabled. Everything else,
  including a request that cannot be read and a raw sendMessageBin frame
  carrying an ingress or host request, is answered "blocked in practice mode"
  in its channel's wire shape and never reaches Rust or the host.
- bridge/BridgeRegistry.ts: getBridgeInstance() is the practice view while the
  sandbox is on. Every path to native code (callBin, the ingress, host
  requests) takes its bridge from here.
- practiceMode: enter/leave switch the sandbox. The dsmClient overrides keep
  only the simulated answers; the verb regex and its refusals are deleted.
- The practice addContact answered { ok: true }, which contactsStore read as a
  refusal, and stored a fixed practice id for every added device. It now
  answers AddContactResult for the card Rust read, with the card's ids, named as
  Rust names a contact added with no alias.
- tests/helpers/rustIngressRecord.ts: the record-answering bridge, now shared,
  logs what reached it.

Tests (bridge/__tests__/practiceGate.test.ts):
- practice on: direct imports burnToken, sofi.trade and sofi.resolve are
  blocked, and a raw ingress frame too; the bridge receives nothing;
- a read (wallet.amount) crosses and Rust answers it;
- ui_theme crosses, lock and diagnostics preferences do not;
- the QR camera crosses, an NFC write does not;
- practice off: the same writes reach the bridge;
- practiceMode: dsmClient.burnToken / forgetToken are blocked, a practice send
  crosses only wallet.amount reads, and leave() ends the sandbox.
…or Bitcoin

Owner ruling 2026-10-01: the tour "needs to be expanded… cover everything…
Except for Bitcoin for now, and the offline for now", with walkthroughs that
highlight the real UI and keep the action blocked where Rust offers no
practice route, and hands-on add-contact-by-code.

Removed: the five offline-send steps (mode, go-offline, offline-funding,
appliance, go-online) and the Bitcoin tab in the wallet-tabs text.
Fixed: the lock step named a fingerprint unlock the app does not have; the
storage step said two tabs where there are three; token-detail described Burn
and Forget on a card that has neither.

Added, 79 steps from 57:
- Wallet: recent activity, wallet identity, the History tab, the inbox (the
  real one, only read) and closing it.
- Tokens: the creation wizard opened, walked through, and closed; Add Token by
  anchor; Burn and Forget; sharing a token by its anchor and QR.
- Trade: what you pay, what you get, the quote and trade; a liquidity vault's
  reserves and fee, closing a vault, relaying a fulfillment.
- Contacts, hands-on: copy bob's code, paste it, Add; open a contact and its
  facts and stitched receipts.
- Storage: the DLVs tab's liquidity vaults.
- Settings: the lock setup screen, the ring backup and the recover screen.

Bob's code is Rust's. The dsm_sdk test
the_tour_practice_contact_code_is_one_rust_reads builds his card on the beta
network, requires read_contact_code to read it back, and holds
components/tour/practiceContact.ts equal to its encoding
(DSM_WRITE_FRONTEND_FIXTURES=1 rewrites it). In the tour, pasting it runs the
real contacts.readContactCode, a read the sandbox lets through, and Add is
practice mode's.

- GuidedTour: a step can carry text the user needs, shown selectable with a
  Copy button.
- Anchors where no stable selector existed: recent-activity, add-token,
  swap-pay, swap-get, liquidity-create, liquidity-close, contact-code,
  contact-list, liquidity-vaults; and the classes wallet-identity, sofi-relay.
  The DLVs anchor wraps the vault section in every state, so the step does not
  wait forever on a wallet with no vaults.

Tests (tour.test.tsx):
- the steps' data-tour anchors are exactly the fourteen, each in the app;
- every target, wait and back-out selector names only attribute values,
  classes and ids the app's screens or the shell render;
- no step teaches offline sending or Bitcoin, or targets their anchors.
practiceMode.test.ts:
- Add Contact answers AddContactResult under the card's ids;
- with no alias, the contact is named as Rust names it.
clippy -D warnings refused the `..Default::default()` in the tour practice
contact test: the literal already names every ContactQrV3 field.
…ce id

After #1096 an online send names its recipient by device id, never by
alias, and practice mode finds the contact the same way. The sandbox test's
practice send now goes to PRACTICE_CONTACT_DEVICE_ID.
…olds

The real-code guard flagged `accepted: true`, a literal. The answer is now
whether the practice wallet holds the contact after the add, as the real
answer reports the contact stored. The tour suites pass (146).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
One conflict, practiceMode's imports: this branch's encodeBase32Crockford
and main's routerQueryBin and decodeFramedEnvelopeV3 (practice ERA's
protocolDefined from Rust's balance.list, #1098). Both kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
…1098

Since #1098, practice ERA's protocolDefined is Rust's balance.list row, so a
simulated practice send reads balance.list as well as wallet.amount. Both
are reads the sandbox lets through; nothing else crosses the bridge.

Full frontend suite: 907 passed. tsc, eslint and the real-code guard clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
@cryptskii
cryptskii merged commit e607197 into main Oct 2, 2026
23 checks passed
cryptskii pushed a commit that referenced this pull request Oct 2, 2026
No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
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.

2 participants