aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark/manager.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 19:02:25 -0700
committerAdam Soltys <[email protected]>2026-08-01 19:02:25 -0700
commitb73bb9ee51b9be33bc1859fd56d3e11905c3d456 (patch)
tree8a008d57e450961d1ab203a663e9d8fa233a2686 /src/ark/manager.js
parent2a7a825ce5850a2a836effae820adbae0b66d093 (diff)
downloadcoinosv3-b73bb9ee51b9be33bc1859fd56d3e11905c3d456.tar.gz
coinosv3-b73bb9ee51b9be33bc1859fd56d3e11905c3d456.zip
Background NWC: the worker now answers — and pays — from the pouch
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]>
Diffstat (limited to 'src/ark/manager.js')
-rw-r--r--src/ark/manager.js34
1 files changed, 32 insertions, 2 deletions
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';