aboutsummaryrefslogtreecommitdiffstats
path: root/lib/payments.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-01-02 15:19:12 -0800
committerAdam Soltys <[email protected]>2024-01-02 15:19:12 -0800
commit80f67a648137a8ea2ef3715b3f80af290f384bb2 (patch)
tree14dfd949ca91eb5159664140144717dd17951f5b /lib/payments.js
parent5c6f1c88a7431103a63702e5e750a53d3a4188fb (diff)
downloadcoinos-server-80f67a648137a8ea2ef3715b3f80af290f384bb2.tar.gz
coinos-server-80f67a648137a8ea2ef3715b3f80af290f384bb2.zip
liquid payments
Diffstat (limited to 'lib/payments.js')
-rw-r--r--lib/payments.js128
1 files changed, 121 insertions, 7 deletions
diff --git a/lib/payments.js b/lib/payments.js
index 88cf6845..b2c01e2f 100644
--- a/lib/payments.js
+++ b/lib/payments.js
@@ -5,7 +5,7 @@ import { v4 } from "uuid";
import { notify } from "$lib/notifications";
import { db, g, s, t } from "$lib/db";
import { l, err, warn } from "$lib/logging";
-import { fail, sleep, wait, SATS } from "$lib/utils";
+import { btc, fail, sleep, wait, SATS, sats } from "$lib/utils";
import { callWebhook } from "$lib/webhooks";
import got from "got";
import { spawn } from "child_process";
@@ -14,10 +14,18 @@ import fs from "fs";
import EventEmitter from "events";
import crc32 from "buffer-crc32";
import { mqtt1, mqtt2 } from "$lib/mqtt";
-import ln from "$lib/ln";
import { mail, templates } from "$lib/mail";
-export const types = {
+import lq from "$lib/liquid";
+import bc from "$lib/bitcoin";
+import ln from "$lib/ln";
+
+let api = {
+ bitcoin: "https://mempool.space/api/v1",
+ liquid: "https://liquid.network/api/v1",
+};
+
+export let types = {
internal: "internal",
bitcoin: "bitcoin",
lightning: "lightning",
@@ -70,7 +78,7 @@ export let debit = async ({
amount || 0,
tip || 0,
fee || 0,
- ourfee || 0
+ ourfee || 0,
);
if (r !== "ok") fail("Problem debiting account");
@@ -193,7 +201,7 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => {
sats:
"⚡️" +
new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(
- p.amount
+ p.amount,
),
link: `${process.env.URL}/payment/${p.id}`,
});
@@ -201,12 +209,12 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => {
mqtt1.publish(
username,
- `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}`
+ `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}`,
);
mqtt2.publish(
username,
- `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}`
+ `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}`,
);
return p;
@@ -319,3 +327,109 @@ export let payLightning = async (user, payreq, amount, maxfee, memo) => {
return p;
};
+
+export let getNode = (type) => {
+ if (type === types.bitcoin) return bc;
+ else if (type === types.liquid) return lq;
+ else fail("unrecognized transaction type");
+};
+
+export let build = async ({ amount, address, feeRate }, user) => {
+ let type;
+
+ try {
+ await bc.getAddressInfo(address);
+ type = types.bitcoin;
+ } catch (e) {
+ try {
+ await lq.getAddressInfo(address);
+ type = types.liquid;
+ } catch (e) {
+ fail("unrecognized address");
+ }
+ }
+
+ let node = getNode(type);
+ amount = parseInt(amount);
+
+ let fees = await fetch(`${api[type]}/fees/recommended`).then((r) => r.json());
+
+ if (!feeRate) {
+ feeRate = fees.halfHourFee;
+ }
+
+ let replaceable = true;
+ let ourfee = Math.round(amount * config.fee);
+ let credit = await g(`credit:${type}:${user.id}`);
+ let covered = Math.min(credit, ourfee) || 0;
+ ourfee -= covered;
+
+ let outs = [{ [address]: btc(amount) }];
+
+ if (type === types.liquid)
+ outs = outs.map((o) => ({ ...o, asset: config.liquid.btc }));
+
+ let raw = await node.createRawTransaction([], outs, 0, replaceable);
+
+ let tx = await node.fundRawTransaction(raw, {
+ fee_rate: feeRate,
+ replaceable,
+ subtractFeeFromOutputs: [],
+ });
+
+ let fee = sats(tx.fee);
+ let dust = 547;
+
+ let balance = await g(`balance:${user.id}`);
+
+ if (amount + fee + ourfee > balance) {
+ if (amount <= fee + ourfee + dust) fail("insufficient funds");
+
+ outs = [{ [address]: btc(amount - ourfee) }];
+ raw = await node.createRawTransaction([], outs, 0, replaceable);
+
+ tx = await node.fundRawTransaction(raw, {
+ fee_rate: feeRate,
+ replaceable,
+ subtractFeeFromOutputs: [0],
+ });
+
+ fee = sats(tx.fee);
+ }
+
+ return { feeRate, ourfee, fee, fees, hex: tx.hex };
+};
+
+let seen = await db.sMembers("missing");
+let catchUp = async () => {
+ try {
+ let txns = [];
+ for (let [type, n] of Object.entries({ bitcoin: bc, liquid: lq })) {
+ txns.push(
+ ...(await n.listTransactions("*", 50)).filter((tx) => {
+ tx.type = type;
+ return tx.category === "receive" && tx.confirmations > 0;
+ }),
+ );
+ }
+
+ for (let { txid, type } of txns) {
+ try {
+ if (seen.includes(txid)) continue;
+ await got.post(`http://localhost:${process.env.PORT || 3119}/confirm`, {
+ json: { txid, wallet: config.bitcoin.wallet, type },
+ });
+
+ seen.push(txid);
+ } catch (e) {
+ err("problem confirming", e.message);
+ }
+ }
+ } catch (e) {
+ err("problem syncing", e.message);
+ }
+
+ setTimeout(catchUp, 2000);
+};
+
+catchUp();