From 15e0b236e7ff2cafcb439c891c790deea8ec16a7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 5 Jul 2026 20:39:02 -0700 Subject: Unilateral exit: put vtxos on-chain without the server 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 --- src/ark/manager.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'src/ark/manager.js') diff --git a/src/ark/manager.js b/src/ark/manager.js index efcc18a..cfc37a9 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -41,6 +41,7 @@ import { prepareOffboard, validateOffboardTx, signOffboardForfeits, finishOffboard, P2TR_DUST, } from './offboard.js'; +import { signedExitTxs } from './exit.js'; import { validateVtxo, VtxoValidationError } from './validate.js'; const EMPTY_STATE = () => ({ @@ -268,6 +269,26 @@ export class ArkManager { } } + // ---- unilateral exit ---- + // Marks the vtxo pending and records the action. The ark FEATURE drives the + // steps (it owns the on-chain wallet needed for fee-bump coins and the + // claim destination); crash-safety comes from the persisted action like + // every other flow. signedExitTxs() up front validates the chain is fully + // signed before anything is committed. + startExit(vtxoId) { + const v = this._vtxo(vtxoId); + if (!v || v.state !== 'spendable') throw new Error('vtxo is not spendable'); + const txs = signedExitTxs(this._decoded(v), this.serverPub); + const action = { + id: `exit-${Date.now()}-${vtxoId.slice(0, 8)}`, type: 'exit', step: 'chain', + vtxoId, amountSat: v.amountSat, txids: txs.map((t) => t.txid), + }; + v.state = 'pending'; + this.state.actions.push(action); + this._save(); + return action; + } + // ---- offboard (collaborative exit: all spendable vtxos -> one on-chain output) ---- async startOffboard(spk, address) { if (this.pendingActions().some((a) => a.type === 'offboard')) { -- cgit v1.2.3