Seedelf-Wallet

Architecture

Keep it light: a small Manifest V3 extension, the Seedelf crypto and transaction building we already have in Rust (compiled to WebAssembly), and Koios for chain data. Nothing else unless it earns its place.

Shape

flowchart LR
  UI["UI<br/>(full tab or side panel)"] -- "typed RPC (a port per request)" --> SW
  subgraph SW["Service worker"]
    Vault["Vault + lock"]
    Wallet["Wallet state"]
    Crypto["seedelf-crypto (WASM)"]
    Tx["Tx building + signing"]
  end
  SW -- "fetch" --> Koios["Koios"]
  SW -- "fetch" --> Collat["giveme.my collateral"]
  SW -. "fetch, only when used" .-> Others["CoinGecko (mainnet), Minswap's aggregator,<br/>the IPFS gateway"]
  Page["dApp page"] -. "CIP-30 via content scripts<br/>(only when the user turns it on)" .-> SW

Service worker

Crypto

Transaction building

Decided: Rust (Pallas 0.35, 0.33 until chunk 16), compiled to WebAssembly.

Status (chunk 13): every v1 transaction is built on it: move-in, creating a Seedelf, transfer, withdraw, removing a Seedelf, a send from the Cardano account, and staking (delegate, vote, withdraw rewards, stop). Built on it since: a private session’s funding and return (chunk 15; Minswap builds the swap, and the wallet only signs it), Lovejoin’s deposits, mixes and withdraws (chunk 16), the account’s own DRep’s registration, update, retirement and votes (chunk 21), and a session’s cancel of its DEX orders (chunk 24, seedelf-core/src/orders.rs).

In the CLI, every script spend ends in seedelf-cli/src/commands/spend.rs: prove, evaluate, finish, giveme.my, sign, submit. Only create and fund still build inside their run()s; the web wallet doesn’t need them.

Tests guard it. The CLI’s offline integration tests (seedelf-cli/tests/cli/) check value conservation, min-UTxO and valid change registers for each command, and seedelf-core/tests/build_test.rs checks the builders directly.

Reading a transaction

Decided: in WebAssembly, with Pallas, as a function of its bytes alone (wasm/src/decode.rs, chunk 17). decodeTx(network, txCbor) answers a TxDetail: CBOR in, structure out. It takes no account, no keys and no UTxOs, so it makes no network call, a fixture file is a whole test of it, and it cannot be wrong about the account because it never sees one. The network only names Seedelf Wallet’s own contract; every address is read from its own bytes.

Three readers, three jobs. None of them is the others.

Both Rust readers decode Conway’s format alone (conway::MintedTx in decode.rs, conway::Tx in cip30.rs). Chunk 8 chose that partly for size, when Pallas’s all-era MultiEraTx cost about 540 KB of module. Since chunk 16, seedelf-core’s evaluator (eval.rs) and Lovejoin’s withdraw context (lovejoin.rs) decode with MultiEraTx, so the module carries it anyway and that saving is no longer a reason.

What the decoder does that matters:

Three rules came out of reading Conway’s CDDL against what JavaScript and a screen can hold. Each is a way the bytes could have been shown wrongly:

  1. Every number the bytes decide is a decimal string, not only the lovelace ones. coin, slot, epoch and metadatum_label are uint up to 2^64-1 there and ex_units is 0 .. 2^63-1, while JSON.parse rounds anything past 2^53: a ttl of 2^60+1 showed as 1152921504606846800, off by 177. Only counts the decoder works out itself — the size, an output’s position, how many witnesses — are numbers.
  2. Bytes that aren’t the thing they should be are shown as themselves. The CDDL types an output’s address and a withdrawal’s reward_account as plain bytes, so a transaction can carry bytes that are no address at all, and one of those used to hide the whole transaction behind “Output 0 has an unreadable address”. They show as hex now, with the kind unreadable, and every other field still reads.
  3. Text from the chain is escaped where it would show as something else. A metadatum, a token name, an anchor’s URL and a relay’s host are whatever was written — a right-to-left override turns “drowssap” into “password” on the screen, and a zero-width joiner hides a word break. Each such character is written out as \u{...} (decode::shown, the same set as ui/tokens.ts’s HIDDEN), and unicode-bidi: isolate in the stylesheet keeps a right-to-left script, which is nobody’s trick, inside its own element.

It also checks the metadata against the body’s hash (metadataHashMatches). Metadata whose hash the body doesn’t carry is metadata the chain will never hold, and the view would otherwise show a note that could never reach it; a transaction whose two disagree gets a warning and is still shown in full.

What it refuses, in words, rather than showing partly: a certificate from before Conway (a genesis key delegation or a move of instantaneous rewards, variants 5 and 6, which Conway dropped and no node accepts today) — Pallas can’t decode one, and showing a transaction with a certificate missing would be worse than saying so. A body field from an older era, such as Shelley’s update (key 6), is only a field: it’s reported with its hex and the rest reads.

In the worker, tx-view.ts is the cheapest handler there is: no Koios request, no storage write, nothing kept. Every flow parks its built transaction in session storage under its own key and script-spend.ts strips the CBOR out of the summary the screen gets, so the view is asked by transaction hash — which every review already has — and the worker looks the bytes up where they live, walking the kept record rather than knowing each key’s shape (a session’s return keeps its chain inside it; Bring everything back keeps a list). A chain through Lovejoin still being sent is looked for where it’s sent from (SESSION_LOVEJOIN_SENDING, and each session’s SESSION_CHAIN_PREFIX key, by the indices the device knows), since its transactions move there from the key they were kept for Send under; the Lovejoin page’s row for the chain opens each (O3, chunk 25). It’s looked for there first, before what’s kept for Send (a session’s return keeps the same bytes there until its first submit), and the answer says so (TxView.chain): Transaction details then says its signatures were made when the chain was prepared, never that they wait for a confirm (1.3.0’s release review, C17). A hash it no longer holds that a recorded chain names (inChain in lovejoin.ts: the sealed schedule’s deposit, mixes and last, no Koios) gets “The wallet no longer holds that transaction: its chain through Lovejoin was all sent, or stopped partway.” rather than “Review it again” (C28). A site’s are in the connector’s own queue (Waiting.txCbor), held only while the request waits. It shows sentCbor where there is one, so a payment Koios didn’t answer shows the bytes that will be sent again. It also marks each output that pays the user (TxOutput.yours, blind test §9.9): the public accounts’ payment keys (the active one’s from its keys and the last reading, others’ derived on the device), the user’s own register in the wallet contract, and the private sessions’ one-time keys. Still no request; locked, nothing is marked.

The bytes never reach the screen until it asks. That is what makes opening or closing the view safe: Send reads the transaction back out of its session key, not out of a component, so the signed bytes were never in the view’s hands. A test pins it, and so does an end-to-end one in the browser.

Networks

One build, both networks: mainnet by default, preprod for testing (the owner’s call, 2026-09-26). Every build has both (enabledNetworks in networks.ts), the dev build included:

The switch (launch review M3):

What’s kept per network, checked for the switch:

What Where
Balances, the contract scan, the account’s UTxOs, addresses and Activity pages seedelf.balances.<network>, seedelf.contract.<network>, seedelf.accountUtxos.<network>, seedelf.accountAddresses.<network>, seedelf.accountActivity.<network>
The Seedelf history, coin control, private sessions, Lovejoin’s schedule and chains, a payment that may still go through, a mix from the public account kept by Remove wallet, who paid for each Seedelf minted here sealed history.<network>, coins.<network>, sessions.<network>, lovejoin.<network>, maybeSent.<network>, lovejoinMaybe.<network>, mintedBy.<network>
Contacts, connected sites one sealed record each, every entry with its network
The payment the pending watch follows, maybe sent or not seedelf.pendingTx.<network>, one per network: a Send on one never drops the other’s watch
A chain being sent, and what chains reserve seedelf.session.chain.<network>.<index>, seedelf.lovejoin.sending.<network>, seedelf.reserved.<network>
Transactions kept for Send one slot each, recording its network; the submit checks it
The pool list, the live governance actions, the dApp connector’s reading seedelf.pools.<network>, seedelf.govActions.<network>, seedelf.dapp.view.<network>, seedelf.dapp.signed.<network>
ADA’s price mainnet only; nothing is asked on preprod

What isn’t per network: the vault (keys don’t depend on the network), the settings, the active account (seedelf.account) and the language (seedelf.language), and the spent outpoints (seedelf.spent), which no two networks share. What sites’ submits spent is kept apart from the wallet’s own (seedelf.spent.sites, the newest 5,000, never in the list of sent transactions), so a site can’t push out what the wallet spent; spentSet and lastSpentAt read both (independent review L7).

One network value drives everything network-specific. The Rust side already takes a network_flag everywhere (true = preprod), and the WebAssembly API passes it through. This is how the CLI’s --preprod works.

  Preprod Mainnet
Koios https://preprod.koios.rest/api/v1 https://api.koios.rest/api/v1
Collateral service https://www.giveme.my/preprod/collateral/ https://www.giveme.my/mainnet/collateral/
Contract config get_config(variant, true) get_config(variant, false)
Addresses addr_test… addr…
Minswap’s aggregator aggr.monorepo-testnet-preprod.minswap.org agg-api.minswap.org
Lovejoin’s mix_box 67ffe4ed…ecc5 c145c10f…1fad
Lovejoin’s pool floor none 30 others’ boxes
ADA’s price none CoinGecko
An NFT’s image (chunk 20) ipfs.blockfrost.dev ipfs.blockfrost.dev

Chain data

Mainnet through the wallet’s own data layer (chunk 26b), at https://mainnet.seedelf.logicalmechanism.io since 1.4.0. The API is seedelf-data/, on a DigitalOcean droplet that reads home’s node, db-sync and Kupo through WireGuard (its runbook); the plans are chunk 26b and 26c. Preprod, and mainnet with the Koios-only switch on, read Koios as the rest of this section describes, and so does each part while it’s held on Koios. The data layer:

Koios, same as the CLI. Balances were built in chunk 6: extension/src/background/koios.ts (the client), chain.ts (pure helpers) and balances.ts (the service).

What one balance reading asks Koios (four requests: the contract’s and the stake key’s alongside the account’s two, which run one after the other):

Request For
credential_utxos with the wallet contract’s script hash Every UTxO in the contract, or only those after the last block seen (below), to find the owned ones
account_addresses with the Cardano account’s stake address (_empty: true) Every address that has used the stake key, including empty ones, for discovery
credential_utxos with the account’s payment key hashes in range (chunk 12) Every UTxO under those keys, whatever the address’s staking part. At most 75 keys a request (Koios’s public tier refuses bodies over 5,120 bytes), so an account with more than about 35 used addresses takes more than one.
account_info with the stake address (chunk 13) Whether it’s registered, its pool, its vote delegation, its rewards and its deposit. No row means never registered. The pool’s ticker comes from what the session has read, the pool list on the device, or one pool_info a session.

Storage

Permissions: storage, alarms, sidePanel and scripting, plus the host permissions for the build’s networks’ Koios and giveme.my (and CoinGecko, in a mainnet build). The sites (https://*/*, http://localhost/*, http://127.0.0.1/*) are optional host permissions, asked for when the dApp connector is first turned on and kept after (see dApp connector). One host under them, https://ipfs.blockfrost.dev/*, is asked for on its own the first time the user shows an NFT’s image (chunk 20).

Where Key What
chrome.storage.local seedelf.vault The encrypted vault: a single SecretBox blob, see keys-and-accounts.md
chrome.storage.local seedelf.unlockFailures { count, lastFailureAt } for the unlock back-off
chrome.storage.session seedelf.entropy The vault entropy, only while unlocked
chrome.storage.session seedelf.lastActivity When the user last did something, for auto-lock
chrome.storage.session seedelf.unlockedAt When the wallet last unlocked (or was created or restored), for the unlock’s fresh draws; only while unlocked (independent review M10, L10)
chrome.storage.session seedelf.restored true from a restore until a Home has said so with a reading, or the note was dismissed (restored, blind test T20a, T20b): the fact alone, so the note shows in whichever page opens Home next. Wiped by a lock and Remove wallet, so a later unlock is never called a restore; a create never sets it
chrome.storage.session seedelf.sends When the wallet last sent something before a lock (or, while unlocked, the last try of a payment let go as unseen: final review F8), and when session storage began (noted at each worker start if missing), for Lovejoin’s quiet rule. Kept across a lock and Remove wallet, cleared when the browser closes or the extension updates: two times, nothing of what was sent (independent review M10)
chrome.storage.session seedelf.dapp.refused The sites the user declined, turned away unasked for a while: by origin, until, how many in a row, the network and whether one asked again, never a page’s title (dapp.ts keepRefusals; blind test §9.2 and its cross-area review). Kept across a lock and a trap, not Remove wallet
chrome.storage.session seedelf.balances.<network> The last balance reading, only while unlocked
chrome.storage.session seedelf.contract.<network> This wallet’s contract UTxOs, where each Seedelf is (outpoint, address, register) and the last block seen (contract-scan.ts), only while unlocked
chrome.storage.session seedelf.accountAddresses.<network>, seedelf.accountActivity.<network> The account’s stake address, addresses and payment keys (from the balance reading), and its Activity pages, only while unlocked
chrome.storage.session seedelf.accountUtxos.<network> The account’s UTxOs with their key paths, from the balance reading, for the UTxOs screen and what’s locked; only while unlocked
chrome.storage.local seedelf.preferences The user’s settings: spendRewards (chunk 13); hideBalances, lockAfterMinutes and currency (chunk 14); dappConnector and dappPassword (chunk 15); lovejoinReturns, lovejoinDepth and lovejoinDelay (chunk 16, privacy review §4.1); dappAccount, the public account connected sites use (chunk 18); koiosOnly, Read Cardano through Koios only (chunk 26b). Not sealed: nothing in it is about money, though it shows which settings were changed. Deleted with the wallet, but for koiosOnly: with it on, Remove wallet and Forgot password write it back after the reset, the rest at their defaults, as they keep the network (1.5.0’s release review, C08).
chrome.storage.local seedelf.network The network the wallet is on, in a build with both (see Networks). Kept when the wallet is removed.
chrome.storage.local seedelf.account Which public account the wallet works on, as an index from 0 (chunk 18). Unsealed, beside the network and for the same reason: Wallet reads it while deriving the keys, before anything is unlocked. One integer for the wallet, not one per network — the account’s keys are the same on both. Which accounts the phrase has used, and what they are called, are sealed (accounts). A create or restore puts it back to 0.
chrome.storage.local seedelf.language The language the wallet is in, en, es or ja (chunk 19). Unsealed, beside the network and the account and for the same reason: the screens before the unlock are shown in it. Kept when the wallet is removed.
chrome.storage.local seedelf.openIn Where the toolbar button opens the wallet: a tab or the side panel (chunk 14). The browser’s, so kept when the wallet is removed.
chrome.storage.local seedelf.pools.<network> Every live pool, for a day (chunk 13). The same for everyone, but its time shows when staking was last browsed. Deleted with the wallet.
chrome.storage.local seedelf.govActions.<network> The live governance actions, for an hour (chunk 21). The same for everyone, but its time shows when they were last read. Deleted with the wallet.
chrome.storage.session seedelf.prices ADA’s price in every currency, for five minutes (mainnet only), so when it was read stays off the disk (privacy review §3.13); a copy an older version kept on disk is removed at the next read, and with the wallet.
chrome.storage.session seedelf.poolRefs.<network>, seedelf.stake.built The tickers of pools read this session (the user’s among them), and the staking transaction built last; only while unlocked
chrome.storage.local seedelf.private.<record> Sealed private records: accounts (which public accounts the phrase has used and what the user called them, chunk 18), contacts, history.<network> (the Seedelf history, which each UTxO’s history class is read from), coins.<network> (the locked private UTxOs, shared, and each public account’s own locked UTxOs and collateral, by index, with the payments that set its collateral, the newest 20, kept through a reclaim, for Activity to name (collateralPaid, chunk 31); the pre-chunk-18 shape reads as account 0’s and is folded in on the first write), dapps (the sites connected, per network, chunk 15; no account is recorded per site, since every public-account connection is to the one dApp account, Preferences.dappAccount), maybeSent.<network> (the payment being sent, from just before its submit, and one that may still go through, until it’s settled; kept by Remove wallet), sessions.<network> (private sessions: indexes, stages, transactions, with what each funding and top-up paid the session’s account (for a session the restore scan found, what the scan read there) and what a site’s signed transactions pay it, the newest 40 outputs, and, where a swap’s Splash order was filled in part, which transactions at its address were fills of it and which a stranger’s (remnants); a site’s session’s record goes when it’s disconnected, and a found one’s when it ends with nothing left (1.5.0’s release review, C02), unless something left behind is still at its account; while a found one whose record holds a swap waits to end, when a refresh first found it over (recovered.quietAt, 1.5.0’s cross-area review, X01); and, from a restore until a scan finishes on that network, a mark that it hasn’t (unscanned, C19), so a restore writes the record of every network the build has at once), lovejoin.<network> (boxes’ due times and their unlock marks, each chain the wallet sends with its leaves, a withdraw being sent, signed, with its box’s due time, from just before its submit until it’s seen, refused or 2 hours old, and what Koios said made each box no record accounts for, with the transactions it hasn’t answered for yet, kept until 3 hours after a pool read last listed a box of them), and mintedBy.<network> (who paid for each Seedelf minted here), and lovejoinMaybe.<network> (a mix from the public account stopped at a transaction that may have gone through, written only by Remove wallet while unlocked; final review F1). See below.
chrome.storage.session seedelf.dapp.view.<network>, seedelf.dapp.signed.<network> The account as the dApp connector last read it (kept 30 s), and the account’s outputs of the last 32 transactions it signed or sent for sites, for chaining and for answering a site’s resubmit with its id (independent review M3, final review F12), per account (…:i for a session); only while unlocked
chrome.storage.session seedelf.moveIn.built, seedelf.mint.built, seedelf.transfer.built, seedelf.withdraw.built, seedelf.remove.built, seedelf.send.built, seedelf.collateral.built, seedelf.session.out, seedelf.session.site-out, seedelf.session.top-up, seedelf.session.mix-out, seedelf.session.tx, seedelf.session.back, seedelf.session.claim, seedelf.lovejoin.public Each flow’s transaction as its review built it, kept for Send and recording its network (a Seedelf spend’s with its one-time key’s seed); only while unlocked
chrome.storage.session seedelf.pendingTx.<network> The payment the pending watch follows, one per network (see Submits); only while unlocked, with a maybe-sent one sealed too (maybeSent.<network>)
chrome.storage.session seedelf.spent, seedelf.spent.sites, seedelf.sent.<network>, seedelf.reserved.<network> What the wallet’s own submits spent and when (for 2 hours, the newest 10,000), what sites’ submits spent (the newest 5,000), the transactions it sent there whole, for 2 hours, the newest 16 of its own Sends (each with the public account its review was built on) and, apart, the newest 16 a chain sent (sites are answered from those of the last 20 minutes), and what each chain being sent reserves; only while unlocked
chrome.storage.session seedelf.session.chain.<network>.<index>, seedelf.lovejoin.sending.<network> A Lovejoin chain being sent, a window at a time: a session’s return, or a mix from the public account; only while unlocked
chrome.storage.session seedelf.koios.until When Koios’s limit lets the wallet ask again, after a 429, so a restarted worker honours it
chrome.storage.session seedelf.readFailed.<network> When the last balance reading on a network failed (balances.ts), so Home’s alert follows the worker’s last reading, across a trip to Settings or a reload, until one succeeds (blind test E05); its words stay in the worker’s memory alone. Only while unlocked
chrome.storage.session seedelf.balancesPrivateStale.<network>, seedelf.readingTooLarge.<network> That only the kept reading’s private side is behind (privacy review §2.9), and when a reading too large to keep was made; only while unlocked

UI

dApp connector

Built in chunk 15 for the public account, and in chunk 15c for private sessions. The plans are archive/plans/chunk-15-dapp-connector.md and archive/plans/chunk-15c-private-cip30.md.

flowchart LR
  subgraph Tab["A site's page (top frame)"]
    P["cip30-page.js<br/>window.cardano.seedelf"] -- "postMessage" --> B["cip30-bridge.js<br/>(isolated world)"]
  end
  B -- "port seedelf.cip30<br/>(origin from Chrome)" --> D["dapp.ts"]
  D -- "approvals" --> W["The connector's window<br/>(?view=dapp)"]
  D -- "inspectDappTx, signDappTx,<br/>signDappData" --> X["WebAssembly (cip30.rs)"]
  D -- "reads, utxo_info, submittx" --> K["Koios"]

Private sessions

Chunk 15, step 2: a swap through Minswap’s aggregator, run from a one-time account funded from the private balance and brought back into it. Since chunk 15b it runs itself after one approval. It’s reached from the dApp browser (screens/Dapps.tsx: Home’s dApps row, a grid of tiles, Minswap’s opening screens/Swaps.tsx). The flow is in flows.md.

flowchart LR
  UI["dApps → Minswap<br/>(Swaps.tsx)"] -- "sessions, swap-quote, session-out,<br/>session-advance/stop/resume" --> S["sessions.ts"]
  A["chrome.alarms<br/>seedelf.sessions, unlock"] -- "runAll" --> S
  S -- "estimate, build-tx" --> M["Minswap's aggregator"]
  S -- "credential_utxos, tx_status,<br/>utxo_info, datum_info, epoch_params,<br/>address_utxos, tx_info, submittx,<br/>account_addresses, account_txs" --> K["Koios"]
  S -- "buildWithdraw (out),<br/>readDexOrder, checkOrderCancels,<br/>buildOrderCancel,<br/>inspectSessionTx, signSessionTx,<br/>attachWitnesses, buildSessionReturn" --> X["WebAssembly"]

Lovejoin

Chunk 16: Lovejoin, a mixer of fixed 10 ₳ boxes, on preprod and, since 2026-09-26, on mainnet, where a return goes through it too once its pool holds 30 boxes that aren’t the wallet’s (the pool floor; below it a return comes back directly and says why). It has had no third-party audit, only its makers’ own review, and the wallet says so where Lovejoin is chosen. The plan has the protocol and the decisions: archive/plans/chunk-16-lovejoin.md.

Seeding the pool. A mix takes two other people’s boxes for each of its own, and the wallet’s own boxes never count towards the floor, so an empty pool can’t be mixed into and no wallet could ever start one: the mainnet pool was empty at launch. So the public account can seed, a chain of depth 0: the deposit alone, no mixes (publicBuild’s seed, check_deposit in the WebAssembly, fresh_for in seedelf-core). It needs no other boxes, and the floor doesn’t apply to it, because it draws nothing from the pool. It buys the seeder nothing — the box comes back to the same owner and the chain shows it — so the review says that plainly, in a warning. The page offers it as soon as it opens on a pool under the floor, without waiting for a mix to be refused: lovejoin-status carries others and floor for that, both from the pool read the page already makes (ownership works out the other boxes anyway). It also says there that the wallet’s own boxes don’t count, so seeding and mixing in one wallet never unblocks it. A seed’s count is typed, not stepped, and defaults to what the pool still needs: it makes no mixes, so it isn’t held to a mix chain’s MAX_MIX_BOXES but to MAX_DEPOSIT_BOXES, the transaction’s own size — the whole floor goes in one deposit. It seeds from whichever side is chosen, as a mix does. From the public account it’s that one deposit, which spends no script and so puts up no collateral at all (Funding.collateral is None; only mix reads it, and a seed makes none). From the private balance it’s a one-time account funded by a Seedelf spend, which puts up giveme.my’s collateral as every Seedelf spend does, and deposits from there — the session records mix.seed so the chain it runs later is depth 0 too. The private side spends private UTxOs for a seed that hides nothing, so the page says the public account is the cheaper side. A seed’s boxes get a due time like a mix’s: at depth 0 its leaves are the deposit’s own boxes, which unmixedOf counts as mixed, so the first pool read schedules each one back after the delay, unmixed. The owner kept that (2026-10-06); the reviews say it.

flowchart LR
  R["a session's return<br/>(sessions.ts)"] -- "chain" --> L["lovejoin.ts"]
  T["Lovejoin page<br/>(Lovejoin.tsx)"] -- "mix: private (a mix session),<br/>public, again, status, withdraw now" --> L
  A["unlock, seedelf.sessions alarm"] -- "withdrawDue" --> L
  L -- "credential_utxos (mix_box),<br/>ogmios (first mix), submittx,<br/>tx_status, utxo_info,<br/>tx_info (what made a box)" --> K["Koios"]
  L -- "giveme.my (withdraws)" --> G["giveme.my"]
  L -- "buildLovejoinChain, planLovejoin, lovejoinFunding,<br/>buildLovejoinFromAccount, buildLovejoinAgainFromAccount,<br/>lovejoinOwned, buildLovejoinWithdraw, finishLovejoinWithdraw,<br/>ogmiosUtxos, declaredCovers" --> X["WebAssembly"]

What we borrow from Lace

Paths are relative to a lace-extension@2.4.0 checkout (see the README).

What Lace path How we use it
SecretBox (Argon2id + ChaCha20-Poly1305) packages/lib/core/src/secret-box/ Adopted in extension/src/background/secret-box/ (Apache-2.0, without the EMIP-003 path)
Typed RPC between extension contexts packages/lib/extension-messaging/src/ Adapt; it’s about 1.8k lines and needs RxJS
Service-worker boot order and install preloading apps/lace-extension/src/sw-script/ Pattern
Worker-owned store mirrored in the UI apps/lace-extension/src/util/connect-store.ts, apps/lace-extension/src/sw-script/create-remote-store.ts Pattern
Lock state machine and inactivity timer packages/contract/app-lock/src/store/ Pattern
Unlock back-off packages/contract/authentication-prompt/src/store/unlock-backoff.ts Pattern
MV3 manifest and CSP apps/lace-extension/assets/manifest.json Pattern
CIP-30 connector (chunk 15) apps/lace-extension/src/content-scripts/, packages/lib/dapp-connector/, packages/module/dapp-connector-cardano/ Pattern: a page script and a bridge, the origin from Chrome, pings while a prompt waits, a popup window, the collateral rules, chaining. Not its manifest content scripts on every page: ours are registered only while the user has the connector on.
Keeping existing vkey witnesses intact packages/module/blockchain-cardano/src/tx-executor-implementation/merge-pre-existing-vkeys.ts Reference

We don’t take Lace’s contracts, modules or feature-flag framework, its host/guest shell, its analytics (PostHog, Sentry), or anything for Bitcoin or Midnight.

Licensing: Lace is Apache-2.0, and this repository is MIT. Files copied or adapted from Lace stay under Apache-2.0. For each one:

The store package carries every third-party notice in licenses/THIRD-PARTY.txt: SecretBox, the npm packages in the bundle, and the Rust crates in the WebAssembly. extension/scripts/third-party.mjs writes it when npm run package runs. Inter’s and Lucide’s licences sit next to it.