git clone https://git.smesh.lol/tinywallet.git
The wallet half of the browser client: a BIP39 seed, BIP84 P2WPKH addresses, and a write-behind store, with no wallet service and no full node RPC anywhere in it.
The chain data comes from nostrino, the compact-filter light client that reaches a chain node through a Nostr relay tunnel. The wallet never speaks to the node: it holds one end of a chain-data channel per chain and asks for a status, a sync, a script match, or a range of filters. What answers is someday a full node, today the light client, and neither side of the channel knows which.
page
| commands + events
wallet worker -- this module
| |
store (IDB) chain-data channel -- nostrino/pkg/chaindata
|
nostrino wasm instance (one per chain)
|
relay -> gateway -> node
| package | what it is |
|---|---|
pkg/mnemonic | BIP39: word generation, checksum validation, seed derivation. English wordlist, pinned by hash. |
pkg/bech32 | BIP173 v0, which is what a P2WPKH address is. |
pkg/hd | BIP32 private derivation, hardened and not, carried with its path. |
pkg/wallet | A seed and a chain profile as a wallet: m/84'/coin'/account'/change/index, P2WPKH scripts. |
pkg/store | The synchronous state and its asynchronous persistence. idb.go is the browser side. |
pkg/script | Output scripts and addresses: P2WPKH, P2WSH, and the swap's HTLC witness script, pinned to the node's own compilation. |
pkg/domain | The wallet's operations: create, restore, unlock, addresses, sync. Chains are an interface. |
pkg/api | The frames a page and the wallet worker exchange: binary, id'd, cancellable. |
cmd/tinywallet-wasm | The worker: the domain, the store, and one chain-data client per chain. |
No package here opens a socket, reads a config file, or knows a relay. The worker that composes them does that, and it is the only thing that needs a browser.
The seed is the recovery artifact, so the store can lose a write. IndexedDB is
asynchronous and transactional; Bolt's View/Update are synchronous and are what the
rest of the wallet code wants to look like. Rather than put a per-write round trip on
another thread into the hot path — a wallet matching a block writes in bursts — the
cache is the truth and the database is written behind it, in one batch, every couple of
seconds, or at a boundary the caller chooses. A flush that never happened costs a
rescan from the seed, not a wallet. Values are versioned so a write that lands while a
flush is in flight is newer than what was written and stays dirty for the next one.
The channel is the seam. The vocabulary is binary frames with request ids, not JSON: a filter range is one buffer that the browser transport can transfer instead of copy, and a script list is bytes. A frame carries an id, so one request in flight can be cancelled precisely, and a close means the far end died — an in-flight request fails instead of waiting for an answer that cannot come. Because both sides speak only that vocabulary, the wallet does not compile the light client and the light client does not compile the wallet.
Working and tested (go test ./...), with the wasm target compile-checked:
vectors; 12, 15, 18, 21 and 24 words;
separated by account (btc account 0, blake account 1);
reopen, versioned changes during a flush, and the wasm IndexedDB persistence;
a reload opens with the passphrase, the same first address after a reload, and two chains from one seed that share no key;
the heights, and refuse a chain that fails rather than recording half of it.
The swap's signing, PSBT and scanning behaviour is surveyed in docs/BLAKE_SWAP_PORT.md: the swap has none of that locally today (it is all node RPC), so the doc lists what has to be built here instead.
tinywallet.open(pagePort, chains) starts it, then the pagesends request frames and reads result frames — status, create, restore, unlock, lock, addresses, sync, matches — with a cancel that reaches the chain that is working, and a close that ends it. Built with TinyGo (2.2 MB).
Not in this cut: the page itself, address discovery beyond the lookahead, fetching the blocks a match names, signing, and the swap module that spends what the chain says is spendable.