aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-15 10:32:40 -0800
committerAdam Soltys <[email protected]>2026-02-15 10:32:40 -0800
commit4d3377ba1d37147b194c1fb0504edee3c73211a5 (patch)
tree43943fe52e3ba574f07f37453fabd273ec8b7757 /lib
parent4a0fcb5f851190eccc7a33907a180ee584db583c (diff)
downloadcoinos-server-4d3377ba1d37147b194c1fb0504edee3c73211a5.tar.gz
coinos-server-4d3377ba1d37147b194c1fb0504edee3c73211a5.zip
ark refresh
Diffstat (limited to 'lib')
-rw-r--r--lib/ark.ts107
-rw-r--r--lib/invoices.ts1
2 files changed, 107 insertions, 1 deletions
diff --git a/lib/ark.ts b/lib/ark.ts
index 0d2d180f..52f441d7 100644
--- a/lib/ark.ts
+++ b/lib/ark.ts
@@ -2,7 +2,14 @@ import { EventSource } from "eventsource";
if (!globalThis.EventSource) globalThis.EventSource = EventSource;
import config from "$config";
-import { SingleKey, Wallet } from "@arkade-os/sdk";
+import {
+ SingleKey,
+ Wallet,
+ Ramps,
+ RestArkProvider,
+ VtxoManager,
+} from "@arkade-os/sdk";
+import { l, warn } from "$lib/logging";
let wallet: any;
@@ -16,6 +23,104 @@ const getWallet = async () => {
return wallet;
};
+let refreshing = false;
+
+const withTimeout = <T>(promise: Promise<T>, ms: number, label: string) =>
+ Promise.race([
+ promise,
+ new Promise<T>((_, reject) =>
+ setTimeout(() => reject(new Error(`${label} timed out`)), ms),
+ ),
+ ]);
+
+export const refreshArkWallet = async () => {
+ if (refreshing) return;
+ try {
+ refreshing = true;
+ const w = await getWallet();
+ const balance = await w.getBalance();
+ l(
+ "ark wallet check — available:",
+ balance.available,
+ "recoverable:",
+ balance.recoverable,
+ "boarding:",
+ balance.boarding.confirmed,
+ );
+
+ const manager = new VtxoManager(w);
+
+ // Recover swept/expired VTXOs
+ if (balance.recoverable > 0) {
+ try {
+ const txid = await withTimeout(
+ manager.recoverVtxos(),
+ 60_000,
+ "ark recovery",
+ );
+ l("ark recovered swept vtxos, txid:", txid);
+ } catch (e: any) {
+ warn("ark vtxo recovery failed:", e.message);
+ }
+ }
+
+ // Renew VTXOs approaching expiry (SDK default: 3 days)
+ try {
+ const expiring = await manager.getExpiringVtxos();
+ if (expiring.length > 0) {
+ l("ark renewing", expiring.length, "expiring vtxos");
+ const txid = await withTimeout(
+ manager.renewVtxos(),
+ 60_000,
+ "ark renewal",
+ );
+ l("ark renewed vtxos, txid:", txid);
+ }
+ } catch (e: any) {
+ warn("ark vtxo renewal failed:", e.message);
+ }
+
+ // Onboard confirmed boarding UTXOs
+ const boardingUtxos = await w.getBoardingUtxos();
+ const confirmed = boardingUtxos.filter((u: any) => u.status?.confirmed);
+
+ if (confirmed.length > 0) {
+ const provider = new RestArkProvider(config.ark.arkServerUrl);
+ const info = await provider.getInfo();
+ const ramps = new Ramps(w);
+
+ // Try each boarding UTXO, freshest first
+ const sorted = [...confirmed].sort(
+ (a: any, b: any) =>
+ (b.status?.block_height || 0) - (a.status?.block_height || 0),
+ );
+
+ for (const utxo of sorted) {
+ try {
+ const txid = await withTimeout(
+ ramps.onboard(info.fees, [utxo], undefined, () => {}),
+ 60_000,
+ "ark onboard",
+ );
+ l("ark onboarded boarding utxo:", utxo.value, "sats, txid:", txid);
+ break;
+ } catch (e: any) {
+ warn("ark onboard failed:", utxo.value, "sats:", e.message);
+ }
+ }
+ }
+ } catch (e: any) {
+ warn("ark wallet refresh failed:", e.message);
+ } finally {
+ refreshing = false;
+ }
+};
+
+// Check every 2 minutes
+setInterval(refreshArkWallet, 2 * 60 * 1000);
+// Initial check after 30s startup delay
+setTimeout(refreshArkWallet, 30_000);
+
export const sendArk = async (address: string, amount: number) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Ark send timed out")), 45000),
diff --git a/lib/invoices.ts b/lib/invoices.ts
index f7bb6897..c5194ce3 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -88,6 +88,7 @@ export const generate = async ({ invoice, user }) => {
type = PaymentType.ark;
text = account.arkAddress;
hash = id;
+ await db.del(`custodial-ark-invoice:${account.arkAddress}`);
} else if (account.seed || (account.pubkey && account.fingerprint)) {
type = PaymentType.bitcoin;
const nextIndex = account.nextIndex || 0;