| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
| |
(INSUFFICIENT_BALANCE or PAYMENT_FAILED, with the reason) is left with the notifier at once and goes out unless someone answers within 5 s. Waiting the grace period out in the worker ran past iOS's ten seconds after a failed send, so the other app heard nothing (PullCap, 2026-10-29). A Lightning send no longer picks a coin at the server's depth cap, so such a coin fails here with a reason instead of at the ASP; a capped coin under 330 sats (a round won't take it) says to receive more instead of "waiting to be renewed", and the worker stops booking renewals the server would refuse as dust
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
|
| |
|
|
|
|
| |
subscription ran out after 19 payments with the app closed, "key window exhausted"), derived once per account and memoized; 60 receive preimages (was 15). When the worker can't act on a request it still wakes the user, and now leaves a "wait, open the app" error with the notifier that goes out only if nobody answers within 20 s, so the other app hears something instead of silence and a sibling's real payment is never shouted over. A request older than its two-minute window (a push that waited at APNs) is ignored quietly instead of showing "Payment request"
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
never marked for revocation by a second wake, lookups report outgoing sends and finish them, and pushes no longer run side by side
PullCap's night of 2026-10-24: every wake's diary stopped 8-12 s after its push (iOS stops the worker there, not at 30 s). Two wakes two seconds apart both drove one send; the second's InitiateLightningPayment was refused ("state: ln-spent") and the paid send went to 'revoking', where every later wake spent its whole budget on it, so no request was answered after midnight.
- manager: an initiate refused as ln-spent follows the payment ('initiated'); a send in 'revoking' asks the ASP first (paid: settle; in flight before expiry: back to waiting; not found: revoke)
- worker: 9 s budget on Apple endpoints (27 s elsewhere) for answers, earlier sends and renewals; every ASP step held to it
- lookup_invoice asks every copy of the connection and drives an outgoing send in flight, answering settled with the preimage
- a lease in the worker's IndexedDB store serialises pushes (the Web Lock didn't); diary notes a missing Web Locks API
- the reply and the mirror save run together; stuck sends are finished before a new payment only when they hold its coins
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
instead of calling the invoice "already paid" with no preimage and locking its coin until the app opens
PullCap's night of 2026-10-23 (hourly 1-sat NWC requests to a closed iPhone
PWA): three of fifteen hours went wrong the same way. The push woke the
worker, it reserved the coin and wrote the ln-pay action to the mirror, and
the phone stopped it before the ASP was ever asked (no cosign in captaind's
log for those minutes). The client's retry ten minutes later hit the
manager's duplicate check, "this invoice was already paid or is being paid",
published as an INTERNAL error that the client read as paid — hours before
the money moved. And since the stuck action held the wallet's only coin,
the next hour's request (a different invoice) found nothing spendable and
fell to "Open Coinos to approve". Everything settled at once when the app
opened at 07:12 and resumed the actions.
- nwc-respond.js: pay_invoice first looks for a send already on the books
for this invoice — finishing one in flight and answering with its
preimage, repeating the preimage of one already done (nothing new on the
spend log) — and before paying a new invoice drives any other stuck send
to completion so its coin comes back. tendLnPays() does the same on every
wake, lookups included, and hands back "Payment sent" lines for what
settled. rec.pendingPays remembers which connection a send belongs to
(the action is the manager's); the app's mirror rewrite carries it.
- manager.js: the duplicate check says "already paid" only for a settled
send, "already in progress" for one on its way.
- features/nwc.js: the open app answers a repeated invoice from the local
action (preimage when settled, an honest "still in progress" otherwise)
before asking the ASP about a sibling device.
- sw-nwc.js: one push at a time over the shared mirror (Web Lock), so a
client's lookup + retry seconds apart can't double-drive one send. A
diary of what the worker did — each push, each ln-pay step as the mirror
saves it, every notification shown or refused, the fallback taken — is
appended line by line to IndexedDB (nwc-bg.js appendBgLog), so a worker
the phone stops mid-push leaves its last step behind. Settings →
Notifications → "Background activity log" copies it.
- tests: nwc-sw-test covers the four resumed-send cases; sw-push-quota
mocks the new seams and its "suspended app" case now expects the
stand-in-first order shipped in 91dbee4 (it had been failing since).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
counts as shown
PullCap's iPhone paid four times overnight with the "payments sent"
notifications on and never showed a "Payment sent" line. The worker hands
the handler the heads-up (covered by a test now); the handler flagged the
push as answered BEFORE calling showNotification, so a refusal left the
push with no line at all — on iOS a silent push, a strike towards losing
the subscription. Now the flag follows success, a refused notification is
retried with the plainest options, and the stand-in covers the rest. iOS
never replaces by tag, so renotify is left out there.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
exit cheap" renews a coin once its history is four hops deep
Why: a unilateral exit publishes one on-chain transaction per off-chain hop, so depth — not age — is what makes an exit expensive, and a renewal a week before expiry flattens exactly the same history a renewal in the last hours does. The week-ahead "early" tier only guarded against the server being unreachable at the very end, which is not the case worth selling. (Adam, 2026-10-23.)
exit = self (last-hours renewal, free bracket + base, the same grid-snapped backstop) plus: any coin whose genesis chain is 4+ hops deep is renewed in the next round the app runs, paying the top bracket + base — but only when that fee is under a tenth of the coin (flattening a 21-sat coin for 300 sats helps nobody; its exit is uneconomic anyway). Deep coins ride along with an expiry renewal when one is due, or get a round of their own. The server-side backstop stays near-expiry only: depth changes only while the wallet transacts, i.e. while the app is open. A stored 'early' reads as 'exit'. Card copy says what the hop count means and what the two prices are for.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
renews on its own and no renewal fee is paid unless the user opts in
Why: renewal fees were "money passively draining" to people who thought they had a custodial wallet (Stacker News, 2026-10-22). Measured over Sep–Oct: 84% of what users paid came from the client's own consolidation round at 20 coins (0.2% + 300 on every fresh coin), the rest from 300-sat rounds that each cohort of Lightning-received coins (30-day pool lifetime) triggered on its own schedule. The ASP itself honors a coin past its timelock — rounds and offboards never checked expiry, and captaind's allow_expired_arkoor (now on at ark.coinos.io) lets arkoor spends through too — so expiry only ever meant "the exit is gone", which most people coming from v2 never had.
Settings → Spending gets a Custody card with three tiers:
- coinos holds my coins (default): no renewals, no fees; a coin past its timelock shows as "held by coinos", stays spendable and receivable, and is left out of a unilateral exit (there is nothing on-chain for it to take).
- Keep my own exit: renew in the last hours (free bracket + base), the server backstop registered 36 blocks out and snapped to a 48-block grid so every wallet's orders share one round.
- Renew a week early: 1008 blocks out on a 144-block grid (0.1% + base).
renewPolicy() in ark/manager.js is the one table; the app, the service worker (the mirror carries the tier) and the Manage page all read it. Only shown on servers that honor expired coins (presets carry honorsExpired); elsewhere renewal stays automatic.
Manager: honorsExpired makes a past-timelock coin ordinary money (balance, selection, receive validation via acceptExpired) while a server refusal (expiryRejected) still excludes it; such coins spend LAST so an older build on the receiving side almost never meets one. A once-per-wallet rescan re-reads the mailbox and adopts coins older builds refused as expired, un-forgets expired dust, and clears stale refusals. The refresh gate that retired a renewal of expired inputs is gone (the server accepts them; the Manage page's Renew of exactly those coins was failing silently). Automatic consolidation is gone; the send cap goes 24 → 48 coins; small change (<1,000 sats) folds into a renewal that happens anyway.
Server (cs:~/ark/captaind-main.toml, restarted): allow_expired_arkoor = true; pool vtxo_lifetime 4320 → 8640 so Lightning-received coins live as long as the rest.
Tests: tools/ark/custody-test.js (policies, honoring, ordering, grid snap, validator), tools/ark/custody-ui-test.js (the card, held coins, exit count, switching), nwc-sw-test updated for the tier the worker reads.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
|
| |
a closed wallet keeps its coins renewed. Three asks from a subscription service builder (PullCap, by DM, 2026-10-16) testing a 1-sat-an-hour pull against a locked phone. (1) "For a monthly subscription a monthly cap would be tighter": a connection's budget now resets every day, week (Monday) or month, in UTC (nwc-budget.js: the window key stamps the spent counter; both the app and the worker count by it), chosen on the form, read on the card ("21,000 a month · 0 spent this month"); the worker now also counts what the APP spent this window (the counter rides the mirror) — it only ever counted its own log before, so a daily budget could be exceeded across the two; get_budget answers used/total/renews_at/renewal_period (NIP-47), in get_info's method list and the 13194 event. (2) "Does V3 reply with an NWC error when a connection is revoked, or just stop answering?" — it stopped: the open app dropped the request, the mirror forgot the key, the device even de-registered it from the push notifier, so a revocation looked exactly like a phone that is off. Now a revoked connection is kept for 30 days (revokedAt; the earlier date wins in a merge): still subscribed, still registered for pushes, still in the mirror flagged, and every request on it — from the app or the worker, in the asker's scheme — is answered UNAUTHORIZED "this connection was revoked"; no capability event for it; after the grace it is forgotten. (3) "If a user is only woken once a month by an NWC request, will V3 renew the coins on its own?" — only for one cycle: the app registers a standing order per coin, but a coin the worker SPENDS voids its order and the change had none until the app next opened, and a renewed coin sat unclaimed until then too. Now every NWC wake, after the answer and inside what is left of the push budget (tendRenewals, sw-nwc.js): claim orders whose round has run — and drive a claim left in flight on, since one takes several round trips and confirmations — then register an order for every coin without one (backstopMargin shared with the app; limited to the output keys the mirror carries; the mirror now also carries the output key a standing order or a claim in flight burned, which was below the window). Standing orders merge by unlock hash when the app returns (mergeScheduled, in mergeArkStates), so it adopts the worker's. tools/ark/bg-renewal-test.js on regtest, end to end: the worker registers from the mirror alone, a worker spend kills the order and the next wake covers the change, the round runs with nobody online and the next wake claims the renewed coin — and covers it in the same wake — then the app merges it all (fresh coin, right balance, the order adopted, not busy, one history row). tools/nwc-budget-test.js (windows, renews_at, counting with the worker's unabsorbed spends); nwc-sw-test.js (monthly budget over app+worker spends, last month's not counted, get_budget, UNAUTHORIZED in both schemes, the mirror's keys and flags, tending: claim/drive/register and their limits); nwc-test.js (monthly refusal naming the period, get_budget, a counter from last month reads as nothing, the revoked connection answered UNAUTHORIZED, no 13194, revokedAt in the snapshot, forgotten after 31 days, the earlier date wins); nwc-settings-ui-test.js (the form's period choice, the card's wording, Revoke dates it); ark-renewal-sync, sync-blob, sync-outbox and spending-restore tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Every awake device of a wallet hears a wallet-connect pay request within
the same second, and the per-tab lock only reaches one browser profile:
two profiles on one laptop each locked an HTLC for the same 21-sat zap.
The push notifier now hands each pay request to the first device that
claims it; a device that loses the claim stands down silently, and a
device that cannot reach the notifier answers as before so nobody goes
quiet over a hiccup. The worker path claims the same way.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
|
| |
|
|
|
|
|
| |
stale sibling copies of its connections, the responder pays from the record whose network the invoice names (standing down when none fits), a ghost coin reconciles and retries instead of dying, and error replies wait out a grace period so a sibling's success reaches the client first
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017Fc8SHJRvBBXgMCntjNCri
|
| |
|
|
| |
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
|
|
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]>
|