aboutsummaryrefslogtreecommitdiffstats
path: root/tools
Commit message (Collapse)AuthorAgeFilesLines
* One question fewer on the way in, and an 'or' that means itAdam Soltys2026-08-041-0/+3
| | | | | | | | | | | | | | | | | | | | | The sign-in screen stacked 'Connect a signer app' directly above a bunker/nsec field, which reads as two steps rather than two ways in. A divider says which it is. The wizard's second step asked whether you have a nostr account, then sent you to a screen that asks the same thing. Step one now offers both paths directly — Get started, or sign in with Nostr — and the middle step is gone, along with its strings. The welcome blurb goes too: the screen is one decision, and prose above it is something to read before you can make it. The footer sinks to the bottom of the window rather than trailing the content up the page. #app is a full-height column now; the chat card sizes itself against the viewport, so the layout test covers it at three sizes. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Offer a password the first time you lockAdam Soltys2026-08-041-0/+59
| | | | | | | | | | | | | | | | | | | | | Locking with no password locks nothing — the wallets sit on the device under an empty one and reopen with a tap. So the first deliberate lock now asks whether to set one, explaining plainly what happens if you don't. 'Not now' is a real answer, recorded, and never asked again: a wallet that nags is a wallet people stop reading. The offer rides the deliberate lock only. lock() is also where the idle timer, the last-wallet-removed path and boot-after-expiry land, and a password form appearing because you went to lunch is a different app than the one you left. Two subtleties worth naming: the offer is set after lock()'s ui reset block, which clears ui.pw and had been swallowing it; and taking the offer re-enters lock(), which now finds a password and lands on the unlock screen instead of the wallet list. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Call it 'Lock wallet', because that is what it doesAdam Soltys2026-08-031-6/+6
| | | | | | | | | | | | | | | | 'Logout' promises a return to a fresh app with nothing of yours left on it. This closes the open wallet and keeps everything saved, which is why it read as interchangeable with 'Clear all' — the one that really does send you back to the beginning. Renamed via a new key rather than rewording the old one: nineteen locale tables carry their own word for 'Logout' and t() prefers the locale's value, so reusing the key would have kept the wrong meaning in every language but English. The dead key is dropped from those files; they fall back to English until translated. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Logout shouldn't ask for a password nobody setAdam Soltys2026-08-031-0/+107
| | | | | | | | | | | | | | | | | | | | | | | Wallets save themselves to this device as soon as you open one, encrypted under an empty password unless you choose one. Logging out still sent you to 'Unlock saved wallets — enter your password', which asks for something that doesn't exist and makes logout look like it did something drastic. It didn't: logout only drops the keys from memory, and 'Clear all' is the one that deletes. With no password, logout now returns to the wallet list — honest, since nothing is protecting those wallets and a blank prompt protects nothing. With a password set it prompts exactly as before. Fixed underneath: mergeVaultList derived the comparison xpub with accountXpubFor's mainnet default, so on any other network it never matched the stored one and every unlock re-added the same wallet as a fresh entry — visible as the same wallet listed twice, once watch-only. The seed-mismatch checks in 'load recovery phrase' had the same default and would have rejected the correct seed on testnet. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* The unread dot holds stillAdam Soltys2026-08-031-0/+77
| | | | | | | | | | | | | | | It looked like the dot was flickering on and off after a refresh. It wasn't: measured over the first twelve seconds it appeared once and never went away, while its entrance animation started seven separate times. render() rebuilds the whole DOM, so the pop replayed on every repaint. A dot that means 'someone is waiting' has nothing to animate about, so the animation is gone rather than gated. tools/unread-dot-test.js counts animation starts against real appear/disappear transitions, so a future entrance effect here fails instead of shipping as a flicker. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* The chat window fits the screen, and the header stops asking questionsAdam Soltys2026-08-031-0/+133
| | | | | | | | | | | | | | | | | | | | | | | | | | | | The message log had a fixed 62vh height, so the card was head + panels + log + composer tall and the input fell off the bottom of the screen. The card is now sized to the viewport and the log is the only part that flexes: opening the invite or member panel shrinks the scroll area instead of pushing the composer out of reach. Three header changes, all the same idea — don't show a control for a choice nobody is making: '+' becomes 'Add person', because a plus sign doesn't say what it adds. A lone channel isn't named at all. '#general' next to a composer that says 'Message #general' points at a choice of one; several channels become a select, which is what a choice looks like. The new-channel field hides behind a 'New channel' button. Sitting open right after you create a community, it reads as something you're supposed to fill in. tools/chat-layout-test.js holds the line at three viewport sizes with both panels open, and covers the header behaviour through actually adding a second channel. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* A deploy shouldn't refresh the page out from under youAdam Soltys2026-08-031-0/+95
| | | | | | | | | | | | | | | | | | | | | | The app is inlined in the page, so when a new service worker took control the page reloaded — otherwise it would keep running the old build. But navigations are network-first: the refresh that triggered the update had already fetched the new page, so the reload landed a second or two later as an unasked-for second refresh. The page and the worker now carry the same build token and the page asks which build took over, reloading only on a real mismatch. The token is hashed before substitution, since a page cannot contain a hash of itself. tools/sw-reload-test.js pins all three cases against a swappable server: a first visit doesn't reload itself, a refresh after a deploy is the only refresh, and a page left sitting open across a deploy is still reloaded — exactly once. The previous build takes two loads for one manual refresh; this one takes one. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Speak as the account you logged in with, or not at allAdam Soltys2026-08-031-0/+89
| | | | | | | | | | | | | | | | | | | | | | | | | | Logging in with a remote signer and reloading left the app showing your avatar and profile while signing everything with the wallet's own key — so chat messages arrived under a different npub with a stranger's punk on them. identity() had a fallback: when the login signer wasn't available it quietly used the wallet key instead. That fallback is gone. If you logged in as a nostr account, the app speaks as that account or it refuses and says so. Refusing is only reasonable if the signer usually comes back, so a login now persists what it takes to be that identity again. A remote signer stores its NIP-46 client key and pointer — the signer authorised that client key, not one page load, so rebuilding from it resumes the session without prompting. A pasted key stores the key, which is already the seed of everything here since the wallet's mnemonic derives from it. Resume is single-flight with a pause after failure, because identity() is asked on every send and a signer that isn't answering must not mean a relay connection per keystroke. Existing remote-signer logins predate the stored session, so they need one more sign-in to become resumable. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Only people you know get to buzz your phoneAdam Soltys2026-08-031-0/+101
| | | | | | | | | | | | | | | | | | | | | | | | | | | | A NIP-17 wrap is signed by a throwaway key with the sender sealed under two layers of nip44, so no server can filter DMs by who sent them. The notifier now ships the wrap inside the push payload and the service worker opens it locally: a message from someone you follow arrives with their name on it, a stranger's is dropped, and your own sent-copy — every DM is wrapped to the sender too — no longer notifies you about what you just typed. That needs the nostr identity key in the worker's IndexedDB, which dm-inbox.js states plainly: for a seed-derived identity it is a second copy of a key localStorage's seed already yields, and it exists only to keep strangers out. A remote signer never hands its key over, so those wallets store no secret and keep getting unfiltered notifications rather than silence. Silence is always the riskier failure here, so the rule leans the other way whenever it isn't sure: a wrap we can't open notifies, an account with no contact list published notifies (otherwise a new user's welcome message would vanish), and anyone you already have a thread with counts as known whether or not you follow them. The server-side DM cooldown drops to a second — with the device discarding pushes, a throttle there would let a discarded one swallow the message behind it. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Gift records belong to one wallet, not to the browserAdam Soltys2026-08-031-0/+79
| | | | | | | | | | | | | | | | giftRecords() caches the parsed blob on the wallet object, but the app runs a single Wallet for its lifetime and load() never dropped that cache. Switching wallets carried the old wallet's gift links into the new one, where they all rendered as claimed — none of their outpoints are live in a wallet that never issued them — and the first save wrote them back under the new wallet's key. Clear the cache on load alongside the other two gift caches, and forget records whose funding tx isn't in this wallet's own history, to clean up what earlier builds already misfiled. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Drop accidentally committed scratch testAdam Soltys2026-08-021-51/+0
| | | | | Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Offline keeps the classic wallet layoutAdam Soltys2026-08-021-0/+51
| | | | | | | | | | Offline mode forces the settings tab, and the full-page settings layout was hiding the balance card and tab strip there — walling an offline user off from their cached balance, history, and the Savings receive address. Offline now keeps balance + tabs around the settings content. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* One global account: you're in Spending or Savings everywhereAdam Soltys2026-08-021-1/+1
| | | | | | | | | | | | | | | The balance card's label becomes a Spending/Savings toggle and the selection is app-wide state (persisted per device): the headline shows the selected balance with the other quiet below, Receive shows the payment address in Spending and a fresh on-chain address in Savings (the per-page dropdown and its iconSelect widget are gone), and a gift's source follows the account instead of its own toggle. Wallets without Ark see the old single-balance card. Send stays destination-driven — an invoice pays from Spending, an address from Savings, as before. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Chat: Concord encrypted community messaging, with a default coinos roomAdam Soltys2026-08-021-0/+85
| | | | | | | | | | | | Implements the Concord protocol (CORD-01..04) — private streams, the control-plane fold, guestbook membership, owner-rooted authority — and ships a built-in "coinos" community founded by Adam's npub (owner/admin). Messages sign with the nostr login identity when a signer is live, else the wallet's NIP-06 key; all encryption rides derived stream keys so remote signers only ever sign seals. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Fix bunker (NIP-46) login: the pointer goes through fromBunkerAdam Soltys2026-08-011-0/+19
| | | | | | | | | | | nostr-tools' BunkerSigner constructor takes (clientSecretKey, params) — passing the bunker pointer as the second argument left the signer with no pointer at all, so connecting died on "reading 'pubkey'" before any network call. Use the static fromBunker, and reject an address that lists no relays with something a human can act on. Covered by a test using a real Amethyst-issued address. Co-Authored-By: Claude Fable 5 <[email protected]>
* Restore two helpers an earlier edit deleted, and guard the class of bugAdam Soltys2026-08-011-0/+77
| | | | | | | | | | | | | | A bulk replacement removed lookupMine() and the flag along with the block it was rewriting, so names setup threw at runtime — the kind of break every existing test happily passed over because nothing ever constructed a feature and started it. tools/features-smoke-test.js does exactly that: builds every feature against a stubbed wallet with the network cut, runs init/render/stop, and fails on undefined identifiers. It caught the second missing helper immediately. Co-Authored-By: Claude Fable 5 <[email protected]>
* One wallet per Nostr identity, NIP-98 auth, npub payment addressesAdam Soltys2026-08-011-0/+17
| | | | | | | | | | | | | | | | | | | | | | | | | | Consistency fix with teeth: logging in with a pasted key derived one wallet while an extension login for the SAME identity found no backup and generated a different one. Now every signer kind runs one flow — look for the published association first (that's where the money is), then derive (key in hand) or generate (signer only) — and a derived seed is published too. That costs nothing, since the derivation is public and anyone holding the key could compute it anyway, and it makes the identity open the same wallet however you log in. Registrar auth is now NIP-98 (HTTP Auth, kind 27235) instead of a homemade envelope: the signature binds the URL, method and body hash, so a captured header can't be replayed elsewhere. NIP-78 already covered the encrypted association; NIP-26 delegation is deprecated, so owner→manager nomination lives in the registrar's own model: an npub-shaped name may only be claimed by that identity, which nominates the wallet key to keep the record updated afterwards (the wallet key is the only one always available — offline, in the worker). Payment addresses therefore default to the user's REAL npub in all three login methods, falling back to the wallet's own identity when no Nostr account is linked. Co-Authored-By: Claude Fable 5 <[email protected]>
* Log in with Nostr, and link a wallet to a Nostr accountAdam Soltys2026-08-011-0/+60
| | | | | | | | | | | | | | | | | | | | | | | | | Three ways in: a NIP-07 browser extension, a NIP-46 remote signer (Amber, Amethyst — paste its bunker:// address), or a pasted nsec/hex key that lives in memory for the session and is never stored. How an account gets a wallet depends on what we can see, and the two cases are genuinely different: * We hold the key (pasted nsec): the seed is DERIVED from it — sha256("coinos:wallet-seed:v1" | privkey) as BIP-39 entropy. The same Nostr key always opens the same wallet, on any device, with nothing stored anywhere. * We only have a signer: deriving is impossible (we never see the key, and neither signatures nor NIP-44 ciphertexts reproduce), so the seed is ASSOCIATED instead — generated once and published as a replaceable event encrypted to the user's own key, which only their signer can decrypt. Later logins find and decrypt it. The second path means the Nostr key can recover the money; the UI says so before publishing anything. Linking an already-open wallet uses the same publish path and refuses to overwrite an account's existing wallet. Co-Authored-By: Claude Fable 5 <[email protected]>
* Test follows the rebrand: the SW responder announces CoinosAdam Soltys2026-08-011-1/+1
| | | | Co-Authored-By: Claude Fable 5 <[email protected]>
* Zappable offer codes and per-name Lightning offersAdam Soltys2026-08-012-0/+138
| | | | | | | | | | | | | | | | | | | | | | | | | CLINK offers (noffer): every wallet gets a static offer keypair (synced across devices, first-minted wins) answering kind-21001 invoice requests — from open tabs and from the service worker while closed, which mints against the mirrored state (the record now carries chain-4 and chain-5 key windows) and raises an open-to-receive notification. Zap requests ride along and their kind-9735 receipts publish once the payment is claimed. The code is shown in the payment-address card for pasting into a Nostr profile; the notifier watches and relays kind 21001 alongside NWC. Payment names also gain a Lightning offer: the registrar creates a BOLT 12 offer per name on the node and appends lno= to the DNS record; a settle-forwarder pushes each received payment on to the name's ark destination from a small float wallet (custody window: seconds between Lightning settle and the ark send). Name resolution now runs full client-side DNSSEC validation (src/dnssec.js): binary DoH, root-anchor chain walk, RSA + ECDSA via WebCrypto — the resolver is untrusted transport, verified against live signed zones including the canonical BIP-353 test name. Co-Authored-By: Claude Fable 5 <[email protected]>
* Simplify: one Lightning path, one balance, fewer featuresAdam Soltys2026-08-018-772/+39
| | | | | | | | | | | | | | | | | | | | | | | | | Dropped: silent payments (feature, wallet chains, scan worker, indexer config), Boltz submarine/reverse swaps (feature + swap.js + tools), the third-party ark swap bridge (config, client flows, settings card — the ASP's own Lightning send is the one path now), and the Consolidate coins settings action. The NWC pouch is gone with them: background answering now spends the WHOLE ark balance. The IndexedDB record mirrors the real manager state plus the chain-3 keys for its live coins and a change-index window; the worker pays through the ASP exactly like the open app, and the app folds the worker's spends, change, and key allocations back in on connect (additive merge + reconcile against the server). A leftover chain-6 pouch from the short-lived pouch era is swept home automatically using the keys stored in its own record. The stated tradeoff: same-origin script access now reaches the ark balance, not a carve-out — the ark balance is walking-around money, the on-chain wallet's keys are never mirrored, and per-connection NWC budgets remain the enforced ceiling. Bundle: 726KB -> 602KB. Co-Authored-By: Claude Fable 5 <[email protected]>
* Background NWC: the worker now answers — and pays — from the pouchAdam Soltys2026-08-012-1/+141
| | | | | | | | | | | | | | | | | | | | | | | The no-tap half of background wallet-connect. The push payload carries the (encrypted, client-signed) request event; a bundled service-worker responder (src/sw-nwc.js -> nwc-respond.js) finds the matching pouch in IndexedDB, decrypts with the connection's service key, and answers get_info/get_balance/list_transactions from pouch state. pay_invoice pays through the swap bridge with an ArkManager running over the pouch's chain-6 key window (one HTLC lock, one HTTP swap), enforcing per-payment and daily budgets from the pouch's own spend log, checking the notifier's /answered endpoint so it never double-pays or shouts over an open device, and persisting the swap before handing it off so the app can refund a crashed flight. Anything it can't handle falls back to the wake-the-user notification. The notifier grows /publish (verified 23195s only, rate-limited) as the worker's reply pipe and /answered for race avoidance. The app gains pouch funding (mailbox-less arkoor self-send whose recipient bytes we keep), emptying, spend/budget reconciliation on open, and refund recovery with the pouch's own keys. Co-Authored-By: Claude Fable 5 <[email protected]>
* Split device sync into per-domain slots with bounded, dirty-only publishesAdam Soltys2026-08-011-0/+47
| | | | | | | | | | | | | | | | One monolithic snapshot per device outgrew a vanilla relay's 64KB event cap the moment real payment traffic arrived, and sync silently died. Snapshots now fragment into per-domain replaceable events (core wallet, ark, nwc — an extension claims a domain by naming it), published under a new ':x:' d-tag marker that pre-split clients don't match, so they can never mistake a fragment for a full snapshot. Only domains whose content changed republish, a refused publish retries on the next save instead of being marked done, and readers apply full-snapshot semantics only to core-carrying events. The ark projection also caps its history: spent stubs at 200, done actions at 50 — the event size is bounded for life, no special relay rules required. Co-Authored-By: Claude Fable 5 <[email protected]>
* Never let a losing device's error shout over a paid zapAdam Soltys2026-08-011-0/+1
| | | | | | | | | | | Loose leader election for pay_invoice across a user's open tabs: before handling, defer a beat and stay silent if any reply for the request already exists on the relays; and before sending an ERROR reply, re-check — a device that lost the race (mid-init, stale state) was replying 'superseded' after another device's success, and clients trusted the later error, marking paid zaps as failed. Co-Authored-By: Claude Fable 5 <[email protected]>
* Make NWC budgets accumulate and travel across devicesAdam Soltys2026-08-011-0/+14
| | | | | | | | | | | | | onRequest handled every request against the connection object captured at listen() time, so each spend was recorded against a stale snapshot — spentToday restarted from the old value on every payment and the daily budget never actually bound. The connection is now re-resolved from stored state per request, which also stops serving a connection revoked on another device mid-session. recordSpend now publishes the sync snapshot so the spend (and the payment's movement + vtxo change) reaches the user's other devices. Test: two payments must accumulate to 52. Co-Authored-By: Claude Fable 5 <[email protected]>
* Sync NWC connections across devicesAdam Soltys2026-08-011-0/+35
| | | | | | | | | | | | | | | | | | | A connection made on the phone existed only there, so only that device could answer it — and if that device was asleep, a client just spun. Connections now ride the encrypted device-sync snapshot, so whichever device is awake can serve them. The snapshot is nip44-encrypted to a key derived from the seed, the same channel that already carries ark vtxo bytes and gift secrets. The merge is a union by id with two rules that matter: - REVOCATION WINS, so a stale snapshot can never resurrect a revoked connection - the higher same-day spend wins, so a stale device cannot hand a connection back its daily budget Creating or revoking a connection publishes the snapshot immediately rather than waiting for the next state change. Co-Authored-By: Claude Fable 5 <[email protected]>
* Add the NWC pouch: a bounded spending capability for background useAdam Soltys2026-08-011-0/+48
| | | | | | | | | | | | | | | | | | | | | | | | Step 2 of background wallet-connect. Answering while hal is closed means signing in a service worker, and service workers cannot read localStorage where the seed lives — which is a property worth keeping, not working around. So instead of exposing the wallet, carve off a small pouch: - its own key branch (account chain 6), never used for ordinary coins, so a leaked pouch key cannot sign for chain-3 vtxos, the mailbox or LN preimages - only the pouch's private keys and its vtxo bytes go to IndexedDB, which the service worker can read. The seed never does. - it holds only what the user puts in it Threat model, stated in the file rather than implied: same-origin script access or a stolen unlocked device can read IndexedDB and spend the pouch. That is the accepted cost of answering while closed, and it is bounded — the pouch is the blast radius. A user who leaves background NWC off has no pouch. tools/nwc-pouch-test.js asserts the isolation that matters: the pouch branch is distinct from every other branch, exported keys match their pouch pubkeys and cannot control ordinary coins, and no seed material appears in the blob. Co-Authored-By: Claude Fable 5 <[email protected]>
* NWC: own the relay set instead of borrowing the sync feature'sAdam Soltys2026-08-011-1/+6
| | | | | | | | | | | | | | | | | | | | | The connection URI advertised up to four relays (sync relays + profile relays) while the subscription only ever listened on the sync relay, so a client could publish its request somewhere hal wasn't and spin forever waiting for a reply. Amethyst did exactly that. NWC now has its own relay list used for all three things that must agree: the URI, the subscription, and the replies. It no longer depends on device sync being enabled, which would otherwise have made the whole wallet service a silent no-op. Also republish the kind 13194 capability event on every start — a client that can't find it just spins, and a single publish at creation time is easy for a relay to miss. The relay transport is now injectable so the protocol can be exercised without a live relay (tools/nwc-test.js). Co-Authored-By: Claude Fable 5 <[email protected]>
* Actually ship the NWC feature, and guard against the drift that hid itAdam Soltys2026-08-011-0/+40
| | | | | | | | | | | | | | build.js swaps src/features/index.js for a generated module listing only the features in its own ALL_FEATURES registry, so registering nwc in index.js alone was not enough: its i18n strings shipped (they live elsewhere) but its code never entered the bundle, and the settings card silently never rendered. - add nwc to build.js ALL_FEATURES, after ark since it drives ark's headless pay/balance seam - tools/feature-registry-check.js asserts the two lists agree on membership AND on construction order, since hook precedence depends on the order Co-Authored-By: Claude Fable 5 <[email protected]>
* Add Nostr Wallet Connect (NIP-47) — hal as a wallet serviceAdam Soltys2026-08-011-0/+112
| | | | | | | | | | | | | | | | | | | | | | | | | Other apps can connect over nostr and pay from this wallet without touching its keys, within a per-payment and daily budget the user sets, revocable at any time. hal holds no native Lightning balance, so every operation runs over Ark: pay_invoice takes the swap bridge when configured and falls back to the ASP's lightning-send, make_invoice mints on the ASP. NWC availability therefore tracks Ark availability. - kinds 13194/23194/23195, nip44_v2 and nip04, replying in whichever scheme the request used; every request gets a reply so clients never hang - each connection gets its own service keypair, so revoking one can't affect the others and none is tied to the user's nostr identity - budgets are checked BEFORE spending, not reconciled after - requests are ignored unless signed by that connection's client key, and replayed/stale events (>120s) can't re-trigger a payment - Settings card to create, show (URI + QR) and revoke connections, and it says plainly that this only works while a tab is open tools/nwc-test.js drives it as a real client: both encryption schemes, budget refusal with a genuine over-limit invoice, unsupported-method errors, and a request from an unauthorized key being ignored. Co-Authored-By: Claude Fable 5 <[email protected]>
* Note the Cloudflare managed-WAF exception the ASP hostname needsAdam Soltys2026-08-011-0/+4
| | | | | | | Binary protobuf POSTs trip signature rules above ~1.5KB; the demo now runs entirely over public HTTPS with a path-scoped skip in place. Co-Authored-By: Claude Fable 5 <[email protected]>
* Add the live bridge demo against the coinos mutinynet ASPAdam Soltys2026-07-311-0/+103
| | | | | | | | | | | | | Reference run for the upstream proposal: real deployed ASP, real deployed bridge, real Lightning settlement — no regtest. Boards a fresh wallet, quotes a 25 sat zap (1 sat fee vs second's 20 sat mainnet floor), locks the HTLC, lets the bridge pay and claim, then reads the revealed preimage back out of the ASP via GetHtlcPreimage. Board funding goes through a helper on cs because a board output must be vout 0 and sendtoaddress randomizes output order. Co-Authored-By: Claude Fable 5 <[email protected]>
* Add the trustless Ark->Lightning swap bridgeAdam Soltys2026-07-311-0/+190
| | | | | | | | | | | | | | | | | | | | | | | | | | | A third-party swap provider built on the proposed VtxoPolicy::Htlc: users pay bolt11 invoices from their Ark balance without the ASP being the counterparty and without trusting the bridge with custody. Scope is deliberately one-directional. An ASP's lightning *receive* is already free (0 base / 0 ppm on mainnet); the expensive leg is *send*, with a 20 sat floor that eats 80% of a 25 sat zap. Only that leg is worth undercutting. - bridge/server.js: /info, /quote, /swap, /refund over Bun.serve. Validates the user's HTLC fully (policy, hash, amount, expiry margin, full vtxo chain) before spending anything on Lightning, pays, then claims with the preimage. Crash-safe: state is written atomically and every step re-checks reality (listpays before paying, so a resumed bridge never double-pays). - bridge/ln.js: minimal CLN backend (unix socket or docker exec), named params, maxfee-capped pay, pending/paid introspection. - /refund is safe to expose publicly: the ASP only cosigns a preimage-less HTLC spend when every output pays the refunder, so it cannot be redirected. tools/ark/bridge-e2e-test.js drives the real HTTP API against the patched regtest ASP: a 25 sat zap costs 1 sat, the far-end invoice really settles, the preimage checks out, resubmission is idempotent, an unroutable invoice reports refundable rather than done, and the refund lands every sat back. Co-Authored-By: Claude Fable 5 <[email protected]>
* Client support for third-party HTLC vtxos (trustless swap bridges)Adam Soltys2026-07-311-0/+201
| | | | | | | | | | | | | | | | | | Speaks the proposed VtxoPolicy::Htlc (type byte 0x08): policy encode/decode, two-leaf taproot (claimer-with-preimage / refunder-after-expiry), preimage field on the arkoor cosign request, and GetHtlcPreimage. ArkManager gains the swap primitives: htlcLock (lock funds claimable only against a payment hash, refundable by us after expiry), htlcClaim (reveal the preimage), htlcCosignRefund (hand an unclaimable HTLC back — the server constrains the outputs to the refunder, so this is safe to expose publicly) and acceptHtlcRefund. tools/ark/htlc-bridge-test.js drives a full submarine swap between two independent wallets against a patched captaind, and asserts the ASP rejects both a wrong-preimage claim and a preimage-less spend to the bridge itself. Co-Authored-By: Claude Fable 5 <[email protected]>
* Fix send-form UX around lightning pays; harden ln state machinesAdam Soltys2026-07-311-0/+3
| | | | | | | | | | | | | | | | | | | - a rejected cosign (gRPC INVALID_ARGUMENT) returns the input vtxo to spendable and fails the action instead of leaving it pending forever — the stuck-pending state that pushed small zaps onto Boltz's 25k minimum - ark ln-pay now takes over when funds are settling (retrying quote) rather than falling through to Boltz - typing a destination clears stale send errors (also cleared on tab switch); paste path likewise - typed lightning addresses no longer auto-advance mid-keystroke (the matcher fired at `[email protected]` before `.com`, yanking the form away): the send form offers an explicit "Zap <addr>" button; pasted/scanned addresses still advance immediately - ln-recv: HTLC vtxos anchored to a not-yet-confirmed pool round park and retry instead of throwing; the requested HTLC expiry is anchored on first prepare so retries can't inflate it past the server's grant Co-Authored-By: Claude Fable 5 <[email protected]>
* Dust-isolate arkoor outputs; prove lightning E2E on regtestAdam Soltys2026-07-311-0/+137
| | | | | | | | | | | | | | | | | The server rejects an arkoor mixing dust and non-dust outputs, which broke small Lightning payments (a 45-sat HTLC next to change) and tiny plain sends. Mirror bark's isolation: dust outputs route through a combined isolation output spent by a fanout tx, splitting a non-dust output to pad the dust below-threshold. Outputs can therefore split across two vtxos, so the manager now classifies results by policy instead of index (legacy single-field actions still resume). Regtest now runs the full stack: dedicated cln-ark node (secondark/cln-hold, hold plugin + xpay) wired into captaind with a channel to the coinos dev clc node. tools/ark/ln-e2e-test.js proves pay (5000 sat and a 25-sat dust zap), hold-invoice receive with claim, and spending the claimed vtxos. Co-Authored-By: Claude Fable 5 <[email protected]>
* Add Ark <-> Lightning via the ASP (captaind-native submarine swaps)Adam Soltys2026-07-312-3/+154
| | | | | | | | | | | | | | | | | | | | | | | | | | | The ASP is the HTLC counterparty (second.tech's protocol), so no external swap provider is needed: paying a bolt11 converts pubkey vtxos into ServerHtlcSend vtxos the server can only claim by revealing the invoice preimage; receiving grants ServerHtlcRecv vtxos claimed by revealing ours. Failure paths cosign the HTLC back (revocation) — proven end-to-end against regtest captaind, which independently derives the HTLC taproots. - ark/lightning.js: bolt11 decode, lightning fee schedules, all pay/receive gRPC-web wrappers (cosign, initiate, status, revocation, hold-invoice receive flow, anti-DoS attestation) - ark/send.js: arkoor builder generalized to policy outputs (pubkey / serverHtlcSend / serverHtlcRecv), two-leaf taproot trees, HTLC clause scripts, reusable per-part cosign helpers for package RPCs - ark/manager.js: ln-pay and ln-recv crash-safe action state machines (deterministic receive preimages from the seed, chain 5), mailbox lightning notifications, unpayable-invoice -> instant revocation - proto.js: lightning ArkInfo fields + mailbox lightning message decode - validate/refresh: policy-aware vtxo validation and reconstruction - features: bolt11 routes via ark when the balance covers it (Boltz stays the fallback), lightning-invoice receive on the Ark pane - tools/ark/ln-test.js: harness proving HTLC cosign + revocation against captaind; manager-test.js updated for the consolidated bc-container chain Co-Authored-By: Claude Fable 5 <[email protected]>
* Test harnesses mine on the consolidated bc regtest nodeAdam Soltys2026-07-052-2/+2
| | | | | | | | The standalone ark-regtest bitcoind is retired: captaind, ark-electrs, and the sp indexer all run against the coinos dev node (bc container, RPC published at 127.0.0.1:18453, minrelaytxfee=0 for exit chains). Co-Authored-By: Claude Fable 5 <[email protected]>
* Features phase 2b: silent payments + nostr sync leave the core walletAdam Soltys2026-07-042-1/+149
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | wallet.js drops from 2,374 to ~1,970 lines, holding only BIP84 on-chain logic plus generic feature seams. New seams added this pass: registerBalanceExtra extra confirmed/pending buckets registerInputProvider extra spendable inputs for buildTx registerSendPreparer destinations needing post-selection derivation registerInputSigner non-BIP84 input signing registerHistoryProvider / registerScanHook / registerRealtimeHook / registerLoadHook / registerCacheExtension / registerCacheSavedHook src/features/sp-wallet.js: the whole BIP-352 engine — key derivation, worker-accelerated indexer scanning, the live indexer WebSocket, SP outputs as spendable inputs, sp1 send preparation, and per-output-key signing — installs via those seams (installSpWallet). src/features/sync.js: nostr identity + encrypted cross-device state sync plus its Settings card, installed via load/cache-saved/realtime hooks. profileChip/nostrProfile move here-adjacent (into gifts, the only other nostr consumer), so core app.js no longer imports nostr at all. buildTx/sign are now feature-agnostic (input providers + preparers + signers); _recomputeBalance* use the balance-extra registry, so a core build with no SP no longer calls a missing spBalance() (that regression dropped the minimal build into offline mode — caught by the smoke test). New coverage: tools/sp-test.js drives two browser "devices" through an SP send, indexer discovery, and an SP-input spend on the ark regtest chain (needs tools/sp-indexer.js pointed at bitcoind:18543). Minimal smoke now also asserts no SP/nostr code ships. Minimal profile: 314KB, down from 412KB (full: 580KB). All suites green: minimal smoke, sp-test, ark UI, gift lifecycle. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Features phase 2: gift/swap/ark primitives leave the core walletAdam Soltys2026-07-041-0/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | wallet.js drops from 2,762 to 2,374 lines and now exposes two generic seams instead of feature code: - registerCoinLock()/lockedCoinIds(): features contribute coin ids that spending and the balance treat as unavailable (gift reservations). - loadFeatureState()/saveFeatureState(): namespaced per-wallet persisted state. src/features/gifts-wallet.js takes the whole gift engine — reservation sets, gift records, coin selection, the SIGHASH_SINGLE/NONE PSBT builder, the compact codec, claim building, nostr-locked gifts — as an installGiftWallet(wallet) instance install plus standalone exports. Swap storage/key helpers (loadSwaps/saveSwaps/swapNode/nextSwapIndex) install from swap.js; ark state storage installs from the ark feature. The silent-payment scan worker only embeds when the sp feature ships (wallet.js already tolerated its absence). Minimal profile: 412KB, down from 476KB (full: 578KB). All suites pass: ark UI e2e, ArkManager crash-resume, gift lifecycle, minimal smoke. SP's engine (scanning, BIP-352 signing) stays in wallet.js — extracting it safely wants an SP e2e harness on this stack first. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Build profiles: HAL_FEATURES selects which features shipAdam Soltys2026-07-041-0/+99
| | | | | | | | | | | | | | | | | | | | | | build.js swaps src/features/index.js for a generated module importing only the enabled features (gifts,swaps,ark,sp — unset means all, "none" means a plain on-chain wallet), so excluded feature code and its network endpoints never enter the bundle. Minimal profile: 476KB vs 577KB, with the Ark/gRPC, Boltz, and MuSig2 code verifiably absent. `bun run build:minimal` builds it. tools/minimal-smoke.js proves the minimal profile end-to-end: no receive-mode dropdown, no gift button, no swap/Ark/SP settings cards, no page errors, and a plain on-chain receive still lands. The two blessed profiles are full (everything, the existing e2e suites) and minimal (this smoke test); other combos should work but aren't CI'd. Gift and SP primitives still live inside wallet.js, so those bytes remain in the minimal bundle for now — extracting them is the next step if we want the core leaner still. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* History: gifts are timeline entries, not a separate sectionAdam Soltys2026-07-041-0/+17
| | | | | | | | | | | | | | | Gift links now sort into the send/receive timeline like any entry: a clickable row (🎁, status tag, age, amount) opening a detail page that carries the date/status plus the actions that used to crowd the list row (view gift link, cancel/reclaim with the existing confirm flow). A claimed gift merges into its claim transaction's row — labeled as the gift, clickable to the tx detail, which gains a "This transaction delivered your gift" section (claim txid resolved via outspend and cached on the gift record). The old pinned gifts header and the paginated all-gifts view are gone. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Gift claim: instant 'Payment received!' after claimingAdam Soltys2026-07-041-0/+7
| | | | | | | | | | | | Claiming into an existing wallet used to drop onto the Receive tab (address/QR visibly rotating) until the background scan noticed the credit and fired the celebration. The claim already knows it succeeded and for how much, so the celebration now shows immediately; the scan runs behind it and its receive-index advance is acknowledged so the generic celebration doesn't fire a second time. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Gift claim: correct copy when claiming into an existing walletAdam Soltys2026-07-041-0/+6
| | | | | | | | | "Claim it into a brand-new wallet that only you control." only fits the new-wallet path; claiming into an existing wallet now says which wallet it lands in ("Claim it into your wallet "Wallet 1"."). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Gift claim: make the wallet chooser read as a choiceAdam Soltys2026-07-041-0/+82
| | | | | | | | | | | | The existing-wallet buttons looked like inert inputs. Now: a "Receive into one of your wallets:" label, a chevron on each wallet row so it reads as tappable, and an "or" divider before the new-wallet button. (tools/ark/gift-shot.js: screenshot harness that mints a real regtest gift and opens its link.) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Ark: 'Sent!' screen matches the receive celebrationAdam Soltys2026-07-041-0/+2
| | | | | | | | Tap anywhere to proceed instead of a Done button, and the sent amount shows in red (amount-neg) rather than green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Ark: history rows open a detail viewAdam Soltys2026-07-041-0/+8
| | | | | | | | | | | Ark movements are clickable like on-chain txs. The detail view shows amount, date, status, the destination address (sends, copyable), the vtxo id, and for boards the funding txid with copy + explorer link. Movements now record those fields at completion time; older records without them still render (amount/date/status only). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Ark: hide fee-rate, coin control, Max, and add-recipient for Ark sendsAdam Soltys2026-07-041-0/+5
| | | | | | | | | An arkoor send has no mining fee and no coin selection, so pasting an Ark address now shows just the amount field plus a hint line; the irrelevant on-chain controls stay hidden. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Ark: boarding success screen with txid link instead of a toastAdam Soltys2026-07-041-1/+5
| | | | | | | | | | Broadcasting a board now lands on a checkmark screen showing the amount that will arrive (after the board fee), a note about the confirmation, and the funding txid with copy + explorer link; Done returns to the Receive tab. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
* Ark: boarding lives on the Receive tab's Ark viewAdam Soltys2026-07-031-3/+10
| | | | | | | | | | | Selecting Ark on Receive now shows the address plus an inline "move on-chain funds into Ark" form (with available balance / minimum hints). The Settings card slims down to server choice, coin count, in-progress operations, and refresh/consolidate; the balance already shows on the balance card. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy