From b73bb9ee51b9be33bc1859fd56d3e11905c3d456 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 1 Aug 2026 19:02:25 -0700 Subject: Background NWC: the worker now answers — and pays — from the pouch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/ark/manager.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) (limited to 'src/ark/manager.js') diff --git a/src/ark/manager.js b/src/ark/manager.js index e70acfb..2edcd8a 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -500,6 +500,32 @@ export class ArkManager { return action.id; } + // A send whose recipient is one of the USER's own foreign-branch keys (the + // NWC pouch): no address, no mailbox delivery — the caller receives the + // signed recipient vtxo bytes directly and stores them wherever the branch + // lives. Same cosign machinery as send(), so all its failure handling holds. + async sendToPubkey(destPubkeyHex, amountSat) { + const input = this._selectInput(amountSat); + const changeSat = input.amountSat - amountSat; + const action = { + id: `send-${Date.now()}`, type: 'send', step: 'created', + inputId: input.id, amountSat, destAddress: null, + destPubkey: destPubkeyHex, destBlindedId: null, + changeIndex: changeSat > 0 ? this.state.nextKeyIndex++ : null, changeSat, + }; + input.state = 'pending'; + this.state.actions.push(action); + this._save(); + await this._driveSend(action); + if (action.step !== 'done') throw new Error(action.error || 'send did not complete'); + return { + vtxos: (action.destBytesList || []).map((b) => { + const v = decodeVtxo(hex.decode(b)); + return { id: v.id, bytes: b, amountSat: v.amountSat, expiryHeight: v.expiryHeight }; + }), + }; + } + _sendOutputs(action) { const outputs = [{ amountSat: action.amountSat, userPubkey: hex.decode(action.destPubkey) }]; if (action.changeSat > 0) { @@ -558,8 +584,12 @@ export class ArkManager { this._save(); } if (action.step === 'registered') { - await postArkoorMessage(this.arkUrl, hex.decode(action.destBlindedId), - destList().map((b) => hex.decode(b))); + // a pouch-bound send has no mailbox to deliver to — the caller takes + // the bytes from the returned action instead + if (action.destBlindedId) { + await postArkoorMessage(this.arkUrl, hex.decode(action.destBlindedId), + destList().map((b) => hex.decode(b))); + } for (const b of changeList()) { const change = this._vtxo(decodeVtxo(hex.decode(b)).id); if (change && change.state === 'pending') change.state = 'spendable'; -- cgit v1.2.3