Skip to content

Modern and DGen wallet looks, Simple mode, contact cards and email receipts (DSM Amendment A17), v0.1.0-beta.4 - #1119

Merged
cryptskii merged 34 commits into
mainfrom
feat/simple-wallet-skin
Oct 10, 2026
Merged

cryptskii merged 34 commits into
mainfrom
feat/simple-wallet-skin

Conversation

@cryptskii

@cryptskii cryptskii commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Two looks for the wallet on the same data and sends: Modern (new; flat, black/white/gold/fuchsia, light and dark) and DGen (the Game Boy). Both are full-featured. Simple mode is a Modern-only switch that keeps things clean for people new to Web3. Also contact cards linked to phone contacts, photos, opt-in email receipts (DSM Amendment A17), a beta agreement, and the 0.1.0-beta.4 version. Owner rulings 2026-10-09.

First run

  • Beta agreement. The first screen. Seven points, each with its own checkbox, and "I agree" at the bottom. It covers test value only, no real crypto, and the experimental status. Stored as beta_agreement = beta-agreement-2026-10-09.
  • Look picker. A small fixed box, the same in every look: Modern / DGen, Dark mode and Simple mode switches, OK. Everything behind it previews live. It notes that DGen's SELECT changes the screen colour and backlight. Both choices can be changed later from Settings in either look.

Modern

  • Wallet tab. Your banner, photo and name at the top. A tokens card with every holding (not just ERA/dBTC) and a Tokens/Objects switch like DGen's. "Add or create a token" and a features grid.
  • Send. Any held token. Review Send says what is still missing. A comma counts as the decimal point.
  • Your card. Name, email and phone. Profile photo and banner, framed with drag and pinch/slider/wheel zoom. Saving shows a "Saved" pop-up and returns to the wallet.
  • Your code. A QR plus the full written code with Share and Copy, wherever someone adds you (Receive, My card, Add contact).
  • People. Bluetooth pairing status per contact and a pairing banner. Refreshes every 5 s and on pairing events. Photos for any contact, from the phone's contacts or picked and framed.
  • Every DGen screen is reachable. Screens without a Modern version open with a Modern Back button, and the shared parts, dialogs, toasts and sheets are restyled under html[data-skin="modern"].
  • No animations. Plain pop-ups replace the FX scenes. Tokens show flat marks (the coin mask, an image, or a letter) instead of spinning coins.
  • Simple mode. Hides SoFi/trading, the Bitcoin bridge, storage nodes and developer screens. A route guard refuses them too. Contacts, tokens with the faucet, and Apps stay. Offline payments show only when their switch is on.

DGen

  • The contact card (My contact info) and email receipts, with the same consent page.
  • Settings shows Version, the look and the tour.

Contacts and your card (A17)

  • Details. ContactProfileV1 (name, email, phone, phone lookup key) is held in the contact row. contacts.setProfile sets it, and contacts.list and contacts.addManual carry it.
  • Your card. contacts.ownProfile and contacts.setOwnProfile. The contact code carries your name, email and phone.
  • Android. The system contact picker, with READ_CONTACTS asked only when used; it reads only the picked contact. The photo is read by lookup key for display only.

Email receipts (A17, outside the protocol)

  • When. After a sent payment, only when receipts are on and the person has an email.
  • How. receipts.email builds the request, the device's AK signs it under DSM/receipt-email, and the SDK posts it to the env config's receipt_service_url.
  • The service. New crate crates/dsm-receipt-service:
    • It checks that the request is canonical, then the fields and the signature.
    • It rate-limits per device and per address.
    • It emails one fixed, escaped template over SMTP.
    • It answers with the address masked and keeps no record.
    • A Dockerfile and README sit beside it.
  • Needs from the owner: a sending domain with SPF and DKIM, SMTP credentials, and the deploy. Until then the wallet says "this network has no receipt service".

App-wide

  • Phone bars. In Modern the page sits between the status bar and the navigation buttons: the root is padded by the insets and the contrast scrim is off. DGen stays edge to edge. setSystemBars takes light | dark | device.
  • Version. "DSM v0.1.0-beta.4 Pre-release" in Settings and on Home, read from the Android versionName at build time. Crates, lockfiles and the frontend are bumped to 0.1.0-beta.4.
  • One send path for both looks. domain/sendTransfer. Online sends return the transfer's hash.

CI fixes riding along

  • Bridge gate. Contact photos are encoded with FileReader, with no btoa/fromCharCode.
  • Faucet claim. When its own release is held by the leader, the claim rewrites and walks on with backoff instead of failing. The walk backs off too.
  • Escrow e2e test. It counts the poll's GET reads only, so the test also passes on main.
  • Merged with main. Domain tag count 403. The Explainer pin is updated.
  • SPHINCS+ caller inventory regenerated for the receipt signer.
  • Intent pins re-pinned against this head's board.

Not in this PR

  • Setup screens and the guided tour stay in DGen style. The tour runs only in DGen.
  • Offline payments still say Under construction. The phone build does not yet include the anchor transport.
  • The receipt service does not read the device directory: a signature proves a device asked, not that it is in the directory. This is recorded as open in §6.83.
  • No local-currency figure: there is no rate source.

Verification

  • Board. cargo test --locked --workspace --exclude dsm_storage_node --release. At a0f8245: 2987 passed, 0 failed (33 binaries). The storage node cargo test -p dsm_storage_node --release gives 91 passed, 0 failed.
  • Lint and checks. make lint is clean. The frontend suite passes. The receipt service passes 7/7, including an end-to-end run over a real SMTP exchange.
  • Gates. The real-code guard, the bridge contract gate, ci/conformance_evidence.py (MR-DSM-0322..0325, §6.83, matrix rows) and the clock gate pass.
  • Mutations, each red:
    • the contact code without the card;
    • the service's signature check skipped;
    • the route guard letting SoFi through in Simple mode;
    • the offline switch shown regardless.
  • Visual. Modern rendered at phone width, light and dark, with Simple on and off. Installed and checked on the test phones.

… Amendment A17)

What the Simple wallet shows for a person, and the receipt it can email.
Display and courtesy only: no validity check reads any of it.

Contact details:
- ContactProfileV1 holds a name, email, phone, and the lookup key of a
  linked phone contact.
- A contact's details live in its row's metadata.
- contacts.list answers them; contacts.addManual takes them;
  contacts.setProfile replaces them.

Your own card:
- Kept in settings and read and set through contacts.ownProfile and
  contacts.setOwnProfile.
- No card yet is answered as an own_profile state with no value.
- The contact code carries the card's name (preferred_alias, empty until
  now) and its new email and phone fields.

Email receipts:
- receipts.email builds a ReceiptEmailRequestV1 from the contact's email
  and the owner's name.
- The device's AK signs H(DSM/receipt-email ‖ the request with an empty
  signature).
- The request is posted to the env config's receipt_service_url. A network
  with none says so.
- New domain tag DSM/receipt-email; the tag count is 397.

Tests (database-backed):
- details are kept with the row, replaced whole, and refused for no
  contact or a bad email;
- the owner's card reads back with no phone key, and the contact code
  carries it;
- a receipt request names the contact's email, is signed by the device,
  and stops verifying when redirected;
- no email means no receipt.

Mutation: building the contact code without the card makes its test red.
…r the skin

For the Simple wallet (DSM Amendment A17):
- pickPhoneContact opens the system contact picker after asking for
  READ_CONTACTS (declared, and asked only then). It reads only the picked
  contact: name, first phone, first email and lookup key.
- The pick reaches the page as a PHONE_CONTACT_PICKED host event, with no
  bytes when nothing was picked or the read was refused.
- phoneContactPhoto reads a linked contact's photo by lookup key, for
  display only.
- shareText opens the share sheet with the user's own contact code.
- setSystemBars colours the status and navigation bars light or dark for
  the skin in use. The legacy bar painting moves into one helper shared
  with onCreate.
Owner rulings 2026-10-09. A second skin for people new to Web3, drawn on
the same data and the same sends.

The skin:
- Chosen once when the wallet is first ready (default Simple) and
  switchable in Settings, from either skin.
- `html[data-skin="simple"]` puts the device chrome away and the app fills
  the screen, light or dark.
- The phone's bars follow through setSystemBars, asked only on a change.
- Choices are bridge preferences: ui_skin, ui_scheme, simple_offline,
  receipts_email.

Tabs (Wallet / People / Activity / Settings):
- Wallet: the protocol currency's balance; for a new wallet, its welcome
  claim instead of a made-up zero. Send, Receive, People, recent payments.
- People: contacts named by their details, Send beside each, and adding
  someone by scanning their code, then picking them from the phone's
  contacts or keeping the card's name.
- A contact's page: edit details, or link a phone contact.
- Activity: payments only, filtered by direction.
- Settings: the look, offline payments, email receipts behind a consent
  page that states what is sent, your card, the lock, and Report a problem.

Pages are browser history entries, so the phone's back button closes them.

What Simple leaves out:
- A navigation guard refuses sofi, accounts, tokens, vault, storage,
  dev_policy, recovery_pipeline and bluetooth while Simple is the skin.
- Activity lists no SoFi, token or vault rows.
- The offline switch shows only when the owner turns it on, and still
  says Under construction.

One send path for both skins: domain/sendTransfer. The Classic send screen
uses it, and online sends now return the transfer's hash, which receipts
name. After a sent payment, Simple emails a receipt only when receipts are
on and the person has an email.

Tests (no test doubles):
- what Simple shows and leaves out;
- the welcome claim with no zero;
- People to a contact and back through history;
- Activity filters;
- the offline switch;
- the receipt promise only with consent and an email;
- the route guard in both skins;
- the skin choice asked once, after the preferences are read.

Mutations: a guard that lets sofi through, and an offline switch shown
regardless, are each red.

Frontend 915/915, lint, tsc and the real-code guard clean.
…ice signed

DSM Amendment A17, outside the protocol. New crate
crates/dsm-receipt-service (axum, lettre over SMTP).

POST /v1/receipt takes a ReceiptEmailRequestV1. The service:
- refuses it unless it is canonical, every field is within its bound and
  free of control characters, the email is an address, and the sender
  AK's signature over H(DSM/receipt-email ‖ the request unsigned) verifies;
- holds each device and each recipient to a rate over a window, in memory;
- emails one fixed text and HTML receipt, with the HTML escaped;
- answers where it went, masked (j…@example.com).

It keeps no record of who was emailed and logs no address.

Open, stated in the README: the signature proves a device asked, not that
the device is in the network's directory.

Configured by environment (bind, SMTP URL, from, rates), behind an HTTPS
proxy; Dockerfile and README beside it. Sending needs a domain with SPF and
DKIM and SMTP credentials from the owner.

Tests:
- end to end over HTTP and a real SMTP exchange with a listener that keeps
  each message: a signed request is emailed once and answered masked;
  unsigned, wrongly signed and redirected requests send nothing; a device
  over its rate is refused and nothing more goes out;
- unit tests: the template escapes markup, addresses are masked, a
  recipient's rate clears after the window, and fields are checked.

Mutation: skipping the signature check makes the unsigned and redirected
test red.
The tour walks the Game Boy's menus and buttons, which the Simple skin does
not have. It runs while Classic is the skin in use, and is offered once
Classic is the owner's choice, so it never shows beside the skin choice.
…l receipts

Owner rulings 2026-10-09.
- The explainer gains A17.
- MASTER gains MR-DSM-0322..0325 (DSM 56, 1,038 in all), with the
  explainer re-pinned.
- CONFORMANCE gains §6.83 with its §8 rows.
- The matrix gains two rows naming the tests and mutations.
- ci/conformance_evidence.py knows the new crate, dsm_receipt_service.

The requirements:
- details are display only;
- the contact code may carry the card's fields and establishes nothing;
- a receipt is asked for only with consent and an email, is signed by the
  device, and is sent only within the rates;
- the service decides nothing and keeps no record.

Open, as recorded: the service does not read the device directory, and no
service is deployed until the owner names a sending domain and SMTP
credentials.
… its surface

Seen in a render of the Simple screens at phone width: an SVG with no size
filled the People card, pushing its text out, and a card row lost its
background to the row style in dark.
…ad without a default

make lint (Rust 1.98): rustfmt wrapped one line differently from 1.96, and
clippy's map_or_identity flagged the status mapping. An out-of-range status
is now answered as a 500 naming the error, not mapped to 400.
…s before anything

Owner, 2026-10-09: the choice should come before anything else, and setup
should not switch from the Game Boy to the Simple skin partway through.

The choice:
- It is read as soon as the bridge is up, not when an identity is ready:
  the preferences are the app's and hold before any wallet exists.
- It is asked first, over whatever is on screen: a first launch's intro or
  setup, or a wallet set up before the choice existed.
- The page keeps the chosen skin as a hint, so the first paint after a
  restart is already in it; the preference stays the authority.

Simple draws every phase itself (SimpleBoot):
- starting up;
- a welcome with "Create my wallet" and "Restore from my recovery ring";
- the recovery phrase (SimplePhrase, the same store and checks as the
  Game Boy's);
- publishing, securing (as progress), the PIN lock, and errors;
- the ready wallet.

The Game Boy's intro waits for its A button, so Simple shows no intro. A
wallet locked with a button combo is entered on the Game Boy's buttons, so
its lock screen still shows the device. Recovery stays reachable in Simple.

Tests: the skin of every phase, the welcome screen's two ways in, securing
as progress, and the phrase read, checked and a wrong pick caught.
Frontend 919/919.
…picks the colours too

Owner, 2026-10-09: the Simple-styled panel laid over the Game Boy's intro and
setup showed two looks at once.

- A phone that has not chosen sees only SkinChoiceScreen, on the Game Boy
  every phone starts in: SIMPLE · LIGHT, SIMPLE · DARK or CLASSIC.
- It is touch and D-pad driven, and A cannot fall through to INITIALIZE.
- Picking sets the colours, then the look, so the whole app switches at
  once.
- The intro plays only once Classic is the choice. A first launch waits on
  the loading screen until the choice is shown, with no cutscene before it.
- The overlay panel is deleted.

Tests: the choice is all a phone with none sees (no intro, no INITIALIZE),
and each option sets the look and its colours.
The intro plays once Classic is the owner's choice (the previous commit).
These three tests are about the Game Boy's intro, so their runtime store
says Classic is chosen. I pushed that commit with these three red; this
makes the suite whole again: frontend 920/920.
…read

On the phones the choice screen reused setup's brick menu, which fits one
short word: the three labels sat small at the top of each brick, half
covered, and the screen looked like setup's INITIALIZE with its buttons
swapped. It is now its own titled screen (ScreenFrame, 'Choose Your
Wallet'): a line saying the choice comes before setup and can be changed
later, then a full-width button for each look (Simple · Light, Simple ·
Dark, Classic) with a line under it saying what it is. Touch and the D-pad
work as before. Frontend 920/920.
…ook picker

The second skin is Modern, not Simple: it reaches everything the DGen Game
Boy does (tokens and the faucet, trading, the Bitcoin bridge, storage, apps,
scanning, every setting), opening the DGen screens inside itself. Simple mode
is a Modern-only switch that puts trading, the Bitcoin bridge, storage and the
developer tools away; tokens, people and apps stay.

- The look picker is a small fixed black box over the app: Modern or DGen,
  Dark mode and Simple mode switches, a note on DGen's SELECT colours, OK.
  The app behind it is drawn in the look picked; nothing is kept until OK.
- The Modern wallet lists every token held (created ones included), sends
  any of them, and has a grid for the rest of DSM.
- styles/modernKit.css draws the shared DGen parts (screens, cards, buttons,
  fields, tabs, every pop-up and dialog) in Modern while Modern is the skin,
  so pop-ups added later are Modern without a second copy.
- DGen gets the contact card (on My QR), the phone-contact pick when adding
  someone, and email receipts (Settings permission, receipt after a send).
- The version shown is the APK's own versionName, read from the Android build.
…hsia in both schemes; still token marks

- Light and dark now share the four colours: fuchsia for Send, the active
  tab, selected segments and highlights; yellow-gold for Receive, the balance
  card, chips, the current step and money received; black and white beneath.
  Light had no gold and dark no fuchsia.
- The Modern skin draws no spinning coins: a token is its policy icon or a
  still letter badge in fuchsia or gold (FlatTokenMark). TokenMark, TokenCoin
  and the screens that drew the ERA and dBTC GIFs directly (CoinArt) all
  follow; the DGen Game Boy keeps its coins.
… animated scene

A cue (sent, sealed, not sent, ready, …) in the Modern skin is a plain
sheet: its title, the amount, its words and OK, closing on its own after a
moment unless it reports a failure. No scene, no replay. The DGen Game Boy
keeps its animations. The two share how a cue closes and takes focus.
…e code, wherever you are added

Receive showed the QR with only the code's last sixteen characters, and My
Card and Add Contact showed neither, so nobody could add you from Modern.
ModernMyCode is the QR, the whole code written out to select and paste, and
Share and Copy; Receive, My Card (read again after the card is saved, since
the code carries it) and Add Contact all show it.
… discs, Game Boy traces removed

- Token badges drew an empty circle: the letter was sized against the badge
  itself, which a container query cannot do. The letter now sits in its own
  element, sized by the badge.
- A coin that only decorated a card (the faucet, adding a contact) is left
  out in Modern, disc and all (HeroCoin); still marks stay where they name a
  token.
- DGen screens inside Modern: Back sits in the title row, the D-pad's dashed
  cursor is gone, Refresh and Inbox get Modern glyphs, the "i" badge and the
  small labels take Modern sizes.
- Button and title text the Game Boy capitalises in CSS is written in
  sentence case (Claim faucet, Confirm, Version, Developer options,
  Authentication required), so DGen looks the same and Modern stops shouting.
…rm dialogs and the toast lose their Game Boy parts

- Review Send no longer sits grey without a reason: tapped while something
  is missing it says what (who, a token, an amount above zero), and lights
  once nothing is. A comma is taken as the decimal point, and one point kept.
- The under-construction, confirm and incoming-transfer dialogs drew their
  message in a green dashed Game Boy box under a green rule inside the Modern
  sheet. Every part of them (message, info rows, rules, status card, progress
  steps, spinner, button hover) is Modern while Modern is the skin, as is the
  toast at the bottom of the screen, whose colours are set inline.
…ed by who the wallet is

- My Card: tap the banner or the photo circle to choose a picture from the
  phone; each is cropped to its shape (banner 3:1, photo square), kept as a
  small JPEG, and can be removed. They stay on this phone as app
  preferences: a contact code is a QR and carries only the name, email and
  phone.
- The Wallet tab opens on the owner's banner, photo and name (tap for your
  card) instead of "DSM Wallet" over a "Wallet" title; no banner is a gold to
  fuchsia gradient, no photo the name's first letter. The top bar's card
  button on the other tabs shows the photo too.
- Modern only; the DGen Game Boy is unchanged.
…follow the look on screen

- A created token's artwork is a logo shape (dsm:coin:v1 mask), not a
  picture: Modern drew it raw or fell back to the DGen coin. The badge now
  draws that shape in white on its fuchsia or gold; a linked picture is shown
  as itself; a token with neither shows its letter.
- Behind the first-run picker the app previews a look before one is kept;
  token marks, coin art, cue pop-ups and decorative coins asked only for the
  kept look, so a Modern preview drew Game Boy coins. They follow the look on
  screen (skinShown).
- The picker's D-pad ring follows a tap, so it is never on one look while
  another is picked.
…r; Tokens / Objects switch in Modern

- The first screen of a phone that has not accepted it: the beta agreement.
  Seven points (experimental software; no real-world value; never send real
  Bitcoin or anything of value; resets; the recovery phrase is the only way
  back; no advice; as is, at own risk), each ticked on its own; "I
  understand and agree" works once all are. Kept under its version, so new
  points ask everyone again. It comes before the choice of look and the
  intro, in its own plain look. Plain-language acknowledgements, for counsel
  to review before a public release.
- A chosen photo or banner opens a framing screen: drag to move it, pinch,
  the slider or a mouse wheel to zoom; the picture always covers the frame
  (a circle for the photo, a 3:1 band for the banner); Save keeps exactly
  what the frame shows. The picked file is held only while framing.
- The Modern wallet's token list has the DGen wallet's switch: fungible
  tokens, or the one-of-a-kind objects (creatures, items).
…saving the card is a pop-up and back to the wallet

- From Android 15 the page ran under the status bar and the navigation
  buttons, and the bars stayed dark: Modern's tab bar and the agreement's
  last lines sat under the buttons, with a grey band over Modern light.
  setSystemBars takes `light` / `dark` (Modern, and the agreement page) or
  `device` (the DGen Game Boy): Modern is kept between the bars by their own
  heights, the bars in the page's colour (white, or near-black); the Game Boy
  runs edge to edge under dark bars as before.
- Saving My Card no longer rebuilds the page under the owner (the code was
  read again and a status line came and went): it says "Saved" in a pop-up
  and goes back to the wallet. A new photo or banner is kept quietly, the
  header showing it; only a failure is said.
…which the phone already keeps below the bar

The WebView still reports the camera cut-out as a top inset after the page
is placed below the status bar, so Modern and the agreement page left a
second, empty band above the banner.
On a contact's page, tap their picture to choose a photo and frame it (move
and zoom, as for your own); Remove photo takes it away. It is kept on this
phone, like your own photo (a contact code carries no picture), and shows
wherever the contact does: People, Send, Activity and their page. A photo
set here comes before the linked phone contact's photo, which comes before
the initial.
…ey scrim behind them

- As at the top, the WebView still reports the button area after the page is
  placed above it, so Modern's tab bar, sheets, framing screen and the
  agreement left an empty band over the buttons. The phone's own placement
  is the only inset now.
- Behind three-button navigation Android 15+ lays a grey contrast scrim:
  Modern light showed a grey strip under its white page. The scrim is off
  while Modern (or the agreement) shows; the Game Boy keeps it.
…ontacts screen does

Pairing runs on its own (Rust's loop, while the app is open with Bluetooth on
and permitted), but Modern never showed it: no reading the list again while
open, no "looking for your contacts nearby", nothing on each contact. People
now reads the list on opening, every five seconds and on every pairing
status event; shows where pairing has got (looking nearby / connected,
exchanging identity); and names each contact's pairing (Paired, Pairing…)
with the Bluetooth mark. A contact's page says it in full. The words are
shared with DGen's contacts screen (domain/pairing.ts).
…hrough a binary string

The bridge contract gate refuses binary-string adapters on bridge payloads;
phoneContactPhoto built one to base64 the photo bytes. FileReader encodes
the bytes as a data URL directly.
# Conflicts:
#	dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/mod.rs
#	specs/requirements/MASTER_REQUIREMENTS.md
…status test counts reads only

- A claim whose release held the reserve cell's leader link, before the
  chain had three links, returned "retry with the same bytes" to its caller
  (a wallet's first claim could fail so; on CI the two-device fixtures'
  funding panicked on it). The claim now continues that chain along the
  remaining route (storage spec §9 rule 8), as the walk does for any release
  it finds there, and walks again; the next attempt finds the release final
  and keeps it as the claim's. Both continuations pause a moment, longer
  each attempt, so the members carry the chain on before the next walk.
- a_collects_status_is_read_once_and_answered_from_its_record asserted that
  a poll reads nothing again, but counted every request a member received,
  index appends (POSTs) included: an append another step landed during the
  poll failed it on main's CI. It counts cell and index GETs, the reads.
The Android app already ships as 0.1.0-beta.4; the Rust crates, their path
dependencies and lockfiles, and the frontend package said beta.3.
The receipt service and the SDK's email receipts sign and verify with
SPHINCS+, and a test moved by one line; the Lean 4 proofs job checks
this inventory is current.
The beta.4 version bump renamed every rust-analyzer symbol
(dsm 0.1.0-beta.3 -> 0.1.0-beta.4), moving all 631 pins. Each symbol
move was checked to differ by the crate version alone; the evidence
passed in the full board (2987 passed) and the storage-node run (91
passed) at a0f8245.
@cryptskii cryptskii changed the title The Simple wallet skin, contact cards and email receipts (DSM Amendment A17) Modern and DGen wallet looks, Simple mode, contact cards and email receipts (DSM Amendment A17), v0.1.0-beta.4 Oct 10, 2026
@cryptskii
cryptskii merged commit 41d70a0 into main Oct 10, 2026
27 checks passed
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