From 1d9cac6ca92187718eafbd567562875d25fa772b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 4 Jul 2026 19:29:08 -0700 Subject: Ark: recover from stale vtxo state Match captaind's actual 'not spendable' error so a crashed or foreign spend fails the action instead of retrying forever, and reconcile locally-spendable vtxos against the server (GetVtxoStatus + ownership attestation) when an ark address lands in the send form. boardFee now treats absent proto3 fee fields as zero (Second's signet boards free). Co-Authored-By: Claude Fable 5 --- src/ark/proto.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'src/ark/proto.js') diff --git a/src/ark/proto.js b/src/ark/proto.js index 728686b..1cbc0ef 100644 --- a/src/ark/proto.js +++ b/src/ark/proto.js @@ -363,7 +363,8 @@ export function decodeVtxo(bytes) { export async function getArkInfo(ark) { const data = await grpcCall(ark, 'bark_server.ArkService/GetArkInfo', new Uint8Array(0)); - const info = {}; + // Fee schedules a server never sends (or sends all-zero) mean "free". + const info = { boardFees: {}, refreshFees: { baseFeeSat: 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); @@ -446,6 +447,24 @@ export function decodeMailboxMessage(value) { return msg; } +// VtxoSpendState values from GetVtxoStatus (bark_server.proto). +export const VTXO_STATE_SPENDABLE = 1; +export const VTXO_STATE_SPENT = 2; + +const VTXO_STATUS_PREFIX = te.encode('Ark VTXO status query challenge '); // 32 bytes + +// Ask the server for a vtxo's authoritative spend state. The attestation is a +// BIP340 signature with the vtxo's user key, so only the owner can query. +export async function getVtxoStatus(ark, vtxoIdRaw /* 36B outpoint */, vtxoPrivkey) { + const attestation = schnorr.sign(sha256(concatBytes(VTXO_STATUS_PREFIX, vtxoIdRaw)), vtxoPrivkey); + const w = pbWriter(); + w.bytesField(1, vtxoIdRaw); + w.bytesField(2, attestation); + const data = await grpcCall(ark, 'bark_server.ArkService/GetVtxoStatus', w.finish()); + for (const { field, value } of pbFields(data)) if (field === 1) return Number(value); + return 0; // proto3 omits a zero enum: UNSPECIFIED +} + // Build an authenticated MailboxRequest (shared by read + subscribe). export function mailboxRequestBytes(mailbox, checkpoint = 0) { const expiry = Math.floor(Date.now() / 1000) + 60; -- cgit v1.2.3