aboutsummaryrefslogtreecommitdiffstats
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
...
* Grayscale button hover — the last orange hardcodeAdam Soltys2026-08-011-1/+3
| | | | Co-Authored-By: Claude Fable 5 <[email protected]>
* Coinos branding: logo and black & white themeAdam Soltys2026-08-014-11/+13
| | | | | | | | | Icons, favicon, and PWA assets come from the coinos brand set; the accent goes monochrome (black in light mode, white in dark, with a proper on-accent text variable so buttons stay legible both ways) and the inline bitcoin mark inherits the text color. Co-Authored-By: Claude Fable 5 <[email protected]>
* Coinos v3: rebrand from Hal, payment addresses move to @coinos.ioAdam Soltys2026-08-015-17/+17
| | | | | | | | | | | | | | | The wallet is the next version of coinos, deployed at v3.coinos.io (same build as halwallet.app) ahead of an eventual migration of coinos.io itself. App name, manifest, notifications, and the NWC alias all say Coinos; the Hal Finney tribute stays. The registrar is multi-domain (per-domain Cloudflare zones; existing halwallet.app names keep working) and new claims default to @coinos.io. Names belonging to existing coinos users are reserved for them — the registrar checks the live coinos API before granting a claim — until the migration gives those users a way to claim their own. Co-Authored-By: Claude Fable 5 <[email protected]>
* Zappable offer codes and per-name Lightning offersAdam Soltys2026-08-0110-80/+699
| | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* Payment names: ₿[email protected] via BIP-353 DNS payment instructionsAdam Soltys2026-08-016-1/+283
| | | | | | | | | | | | | | | | | | | | Receiving: a Settings card claims a username, signed with the wallet's nostr key; the new names registrar (names/server.js, deployed at names.halwallet.app) enforces first-come ownership by that key and mirrors the claim into a DNSSEC-signed TXT record at <name>.user._bitcoin-payment.halwallet.app whose BIP-21 URI carries the wallet's reusable ark address. The record re-registers itself if the ark address ever changes, and a collapsible hint shows the single CNAME a user adds to serve the same name from their own domain (BIP-353 supports delegation when both zones are DNSSEC-signed). Sending: a pasted name@domain resolves over DNS first — two independent DoH resolvers must agree and both report the DNSSEC AD flag (full client-side chain validation is the stated follow-up) — an ark= instruction pays natively, an on-chain path fills the send form, and a name with no DNS record falls back to the Lightning-address zap flow. Co-Authored-By: Claude Fable 5 <[email protected]>
* Simplify: one Lightning path, one balance, fewer featuresAdam Soltys2026-08-0117-2194/+307
| | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* Default new mainnet wallets to the coinos ASP (ark.coinos.io)Adam Soltys2026-08-011-3/+6
| | | | | | | | Existing wallets with an explicitly chosen provider keep it; a wallet that was riding the old default keeps its second.tech coins under that server's namespace and can reselect it in Settings at any time. Co-Authored-By: Claude Fable 5 <[email protected]>
* Background NWC: the worker now answers — and pays — from the pouchAdam Soltys2026-08-017-23/+552
| | | | | | | | | | | | | | | | | | | | | | | 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]>
* Refresh the push-notifier registration whenever connections changeAdam Soltys2026-08-011-1/+29
| | | | | | | | | | The notifier only wakes a device for service pubkeys it was told about at registration time, so recreating a connection silently killed background wake-ups — the registration kept watching the revoked key forever. Re-register (debounced; the server replaces by endpoint) on init, create, revoke, and sync-merge whenever background alerts are on. Co-Authored-By: Claude Fable 5 <[email protected]>
* Split device sync into per-domain slots with bounded, dirty-only publishesAdam Soltys2026-08-014-18/+89
| | | | | | | | | | | | | | | | 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]>
* Keep sync snapshots under relay size limits, and say when they bounceAdam Soltys2026-08-012-2/+12
| | | | | | | | | | | | Tonight's payments pushed the encrypted snapshot past strfry's 64KB maxEventSize and every publish after the first zap was silently refused — the other device just showed no history, with nothing logged anywhere. The relay's limit is raised to 256KB; client-side, movements older than the last 50 now drop their bolt11/preimage (~half a KB per ln-send) and the cap tightens to 200, and a refused snapshot publish logs the relay's reason. Co-Authored-By: Claude Fable 5 <[email protected]>
* Never let a losing device's error shout over a paid zapAdam Soltys2026-08-012-4/+33
| | | | | | | | | | | 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]>
* Add an NWC subscription watchdogAdam Soltys2026-08-011-2/+20
| | | | | | | | | | | The pool's reconnect doesn't heal every socket death (an errored socket sets skipReconnection and gives up), and a wallet service with a dead subscription looks exactly like one that's closed — the client spins with no error, which is what happened to tonight's zaps. Re-subscribe every 90s and on online/visibility transitions; the since window plus the replay guard make the churn and any duplicate deliveries harmless. Co-Authored-By: Claude Fable 5 <[email protected]>
* Reconcile vtxos against the server before a headless NWC paymentAdam Soltys2026-08-011-0/+5
| | | | | | | | | | The sync merge is additive by design, so a vtxo spent on one device can survive in another device's state looking spendable. The interactive send path reconciles on intent; the NWC path connected and spent blind, so a wallet answering from stale state picked an already-spent coin and the ASP refused with 'vtxo … is not spendable (state: spent)'. Co-Authored-By: Claude Fable 5 <[email protected]>
* Make NWC budgets accumulate and travel across devicesAdam Soltys2026-08-011-0/+11
| | | | | | | | | | | | | 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]>
* Default the mainnet ark bridge to coinosAdam Soltys2026-08-011-1/+4
| | | | | | | | | | Small bolt11 sends were unroutable by default: the ASP path's CLN fee budget is a fraction of the ASP's own minimum fee, which rounds to zero sats of routing budget on a zap-sized payment. The coinos bridge now runs open (token gate removed server-side) so the default works for every wallet; users can still pick Off or Custom in settings. Co-Authored-By: Claude Fable 5 <[email protected]>
* Keep relay sockets alive: enable pool ping + auto-reconnectAdam Soltys2026-08-011-1/+5
| | | | | | | | | | | | A silently dropped WebSocket (proxy idle timeout, network change) used to kill every live subscription with nothing re-subscribing — device sync stopped merging and the NWC service stopped answering until the next reload. That is how a connection created on the phone stayed invisible to an open laptop tab. The pool now pings every 29s (dummy-REQ fallback in browsers) and reconnects with backoff, re-firing open subscriptions from the last event seen. Co-Authored-By: Claude Fable 5 <[email protected]>
* Surface refused relay publishes; publish NWC info once per sessionAdam Soltys2026-08-012-5/+23
| | | | | | | | | | | | | relay.coinos.io's write policy was rejecting every 23195/13194 hal sent (kinds were whitelisted to coinos server pubkeys — incompatible with hal's per-connection service keys) and the rejections temp-banned the IP. publishOn swallowed all of it, which disguised a relay-side policy problem as a client bug. The relay policy is fixed separately; here, refused publishes now log the relay and reason, and the 13194 info event goes out once per connection per session instead of on every listen() so replies keep their share of the relay's per-IP rate budget. Co-Authored-By: Claude Fable 5 <[email protected]>
* Pass a single filter to subscribeMany — arrays are rejected by relaysAdam Soltys2026-08-011-2/+6
| | | | | | | | | | nostr-tools ≥2.23 changed subscribeMany to take one filter object; the old array form now serializes as ["REQ",id,[{...}]], which every relay answers with CLOSED "provided filter is not an object". That killed the NWC request subscription (the Amethyst forever-spinner), the device-sync live subscription, and the push notifier's relay watch — all silently. Co-Authored-By: Claude Fable 5 <[email protected]>
* Instrument the silent NWC request-drop pathsAdam Soltys2026-08-012-7/+24
| | | | | | | | | Every guard in onRequest now names itself when it drops an event, and listen()/stop() log subscription generations so a later listen() killing the live sub becomes visible. subscribeOn passes extra params through so relay-initiated CLOSED reasons surface too. Co-Authored-By: Claude Fable 5 <[email protected]>
* Never silently drop an NWC request addressed to usAdam Soltys2026-08-011-1/+30
| | | | | | | | | | | | | | | A request that fails to decrypt was returning without a reply, on the theory that undecryptable traffic wasn't ours. But the event is addressed to this connection's service key and signed by its client key — it IS ours, so a failure there is our bug, and dropping it looks exactly like the wallet being offline. That is why Amethyst spun with no way to tell why. Now it logs the reason, records it for the Settings card, and replies with an INTERNAL error (in both schemes, in case scheme detection was itself wrong) so the client fails fast. The card also shows the last request accepted, so the listener can be seen working without devtools. Co-Authored-By: Claude Fable 5 <[email protected]>
* Wake hal for wallet-connect requests while it is closedAdam Soltys2026-08-013-0/+97
| | | | | | | | | | | | | | | | | | | Step 3, first half. Android Chrome freezes backgrounded PWAs and closes their relay sockets, so a phone with hal "open" answers nothing — which is what made Amethyst spin. Push is the only way to reach a frozen app. - service worker gains a `push` handler. It deliberately does NOT spend: if a window is open it nudges it to re-listen, otherwise it raises a notification that opens hal on tap. Putting a spend path in the worker is the remaining half and wants its own testing. - hal-side: request permission, subscribe with the notifier's VAPID key, and register its service pubkeys with nwcpush.coinos.io. Turning it off deregisters, unsubscribes, and clears the pouch — no reason to leave spendable keys where a worker can read them if nothing will. - a Settings toggle, and the ark feature exposes the ASP context the pouch record needs. Co-Authored-By: Claude Fable 5 <[email protected]>
* Sync NWC connections across devicesAdam Soltys2026-08-011-1/+52
| | | | | | | | | | | | | | | | | | | 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/+124
| | | | | | | | | | | | | | | | | | | | | | | | 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-012-10/+33
| | | | | | | | | | | | | | | | | | | | | 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]>
* Add Nostr Wallet Connect (NIP-47) — hal as a wallet serviceAdam Soltys2026-08-016-1/+417
| | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* Stop the nostr sync snapshot from importing another ASP's coinsAdam Soltys2026-08-012-3/+24
| | | | | | | | | | | | | | | | | | | | | The phantom balance came back through the sync path. Snapshots published by older builds carry no ASP tag, so the tag check passed them, and with the local namespace empty the merge returned the snapshot wholesale — writing second.tech's vtxos straight into the coinos namespace, where the manager then stamped them as coinos'. Two fixes, both grounded in the fact that a vtxo names its own cosigner: - the sync path derives the snapshot's true owner from its vtxos and parks it under that owner instead of merging when it isn't provably ours - ArkManager.init drops any state whose coins were cosigned by a different server, whatever the stamp claims. Stamps are something we wrote; the vtxo bytes are evidence. Found by tracing localStorage reads in the live tab: the namespaced key was being *created* during load, which pointed at the merge rather than at storage keying. Co-Authored-By: Claude Fable 5 <[email protected]>
* Repair Ark state that an earlier build stamped with the wrong serverAdam Soltys2026-08-011-1/+36
| | | | | | | | | | | | | | | The first namespacing attempt could brand one ASP's coins with whichever server happened to be connected, filing them under the wrong namespace — a phantom balance that survives both later fixes because the damage is already written to localStorage. loadArkState now checks the stamp against the vtxos themselves. A vtxo names its cosigner in its own bytes, which is fact; the stamp is just something we wrote. When they disagree the state is misfiled: it moves to a holding key under its true owner and this namespace shows nothing. adoptArkState claims from that holding area when the matching server connects. Co-Authored-By: Claude Fable 5 <[email protected]>
* Never render another ASP's Ark state before connectingAdam Soltys2026-08-012-7/+35
| | | | | | | | | | | | | | | | The previous fix still let a phantom balance through: loadArkState fell back to the pre-namespacing key when a namespace was empty, and the UI renders from that before any server is contacted — so the guard, which runs on connect, never got a say. loadArkState now returns only the namespaced state. Legacy state is claimed through adoptArkState(serverPubkey), which proves ownership first: from the state's own stamp, or failing that by decoding a vtxo, whose wire format names its cosigner. No network call, and it cannot guess wrong. On a match the state moves into the namespace and the legacy key is retired; on a mismatch it is left untouched for its real owner. Co-Authored-By: Claude Fable 5 <[email protected]>
* Keep Ark state per ASP, not just per walletAdam Soltys2026-08-012-15/+61
| | | | | | | | | | | | | | | | | | | | | Switching providers showed the previous server's balance and tried to resume its in-flight board ("1 deposit confirming" against a server that had never heard of it). A vtxo is only meaningful against the ASP that cosigned it, so state must be scoped to both the wallet and the server. - ark state keys gain a namespace derived from the ASP endpoint, so switching providers switches state and switching back restores it - state is stamped with the server pubkey; on connect a mismatch is discarded rather than displayed. This also self-corrects the one-time adoption of pre-namespacing state, which can only guess which ASP it belonged to - mergeArkStates refuses to fuse two servers' states, and the nostr sync snapshot carries (and filters on) the ASP it came from, so a device on a different provider can't inject foreign vtxos Nothing is deleted: each ASP's state lives under its own key, and the legacy key is left in place for whichever server rightfully claims it. Co-Authored-By: Claude Fable 5 <[email protected]>
* Settings: coinos ASP presets + swap-bridge providerAdam Soltys2026-07-313-3/+251
| | | | | | | | | | | | | | | - ark presets gain coinos entries (ark.coinos.io on mainnet, ark-staging.coinos.io on mutinynet) alongside Second's - new per-network swap-bridge setting (preset/custom URL + access token) with its own Settings card When a bridge is configured, a bolt11 quote asks it first and shows its fee; paying locks an HTLC only the bridge can claim, and only by revealing the invoice preimage. Any failure — unreachable bridge, refused swap, unpayable invoice — refunds and falls back to the ASP's own lightning-send, so the payment never dead-ends. Hanging swaps are refunded on reconnect. Co-Authored-By: Claude Fable 5 <[email protected]>
* Client support for third-party HTLC vtxos (trustless swap bridges)Adam Soltys2026-07-314-5/+187
| | | | | | | | | | | | | | | | | | 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]>
* Re-render the send form when a destination annotation appearsAdam Soltys2026-07-311-2/+6
| | | | | | | | | The lightning-address zap button is computed at render time, but onInput only re-rendered when on-chain validity flipped — an lnaddr never does, so the button never appeared. Track sendFormNote presence like readiness and re-render on a flip. Verified with a headless-browser check. Co-Authored-By: Claude Fable 5 <[email protected]>
* Fix send-form UX around lightning pays; harden ln state machinesAdam Soltys2026-07-315-55/+119
| | | | | | | | | | | | | | | | | | | - 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-313-95/+201
| | | | | | | | | | | | | | | | | 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-319-48/+1073
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* Sync silent-payment scan across devices so the catch-up runs onceAdam Soltys2026-07-311-1/+35
| | | | | | | | | | | | | | | | | | | | | | | | | | SP state (lastSpScan watermark + found spUtxos) was in the nostr snapshot but, unlike Ark/gifts, its cache extension wasn't mergeAlways — so it only applied via the newest full snapshot at load time, last-writer-wins. The watermark could regress (a device behind on SP, or mid-rescan, publishing a newer snapshot would reset another device's watermark), so every device tended to re-grind the ~1M-tweak backlog independently. Make it a commutative merge like Ark: - mergeAlways: true, so it merges live and from older snapshots too. - lastSpScan takes the MAX (monotonic high-water mark, never regresses) — this is what makes the block catch-up run once across all devices. - spUtxos is an additive union by outpoint; a coin we already track is never mutated, and spend detection stays each device's own job (_refreshSpUtxos drops backend-spent coins), so stale merged-in coins self-heal on refresh. - Anti-entropy: republish our superset when we know more, guarded so it stops once devices converge. A manual rescan still works — it forces from=1 synchronously (no await before capturing it), so a concurrent max-merge can't sabotage the re-scan. Verified with a merge unit test driving the real extension (max/union/converge). Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Fix silent-payment catch-up pegging the CPU (both threads)Adam Soltys2026-07-301-12/+29
| | | | | | | | | | | | | | | | | | | | | | | | | | A wallet a few thousand blocks behind gets the whole scan window from the indexer at once (~1M tweaks over ~3900 mainnet blocks). The catch-up loop chunked by 100 BLOCKS, but the cost is one EC point-multiply per TWEAK and mainnet blocks are wildly uneven (up to 2565 tweaks each) — so a 100-block chunk could be ~25k tweaks, seconds of work that blew the worker's 30s timeout. On timeout the caller ran the SAME chunk inline on the main thread while the orphaned worker kept grinding it, pegging both threads and freezing the visible tab for the whole ~20min catch-up. Two fixes: - Chunk by tweak count (budget 2000, always ≥1 block) instead of block count, so every worker call stays sub-second regardless of block density. Verified against live mainnet data: max chunk drops from ~25k tweaks to ~2.8k (~850ms, <3% of the timeout). - On a worker-call timeout, TERMINATE the worker instead of leaving it grinding the orphaned request behind the caller's inline fallback — that double-grind was the both-threads-pegged symptom. _ensureSpWorker respawns a fresh worker on the next call. Diagnosed via CDP CPU profiling of the live tab (95% self-time in _spHitBlocks -> _spCandidate -> secp256k1 wNAF/precompute). Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Add NIP-57 Lightning zaps (npub / lightning address / lnurl)Adam Soltys2026-07-308-15/+367
| | | | | | | | | | | | | | | | | | | | | | | | | | Send a Lightning zap to a nostr npub, a lightning address (name@domain), or an lnurl. New src/lnurl.js does the resolution half — target → LNURL-pay params → signed kind-9734 zap request → callback → bolt11 — and the new zaps feature (src/features/zaps.js) drives the amount/comment UI, then hands the invoice to the existing Boltz submarine-swap flow via a new swaps.startLnPay hook, so there's one tested pay/review/confirm/success path. Routing: a bolt11 is still caught by swaps; an npub still prefers an instant free Ark zap when Ark can serve it, and only falls through to a Lightning zap otherwise — ark's "no Ark address" screen also offers an explicit "Zap over Lightning" fallback (gated on the zaps feature's canLnZap). A real zap receipt is attributed only when the server advertises allowsNostr and we know the recipient's pubkey; otherwise it degrades to a plain LNURL-pay. Nostr seam gains nostrSign (finalize without publishing), nostrProfile (kind-0 with lud16/lud06), and nostrRelays. Lightning addresses are debounced in matchSendText since their regex matches mid-typing. Verified: unit tests for parse/encode/zap-request; a live resolve→signed 9734→payable mainnet bolt11 against coinos; and an in-browser DOM smoke (zap form + LNURL params over CORS + allowsNostr hint + handoff). Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Filter swap providers by network; guard cross-network selectionAdam Soltys2026-07-292-16/+28
| | | | | | | | | | | | | | Tag each BOLTZ_PRESET with the network(s) it serves and add boltzPresetsFor(net) so the provider dropdown only lists backends valid for the active network — a mainnet wallet no longer shows the local (regtest) or coinos-staging (mutinynet) backends. getBoltzProviderId() now only honors a stored choice if it's valid for the current network, so switching to mainnet can't silently keep using a regtest/mutinynet backend, and the getBoltzProvider() fallback can no longer resolve to staging. Also point the testnet default at custom (was the mainnet Boltz API). Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Coalesce background renders so user input takes priorityAdam Soltys2026-07-081-1/+19
| | | | | | | | | | | | | | | | | Every wallet.emit() re-rendered the whole screen synchronously. On refocus after the PWA was backgrounded, a burst of emits (WS reconnect, the wake scan's several emits, syncFromNostr merge, each live device snapshot) fired a dozen full-screen rebuilds back-to-back, starving input — navigation lagged ~2s until the burst drained. Route the subscription through scheduleRender(), which coalesces emits into at most one render per animation frame. Direct user actions still call render() synchronously and cancel any pending frame, so a tap or navigation always renders immediately and takes priority over background repaints. rAF also skips rendering while hidden and fires once on refocus. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Fee rates from live explorer + reconnect half-open socket on wakeAdam Soltys2026-07-063-2/+31
| | | | | | | | | | | | | | | | - Fee rates on the electrum backend came from Fulcrum's estimatefee (Core's estimatesmartfee): historical, laggy, floored at 1 sat/vB, so it showed 1 across the board while mempool.space showed 11/10. Pull the configured explorer's live recommendation instead (/v1/fees/recommended, then /fee-estimates), falling back to estimatefee only when the explorer is unreachable — self-hosted/regtest behaviour unchanged. - On mobile a backgrounded page leaves the socket half-open: TCP is dead but readyState still reads OPEN, so wake() never reconnected and fired scan RPCs into the void, waiting ~20s for the heartbeat to recover. wake() now forces a reconnect when the socket has been silent past the heartbeat window, so a payment shows the instant you refocus. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Gift cross-device sync, snapshot size cap, reliable SW updates, gift-detail ↵Adam Soltys2026-07-064-6/+62
| | | | | | | | | | | | | | | | | | | | | layout - Gifts now sync across devices: gift records + reserved/reclaimed coin sets ride the encrypted snapshot as a merge-safe extension (like ark), and every gift state change (create/reclaim/revoke) triggers a saveCache so it actually publishes. A gift made on one device now shows on the others. - Cap the published snapshot at ~40KB JSON (relay rejects >64KB events, nip44 expands ~1.5x): trim oldest on-chain txs when oversized; another device backfills via rescan. Typical wallets untouched. - Service worker: register with updateViaCache:'none' so a new build is detected immediately (not held by the 4h HTTP cache), and reload once on controllerchange so the fresh app runs without a manual second refresh. - Gift detail: View/Reclaim share a row, Revoke on its own full-width line so 'Revoke now' stops wrapping. Co-Authored-By: Claude Fable 5 <[email protected]>
* Slim ark state for sync so it fits the relay's 64KB event capAdam Soltys2026-07-063-11/+31
| | | | | | | | | | | | | | Cross-device ark sync silently failed: the full snapshot (deep-genesis vtxo bytes + done-action signing hex) was ~153KB and relay.coinos.io rejects events over 64KB, so no publish ever landed and other devices never saw the balance. The synced ark state is now slimmed — spent vtxos drop their signed bytes (kept only for spendable/pending, which a device might spend/exit), done board/offboard/exit actions keep just the fields history labels need (hex stripped), in-flight actions and movements beyond the last 300 are dropped. Local storage keeps the full state; only the published copy is slim. ~150KB -> well under 64KB. Co-Authored-By: Claude Fable 5 <[email protected]>
* tmp: log publish result (debug)Adam Soltys2026-07-061-2/+3
|
* tmp: instrument ark connect + sync publish (debug)Adam Soltys2026-07-062-2/+4
|
* Publish ark state on connect so idle devices share their balanceAdam Soltys2026-07-061-0/+6
| | | | | | | | | | | | | Cross-device ark sync only re-published on a state change (send/board), so a device holding ark funds but making no transaction never wrote its state to the relay — another device could never see those coins (and a legacy shared-tag snapshot that got clobbered to zero-ark stayed that way). connectArk now calls saveCache once on connect when there are vtxos, publishing this device's balance to its per-device slot so every device's ark funds reach the relay and get merged, no transaction needed. Co-Authored-By: Claude Fable 5 <[email protected]>
* Show ark balance for watch-only wallets (read from persisted state)Adam Soltys2026-07-061-4/+11
| | | | | | | | | | | | | | After a refresh a wallet reverts to watch-only unless the seed was saved to device — and ark balance/history were gated behind arkAvailable() (which excludes watch-only), so the on-chain balance showed but the ark balance vanished. The ark state is persisted locally and should display read-only exactly like the on-chain balance. arkStateNow() now reads it whenever Ark is configured for the network, regardless of watch-only; the Exit link (which needs keys to act) is hidden for watch-only. Verified: full wallet shows balance + Exit link, a locked/watch-only wallet shows the balance alone. Co-Authored-By: Claude Fable 5 <[email protected]>
* Consolidate ark exit into one page: cooperative + unilateralAdam Soltys2026-07-062-41/+88
| | | | | | | | | | | | | The cooperative offboard was buried under the receive-tab board form and the unilateral exit lived in Settings — both unintuitive. Now a small 'Exit' link next to the Ark balance opens a dedicated page that explains and offers both: cooperative (server co-signs, fast, no on-chain funds needed, small server fee) and unilateral (trustless, timelocked, pays mining fees). In-flight exit progress (hop counter, timelock countdown, cancel) moved here from Settings; the receive pane loses the offboard button and Settings loses the exit button. Co-Authored-By: Claude Fable 5 <[email protected]>
* Gift claim into an existing wallet is one stepAdam Soltys2026-07-061-5/+26
| | | | | | | | | | | Choosing a wallet used to just load it and show a second screen with a Claim button. Now tapping a wallet claims immediately (the pre-claim check auto-fires doClaim), and the claim fee — network fee estimate, or 'instant, no fee' for ark gifts — shows on the choose screen so it's still visible before you pick. Fresh-wallet claims keep the welcome+backup flow. Co-Authored-By: Claude Fable 5 <[email protected]>