aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-05 23:27:12 -0700
committerAdam Soltys <[email protected]>2026-07-05 23:27:12 -0700
commita3783d7e7f53709a8f8352494b9e89583e46f85b (patch)
treedc8ad0c8aef4f447537110b44a2274764acdabdf
parent81a55be4ea07d4dee4a83a3728c8ba0914744cd5 (diff)
downloadcoinosv3-a3783d7e7f53709a8f8352494b9e89583e46f85b.tar.gz
coinosv3-a3783d7e7f53709a8f8352494b9e89583e46f85b.zip
Docs: How-it-works covers Lightning/Ark/SP/gifts; README rewrite
The in-app page grows plain-language cards for Lightning swaps, Ark (instant off-chain, the unilateral escape hatch, expiry housekeeping, fees), silent payments, and gift links. The README catches up with reality: the native-JS Ark client headlines the feature list, and the modular HAL_FEATURES build system gets its own section — every feature is a plugin behind a build seam, and excluded code never ships. Co-Authored-By: Claude Fable 5 <[email protected]>
-rw-r--r--README.md193
-rw-r--r--src/app.js18
-rw-r--r--src/i18n.js10
3 files changed, 151 insertions, 70 deletions
diff --git a/README.md b/README.md
index 12ba691..c15bf76 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,8 @@
A self-contained, **single-file** Bitcoin wallet that runs entirely in the
browser — think bitaddress.org, but a modern BIP84 HD wallet from a seed phrase
-that scans history, watches for payments, and spends. The whole wallet is one
+that scans history, watches for payments, spends on-chain, pays Lightning
+invoices, and holds an off-chain balance on **Ark**. The whole wallet is one
static `index.html` you can save and run offline, forever.
Live at **https://halwallet.app** (the built `index.html` also runs straight
@@ -12,42 +13,98 @@ The name is a nod to [Hal Finney](https://en.wikipedia.org/wiki/Hal_Finney_(comp
## Features
+### Core (always included)
+
- **BIP84 / native SegWit (p2wpkh)** HD wallet from a 12-word BIP39 seed
- (imports any valid BIP39 phrase; optional passphrase). Mainnet.
+ (imports any valid BIP39 phrase; optional passphrase).
+- **Per-wallet networks** — each wallet is stamped with its network (mainnet,
+ testnet, signet, mutinynet, regtest); switching wallets switches networks,
+ and wallets on different networks coexist in one session.
- **One fresh address at a time** — a new receive address is only handed out
after the current one is paid, so used addresses stay contiguous and scans
stay tiny (no 20-address gap probing).
-- **Choose your own block explorer** — mempool.space by default (with
- blockstream.info as a silent failover), blockstream.info only, or a **custom
- Esplora / electrs REST URL** (e.g. your own node) in Settings.
-- **Real-time** via the mempool.space WebSocket (when that explorer is
- selected): an incoming payment shows a "Payment received!" screen the moment
- it hits the mempool. Other explorers fall back to polling plus a periodic full
- reconcile. A ping/pong heartbeat keeps the socket alive (auto-reconnect on
- drop, sleep, or network change).
-- **Spending** with proper coin selection + fee estimation (`@scure/btc-signer`
- `selectUTXO`), multiple recipients, a fee-rate picker, send-max, and manual
- coin control.
-- **QR scanner** on the send page — scan a Bitcoin address, a BIP21 URI (fills
- the amount too), or a signed transaction to broadcast it. Uses the native
- `BarcodeDetector` where available and lazy-loads jsQR otherwise.
-- **Optional encrypted cross-device sync over Nostr** — wallet state is
- NIP-44-encrypted to yourself and stored as a replaceable event (kind 30078) on
- the relay(s) you choose (default relay.coinos.io). The Nostr identity is
- derived from the same seed (NIP-06). On by default, toggled off in Settings.
-- **Offline / air-gapped signing** — export a keyless JSON snapshot of your
- coins on an online device, import it on an offline one, sign locally, and
- export the signed transaction (hex / file / QR) to broadcast elsewhere. You
- can also sign-and-export online (broadcast from another device).
-- **Installable PWA** — installs to your home screen and works offline once
- installed (the app shell, icons, and QR decoder are precached).
-- **Global sats/BTC toggle** (defaults to sats) — every amount label is
- clickable to switch, persisted across sessions.
+- **Choose your data source** — Electrum-over-WebSocket servers or Esplora
+ REST explorers, per network, with silent failover — or point it at your own
+ node.
+- **Real-time** incoming-payment push over WebSocket where the source supports
+ it, with polling + periodic reconcile as the fallback. Replaced (RBF)
+ transactions are detected and pruned from history automatically.
+- **Spending** with proper coin selection + fee estimation
+ (`@scure/btc-signer`), multiple recipients, a fee-rate picker, send-max,
+ manual coin control, and a QR scanner (address / BIP21 / signed tx).
+- **Offline / air-gapped signing** — export a keyless snapshot on an online
+ device, sign on an offline one, broadcast anywhere.
+- **Installable PWA**, global sats/BTC toggle, 19 languages.
+
+### Ark — instant off-chain payments (feature: `ark`)
+
+A **native-JS Ark client** (speaks gRPC-web + MuSig2 directly to
+[bark/captaind](https://github.com/ark-bitcoin/bark) servers — no WASM, no new
+dependencies, ~2k lines):
+
+- **Board** on-chain coins into an Ark server; **send and receive instantly**
+ off-chain with zero mining fees; server fees shown before anything moves.
+- **Refresh / consolidate** vtxos (the round-based maintenance that resets
+ expiry and chain depth).
+- **Collaborative offboard** — move the whole Ark balance back to your own
+ on-chain address in one server-cosigned transaction.
+- **Unilateral exit** — the trustless backstop: publish the vtxo's pre-signed
+ transaction chain on-chain (CPFP-bumped, TRUC-aware, one confirmed hop at a
+ time), wait out the CSV timelock, and claim with your key alone. Works even
+ if the server is gone; live progress (hop counter, blocks-left countdown) in
+ Settings.
+- **Ark gifts** — bearer gift links whose secret *is* the link: claimed
+ instantly and free, even into a brand-new empty wallet with zero on-chain
+ footprint. Revocable until claimed.
+- Crash-safe by construction: every multi-step ceremony checkpoints to storage
+ around each server round-trip, and local state reconciles against the
+ server's authoritative vtxo status before money moves.
+
+### Lightning (feature: `swaps`)
+
+- Pay bolt11 invoices and receive over Lightning via **Boltz-compatible
+ submarine swaps** — non-custodial either way: a provider can fail a swap but
+ never take funds (claims/refunds are enforced on-chain). Pick a provider per
+ network or point at your own.
+
+### Silent payments (feature: `sp`)
+
+- A static, publicly shareable **BIP-352 silent-payment address** (`sp1…`);
+ every payment lands at a fresh unlinkable on-chain address. Scanning uses a
+ configurable (self-hostable) tweak indexer, with the EC math in a Web Worker
+ and a chunked, resumable catch-up scan.
+
+### Gifts (feature: `gifts`)
+
+- **Gift links** — presigned bearer transactions claimable by whoever opens
+ the link, straight into a fresh wallet (with seed backup flow) or an
+ existing one. Reclaim or revoke unclaimed gifts. Optionally **lock a gift to
+ a nostr account** (the claim code is DM'd to them; the link alone is not
+ enough).
-Built with the audited [`@scure`](https://github.com/paulmillr/scure-btc-signer)
-/ [`@noble`](https://github.com/paulmillr/noble-hashes) libraries plus
-[`nostr-tools`](https://github.com/nbd-wtf/nostr-tools). Pure JS, no
-`crypto.subtle`, so it works from `file://` with no server.
+### Cross-device sync (feature: `sync`)
+
+- Optional **encrypted state sync over Nostr** — wallet state NIP-44-encrypted
+ to yourself as a replaceable event, per network, on relays you choose. The
+ nostr identity derives from the same seed (NIP-06).
+
+## Modular builds
+
+Every feature above is a **plugin behind a build seam**. `HAL_FEATURES`
+selects what ships; excluded features never enter the bundle — their code,
+their network endpoints, their crypto:
+
+```bash
+bun run build # everything (~610 KB)
+HAL_FEATURES=ark,gifts bun run build # core + ark + gifts
+bun run build:minimal # core on-chain wallet only (~320 KB)
+```
+
+`tools/minimal-smoke.js` verifies the minimal profile is a fully working
+on-chain wallet with zero feature code in the bundle. Features integrate
+through fixed seams on the core (`registerCoinLock`, `registerInputSigner`,
+cache extensions, UI hooks), so a stripped build isn't a crippled build — it's
+the same core wallet, smaller.
## Develop
@@ -58,6 +115,12 @@ bun install
bun run dev # http://localhost:5173 (rebuilds on each refresh)
```
+The dev server also proxies local regtest backends (electrum, esplora, boltz,
+SP indexer) so the app works unchanged from a phone on the LAN. End-to-end
+suites in `tools/` drive a headless Chrome through the real flows (boarding,
+ark sends, offboard, unilateral exit, gifts, swaps, silent payments) against a
+local regtest stack.
+
## Build
```bash
@@ -76,50 +139,44 @@ Three layers, fastest first:
1. **sessionStorage** — keeps the wallet open across a refresh (cleared on
logout / tab close).
-2. **localStorage** — caches scanned state (addresses, UTXOs, history) keyed by
- a hash of the seed, so a reload shows balances instantly.
-3. **Nostr** — encrypted, replaceable cross-device state on your configured
- relay(s) (when sync is enabled).
-
-On load and on a 2-minute timer the wallet does a full reconcile; between those,
-a light poll re-checks the fresh frontier and the addresses currently holding
-coins. A manual **Settings → Rescan** forces a full re-scan on demand.
-
-## Offline / air-gapped spending
+2. **localStorage** — caches scanned state per wallet *and per network*, so a
+ reload shows balances instantly.
+3. **Nostr** — encrypted, replaceable cross-device state per network (when
+ sync is enabled).
-1. **Online device:** open the wallet, let it scan, then **Settings → Export
- snapshot** (a keyless JSON of your UTXOs + fee rates).
-2. Move the file to an **offline device** running the saved `index.html`.
-3. There, enter your **seed phrase** (it auto-detects no network), then
- **Settings → Import snapshot**.
-4. Build and **sign** a transaction; copy/download the signed hex (or scan the
- QR) and broadcast it from any online device.
+On load and on a timer the wallet reconciles against its data source; a manual
+**Settings → Rescan** forces a full re-scan on demand.
## Layout
| File | Purpose |
| --- | --- |
-| `src/wallet.js` | BIP84 derivation, scanning, coin selection, signing, realtime, cache, sync |
-| `src/api.js` | Esplora wrapper + explorer selection (mempool / blockstream / custom) with throttle/cooldown/timeout |
-| `src/nostr.js` | Optional encrypted cross-device state sync over Nostr (configurable relays) |
-| `src/scan.js` | Camera QR scanner (native BarcodeDetector / lazy jsQR) |
-| `src/app.js` | UI controller (vanilla DOM) |
-| `src/qr.js` | QR → SVG (zero-dep) |
-| `src/format.js` | sat/BTC formatting helpers |
-| `src/i18n.js` | UI strings + translations |
-| `src/style.css` | Hand-rolled styles |
-| `build.js` / `dev.js` | Bun bundler → inlined `index.html` + PWA sidecars, and dev server |
+| `src/wallet.js` | BIP84 core: derivation, scanning, coin selection, signing, realtime, cache — plus the feature seams |
+| `src/api.js` | Data-source selection per network (electrum/esplora presets, throttle/cooldown/failover) |
+| `src/ark/` | Native-JS Ark protocol: gRPC-web codec, arkoor sends, boarding, refresh, offboard, unilateral exit, validation, crash-safe manager |
+| `src/features/` | Feature plugins: `ark`, `swaps`, `gifts`(+`gifts-wallet`), `sp`(+`sp-wallet`), `sync` — and `index.js`, rewritten at build time by `HAL_FEATURES` |
+| `src/swap.js` | Boltz submarine/reverse swap engine |
+| `src/silentpay.js` / `src/sp-worker.js` | BIP-352 math + its Web Worker |
+| `src/nostr.js` | NIP-44 state sync, NIP-17 DMs, profiles |
+| `src/electrum.js` / `src/scan.js` / `src/qr.js` | Electrum transport, camera QR scanner, QR rendering |
+| `src/app.js` | UI controller (vanilla DOM) + feature registry |
+| `build.js` / `dev.js` | Bun bundler → inlined `index.html` + PWA sidecars; dev server with regtest proxies |
## Security notes
-- Self-custody software handling real keys — review the code before trusting it
- with funds, and prefer an offline machine for the seed phrase. It's a hot
- wallet (keys live in the browser); for large amounts use a hardware signer or
- run this air-gapped on a dedicated offline device.
+- Self-custody software handling real keys — review the code before trusting
+ it with funds. It's a hot wallet (keys live in the browser); for large
+ amounts use a hardware signer or run it air-gapped.
- The seed/passphrase live in memory while the page is open and in
sessionStorage for the tab session; locking or closing the tab clears them.
-- The localStorage cache and the Nostr event hold public chain data (addresses,
- UTXOs, amounts); the Nostr copy is encrypted and keyed to your seed-derived
- identity, but a relay can see that an identity is publishing app data.
-- Whichever block explorer you query sees your addresses and IP — point it at
- your own node for full privacy.
+- **Ark trust model**: off-chain sends are co-signed by the Ark server, which
+ is the arbiter of off-chain spent/unspent state — but it can never spend
+ your coins, and the pre-signed exit path means you can always return
+ on-chain without its cooperation. Ark coins expire (weeks) if never
+ refreshed; the wallet surfaces this, but don't park long-term savings in an
+ Ark.
+- Swap providers are non-custodial by protocol: worst case a swap fails and
+ refunds on-chain.
+- Whichever data source you query sees your addresses and IP — point it at
+ your own node for full privacy. The localStorage cache and the Nostr event
+ hold public chain data; the Nostr copy is encrypted end-to-end.
diff --git a/src/app.js b/src/app.js
index 7eacff1..fa0fe90 100644
--- a/src/app.js
+++ b/src/app.js
@@ -504,9 +504,23 @@ function howItWorksScreen() {
para('hiwBasics2'),
para('hiwBasics3'),
para('hiwBasics4'),
- para('hiwBasics5'),
- h('p', { class: 'small muted hiw-tribute', style: 'margin:0' }, ...linkify(t('hiwTribute')))
+ para('hiwBasics5')
),
+ h('div', { class: 'card col', style: 'gap:14px' },
+ h('h3', {}, '⚡ ' + t('hiwLnTitle')),
+ para('hiwLn1')),
+ h('div', { class: 'card col', style: 'gap:14px' },
+ h('h3', {}, '⚔ ' + t('hiwArkTitle')),
+ para('hiwArk1'),
+ para('hiwArk2'),
+ para('hiwArk3')),
+ h('div', { class: 'card col', style: 'gap:14px' },
+ h('h3', {}, '👤 ' + t('hiwSpTitle')),
+ para('hiwSp1')),
+ h('div', { class: 'card col', style: 'gap:14px' },
+ h('h3', {}, '🎁 ' + t('hiwGiftsTitle')),
+ para('hiwGifts1'),
+ h('p', { class: 'small muted hiw-tribute', style: 'margin:0' }, ...linkify(t('hiwTribute')))),
h('button', { class: 'btn-block', onClick: back }, t('back'))
);
}
diff --git a/src/i18n.js b/src/i18n.js
index 586bacb..e45d662 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -129,6 +129,16 @@ const STR = {
hiwBasics3: 'There’s no server and no account. The whole app is a single page that runs in your browser, so nobody holds your coins and nothing on a server ever sees your keys. To show your balance and history it reads from a public block explorer (mempool.space by default, or your own node if you set one in Settings).',
hiwBasics4: 'You can even save this page and open it straight from your computer with no internet at all. It keeps working offline.',
hiwBasics5: 'It follows the usual Bitcoin standards (a 12-word BIP39 phrase and BIP84 native SegWit keys), so if this site ever disappears you can restore the same phrase in Sparrow, Electrum, or any other standard wallet and your coins will be right there.',
+ hiwLnTitle: 'Lightning',
+ hiwLn1: 'You can pay Lightning invoices and receive Lightning payments. Behind the scenes a swap provider exchanges between your on-chain coins and Lightning without ever holding your money — if a swap fails, the coins come back to you. A small swap fee applies, shown before you confirm.',
+ hiwArkTitle: 'Ark',
+ hiwArk1: 'Ark is a newer way to pay off-chain: sends arrive instantly, cost no mining fees, and stay self-custodial. You "board" coins from your on-chain balance into an Ark server, pay and receive freely, and move funds back on-chain whenever you like.',
+ hiwArk2: 'The server can never take your money: every coin you hold in the Ark comes with a pre-signed escape route, and the wallet can put it back on-chain even if the server disappears (“Exit on-chain without the server” in Settings — it takes a while and needs a little on-chain balance for fees, but it needs nobody’s permission).',
+ hiwArk3: 'The housekeeping: Ark coins expire after some weeks, so refresh them (Settings) before then — a wallet left untouched past expiry can be swept by the server. Boarding pays a small server fee plus a normal mining fee; paying inside the Ark is free; withdrawing on-chain pays a server fee taken from the amount.',
+ hiwSpTitle: 'Silent payments',
+ hiwSp1: 'Your wallet also has a silent-payment address (sp1…) that never changes and is safe to share publicly — every payment to it lands at a fresh on-chain address nobody can link to you. Finding those payments uses an indexer service, configurable (or self-hostable) in Settings.',
+ hiwGiftsTitle: 'Gifts',
+ hiwGifts1: 'You can gift bitcoin with a link: whoever opens it claims the coins into their own wallet — no app, no signup. On-chain gifts lock the coins until claimed and can be cancelled or reclaimed; Ark gifts claim instantly and free, even into a brand-new empty wallet. A gift can also be locked to a specific nostr account so only they can claim it.',
hiwTribute: 'The name Hal Wallet is a tribute to Hal Finney (1956–2014).',
balance: 'Balance',
pending: 'Pending',