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 +++++++++++++++++++++- src/routes/(app)/+layout.svelte | 11 +++- .../(app)/send/bitcoin/[address]/+layout.server.ts | 7 ++- 3 files changed, 76 insertions(+), 6 deletions(-) (limited to 'src') 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; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 2d252385..2eda8072 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -1,5 +1,5 @@ diff --git a/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts index 6acb36b6..acf6a370 100644 --- a/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts +++ b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts @@ -5,7 +5,8 @@ export async function load({ cookies, params, parent }) { const { user } = await parent(); const aid = cookies.get("aid") || user.id; const { address, amount } = params; - const { balance } = await get(`/account/${aid}`, auth(cookies)); + const account = await get(`/account/${aid}`, auth(cookies)); + const senderIsVault = account.pubkey || account.seed; let invoice; try { @@ -14,7 +15,7 @@ export async function load({ cookies, params, parent }) { // invoice not found } - if (invoice) { + if (invoice && !senderIsVault) { const isVault = invoice.account?.pubkey || invoice.account?.seed; if (isVault) { // Vault account — stay on /send/bitcoin for on-chain send @@ -24,5 +25,5 @@ export async function load({ cookies, params, parent }) { } } - return { balance }; + return { balance: account.balance }; } -- cgit v1.2.3