aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark/manager.js
Commit message (Collapse)AuthorAgeFilesLines
* Ark: a double-tapped zap no longer spends one coin twiceAdam Soltys11 days1-3/+29
| | | | | | | | | | | | A reconcile pass that began before the first zap took its coin handed it back as spendable (the server still said so) while the zap was in flight; the second zap picked it and the server refused: 'vtxo … already spent'. Coins whose state changed during the pass are left alone. And a payment refused for an already-spent coin now reconciles and pays once more instead of failing with the raw error. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Renewing asks first, and a renewal links to its round's transactionAdam Soltys12 days1-1/+4
| | | | | | | | | | | | | | | | | | | | | - The coins page's Renew button opens a confirmation instead of renewing at once: how many coins, their amount, the fee, and what is left to spend meanwhile — with a red notice when that is nothing (the button selects every coin by default, which is how a user locked their whole balance for an hour on 2026-10-03). It says the coins can't be spent until the round's transaction confirms and that a renewal can't be cancelled; the wallet renews for free near expiry anyway. The inline notice from 328f2db moves into it. - A renewal keeps its round's transaction id as soon as the round has run (before it confirms): the in-progress card on the coins page links to it, and the finished renewal's history row carries it, so its detail shows the id with View/Copy like any on-chain movement. Renewals finished before this change have no id recorded. tools/ark/renew-confirm-ui-test.js, tools/ark/renew-txid-test.js Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* A Lightning payment that can't be covered says why, not 'Payment failed'Adam Soltys12 days1-2/+6
| | | | | | | | | | | | | | | | | | | | | | | | A user renewed all ten of their coins (55,611 sats) from the coins page, tried to pay a 55,300-sat invoice three minutes later, and was told only 'Payment failed'. Nothing had failed: the renewal's round ran at once, but its coins stay unspendable until the round's transaction confirms on-chain, and that block took 64 minutes. The quote found no spendable coin and gave up with the generic message; nothing reached the server. - The quote now tells apart (lnShortfall): money held by another payment (wait, as before — now by sum, not one coin covering it all), money a renewal holds ('Nothing was sent. N sats are being renewed…'), and a real shortfall ('needs X sats with its fee, Y available'). Coins leaving in an offboard or exit are never waited for. - The manager's 'insufficient ark balance' names a renewal too. - The coins page says before the tap that renewed coins can't be spent until the round confirms, and that renewing every coin leaves nothing to pay with meanwhile. tools/ark/ln-shortfall-test.js Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* NWC: lookup_invoice, so BTCPay Server can connectAdam Soltys13 days1-0/+44
| | | | | | | | | | | | | | | | | | | BTCPay's NWC connector requires get_info, make_invoice, lookup_invoice and list_transactions; we never offered lookup_invoice, so it refused the connection string. - ArkManager.lnLookup answers lookup_invoice from wallet state (by hash or bolt11, either direction; pending/settled/expired/failed, preimage once claimed). A retry's several actions on one hash defer to the furthest. - The open app pushes a pending receive along before answering, so a merchant polling sees it settle. - The service worker now answers make_invoice and lookup_invoice too, and claims a paid invoice's HTLCs during the lookup: a BTCPay sale settles with the wallet closed. Its list_transactions reports receives as well. - get_info and the 13194 info event list the new method. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Sending on-chain from Spending states the fee and minimum up front.Adam Soltys14 days1-1/+21
| | | | | | | | | | | The form asks the ASP for a co-operative offboard quote (mining share + service fee, minimum = fee + dust) for the address on screen, disables Send for a balance below it and points to Lightning. A too-small amount reads in words instead of the manager's internal error, and the unilateral-exit note only appears when co-operation actually failed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
* A zap refused for an old expired coin goes again on the other coins instead ↵Adam Soltys2026-09-281-6/+25
| | | | | | | | | | | | | of showing the server error. Sub-dust coins skip the local expiry check, so the ASP can refuse one at cosign ("vtxo … expired at height …"). The coin was already marked excluded, but the error still surfaced and the user had to tap again. send() and payLnInvoice() now retry (up to 5 attempts) when that exact refusal marks an input, without a failed history row for the retried try. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Du1qT1wbkYwYK6zkGH79Rp
* Expired dust gets a quiet line and a Forget button, not a red warning.Adam Soltys2026-09-271-0/+15
| | | | | | | | | | | A coin past its expiry and under the 330-sat floor can be renewed by nobody and exited by nobody; the home page said "the server can still renew it" in red over 21 sats. Now a muted line points at Manage, where the coin's row offers Forget: held as spent, quietly, so it stops counting and stops warning. Larger expired coins keep the warning. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* A renewal the round would refuse is retired, not resubmitted every sync; ↵Adam Soltys2026-09-271-0/+17
| | | | | | | | | | | | | | | /feed opened before the wallet no longer doubles Popular. A refresh at the created step with an input past its expiry (only the server's backstop can renew it now), or whose fee leaves less than the 330-sat floor (a 21-sat coin with a 150-sat fee sat "in progress" for days), is failed quietly and its inputs go back to spendable. /feed read before the wallet restored made a visitor's session copy of Popular beside the built-in one; the wallet's arrival now folds it away. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Three fixes: orphaned pending coins settle with the server; a saved start ↵Adam Soltys2026-09-271-2/+22
| | | | | | | | | | | | | | | | | screen no longer shadows a restored wallet; stop() no longer throws. Reconcile now asks the server about a 'pending' coin no live action or standing renewal order holds: spendable again, or gone. Thirty zap-sized coins another device had already spent counted in a balance for weeks. A nav saved by a page with no wallet (the start screen) was restored over a wallet that had since been saved to the device, painting the start page. messages.stop() referenced feedAt, which no longer exists, so the rest of the teardown never ran on an account switch. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Copy: no more em dashes, anywhere people read.Adam Soltys2026-09-271-6/+6
| | | | | | | | | Every English string, the translations, the README and the messages the code shows people now break with a full stop, a colon or a comma instead. Only console logs and the "—" placeholder for an empty cell keep the dash. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Renewal repair: a coin already held as spent is not asked about (its bytes ↵Adam Soltys2026-09-261-2/+8
| | | | | | | may be pruned). Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Renewal: a submitted round the server has forgotten is repaired, not asked ↵Adam Soltys2026-09-261-1/+34
| | | | | | | | | | | | | | | | | | | | | | about forever. Adam's wallet carried a refresh action from August 31 in step 'submitted' whose unlock hash the server never ran: the round went through under a later submission (another hash), the inputs were spent by it and the 5,233-sat output reached the wallet through the mailbox — but the original action kept asking RoundParticipationStatus on every sync for four weeks, and since this week's coins page shows renewals in flight, it showed up as "in progress" with the raw gRPC error underneath. On "not found" the manager now checks the inputs: all spent means the round ran under another submission and the record is retired (failed, superseded — no history row, no card); none spent means the server dropped it before a round ran, so it is submitted again; a mix goes through the existing input repair. The coins page never prints a server error string; a failed check reads as "the wallet keeps trying". Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Coins page: a renewal in flight keeps the whole page; a renewal you asked ↵Adam Soltys2026-09-241-3/+5
| | | | | | | | | | | | | | | | for is in history. Tapping Renew marked the coins pending and the page, finding nothing spendable, collapsed to a single "Pending: N sats" line — no table, no exit explainer. Coins in a round now stay in the table (greyed, tagged renewing), with a card saying where the round is (queued, in a round waiting for a confirmation, finishing). The page no longer closes on Renew. History: a manual renewal shows as "Renewing · in progress" while the round runs, and as "Renewed" after, even when it was free (automatic free renewals stay out of history, as before). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Ark: the receive rescan starts at index zero — another device of the same ↵Adam Soltys2026-09-221-2/+4
| | | | | | | wallet may have minted lower. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Ark: rediscover Lightning receives the state has no record of.Adam Soltys2026-09-221-0/+40
| | | | | | | | | | | | The background answerer minted an invoice for [email protected] into the NWC pouch; the app's next mirror write replaced the pouch with its own state, and the receive record was gone before anything could claim it. Preimages come from a counter, so on each connect the manager now asks the server about the next few indices and rebuilds any receive it has no record of, then claims it like any other. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Ark: an invoice the server hands back for a hash it already holds is not ours.Adam Soltys2026-09-211-6/+20
| | | | | | | | | | | | | StartLightningReceive on a payment hash with an unpaid invoice returns that invoice — an earlier sale's amount and memo under a fresh QR (a 0.60 CAD bill showed an 8,467-sat invoice on 2026-09-21). The returned invoice must now carry the amount asked for and a comfortable expiry, or the index is treated as spoken for and skipped. The counter is also floored above every index an ln-recv action of ours already names, so a restored or merged state can't rewind it. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Ark: an issued-but-unpaid invoice on a preimage index is skipped too.Adam Soltys2026-09-211-2/+5
| | | | | | | | | The ASP answers a settled hash with "already been paid" and a merely issued one with "UNIQUE constraint failed: invoices.payment_hash"; both mean the index is taken. Headroom raised to 200 skips. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Ark: a Lightning invoice steps past a preimage index another device already ↵Adam Soltys2026-09-211-7/+22
| | | | | | | | | | | | | | spent. The ASP answered "invoice has already been paid" for a point-of-sale invoice: the receive counter on the phone was behind the one the laptop had used. createLnInvoice now retries on that answer with the next index (the counter only moves up; the key index is allocated once an invoice exists), so the Receive tab, NWC make_invoice and the point of sale all recover instead of failing. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Zaps: tap again while one flies, hold the bolt to change the amount.Adam Soltys2026-09-191-1/+5
| | | | | | | | | | | | | | | | A pending zap no longer refuses the next tap: the optimistic tally keeps one entry per zap and matches each report and receipt to the oldest it can describe, so two default-amount zaps stack, settle and hand over to their receipts one at a time. Instant post zaps run off the send screen's zap slot, so a second tap can't evict the one still resolving, and send action ids are unique within a millisecond. Holding ⚡ (500ms) opens the amount screen in an edit mode — Save alone, or Save and zap this note — from the action bar and the chat chips alike; the press's timer lives on the element so a background repaint mid-press can't turn a lifted finger into a hold. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* A zap answers as fast as the route allows.Adam Soltys2026-09-141-0/+15
| | | | | | | | | | | | | | | The wallet used to add two to four seconds of its own around a Lightning send: a random pause plus a relay look-back before paying, and a two-second poll for the result after. The notifier's claim now decides who pays in one round trip, with the relay look-back kept only for replays and for when the notifier gives no verdict; a phone answering from behind another app waits a second so a desktop tab wins the claim instead of paying at a throttled crawl. After the send, one held CheckLightningPayment call brings the preimage the moment CLN has it — for wallet-connect pays, the background worker, and the app's own Send. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* A zap answered by two tabs no longer reports as failed.Adam Soltys2026-09-141-1/+22
| | | | | | | | | | | | | | | Every open tab of the same wallet sees every wallet-connect request. Two hidden tabs on one laptop both tried to pay the same zap: the ASP refused the second with "payment already in progress", and that error reached Amethyst half a second before the first tab's preimage. A Web Lock per request now lets one tab (or the worker) answer while the rest stay quiet, and a device that does meet "already in progress" waits for the sibling payment's outcome — relaying its preimage only if the payer never speaks — instead of shouting a failure over sats that went through. The losing side also stops writing a failed row into history for it. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* The renewal happens whether or not anyone opens the app.Adam Soltys2026-09-131-1/+172
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Every coin expires, and until now the only thing standing between a balance and its expiry was maybeAutoRefresh: it renews when the wallet is opened inside the last 96 blocks, roughly sixteen hours. Bark's own developers say the same premise fails on mobile — people don't reliably open a wallet at the right hour, which is why every mobile Ark client they know of has moved to delegated refreshes and why they are building server-driven renewal. captaind already has it, and ark.coinos.io has been carrying the column unused: SubmitRoundParticipation takes a scheduled_height, and the server holds that participation out of every round until the height arrives, then runs it with nobody online — the round tree branch is signed by the server, which is the whole point of a delegated refresh. The fee is priced at the height it will run at, so a standing order lands in the free bracket rather than today's. So each spendable coin now gets one registered 36 blocks before its expiry, six hourly rounds of slack. It is deliberately not an action: the coin stays spendable, the wallet never reads as busy, and spending it simply kills the order — the server drops a participation whose input is gone, and _driveScheduled drops ours. The claim comes later, whenever the wallet next opens: the mailbox breadcrumb we already post wakes it, and the leaf cosign and forfeit are the ordinary refresh path from there. Nothing about the input's expiry blocks that claim, which is what makes this worth having. A wallet that goes quiet for a month comes back to a coin already renewed and anchored in a fresh tree, instead of one the ASP has swept and would have to honor out of goodwill. One cycle per visit is all it can buy — the replacement's id doesn't exist yet, so its own backstop waits for the next time the app is opened. tools/ark/scheduled-refresh-test.js covers the five behaviours on regtest: the server holds the order until its height, runs it while the wallet is closed, the wallet claims on return, a spend voids the order, and a claim still works after the input has EXPIRED (the last one mines a coin's whole lifetime, so it also re-refreshes the funding wallet first — an expired vtxo can be renewed but not sent). Riding along: the expired-coin notice that was already sitting in the working tree, and manager-test's tampered-vtxo check, which had been reading base64 vtxo bytes as hex since they moved to IndexedDB. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019fMdLypdg5JaU7bdeUFF8v
* Report rejected Ark sends and exclude server-rejected expired inputsAdam Soltys2026-09-111-2/+24
|
* Ark: persist vtxo bytes as base64 (a third smaller than hex); hex still ↵Adam Soltys2026-09-101-10/+12
| | | | | | | | | | | | | | | | | | read, hex still on the wire. The live coins' signed bytes are most of a deep wallet's state — 560KB of hex, which localStorage then holds as UTF-16. vtxoBytesToStr/FromStr in proto.js persist base64 and read either form, _addVtxo writes base64, and pruneArkState converts hex to base64 on every save, so an existing state migrates itself the first time it saves. Measured on a live deep wallet: 904KB → 665KB (-26%), all 41 coins decoding identically. The sync wire (slimArkForSync) keeps sending hex for now: a device still on an older build would otherwise fail to decode every coin merged from a newer one. Flip it once pre-2026-09-10 builds are gone. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
* Ark: local state stops growing forever — spent coins shed bytes, done ↵Adam Soltys2026-09-101-0/+52
| | | | | | | | | | | | | | | | | | | actions shed hex. pruneArkState runs on every manager save: a spent coin loses its signed bytes once its spend is 30 days old and no in-flight action names it (a spent coin can never take the exit those bytes exist for), and an action done more than 7 days ago loses the output-vtxo hex it minted (the change / HTLC coins live in `vtxos` in their own right). Ids, amounts, inputIds and preimages stay for history rows and spend dedupe; failed ln-pays keep their HTLC bytes for the refund. Selective and idempotent; unit-tested. Measured on a live deep wallet the immediate saving is small (every recent spend is inside the window) — what remains is the live coins' own chains, which only a renewal shrinks. The change is a growth cap, not a diet. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
* The carousel stutter is the ark history memo re-cooling on a timer: ↵Adam Soltys2026-09-041-0/+4
| | | | | | | rebuilding it costs ~60ms (sort + dedupe + gift resolve + secp256k1), and its 1.5s TTL meant an unhurried flick always hit a cold rebuild, stuttering the slide's tail — 'warms up if I keep going fast, cold if I pause, refresh reproduces' is the classic TTL-expiry signature. Now keyed on a state revision (_save bumps state._rev) so it's reused until the data actually changes and never re-cools between flicks. Diagnostics still on to confirm Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Ark reconcile signs in time-sliced batches: it built a status query (schnorr ↵Adam Soltys2026-09-031-3/+12
| | | | | | | sign + vtxo decode, real secp256k1) for every spendable coin in one synchronous map — a ~100ms+ EC burst on boot, the local work blocking a carousel drag right after a refresh (community/DM wraps arrive over the network later; this fires immediately on init). The requests now kick off in ≤6ms slices that yield; network calls still overlap Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Ark key derivation gets memoized — the boot CPU hog. A profile of the ↵Adam Soltys2026-09-031-2/+13
| | | | | | | first 9s after refresh showed ~800ms of secp256k1 (q0/mul/TJ) dominating the main thread; the source was _key(index) recomputing a BIP32 derive + a getPublicKey point-mul on every call, and reconcile() calls it for every spendable coin (56 here), with decode/send/exit/fee-math hitting it again. A key for an index is constant per wallet, so it's cached by index now (mailbox key too) — dozens of EC muls per boot collapse to one each Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* An offboard's price stops hiding in one number: the miners' cut and the ↵Adam Soltys2026-09-021-3/+9
| | | | | | | ASP's service charge (base + lifetime ppm) record separately and the history detail shows them on their own lines — settling the mystery of a ~350-sat chain fee inside a ~650-sat total. The community row's member count anchors to last session's settled figure instead of ticking upward through the guestbook replay, and a profile's Pay button reached from search actually reaches Send — the lingering search screen was winning the router Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The 200ms of secp256k1 hiding in every repaint: the depth advisory priced a ↵Adam Soltys2026-09-021-1/+10
| | | | | | | full uncooperative exit — every hop of every coin, an EC tweak per hop — on each wallet render; the estimate now memoizes on coin set + feerate, and decoded vtxos cache by id (bytes are immutable). Profiled on a real phone: point add/double/wNAF was 1.3s of a 4.3s six-tab-switch trace Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The tester with 233 sats gets to spend them: invoice coverage judges the SUM ↵Adam Soltys2026-09-021-1/+5
| | | | | | | of spendable coins (the single-coin gate silently declined anything over the largest coin, though multi-input pays gather 24), an uncoverable invoice now owns its paste — board the shortfall from Savings, or say what's short — instead of dropping the bolt11 into the address field like a dead click, and chain calls gain a last-resort relay through the registrar (/esplora passthrough, tip cached) for the network that blackholes every public esplora Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The chain adapter learns a second road: mainnet esplora calls fail over ↵Adam Soltys2026-08-311-6/+30
| | | | | | | between the configured host, blockstream.info, and mempool.space (remembering whichever answered last, rotating on network failures, 429s and 5xx) — one tester's network simply cannot reach mempool.space and every chain-touching feature was dying for it, stranded mailbox receives included. And a failed quote no longer wedges the Lightning pay form at an eternal spinner: it prices at the outage fallback and lets Pay proceed, where the payment path quotes again for real Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The mailbox cursor stops leaping wounded messages: it advances only after a ↵Adam Soltys2026-08-311-2/+31
| | | | | | | message's work is DONE (a final rejection counts, a transient failure does not), holds at the lowest failed checkpoint so a healthy message behind a sick one can't skip it, and the poll defers the rest of a batch on failure — one esplora hiccup mid-validateVtxo used to skip an arkoor receive forever (monogram's 204 sat, twice) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* One spend, one row: send and lightning-pay movements now record which coins ↵Adam Soltys2026-08-301-0/+4
| | | | | | | they consumed, spend actions sync as slivers (id + net amount + inputIds), and the reconcile exclusion generalizes from renewals to ANY accounted-for coin — a laptop send used to paint the phone's history twice, once as the real Sent and once as a red 'Spent on another device' at full input magnitude. The red row survives only for spends nothing anywhere explains Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Staging gets the mainnet fee model end to end: /lnquote?network=mutinynet ↵Adam Soltys2026-08-291-1/+3
| | | | | | | quotes from the staging ASP's own CLN (helpers take their backend), the client asks it by default on mutinynet, and the quote URL concat survives a query string Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
* A round's output can no longer strand: every refresh asks the server to drop ↵Adam Soltys2026-08-291-12/+39
| | | | | | | its unlock hash in the mailbox, and any device that finds such a breadcrumb with no matching action adopts the claim itself (input ids now read from the status response — the rescue needs no operator). History learns to tell the whole truth and nothing more: coins the server saw spent elsewhere get a folded row (skipping coins a known renewal consumed), renewals fold across devices on their unlock hash and appear only when they cost something, and a rescue records the fee the round actually took. The account page keeps its header icons when reached from an open wallet; the messages page folds the control/guestbook replay once per burst instead of painting every intermediate state — no more blinking messages from superseded ban lists or member counts crawling to their total, which now paints from last session's settled figure Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
* The wallet learns to read BOLT 12 for itself: offers and invoices decode ↵Adam Soltys2026-08-291-5/+27
| | | | | | | client-side, and every invoice the ASP fetches over onion messages is verified here — payment hash read from the raw TLVs, offer terms mirrored byte-for-byte, the issuer's schnorr signature checked against LDK's exact merkle construction (proven against a live CLN-signed fixture) — so no server can hand back an invoice whose preimage it already holds. Paste an lno1 or resolve a name whose record carries one and the existing HTLC pay flow carries it (captaind's Invoice enum takes lni strings natively); a name's owner may now publish their OWN node's offer in their DNS record, retiring the registrar's stand-in — senders fetch invoices straight from their node and no coinos service ever touches the money. The /lnquote fallback prices blinded paths by entry node + advertised payinfo Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
* The mailbox checkpoint was a timestamp all along: receives are dated by when ↵Adam Soltys2026-08-291-7/+11
| | | | | | | the server actually delivered them (verified to the millisecond), so an import shows hours-old payments as hours old — no celebration for news that isn't; a wallet whose Savings holds nothing opens onto the Spending that holds everything; and same-payment duplicates fold at display time on every device, however the state arrived Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
* A payment that lands now is dated now: expiry-based age estimation applies ↵Adam Soltys2026-08-281-2/+8
| | | | | | | only to the first catch-up — it mis-dated a live receive by a month after the vtxo lifetime doubled, since the math assumes every coin began with the full current lifetime Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
* One payment, one line: the settle path is idempotent (the mailbox push and ↵Adam Soltys2026-08-281-2/+8
| | | | | | | the status poll raced past each other's step checks — a toque got two history entries), movements carry their payment hash, and the sync merge folds same-preimage lightning records into the earliest telling Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
* Renewals earn a line in the ledger: "Renewed ↻" rows in history showing ↵Adam Soltys2026-08-281-1/+1
| | | | | | | what the round cost (or "free"), with the in-and-out detail behind them — and the toast is simply "Renewal requested" Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
* Renewal stops billing for tidiness: six refund coins after a failed payment ↵Adam Soltys2026-08-271-1/+1
| | | | | | | no longer trigger a top-bracket consolidation (multi-input sends made fragmentation cheap to live with — the trigger moves to the 24-input ceiling), and the history line for a refresh now names the fee it paid Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
* Dead coins stop travelling: an expired vtxo leaves the balance and the coin ↵Adam Soltys2026-08-241-8/+35
| | | | | | | picker (sub-dust IOUs excepted), a shortfall names the dead weight it can't spend, and the names float renews any coin a day ahead of the reaper — a 100k forward left home on an expired coin and the recipient rightly refused it at the door Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
* The claim sorts its coins the way the server counts them: HTLC vtxos line up ↵Adam Soltys2026-08-181-4/+13
| | | | | | | by outpoint in internal byte order — the display-hex sort read the txid backwards and nine-coin receives bounced off the positional check Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012u4oe1KPx9faCsDBLfZYqi
* A many-coined receive keeps the server's own order: the claim step stops ↵Adam Soltys2026-08-181-3/+6
| | | | | | | alphabetizing HTLC vtxos the ASP matches positionally, and the expiry guard reads the true earliest deadline instead of whoever sorted first Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012u4oe1KPx9faCsDBLfZYqi
* A refresh learns to let go: a participation rejected for unusable inputs ↵Adam Soltys2026-08-181-4/+96
| | | | | | | asks the server which coins died and rebuilds around the survivors instead of resubmitting the doomed set every sync forever; the balance reconciles on a ten-minute cadence so ghosts can't pose as sats; and arkRescue() reclaims a round output whose action a device lost Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012u4oe1KPx9faCsDBLfZYqi
* Money stops arriving without a story: the mailbox checkpoint stays local ↵Adam Soltys2026-08-181-1/+10
| | | | | | | reading progress (a merged one skipped every unprocessed message), vtxos that beat their message here get a backfilled receive, and the commit stamp takes its own footer line Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
* The HTLC expiry stops trusting a cached clock: tip fetches bypass the CDN, ↵Adam Soltys2026-08-181-1/+15
| | | | | | | and when the server names its own tip the payment adopts it and retries instead of dying Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
* The board drive gets a second witness: the wallet's own chain view vouches ↵Adam Soltys2026-08-181-4/+21
| | | | | | | for confirmations when the ark esplora is rate-limited or walled off, and chain errors finally surface instead of masquerading as unconfirmed Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
* A confirmed board stops waiting on a flaky tip: confirmation itself counts ↵Adam Soltys2026-08-181-4/+9
| | | | | | | for one, and a failed mailbox read no longer starves the drives that only need the chain Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz