aboutsummaryrefslogtreecommitdiffstats
path: root/lib/payments.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-05 18:45:08 -0800
committerAdam Soltys <[email protected]>2026-02-06 09:00:22 -0800
commit808e7535d33ffca4c1bf859d8bf0afe134f3e1d7 (patch)
tree87ee9ec7f8e658cf78c33b82f35c991612cbba38 /lib/payments.ts
parent4e9c0c5198c7884cfd97cb4495f6156632e3124a (diff)
downloadcoinos-server-808e7535d33ffca4c1bf859d8bf0afe134f3e1d7.tar.gz
coinos-server-808e7535d33ffca4c1bf859d8bf0afe134f3e1d7.zip
cross-protocol forwarding with race condition fixes
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Diffstat (limited to 'lib/payments.ts')
-rw-r--r--lib/payments.ts73
1 files changed, 54 insertions, 19 deletions
diff --git a/lib/payments.ts b/lib/payments.ts
index 526ae5a2..0fd87559 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -46,6 +46,7 @@ import {
sleep,
t,
} from "$lib/utils";
+import { sendArk } from "$lib/ark";
import { callWebhook } from "$lib/webhooks";
import rpc from "@coinos/rpc";
import { selectUTXO, p2wpkh } from "@scure/btc-signer";
@@ -316,25 +317,24 @@ export const completePayment = async (inv, p, user) => {
let withdrawal;
if (p.confirmed) {
if (inv.forward && !inv.forwarded) {
- try {
- // Skip forward if destination is the same user's own invoice
+ const lockKey = `forward:${inv.id}`;
+ const locked = await db.setNX(lockKey, "1");
+ if (locked) {
+ await db.expire(lockKey, 300);
try {
- const destInv = await getInvoice(inv.forward);
- if (destInv?.uid === id) {
- inv.forwarded = true;
- await s(`invoice:${inv.id}`, inv);
- callWebhook(inv, p);
- return;
+ inv.forwarded = true;
+ await s(`invoice:${inv.id}`, inv);
+
+ // Skip forward if destination is the same user's own Lightning invoice
+ if (inv.forward.startsWith("ln")) {
+ const destInv = await getInvoice(inv.forward).catch(() => null);
+ if (destInv?.uid === id) return;
}
- } catch {}
- inv.forwarded = true;
- await s(`invoice:${inv.id}`, inv);
- const w = await pay({ amount: p.amount, to: inv.forward, user });
- callWebhook(inv, p);
- return w;
- } catch (e) {
- warn(username, "forward failed", inv.forward, e.message);
+ await pay({ amount: p.amount, to: inv.forward, user });
+ } catch (e) {
+ warn(username, "forward failed", inv.forward, e.message);
+ }
}
} else if (autowithdraw && p.type !== "ark") {
try {
@@ -442,6 +442,19 @@ const pay = async ({ aid = undefined, amount, to, user }) => {
sender: user,
});
+ if (to.startsWith("ark") || to.startsWith("tark")) {
+ const fee = Math.max(5, Math.round(amount * 0.02));
+ amount -= fee;
+ const txid = await sendArk(to, amount);
+ return debit({
+ hash: txid,
+ amount: amount + fee,
+ fee,
+ user,
+ type: PaymentType.ark,
+ });
+ }
+
const fee = Math.max(5, Math.round(amount * 0.02));
if (lnurl) {
amount -= fee;
@@ -974,7 +987,10 @@ export const build = async ({
// Non-custodial bitcoin account — use esplora
if (aid !== user.id && type === PaymentType.bitcoin) {
- return buildNonCustodial({ aid, amount, address, feeRate, subtract, user });
+ const account = await g(`account:${aid}`);
+ if (account?.pubkey) {
+ return buildNonCustodial({ aid, amount, address, feeRate, subtract, user });
+ }
}
const node = rpc(config[type]);
@@ -1160,7 +1176,17 @@ export const importAccountHistory = async (account) => {
for (const u of confirmedUtxos) {
const existing = await getPayment(`${u.txid}:${u.vout}`);
- if (existing) continue;
+ 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 created = u.status?.block_time
? u.status.block_time * 1000
@@ -1200,7 +1226,16 @@ export const importAccountHistory = async (account) => {
for (const tx of txsById.values()) {
if (!tx.status?.confirmed) continue;
const existing = await getPayment(tx.txid);
- if (existing) continue;
+ 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;
+ }
let inputsFromUs = 0;
let outputsToUs = 0;