From f9fc0bb3e38c3387f8af5d151aa03d83376ba9cc Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 5 Jul 2026 09:05:20 -0700 Subject: Collaborative offboard: move the ark balance back on-chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PrepareOffboard/FinishOffboard against captaind — per-input ownership attestations, connector-based musig forfeits (with the deterministic fanout tx when there is more than one input), and fee math that mirrors the server bit-for-bit (weight fee rounds up, ppm-by-expiry rounds down) since it rejects any other net amount. The receive pane gains a 'Move back on-chain' button that withdraws everything to the wallet's own fresh address; the incoming tx is labeled 'Ark withdrawal' in History and the ark side records the movement. Crash-safe: the txid persists before FinishOffboard and resume asks GetVtxoStatus whether the forfeits landed (the server broadcasts the tx itself). Also fixes boarding to applySentTx after broadcasting the funding tx — without it the spent coin lingered in the utxo set (a second board double-spent it), and the post-board scan() could resurrect it by racing the explorer's indexing. Co-Authored-By: Claude Fable 5 --- src/ark/proto.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) (limited to 'src/ark/proto.js') diff --git a/src/ark/proto.js b/src/ark/proto.js index 1cbc0ef..ae2342e 100644 --- a/src/ark/proto.js +++ b/src/ark/proto.js @@ -364,7 +364,11 @@ export function decodeVtxo(bytes) { export async function getArkInfo(ark) { const data = await grpcCall(ark, 'bark_server.ArkService/GetArkInfo', new Uint8Array(0)); // Fee schedules a server never sends (or sends all-zero) mean "free". - const info = { boardFees: {}, refreshFees: { baseFeeSat: 0, ppmExpiryTable: [] } }; + const info = { + boardFees: {}, + refreshFees: { baseFeeSat: 0, ppmExpiryTable: [] }, + offboardFees: { baseFeeSat: 0, fixedAdditionalVb: 0, ppmExpiryTable: [] }, + }; for (const { field, value } of pbFields(data)) { if (field === 1) info.network = td.decode(value); if (field === 2) info.serverPubkey = hex.encode(value); @@ -374,6 +378,7 @@ export async function getArkInfo(ark) { if (field === 10) info.requiredBoardConfirmations = Number(value); if (field === 13) info.minBoardAmountSat = Number(value); if (field === 16) info.mailboxPubkey = hex.encode(value); + if (field === 20) info.maxOffboardInputs = Number(value); if (field === 18) { // FeeSchedule for (const f of pbFields(value)) { if (f.field === 1) { // board @@ -385,6 +390,22 @@ export async function getArkInfo(ark) { } info.boardFees = b; } + if (f.field === 2) { // offboard + const ob = { baseFeeSat: 0, fixedAdditionalVb: 0, ppmExpiryTable: [] }; + for (const g of pbFields(f.value)) { + if (g.field === 1) ob.baseFeeSat = Number(g.value); + if (g.field === 2) ob.fixedAdditionalVb = Number(g.value); + if (g.field === 3) { + const e = { thresholdBlocks: 0, ppm: 0 }; + for (const h of pbFields(g.value)) { + if (h.field === 1) e.thresholdBlocks = Number(h.value); + if (h.field === 2) e.ppm = Number(h.value); + } + ob.ppmExpiryTable.push(e); + } + } + info.offboardFees = ob; + } if (f.field === 3) { // refresh const rf = { baseFeeSat: 0, ppmExpiryTable: [] }; for (const g of pbFields(f.value)) { -- cgit v1.2.3