Seedelf-Wallet

Chunk 26b · The wallet’s side of the data layer

Step 4 of chunk 26: Seedelf Wallet reads mainnet through seedelf-data, falling back to Koios one part at a time. Preprod doesn’t change.

Status: ✅ done, in 1.4.0 (#290; built, reviewed and checked 2026-10-09 on web-wallet/data-layer-wallet, from main after #289). Archived at 1.4.1, its open items carried into post-release-roadmap.md’s Kept in mind. Steps 1 to 7 and the developer docs are done; what changed from the plan is at the end. The free checks and the review are in Verification; the owner’s own-wallet parity and everyday spends are what’s left.

Read first:

The VPS comes after, and nothing here waits on it

The owner’s call (2026-10-09): the VPS comes last, on DigitalOcean. It gets a write-up of its own, with a manual deploy action.

The owner’s calls this builds on

From chunk 26 (2026-10-08):

From 2026-10-09:

How it fits together today

All paths in this section are under extension/src/.

Choices made here (the owner can overturn any)

  1. One origin constant, with an override for dev and e2e.
    • networks.ts gets data?: string on mainnet only, left empty until the VPS chunk.
    • VITE_DATA_ORIGIN overrides it in dev and e2e builds, through a define.
    • scripts/package.mjs refuses to package a store build made with the override, as it refuses one without Koios.
  2. The fallback lives in the factory, per method, per part.
    • The closure at sw.ts:173 becomes a factory that returns today’s Koios on preprod, with no origin, or with the switch on.
    • Otherwise it returns a wrapper with the same methods. Each method runs on the data layer, and on a failure that calls for it, runs again from its start on Koios.
    • So a paged read never mixes pages from two backends.
    • The failed part stays on Koios for 5 minutes, remembered in chrome.storage.session as Koios’s 429 hold is.
  3. The private index’s rows become UtxoResponses in TypeScript, so no builder changes. A new adapter fills:
    • payment_cred from the route (contract or mix box);
    • epoch_no from the slot (Shelley’s 432,000-slot epochs);
    • block_time from created.time;
    • block_height as 0, since nothing on the private side reads it once the catch-up cursor is gone;
    • is_spent as false;
    • inline_datum.bytes from datum;
    • a reference_script when script is set;
    • each asset’s fingerprint, worked out locally (CIP-14, as ui/tokens.ts does when one is missing).
  4. The API adds decimals to each row’s assets. This is a small server change, in this chunk.
    • Why: a token missing from the bundled list gets its decimals only from Koios’s rows today. Without this it would show as an NFT-like whole number.
    • How: both sources fill decimals from the registry file the API already loads, so db-sync and Kupo stay byte-identical.
    • The alternative, asset_info per token, would tell the server which tokens a wallet holds.
  5. Watches read the feed every 5 s while one is open.
    • The answer is shared and kept for a block, so each check costs one unit at the edge and nothing at home.
    • The service learns that someone is watching, as chunk 26’s What the service learns accepts.
  6. Home’s reading never waits for a ticker.
    • readStake gives the pool by ID when no ticker is known on the device, and asks pool_info behind the reading. The next render shows the ticker.
    • This was measured in the first-load pass: a pool nobody looked at in 10 minutes held Home up to 3 s.
    • The first-load pass suggested pool_list for the ticker. Its three pages come to about 600 KB, too much for one ticker on a fresh restore.

The steps

1. The origin, the switch and the CSP

2. The public side and submits, through the factory

3. Home’s pool ticker

staking.ts’s readStake stops awaiting poolRef, as choice 6 says. Its tests and Home’s staking card show the ID until the ticker arrives.

4. The private index: the contract

5. Lovejoin

6. Watches onto the feed

Watch Today On the feed
A private spend (Home) tx_status every 15 s its inputs in spent, by its hash
A move-in, an account-paid mint, an account send to a Seedelf, a session’s way back tx_status its rows in created
A session’s way out tx_status the contract rows it spends, in spent
A Lovejoin chain, a withdraw, a box coming back tx_status every 5 s mix-box rows in lovejoin/since
An expiry tip the answer’s tip.slot

7. Tests

Docs: now, and at the VPS

Verification

All of it is free. There’s no preprod for the data layer, and no real-money tests.

What was checked (2026-10-09)

A dev build against the local API, on home’s mainnet db-sync and Kupo. A Playwright script drove a fresh mainnet wallet through the worker’s UI port and logged every request the extension made.

Check What the extension did
A restore contract/snapshot, one contract/since, names; the account on /api/v1/…; nothing to Koios
A lock, then an unlock one contract/since and names: no snapshot, no row checked again
The Koios-only switch on nothing to the API; the contract from Koios’s scan
The API stopped each part’s first request failed, and that part went to Koios; held 5 minutes, then back on the API at the next reading
A wrong DATA_ORIGINS the browser sees a CORS failure, not a 403, and the wallet reads it as no answer: the same as stopped
db-sync unreachable the private index answered from Kupo; only the public side went to Koios, on its 503s
A 429, the bucket emptied both parts to Koios, held 5 minutes, longer than the 4 s Retry-After
Parity, with no wallet needed the index’s contract at the tip against Koios’s credential_utxos: the same 33 rows, field by field (decimals and times too), and the same 10 names

Scale (wasm/scale.mjs, its numbers):

Left for the owner:

The review (2026-10-09)

Three reviewers read the branch by area (the fallback and submits; the private index, its sealed state and Lovejoin; the watches), then one more read the fixes together. Every fix but Home’s has a test that fails without it.

Found Fixed
Watches compared a cursor’s slot time with the send time less 10 minutes: a send in the first minutes after a reading asked tx_status, a clock running fast could pick a cursor from after the landing, and one stale transaction sent its whole group to tx_status dated by when the wallet had each cursor, against a first try stamped before the submit; the sealed cursor read straight from its record; one with none in hand asks alone
A WebAssembly trap in the ownership check read as “not ours”, which the sealed record then kept for good it stops the read and locks the wallet, as everywhere else
A sealed cursor the server refuses left that wallet on Koios’s scan for good a snapshot again; a refused watch cursor forgotten
After a lost connection, Koios being busy or its node down read as “never sent” maybe sent; “spent” gets the mempool check
The API’s plain-text 413 reached the screen as the network’s refusal the data layer’s own refusal, by status
The API answered 502 (maybe sent) when home never answered the connection 503: Koios at once
Home showed a pool’s ID until the next reading the kept reading asked again 3 s later, asking no one
Both feeds’ cursor notes could overwrite each other one at a time

Left as they are:

What a wallet asks, before and after (mainnet)

  Today, through Koios With the data layer
An unlock a full contract scan, every row checked one shared since, only new rows checked
Every 30 minutes another full scan nothing more
Paying a Seedelf by name the kept view, or a catch-up scan one shared names
A Lovejoin look the whole mix box, plus tx_info per box of unknown origin one shared lovejoin/pool
A private spend’s confirmation tx_status every 15 s the shared feed every 5 s

Left for the VPS chunk

Planned in chunk 26c.

Left for later

Found while checking; none blocks the merge.

What changed from the plan

Built 2026-10-09. Where the build departs from the steps above, and why: