aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/ark.js
Commit message (Collapse)AuthorAgeFilesLines
* Stop the Ark card flickering on the settings pageAdam Soltys2026-08-011-2/+15
| | | | | | | | | | | | Every synced snapshot re-ran initArk() while Ark was unconnected, and initArk() calls stopArk() first — so each arriving snapshot tore down and restarted the connection, flashing 'Connecting to the Ark server…' and jittering the page. Automatic re-inits are now skipped while a connection exists or is in flight, and rate-limited to one per 20s; user-driven reconnects (switching servers, the connect button) are unaffected. Co-Authored-By: Claude Fable 5 <[email protected]>
* Fresh-seed restore: connect after parking a synced ark snapshotAdam Soltys2026-08-011-0/+6
| | | | | | | | | | | | A seed imported on a new device (or a new origin — v3.coinos.io) has no local ark state and no live connection, so the anti-poisoning guard can't prove the incoming snapshot's owner and parks it in the hold. Nothing then triggered the connect that would adopt it — ark is lazy on fresh wallets — so the balance never appeared. Parking with no local identity now kicks the init; connecting learns the server pubkey and the held state is adopted iff it matches. Co-Authored-By: Claude Fable 5 <[email protected]>
* Zappable offer codes and per-name Lightning offersAdam Soltys2026-08-011-3/+4
| | | | | | | | | | | | | | | | | | | | | | | | | 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-011-0/+5
| | | | | | | | | | | | | | | | | | | | 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-011-282/+80
| | | | | | | | | | | | | | | | | | | | | | | | | 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-011-2/+110
| | | | | | | | | | | | | | | | | | | | | | | 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-3/+10
| | | | | | | | | | | | | | | | 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-011-1/+4
| | | | | | | | | | | | 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]>
* 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]>
* Wake hal for wallet-connect requests while it is closedAdam Soltys2026-08-011-0/+6
| | | | | | | | | | | | | | | | | | | 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]>
* Add Nostr Wallet Connect (NIP-47) — hal as a wallet serviceAdam Soltys2026-08-011-0/+47
| | | | | | | | | | | | | | | | | | | | | | | | | 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-011-2/+15
| | | | | | | | | | | | | | | | | | | | | 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-011-7/+28
| | | | | | | | | | | | | | | | 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-011-15/+47
| | | | | | | | | | | | | | | | | | | | | 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-311-3/+178
| | | | | | | | | | | | | | | - 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]>
* Fix send-form UX around lightning pays; harden ln state machinesAdam Soltys2026-07-311-29/+49
| | | | | | | | | | | | | | | | | | | - 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-2/+2
| | | | | | | | | | | | | | | | | 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-311-7/+231
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* Add NIP-57 Lightning zaps (npub / lightning address / lnurl)Adam Soltys2026-07-301-0/+5
| | | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* Slim ark state for sync so it fits the relay's 64KB event capAdam Soltys2026-07-061-6/+28
| | | | | | | | | | | | | | 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: instrument ark connect + sync publish (debug)Adam Soltys2026-07-061-1/+2
|
* 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-061-41/+80
| | | | | | | | | | | | | 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]>
* No ark jitter on refresh: render from persisted state before connectAdam Soltys2026-07-061-11/+31
| | | | | | | | | | | | | | | The balance line and history-row icons read the LIVE ark manager, which is null until connectArk() finishes its async handshake — so on refresh the ark balance briefly vanished and ark rows flashed the Bitcoin icon before flipping to the ark mark. Read-only hooks (balance, decorateTxRow, historyEntries, detail views) now fall back to the persisted ark state from storage via arkStateNow(), so they paint correct on first render; the manager connecting later just keeps it fresh with identical data (no visible change). Verified: balance present and board row shows the ark icon from the first 50ms sample after reload. Co-Authored-By: Claude Fable 5 <[email protected]>
* History: Ark glyph in the row circle (no text chip); real Bitcoin logoAdam Soltys2026-07-061-3/+4
| | | | | | | | | | | Off-chain Ark rows (arkHistoryItem) showed an arrow + a redundant 'Ark' text chip; now the Ark mark sits in the direction-colored circle and the chip is gone — the label + signed amount carry direction. The detail header gets the badge instead of the chip. And the receive selector's Bitcoin option uses coinos's real bitcoin.svg (orange disc + tilted white B) instead of a bare ₿ character. Co-Authored-By: Claude Fable 5 <[email protected]>
* Icons in the receive selector and on silent-payment history rowsAdam Soltys2026-07-061-1/+1
| | | | | | | | | | | | | | The receive address-type picker was a native <select>, which can't render SVG in <option>s. Replace it with a lightweight custom dropdown (icon-select) that shows a glyph per rail — ₿ Bitcoin, ⚡ Lightning, the Second mark for Ark, 🕶️ Silent payment — closing on select or an outside tap. Receive modes gained an icon field; the address option is built in app.js. Silent-payment receipts (which merge into history as on-chain-looking rows) now get a rail glyph + SP tag via a decorateTxRow so they're distinguishable, matching how ark/gift rows already decorate. Co-Authored-By: Claude Fable 5 <[email protected]>
* Ark settings header: flat mark, no badgeAdam Soltys2026-07-061-1/+1
| | | | | | | | Use ARK_MARK (bare currentColor glyph) instead of the app-badge in the settings card header, so it sits like the sibling feature glyphs (lightning/SP/gifts) rather than a heavy dark square. Co-Authored-By: Claude Fable 5 <[email protected]>
* Ark icon: Second's logo instead of the crossed-swords placeholderAdam Soltys2026-07-061-6/+6
| | | | | | | | | | | | | | The ⚔ was a U+2694 emoji stand-in. Swap in Second's actual mark (second.tech logo: a triangle with an evenodd inner cutout) as two format.js helpers — ARK_ICON (dark rounded-square app badge + white mark, for card/section headers) and ARK_MARK (bare currentColor glyph, for the history-row circle). Wired into the Ark settings card, the boarding/withdrawal/exit history rows + detail section, and the How-it-works Ark section; the gift source toggle drops the emoji for plain 'Ark'. Verified the badge renders (18px, #00090C square, white evenodd mark). Co-Authored-By: Claude Fable 5 <[email protected]>
* Ark sync: anti-entropy so a device can't clobber another's coinsAdam Soltys2026-07-061-8/+20
| | | | | | | | | | | | | All devices share one replaceable-event slot (same seed -> same nostr key), so a device that published its snapshot before merging in another device's ark state wiped those coins from the slot — exactly what stranded a phone showing no ark balance. Fix: on receiving any snapshot, union it with local and, if we hold vtxos the snapshot lacked, re-publish the superset. The slot converges to the union regardless of publish order; the guard (mergedN>remoteN) stops the push-back once every device agrees, so no loop. Co-Authored-By: Claude Fable 5 <[email protected]>
* Sync ark funds across devicesAdam Soltys2026-07-061-1/+77
| | | | | | | | | | | | | | | | | | | | | | | | Ark vtxos lived only in per-device storage, so a second device with the same seed saw the right on-chain balance (that rides the snapshot) but none of its ark funds — board/change vtxos exist only where created. Ark state now rides the encrypted nostr snapshot as a merge-safe cache extension: additive union (bring in vtxos a device is missing) with each device's own server reconcile() as the authority for spent-ness, so a stale or wrong 'spent' on one device can never propagate and poison the others. A connect-time reconcile catches coins spent elsewhere while a device was away, and a live subscription to our own replaceable event merges another device's new coins within seconds. New seam: registerCacheExtension({mergeAlways:true}) extensions get fed snapshots that aren't newer than local state too (last-writer-wins is right for rescannable chain data, wrong for merge-style vtxo state), and nostr.subscribeStates for the live path. Verified two same-seed devices over a mini relay: B sees A's boarded balance with zero mailbox activity, spends a synced vtxo, A converges to a stable balance with the spent vtxo correctly marked. Co-Authored-By: Claude Fable 5 <[email protected]>
* NWC bridge funding (PoC): ark wallets behind a lightning facadeAdam Soltys2026-07-061-0/+46
| | | | | | | | | | | | | | | | | | | | The compatibility play for ark zaps: an NWC wallet service (coinos, eventually) receives pay_invoice from any nostr client, asks the hal wallet to cover the amount over ark (kind 23196 funding request), hal auto-pays within a user-set allowance and acks (23197), the bridge receives the vtxo in its mailbox and settles the bolt11 over lightning. Custody window is the seconds the sats are in flight. hal side: a live nostr subscription primitive (nostrSubscribe through the sync seam) and an allowance-gated funding handler that arms when ark connects. PoC config via localStorage 'btc-wallet-arknwc' ({bridgePk, budgetSat}); proper UI when the protocol settles. Proven end-to-end on regtest (tools/.arknwc-e2e.tmp.js): mini relay + bridge process (ArkManager in bun + CLN) + NIP-47 client — invoice on clb paid via cl, preimage returned over NWC, hal debited 1,000 sats over ark, zero page errors. Co-Authored-By: Claude Fable 5 <[email protected]>
* Ark zaps PoC — a NIP-61-shaped zap protocol over arkoorAdam Soltys2026-07-061-1/+121
| | | | | | | | | | | | | | | | | | | | | Lightning zaps are welded to LNURL+bolt11; nutzaps (NIP-61) proved the alternative shape, and ark fits it better than Cashu because delivery already exists (the mailbox). Draft protocol, kinds provisional: kind 10037 (replaceable) announces the wallet's ark address+network; the zap itself is a plain instant arkoor send; kind 9737 is the sender-signed receipt tagging recipient, amount, comment, and the delivered vtxo id — so the receipt is checkable against the actual payment, unlike a bare attestation. UX: paste an npub into Send and hal offers the zap (needs both the ark and sync features; npub decoding is inline bech32 so ark-only builds stay lean). The nostr layer gains generic publishEvent/fetchEvents, exposed through the sync seam as wallet.nostrPublish/nostrFetch. Proven on regtest against a mini NIP-01 relay: announce, resolve, zap 1,000 sats with a comment, instant receipt+delivery, vtxo match. Co-Authored-By: Claude Fable 5 <[email protected]>
* Exit status: real progress, blocks-left countdown, honest failuresAdam Soltys2026-07-051-6/+27
| | | | | | | | | | | | | submitpackage answers HTTP 200 even when it rejects the package — the verdict hides in package_msg/tx-results — so a mainnet no-fee-coin exit sat at 'publishing on-chain…' forever believing its bare hop had relayed. Rejections now surface (and become the actionable needs-a-fee- coin message on the bumpless path). The status line shows hop progress (step 2 of 9), the timelock wait counts down in blocks with a rough wall-clock estimate, and a stuck exit that never published anything gets a 'Cancel this exit' link that returns the vtxo to spendable. Co-Authored-By: Claude Fable 5 <[email protected]>
* Zero-coin unilateral exit + honest actionable errorsAdam Soltys2026-07-051-3/+21
| | | | | | | | | | | | | A fresh gift-only wallet has no on-chain coin for the CPFP fee child, so its exit stalled behind 'hit a snag — retrying automatically' — a precondition dressed up as a transient. When no fee coin exists the hop is now tried bare (a zero-minrelay regtest relays it; a gift wallet can exit with literally nothing but the gift), and if the node refuses, the real message shows: exiting needs an on-chain coin for fees. Errors now carry an actionable flag so the status line knows when to tell the truth verbatim. Co-Authored-By: Claude Fable 5 <[email protected]>
* Show the ark board fee before boardingAdam Soltys2026-07-051-1/+15
| | | | | | | | | | 100,000 in, 99,670 out was a surprise: the ASP's board fee (min/base/ ppm from its published schedule; it funds the exit tx's P2A fee anchor) was only visible after the fact. The board form now shows the fee and the net amount live as you type, and the funding tx's detail view gets an 'Ark server fee' line. Co-Authored-By: Claude Fable 5 <[email protected]>
* Exit works on plain-esplora regtest; human-readable ark status linesAdam Soltys2026-07-051-4/+10
| | | | | | | | | | | | submitPackage falls back to sequential /tx posts when the esplora lacks POST /txs/package (regtest electrs) — works wherever the node relays zero-fee v3 parents. The ark card stops speaking developer: in-flight operations summarize as '{n} deposit(s) confirming' instead of 'board:funded, exit:chain, …', exit lines read 'Exiting N sats — publishing on-chain…', and transient errors show 'hit a snag — retrying automatically' instead of raw gRPC/HTTP text. Co-Authored-By: Claude Fable 5 <[email protected]>
* Reconcile ark vtxos when the gift form opens with an ark sourceAdam Soltys2026-07-051-0/+1
| | | | | | | | Same trigger as pasting an ark address into Send: the gift form is a send-intent signal, so stale vtxos (spent server-side while this state was idle) drop before coin selection instead of erroring at cosign. Co-Authored-By: Claude Fable 5 <[email protected]>
* Unilateral exit: put vtxos on-chain without the serverAdam Soltys2026-07-051-9/+141
| | | | | | | | | | | | | | | | | | | | | | The trustless backstop behind the Ark card's promise. The vtxo's genesis chain reconstructs to fully-signed txs (keyspend sigs for cosigned/arkoor transitions, preimage script-path for hark leaves); they are zero-fee v3 with P2A anchors, so each hop broadcasts as a [parent, CPFP child] package via esplora POST /txs/package, one confirmed hop at a time (TRUC allows a single unconfirmed parent). After the vtxo tx confirms and the CSV exit delta passes, the claim spends the delayed_sign leaf with only the user key into the wallet's own receive address. Settings gains 'Exit on-chain without the server' with live per-exit status lines; the claim tx is labeled 'Ark exit' in History. Probing uses /tx/:txid (definitive 404), never /tx/:txid/status, which answers confirmed:false for unknown txids. Also: a fresh visitor opening an ark gift link now adopts the gift's network automatically instead of being sent to Settings. Co-Authored-By: Claude Fable 5 <[email protected]>
* Ark gifts: instant off-chain bearer gifts inside the gift flowAdam Soltys2026-07-051-0/+169
| | | | | | | | | | | | | | | | | | | | | | Ark can't presign a bearer spend, so an ark gift is a vtxo sent to an ephemeral ark identity whose seed IS the link (/ag/<code>: net, amount, 32-byte secret). The claimer rebuilds the identity, reads its mailbox, and sweeps to their own ark address — instant, free, and it works for a brand-new empty wallet with zero on-chain footprint. The sender keeps the secret, which is what makes revoke possible (first cosign wins). The gift form gains an On-chain/Ark source toggle when ark balance exists; ark gifts skip the lock/split logic entirely. Claim, choose- wallet, already-claimed, and nostr-locked (/lg/) flows all branch by code kind; unclaimed ark gifts list with one-tap revoke and lazily mark themselves claimed via GetVtxoStatus. Features stay decoupled: gifts asks for ark support via the new ctx.hook seam, so either feature builds alone. Also restores goHome in the feature ctx — lost in the plugin-seam extraction, it made the already-claimed screen throw on render. Co-Authored-By: Claude Fable 5 <[email protected]>
* Collaborative offboard: move the ark balance back on-chainAdam Soltys2026-07-051-10/+66
| | | | | | | | | | | | | | | | | | | | PrepareOffboard/FinishOffboard against captaind — per-input ownership attestations, connector-based musig forfeits (with the deterministic fanout tx when there is more than one input), and fee math that mirrors the server bit-for-bit (weight fee rounds up, ppm-by-expiry rounds down) since it rejects any other net amount. The receive pane gains a 'Move back on-chain' button that withdraws everything to the wallet's own fresh address; the incoming tx is labeled 'Ark withdrawal' in History and the ark side records the movement. Crash-safe: the txid persists before FinishOffboard and resume asks GetVtxoStatus whether the forfeits landed (the server broadcasts the tx itself). Also fixes boarding to applySentTx after broadcasting the funding tx — without it the spent coin lingered in the utxo set (a second board double-spent it), and the post-board scan() could resurrect it by racing the explorer's indexing. Co-Authored-By: Claude Fable 5 <[email protected]>
* Boarding UX: label the funding tx and show an in-flight ark balanceAdam Soltys2026-07-051-2/+25
| | | | | | | | | | The board's on-chain send showed as a generic 'Sent' and the sats vanished from the balance until the vtxo registered. The funding tx now reads 'Ark board' in History (with a detail section showing the net credit), and an 'Ark (boarding…)' line carries the pending amount from broadcast until the board completes. Co-Authored-By: Claude Fable 5 <[email protected]>
* Ark: recover from stale vtxo stateAdam Soltys2026-07-041-1/+14
| | | | | | | | | | Match captaind's actual 'not spendable' error so a crashed or foreign spend fails the action instead of retrying forever, and reconcile locally-spendable vtxos against the server (GetVtxoStatus + ownership attestation) when an ark address lands in the send form. boardFee now treats absent proto3 fee fields as zero (Second's signet boards free). Co-Authored-By: Claude Fable 5 <[email protected]>
* Features phase 2: gift/swap/ark primitives leave the core walletAdam Soltys2026-07-041-0/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* Features: extract swaps, Ark, and SP behind a plugin seamAdam Soltys2026-07-041-0/+429
First step toward optional build profiles. app.js gains a small feature registry (ctx + FEATURES + hook helpers); swaps/Ark/silent-payments UI moves wholesale to src/features/{swaps,ark,sp}.js, each exporting a factory returning integration hooks: init/stop/networkChanged wallet lifecycle receiveModes/receiveTakeover receive tab isSendDest/matchSendText/ interceptReview/sendView/ hideSendControls/sendFormNote send flow historyEntries/historyDetail/ txDetailSection history balanceLines/settingsCards balance card + settings No behavior change: ark + gift browser e2e suites pass, and a probe confirms LN receive, all receive modes, and the three settings cards render as before. Gifts extraction and build flags come next. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy