From 55ae58daf23028fa0f647f7bb2b4b0f8cd751de7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 3 Mar 2026 21:40:58 -0800 Subject: sync vault --- lib/bip353.ts | 36 +++---- lib/invoices.ts | 1 - lib/payments.ts | 307 +++++++++++++++++++++++++++++++++----------------------- 3 files changed, 203 insertions(+), 141 deletions(-) (limited to 'lib') diff --git a/lib/bip353.ts b/lib/bip353.ts index 4e37c46e..8b00635b 100644 --- a/lib/bip353.ts +++ b/lib/bip353.ts @@ -5,56 +5,58 @@ import { l, warn } from "$lib/logging"; import { v4 } from "uuid"; import { PaymentType } from "$lib/types"; -const { CF_EMAIL, CF_API_KEY, CF_ZONE_ID } = process.env; const cfApi = "https://api.cloudflare.com/client/v4"; -const cfHeaders = { - "X-Auth-Email": CF_EMAIL, - "X-Auth-Key": CF_API_KEY, +const cfHeaders = () => ({ + Authorization: `Bearer ${config.cloudflare?.apiToken}`, "Content-Type": "application/json", -}; +}); const createDnsRecord = async (username: string, bolt12: string) => { - if (!CF_API_KEY || !CF_ZONE_ID) return; + const { apiToken, zoneId } = config.cloudflare || {}; + if (!apiToken || !zoneId) return; const name = `${username}.user._bitcoin-payment.${config.hostname}`; const content = `bitcoin:?lno=${bolt12}`; + const { zoneId } = config.cloudflare; + const listRes = await fetch( - `${cfApi}/zones/${CF_ZONE_ID}/dns_records?type=TXT&name=${name}`, - { headers: cfHeaders }, + `${cfApi}/zones/${zoneId}/dns_records?type=TXT&name=${name}`, + { headers: cfHeaders() }, ); const listData = (await listRes.json()) as any; if (listData.result?.length) { const recordId = listData.result[0].id; - await fetch(`${cfApi}/zones/${CF_ZONE_ID}/dns_records/${recordId}`, { + await fetch(`${cfApi}/zones/${zoneId}/dns_records/${recordId}`, { method: "PUT", - headers: cfHeaders, + headers: cfHeaders(), body: JSON.stringify({ type: "TXT", name, content, ttl: 3600 }), }); } else { - await fetch(`${cfApi}/zones/${CF_ZONE_ID}/dns_records`, { + await fetch(`${cfApi}/zones/${zoneId}/dns_records`, { method: "POST", - headers: cfHeaders, + headers: cfHeaders(), body: JSON.stringify({ type: "TXT", name, content, ttl: 3600 }), }); } }; const deleteDnsRecord = async (username: string) => { - if (!CF_API_KEY || !CF_ZONE_ID) return; + const { apiToken, zoneId } = config.cloudflare || {}; + if (!apiToken || !zoneId) return; const name = `${username}.user._bitcoin-payment.${config.hostname}`; const listRes = await fetch( - `${cfApi}/zones/${CF_ZONE_ID}/dns_records?type=TXT&name=${name}`, - { headers: cfHeaders }, + `${cfApi}/zones/${zoneId}/dns_records?type=TXT&name=${name}`, + { headers: cfHeaders() }, ); const listData = (await listRes.json()) as any; for (const record of listData.result || []) { - await fetch(`${cfApi}/zones/${CF_ZONE_ID}/dns_records/${record.id}`, { + await fetch(`${cfApi}/zones/${zoneId}/dns_records/${record.id}`, { method: "DELETE", - headers: cfHeaders, + headers: cfHeaders(), }); } }; diff --git a/lib/invoices.ts b/lib/invoices.ts index c5194ce3..15de0687 100644 --- a/lib/invoices.ts +++ b/lib/invoices.ts @@ -100,7 +100,6 @@ export const generate = async ({ invoice, user }) => { account.nextIndex = nextIndex + 1; await s(`account:${aid}`, account); await s(`invoice:${hash}`, id); - await db.sAdd("watching", hash); } else if (type === PaymentType.lightning) { let r; if (bolt11) { diff --git a/lib/payments.ts b/lib/payments.ts index b3c63cfa..f2e4fcab 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -1325,153 +1325,214 @@ export const catchUp = async () => { } }; -export const importAccountHistory = async (account) => { - try { - const { id, uid, pubkey, fingerprint } = account; - const user = await getUser(uid); - - let nextIndex = account.nextIndex || 0; - if (!account.importedAt) { - const lastUsed = await findLastUsedIndex(pubkey, fingerprint); - if (lastUsed > nextIndex) nextIndex = lastUsed; - account.nextIndex = nextIndex; - } +export const syncBitcoinVault = async (account, user) => { + const { id, uid, pubkey, fingerprint } = account; + + let nextIndex = account.nextIndex || 0; + const lastUsed = await findLastUsedIndex(pubkey, fingerprint); + if (lastUsed > nextIndex) { + nextIndex = lastUsed; + account.nextIndex = nextIndex; + await s(`account:${id}`, account); + } - const count = nextIndex + 1; - const externalAddrs = deriveAddresses(pubkey, fingerprint, count, false); - const internalAddrs = deriveAddresses(pubkey, fingerprint, count, true); - const allAddrs = [...externalAddrs, ...internalAddrs]; - const addressSet = new Set(allAddrs); - - const utxos = await getAddressUtxos(allAddrs); - const confirmedUtxos = utxos.filter((u) => u.status.confirmed && u.value >= 300); - const total = confirmedUtxos.reduce((sum, u) => sum + u.value, 0); - - const rates = await g("rates"); - const currency = account.currency || user.currency; - let rate = rates[currency]; - if (!rate) await sleep(1000); - rate = rate || rates[currency]; - - for (const u of confirmedUtxos) { - const existing = await getPayment(`${u.txid}:${u.vout}`); - if (existing?.aid === id) continue; - - if (existing) { - existing.aid = id; - await db - .multi() - .set(`payment:${existing.id}`, JSON.stringify(existing)) - .lPush(`${id}:payments`, existing.id) - .exec(); - continue; + const count = nextIndex + 1; + const externalAddrs = deriveAddresses(pubkey, fingerprint, count, false); + const internalAddrs = deriveAddresses(pubkey, fingerprint, count, true); + const allAddrs = [...externalAddrs, ...internalAddrs]; + const addressSet = new Set(allAddrs); + + const utxos = await getAddressUtxos(allAddrs); + const confirmedUtxos = utxos.filter((u) => u.status.confirmed && u.value >= 300); + const pendingUtxos = utxos.filter((u) => !u.status.confirmed && u.value >= 300); + const confirmedTotal = confirmedUtxos.reduce((sum, u) => sum + u.value, 0); + const pendingTotal = pendingUtxos.reduce((sum, u) => sum + u.value, 0); + + const { rate, currency } = await getUserRate(user); + + const newPayments = []; + + // Process confirmed UTXOs (incoming) + for (const u of confirmedUtxos) { + const ref = `${u.txid}:${u.vout}`; + const existing = await getPayment(ref); + if (existing?.aid === id) { + if (!existing.confirmed) { + existing.confirmed = true; + await s(`payment:${existing.id}`, existing); } + continue; + } - const created = u.status?.block_time ? u.status.block_time * 1000 : Date.now(); - const p = { - id: v4(), - aid: id, - amount: u.value, - fee: 0, - hash: u.address, - confirmed: true, - rate, - currency: user.currency, - type: PaymentType.bitcoin, - uid, - ref: `${u.txid}:${u.vout}`, - created, - }; - + if (existing) { + existing.aid = id; + existing.confirmed = true; await db .multi() - .set(`payment:${u.txid}:${u.vout}`, p.id) - .set(`payment:${p.id}`, JSON.stringify(p)) - .lPush(`${id}:payments`, p.id) - .set(`${id}:payments:last`, p.created) + .set(`payment:${existing.id}`, JSON.stringify(existing)) + .lPush(`${id}:payments`, existing.id) .exec(); + continue; } - const txsById = new Map(); - for (const address of allAddrs) { - const txs = await getAddressTxs(address); - for (const tx of txs as any) { - txsById.set(tx.txid, tx); - } - } + const created = u.status?.block_time ? u.status.block_time * 1000 : Date.now(); + const p = { + id: v4(), + aid: id, + amount: u.value, + fee: 0, + hash: u.address, + confirmed: true, + rate, + currency, + type: PaymentType.bitcoin, + uid, + ref, + created, + }; - for (const tx of txsById.values()) { - if (!tx.status?.confirmed) continue; - const existing = await getPayment(tx.txid); - if (existing?.aid === id) continue; - if (existing) { - existing.aid = id; - await db - .multi() - .set(`payment:${existing.id}`, JSON.stringify(existing)) - .lPush(`${id}:payments`, existing.id) - .exec(); - continue; - } + await db + .multi() + .set(`payment:${ref}`, p.id) + .set(`payment:${p.id}`, JSON.stringify(p)) + .lPush(`${id}:payments`, p.id) + .set(`${id}:payments:last`, p.created) + .exec(); - let inputsFromUs = 0; - let outputsToUs = 0; - let totalIn = 0; - let totalOut = 0; - - for (const vin of tx.vin || []) { - const prev = vin.prevout; - if (!prev) continue; - const value = Number.parseInt(prev.value) || 0; - totalIn += value; - if (addressSet.has(prev.scriptpubkey_address)) inputsFromUs += value; - } + newPayments.push(p); + } - for (const vout of tx.vout || []) { - const value = Number.parseInt(vout.value) || 0; - totalOut += value; - if (addressSet.has(vout.scriptpubkey_address)) outputsToUs += value; - } + // Process pending (unconfirmed) UTXOs + for (const u of pendingUtxos) { + const ref = `${u.txid}:${u.vout}`; + const existing = await getPayment(ref); + if (existing?.aid === id) continue; - if (!inputsFromUs) continue; + if (existing) { + existing.aid = id; + await db + .multi() + .set(`payment:${existing.id}`, JSON.stringify(existing)) + .lPush(`${id}:payments`, existing.id) + .exec(); + continue; + } - const net = outputsToUs - inputsFromUs; - if (net >= 0) continue; + const p = { + id: v4(), + aid: id, + amount: u.value, + fee: 0, + hash: u.address, + confirmed: false, + rate, + currency, + type: PaymentType.bitcoin, + uid, + ref, + created: Date.now(), + }; - const amount = totalOut - outputsToUs; - if (amount <= 0) continue; + await db + .multi() + .set(`payment:${ref}`, p.id) + .set(`payment:${p.id}`, JSON.stringify(p)) + .lPush(`${id}:payments`, p.id) + .set(`${id}:payments:last`, p.created) + .exec(); - const fee = Math.max(0, totalIn - totalOut); - const created = tx.status?.block_time ? tx.status.block_time * 1000 : Date.now(); + newPayments.push(p); + } - const p = { - id: v4(), - aid: id, - amount: -amount, - fee, - hash: tx.txid, - confirmed: true, - rate, - currency: user.currency, - type: PaymentType.bitcoin, - uid, - created, - }; + // Process outgoing transactions + const txsById = new Map(); + for (const address of allAddrs) { + const txs = await getAddressTxs(address); + for (const tx of txs as any) { + txsById.set(tx.txid, tx); + } + } + for (const tx of txsById.values()) { + if (!tx.status?.confirmed) continue; + const existing = await getPayment(tx.txid); + if (existing?.aid === id) continue; + if (existing) { + existing.aid = id; await db .multi() - .set(`payment:${tx.txid}`, p.id) - .set(`payment:${p.id}`, JSON.stringify(p)) - .lPush(`${id}:payments`, p.id) - .set(`${id}:payments:last`, p.created) + .set(`payment:${existing.id}`, JSON.stringify(existing)) + .lPush(`${id}:payments`, existing.id) .exec(); + continue; + } + + let inputsFromUs = 0; + let outputsToUs = 0; + let totalIn = 0; + let totalOut = 0; + + for (const vin of tx.vin || []) { + const prev = vin.prevout; + if (!prev) continue; + const value = Number.parseInt(prev.value) || 0; + totalIn += value; + if (addressSet.has(prev.scriptpubkey_address)) inputsFromUs += value; + } + + for (const vout of tx.vout || []) { + const value = Number.parseInt(vout.value) || 0; + totalOut += value; + if (addressSet.has(vout.scriptpubkey_address)) outputsToUs += value; } - await tbSetBalance(id, total); - await tbSetPending(id, 0); + if (!inputsFromUs) continue; + + const net = outputsToUs - inputsFromUs; + if (net >= 0) continue; + + const amount = totalOut - outputsToUs; + if (amount <= 0) continue; + + const fee = Math.max(0, totalIn - totalOut); + const created = tx.status?.block_time ? tx.status.block_time * 1000 : Date.now(); + + const p = { + id: v4(), + aid: id, + amount: -amount, + fee, + hash: tx.txid, + confirmed: true, + rate, + currency, + type: PaymentType.bitcoin, + uid, + created, + }; + + await db + .multi() + .set(`payment:${tx.txid}`, p.id) + .set(`payment:${p.id}`, JSON.stringify(p)) + .lPush(`${id}:payments`, p.id) + .set(`${id}:payments:last`, p.created) + .exec(); + } + + return { confirmedTotal, pendingTotal, newPayments }; +}; + +export const importAccountHistory = async (account) => { + try { + const user = await getUser(account.uid); + const { confirmedTotal } = await syncBitcoinVault(account, user); + + await tbSetBalance(account.id, confirmedTotal); + await tbSetPending(account.id, 0); account.importedAt = account.importedAt || Date.now(); - await s(`account:${id}`, account); + await s(`account:${account.id}`, account); } catch (e) { console.log(e); warn("problem importing account history", e.message, account); -- cgit v1.2.3