From 8905ad48f91e24cbb2427256a88128462a51a7a0 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 15 Feb 2026 22:26:55 -0800 Subject: e2e tests --- src/lib/ark.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) (limited to 'src/lib') diff --git a/src/lib/ark.ts b/src/lib/ark.ts index dcc42868..a90236da 100644 --- a/src/lib/ark.ts +++ b/src/lib/ark.ts @@ -116,10 +116,30 @@ export const addProcessedVtxos = (keys: string[]) => { // Flag to suppress notifyIncomingFunds during sends export let arkSending = false; +const withTimeout = (promise: Promise, ms: number, label: string) => + Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`${label} timed out`)), ms), + ), + ]); + export const sendArk = async (address: string, amount: number) => { const wallet = await getWallet(); if (!wallet) throw new Error("Ark wallet not available"); + // Refresh expired VTXOs before attempting to send + const balance = await wallet.getBalance(); + if (balance.available < amount && balance.recoverable > 0) { + console.log("ark send: refreshing expired VTXOs before send"); + await refresh(); + } + + const freshBalance = await wallet.getBalance(); + if (freshBalance.available < amount) { + throw new Error(`Insufficient funds: ${freshBalance.available} available, need ${amount}`); + } + // Suppress incoming notifications during send to prevent // change VTXOs from being credited as incoming payments arkSending = true; @@ -127,13 +147,21 @@ export const sendArk = async (address: string, amount: number) => { try { let txid; try { - txid = await wallet.sendBitcoin({ address, amount }); + txid = await withTimeout( + wallet.sendBitcoin({ address, amount }), + 45_000, + "Ark send", + ); } catch (e: any) { if (e?.message?.includes("VTXO_ALREADY_REGISTERED")) { await wallet.settle(); const vtxos = await wallet.getVtxos({ spendableOnly: false }); if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); - txid = await wallet.sendBitcoin({ address, amount }); + txid = await withTimeout( + wallet.sendBitcoin({ address, amount }), + 45_000, + "Ark send retry", + ); } else { throw e; } @@ -175,6 +203,38 @@ export const sendArkViaForward = async ({ return post("/post/ark/receive", { amount, hash: txid, iid: inv.id }); }; +export const refresh = async () => { + const wallet = await getWallet(); + if (!wallet) return; + + const { VtxoManager } = await loadArkSdk(); + const balance = await wallet.getBalance(); + console.log("ark refresh: balance", balance); + const manager = new VtxoManager(wallet); + + let acted = false; + + if (balance.recoverable > 0) { + console.log("ark refresh: recovering", balance.recoverable, "swept sats"); + await manager.recoverVtxos(); + acted = true; + } + + const expiring = await manager.getExpiringVtxos(); + if (expiring.length > 0) { + console.log("ark refresh: renewing", expiring.length, "expiring vtxos"); + await manager.renewVtxos(); + acted = true; + } + + // Mark post-recovery/renewal VTXOs as processed so notifyIncomingFunds + // doesn't treat them as new incoming payments + if (acted) { + const vtxos = await wallet.getVtxos({ spendableOnly: false }); + if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); + } +}; + export const settle = async () => { const wallet = await getWallet(); if (!wallet) return; -- cgit v1.2.3