aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark/refresh.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-05 20:39:02 -0700
committerAdam Soltys <[email protected]>2026-07-05 20:39:02 -0700
commit15e0b236e7ff2cafcb439c891c790deea8ec16a7 (patch)
tree960057559e60b4b2e624a7de9e4ac35b1ecb81ad /src/ark/refresh.js
parent0408609fc68fa71ff705ef7a241880efefddcc3f (diff)
downloadcoinosv3-15e0b236e7ff2cafcb439c891c790deea8ec16a7.tar.gz
coinosv3-15e0b236e7ff2cafcb439c891c790deea8ec16a7.zip
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 <[email protected]>
Diffstat (limited to 'src/ark/refresh.js')
-rw-r--r--src/ark/refresh.js9
1 files changed, 6 insertions, 3 deletions
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);