This is how we run and test the extension before a release goes to the Chrome Web Store. The extension lives in ../extension/; its README lists every script.
The web wallet lives on main. seedelf-web-wallet, the long-lived dev branch it was built on, was merged into main on 2026-09-29 (PR #266) and is finished. It held the wallet off main until it worked, so shelving the effort would have left no dead code there; that job is done.
Feature branches and PRs:
main and name it web-wallet/<topic>. Keep the prefix: it reads well in the log, and it’s what tags use.main.
seedelf-platform/.web-wallet/<x>.<y>.<z> — that’s the tag namespace (web-wallet/1.0.0), and a branch and tag sharing a name makes git checkout ambiguous.The old branch:
seedelf-web-wallet; it’s finished and far behind main, and can be deleted whenever.Shared Rust code:
web-wallet/<topic> branch into main too. The builder extraction is the main example.Progress is tracked in roadmap.md, and what comes next in post-release-roadmap.md. The work happens in chunks of about one session each, and each chunk updates the roadmap when it finishes.
Plans: a chunk’s plan lives in plans/ while it’s being built. When the chunk lands, what still holds moves into the design docs (architecture.md, flows.md, privacy.md, keys-and-accounts.md, this file) and the plan moves to docs/archive/plans/, so plans/ only ever holds live specs.
Chrome runs an extension straight from a folder once developer mode is on. There is no store and no packaging.
cd seedelf-web-wallet/extension && npm install && npm run build. This builds the Rust core to WebAssembly and then the extension into dist/. Every build has both networks: mainnet by default, with preprod on the welcome screen and in Settings (see architecture.md). A wallet from before the switch stays on preprod. VITE_ENABLE_MAINNET=false npm run build makes a preprod-only build.chrome://extensions and turn on Developer mode (top right).dist/ folder.After a rebuild, click the reload arrow on the extension’s card. npm run dev rebuilds dist/ on every change, but you still reload the extension by hand.
After packaging, build again. npm run package and npm run package:preprod write a store build into the same dist/: it has no dev key, so Chrome gives it another ID, with empty storage, and package:preprod’s has preprod alone, so the welcome screen has no network to choose and every wallet is a preprod one. Run npm run build before loading or reloading dist/ for everyday use.
Debugging:
Gotchas:
key in extension/src/manifest.ts fixes the ID at jfekiogplaamnceifeehipmomhojngcb.
VITE_STORE_BUILD=true, which npm run package sets), because the store assigns its own ID.On mainnet, every build reads Seedelf Wallet’s own data layer first (chunk 26b, architecture.md): https://mainnet.seedelf.logicalmechanism.io, which answers the store’s ID and the dev build’s. So a plain npm run build reads the live server, and its worker’s network panel shows mainnet.seedelf.logicalmechanism.io where the list below says 127.0.0.1:8099. A dev build can read a local API instead:
seedelf-data/ (its README) with the dev build’s pinned ID allowed: DATA_ORIGINS=chrome-extension://jfekiogplaamnceifeehipmomhojngcb cargo run -p seedelf-data-api. It listens on 127.0.0.1:8099.VITE_DATA_ORIGIN=http://127.0.0.1:8099 npm run build, and reload the extension. Its CSP’s connect-src gains the origin; the manifest asks for no new host.127.0.0.1:8099/seedelf/v1/mainnet/…: a restore’s contract/snapshot, then contract/since/<cursor> and names at each reading, never Koios’s credential_utxos for the contract;127.0.0.1:8099/api/v1/… for the account, staking, governance and submits;contract/since/… every 5 s while Home watches it, and no tx_status for it;DATA_ORIGINS (403s), each part on api.koios.rest within one request, and back on the API 5 minutes later;127.0.0.1:8099.npm run e2e:data builds with VITE_DATA_ORIGIN=https://data.seedelf.test and runs e2e/data-layer.spec.ts against its fake (e2e/data-layer.ts). It writes that build to dist/: run npm run build again for everyday use.extension/.preprod-test-wallet.txt (gitignored): one line, phrase: and 24 words. The live runs restore it. It was funded with 10,000 tADA on 2026-09-24.| Layer | What | How |
|---|---|---|
| Rust | seedelf-crypto, seedelf-core, and the wasm crate |
cargo test. The CLI’s offline integration tests (seedelf-cli/tests/cli/) guard the builder extraction. The WebAssembly’s own JS tests run in Node once it’s built: node --test "seedelf-web-wallet/wasm/tests/*.test.mjs" from seedelf-platform/, as CI runs them. |
| Key derivation | The frozen v1 Seedelf key vectors, and the Cardano account vectors (verified against @cardano-sdk, Lace’s library) |
Checked in Rust, and again from JS through WebAssembly, so both sides agree |
| TypeScript | The manifest, the vault and wallet state, the Koios and giveme.my clients, and the worker’s services and handlers against the real WASM, over recorded preprod answers | Vitest (npm test) |
| End to end, data layer | Mainnet through the data layer’s fake, each part falling back to the Koios fake | npm run e2e:data (a build with VITE_DATA_ORIGIN, then e2e/data-layer.spec.ts), as CI runs it |
| End to end | The built extension in a real browser | Playwright (npm run e2e) launches Chromium with dist/ loaded and drives the side panel’s layout and the full tab. Branded Chrome no longer accepts --load-extension, so it uses Playwright’s Chromium. The dApp connector’s tests (chunk 15) load a copy of the build whose manifest grants the sites from install, because Chrome’s own dialog for an optional permission can’t be answered from automation (withSiteAccess in e2e/support.ts); their dApp is a page served at https://dapp.example/. The harness sets the network before the wallet starts (network, preprod by default, as the fakes answer), so the suite runs on a dev build and on the store’s mainnet build alike. |
| Live reads | The balance scan and the ADA Handle lookup against the real preprod Koios | LIVE_KOIOS=1 npx vitest run tests/live.test.ts; skipped otherwise |
| Probes | Transactions checked against preprod’s node and scripts, submitting nothing | node tests/fixtures/probe-staking.mjs: every staking transaction through Ogmios’s decoder, and an account-paid mint with the rewards through the real policy. probe-governance.mjs does the same for every DRep transaction (register, update, retire, vote), and probe-note.mjs for a public send with a note. The record-*.mjs scripts do the same for the Seedelf spends, and keep what they recorded as fixtures. |
| Live | Real preprod transactions from the built extension, by hand, never in CI | node e2e/live/run.mjs all runs every Seedelf flow in one browser session on the private test wallet, waiting for each to confirm, and prints the hashes. node e2e/live/run.mjs staking stakes, delegates the vote and changes pool; withdraw-rewards and unstake wait until rewards arrive. run.mjs also takes single flows: mint live-1 account + move-in 25.5. |
| Manual | What a script can’t see, before each release | The preprod checklist |
These hold for every change to what the wallet shows, from one label to a whole restyle. They began as chunk 14’s Rules that still hold (archive/plans/chunk-14-style-flow-2.md); this list is the one that governs now.
background/secret-box/ does.extension/scripts/i18n-critical.mjs): what a privacy or warning callout or an alert shows, and any key named .privacy. or .warn..components/Hint.tsx holds what a feature is and how a transaction runs once sent, in one of three places: beside a screen’s title (Screen’s hint), beside a section’s heading or a field’s label (Hinted), or on a row of its own. A note stays on the page when it says what someone else learns or sees (a privacy note, Koios, giveme.my, Minswap), what an action costs or locks up, what can’t be undone, what to do next, or what’s happening now, and so does an empty list’s line and a one-line note, where an icon saves nothing.font-src 'self', img-src 'self' data:, in extension/src/manifest.ts).RateLimit in extension/src/background/koios.ts); a response holds at most 1,000 rows. The e2e tests assert the exact requests a screen makes.e2e/extension.spec.ts only look inside a part (a phrase’s words, a cut name, a review’s rows). A renamed control updates e2e/extension.spec.ts in the same commit, and e2e/store-images.spec.ts and flows.md too: the owner brought the documentation review ahead of the UX pass on 2026-10-04, on the understanding that the pass keeps the docs current as it goes. CI doesn’t run the store images’ spec, which is how it rotted; tests/store-images-copy.test.ts now fails npm test when English it fills, clicks or waits for is gone from en.json.extension/tests/words.test.ts checks the source, and extension/tests/i18n.test.ts every locale.On the built extension (npm run build, then load dist/ unpacked), in the side panel and in a tab.
cargo test --workspace --locked, the WASM tests, npm test and VITE_ENABLE_MAINNET=false npm test, and npm run e2e. Then LIVE_KOIOS=1 npx vitest run tests/live.test.ts, and node e2e/live/run.mjs all on the funded test wallet. Keep the six hashes.$handle;Read every review and every privacy note as you go.
$handle that doesn’t exist.preprod.koios.rest and www.giveme.my (and Minswap’s preprod aggregator, for a swap, and ipfs.blockfrost.dev for an NFT image you asked to see).npm run package, dist/ loaded unpacked in a fresh profile):
mainnet.seedelf.logicalmechanism.io, www.giveme.my and api.coingecko.com (and agg-api.minswap.org, for a swap, and ipfs.blockfrost.dev for an NFT image you asked to see), plus preprod.koios.rest only for something still on its way on preprod, and api.koios.rest only for a part the data layer couldn’t answer;mainnet.seedelf.logicalmechanism.io, and off, the next reading goes there again. The line under it says Using Seedelf’s data layer. with a green light while it’s off, and Using Koios only. in grey once it’s on.This is the repeatable release path for a new Web Store version, in two parts. The version and the refreshed token and DRep lists are committed on the release branch (web-wallet/release-<x.y.z>), with the checks, and reach main through its PR. The package is built after the merge, from the merge commit on main in a clean tree, so the commit the handoff note records gives the same zip. Change release_version to the new, higher version for each later release.
Which number. The store only demands one strictly higher than the last, so the semantics are for the wallet’s own users: a minor bump for a release that adds something a user can do (1.1.0, the transaction view), a patch for a fix or a listing hotfix on its own (1.0.1). The web wallet’s line runs from 1.0.0 and is deliberately apart from the Rust workspace’s 0.x.y — see the root CLAUDE.md.
From the repository root, on a web-wallet/release-<x.y.z> branch from main:
cd seedelf-platform
cargo test --workspace --locked
cd seedelf-web-wallet/extension
npm ci
release_version=1.5.0
npm version "$release_version" --no-git-tag-version
npm run tokens
npm run dreps
git diff -- package.json package-lock.json src/tokens src/dreps
Read those diffs (steps 2 and 3 of Releasing to the Web Store say what to look for), and commit them together. Then the checks:
npm run build
npm test
VITE_ENABLE_MAINNET=false npm test
npm run e2e
LIVE_KOIOS=1 npx vitest run tests/live.test.ts
node e2e/live/run.mjs all
node e2e/live/run.mjs staking
Then complete the manual preprod checklist, including the dApp, private-session, swap, and Lovejoin flows. If the UI changed, regenerate the store images (npm run store:images), and bring the listing in store/README.md up to date. Commit them, and open the PR into main.
main, after the merge: build and test the store packageFrom the repository root, at the merge commit, with nothing changed or added (git status --porcelain prints nothing):
git switch main
git pull --ff-only
git status --porcelain
cd seedelf-platform/seedelf-web-wallet/extension
npm ci
release_version=1.5.0
npm run package
npm run e2e
sha256sum "release/seedelf-wallet-$release_version-mainnet.zip"
git rev-parse HEAD
No npm version, npm run tokens or npm run dreps here. On main the bump fails (“Version not changed”), and the lists’ refresh rewrites bundled files no commit has, so the zip wouldn’t be the recorded commit’s.
If the release changed seedelf-data/, deploy it before the upload: Actions → Data layer deploy, deploy, on main, approved, then a status run (seedelf-data/deploy/README.md). A wallet the store ships must never meet an API older than the one it was reviewed against.
The package to upload is:
seedelf-platform/seedelf-web-wallet/extension/release/seedelf-wallet-$release_version-mainnet.zip
With package.json at 1.5.0, that file is extension/release/seedelf-wallet-1.5.0-mainnet.zip.
npm run package builds the store’s mainnet build (VITE_ENABLE_MAINNET=true, VITE_STORE_BUILD=true) and refuses one whose manifest lacks https://api.koios.rest/*. The second npm run e2e runs the whole suite against it, with preprod chosen before the wallet starts (the fakes are preprod’s). Then do checklist item 9 by hand on it.
Keep for the release record, with the commit git rev-parse HEAD printed and the SHA-256:
wasm/build.sh’s last line, C compiler: <first line of clang --version>, or C compiler: unknown;scripts/package.mjs’s sha256 <hex>, and the line after it, Zipped with Node <version>, zlib <version>.The next npm run package overwrites a zip of the same version, so take the hash from the zip you upload, never from one rebuilt since.
npm run package:preprod makes a preprod-only store build (-preprod.zip) for tests. Never upload it.
extension/release/seedelf-wallet-$release_version-mainnet.zip._locales: fill in English, then Español and 日本語, each with its own description and screenshots (In Spanish and Japanese).The official Chrome upload flow is also described in Publish in the Chrome Web Store.
The listing’s text, its images and the privacy policy are in store/, laid out by the dashboard’s tabs. The owner of the developer account uploads the package by hand.
Steps 1 to 6 happen on the release branch and are committed in its PR; step 7 on runs on main’s merge commit, in a clean tree. The privacy policy and the wallet’s Settings link point at main, so the merge comes before npm run package (store/README.md).
npm version <x.y.z> --no-git-tag-version in extension/. It updates package.json and package-lock.json, and the manifest takes its version from there. Every upload needs a higher version than the last.npm run tokens in extension/. Read the diff of src/tokens/registry.*.json, and any “also claimed by” warning, before committing it. To add a token, vet its unit and put it in src/tokens/list.json first; node scripts/tokens.mjs find <network> <TICKER> shows the registry’s entries for a ticker. The handoff note says what changed, or that nothing did.
seedelf-data/api/data/token-decimals.json), and a private row keeps what it was given until it’s spent. So refresh it too, by seedelf-data/scripts/token-decimals.py’s two lines, and if the file changed, commit it: step 7’s deploy carries it.npm run dreps in extension/. It rewrites src/dreps/<network>.json with every registered DRep that has a name. Koios’s servers disagree about DRep metadata (1.3.0’s prep had three single-pass runs name 371, 407 and 406 mainnet DReps against the committed 449, each missing a different set), so the script asks again for the DReps still unnamed, in up to three passes. A DRep the committed list names that’s still registered but that no pass names keeps its committed name, and a list that would still lose more than 5% of the committed one is refused with nothing written: run it again later. Its line for each network says how many passes it took, how many names it kept as committed, and how many committed DReps are no longer registered. Skim the diff for anything odd before committing it; the handoff note says what changed.npm run build). The live runs expect the dev build’s pinned ID.npm run store:images and look at docs/store/images/, and at its es/ and ja/: each screen is shot in all three languages. They’re made from the recordings on mainnet, so they show the MAINNET badge and no test-network strip (store/README.md, Graphic assets). The Japanese ones need a Japanese font on the machine (Noto Sans CJK JP here), or Chromium draws boxes.main.seedelf-data/ (step 2’s decimals included): Actions → Data layer deploy, deploy, on main, approved, then a status run and curl -s https://mainnet.seedelf.logicalmechanism.io/health (seedelf-data/deploy/README.md). A wallet the store ships must never meet an API older than the one it was reviewed against: a route the API lacks answers 404, and a request it takes in another shape 400; either holds the wallet’s whole public part on Koios for 5 minutes. It goes before the package, so the store build’s check on mainnet (step 9) reads it.main at the merge commit, with git status --porcelain printing nothing: npm run package in extension/.
VITE_ENABLE_MAINNET=true and VITE_STORE_BUILD=true: mainnet by default, preprod in Settings, and no dev key.dist/ with a key, one whose version doesn’t match package.json, and one whose manifest lacks https://api.koios.rest/* (a preprod-only build).Cargo.lock (--locked) with the pinned Rust (rust-toolchain.toml), and carries no local path.licenses/THIRD-PARTY.txt: every Rust crate compiled into the WebAssembly, every bundled npm package, and SecretBox, each with its licence text. It fails if one of them ships no licence and has no known fallback (scripts/third-party.mjs).release/seedelf-wallet-<version>-mainnet.zip and prints sha256 <hex>, then Zipped with Node <version>, zlib <version>. wasm/build.sh ends its part with C compiler: <first line of clang --version>, or C compiler: unknown.PATH.npm run e2e runs every end-to-end test on it, with preprod chosen (e2e/support.ts). Load dist/ unpacked in a fresh Chrome profile once, and do checklist item 9: it opens on mainnet, and the switch works both ways.agg-api.minswap.org, one swap each way and Stop; one Lovejoin box at depth 1 once the pool holds enough others’ boxes; every Seedelf flow.git rev-parse HEAD), the zip’s SHA-256, and the C compiler, Node and zlib lines.Decided (chunk 11c): an unlisted, preprod-only Web Store listing. Superseded on 2026-09-26, when one build came to carry both networks: the store’s build is mainnet, with preprod in Settings for testing. See store/README.md.
dist/: testers load it unpacked the same way we do. It works, but it’s clunky and gets no automatic updates..crx files: these don’t work for normal users. Chrome generally only allows installs from outside the store through enterprise policy.