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/refresh.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src/ark/refresh.js') diff --git a/src/ark/refresh.js b/src/ark/refresh.js index e3be402..54a3fc8 100644 --- a/src/ark/refresh.js +++ b/src/ark/refresh.js @@ -68,8 +68,10 @@ function taprootFromMerkle(internalXOnly, merkleRoot) { const P = secp256k1.ProjectivePoint; const internalPoint = P.fromHex(concatBytes(Uint8Array.of(0x02), internalXOnly)); const outputPoint = internalPoint.add(P.BASE.multiply(BigInt('0x' + hex.encode(tapTweak)))); - const outputXOnly = outputPoint.toRawBytes(true).slice(1); - return { tapTweak, outputXOnly, scriptPubKey: concatBytes(hex.decode('5120'), outputXOnly) }; + const outputCompressed = outputPoint.toRawBytes(true); + const outputXOnly = outputCompressed.slice(1); + const outputParity = outputCompressed[0] === 0x03 ? 1 : 0; + return { tapTweak, outputXOnly, outputParity, scriptPubKey: concatBytes(hex.decode('5120'), outputXOnly) }; } // Cosigned transition input: internal = musig(pubkeys), one timelock leaf @@ -86,7 +88,8 @@ export function harkLeafTaproot(userPub, serverPub, expiryHeight, unlockHash) { const expiryLeaf = tapLeafHash(timelockSignScript(expiryHeight, serverPub.slice(1))); const unlockScript = hashSignScript(unlockHash, internalXOnly); const unlockLeaf = tapLeafHash(unlockScript); - return { ...taprootFromMerkle(internalXOnly, tapBranch(expiryLeaf, unlockLeaf)), unlockScript, internalXOnly }; + // expiryLeaf doubles as the merkle path when spending through the unlock leaf + return { ...taprootFromMerkle(internalXOnly, tapBranch(expiryLeaf, unlockLeaf)), unlockScript, internalXOnly, expiryLeaf }; } // Forfeit claim output: internal = musig(user, server); -- cgit v1.2.3