aboutsummaryrefslogtreecommitdiffstats
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
parent5c6f1c88a7431103a63702e5e750a53d3a4188fb (diff)
downloadcoinos-server-80f67a648137a8ea2ef3715b3f80af290f384bb2.tar.gz
coinos-server-80f67a648137a8ea2ef3715b3f80af290f384bb2.zip
liquid payments
-rw-r--r--lib/payments.js128
-rw-r--r--routes/payments.js185
2 files changed, 198 insertions, 115 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();
diff --git a/routes/payments.js b/routes/payments.js
index fb1bb950..5a46f29e 100644
--- a/routes/payments.js
+++ b/routes/payments.js
@@ -6,7 +6,14 @@ import { db, g, s, t } from "$lib/db";
import { l, err, warn } from "$lib/logging";
import { bail, fail, btc, sats, SATS } from "$lib/utils";
import { requirePin } from "$lib/auth";
-import { debit, credit, types, payLightning } from "$lib/payments";
+import {
+ debit,
+ credit,
+ types,
+ getNode,
+ build,
+ payLightning,
+} from "$lib/payments";
import { mqtt1, mqtt2 } from "$lib/mqtt";
import got from "got";
@@ -14,86 +21,6 @@ import lq from "$lib/liquid";
import bc from "$lib/bitcoin";
import ln from "$lib/ln";
-let getNode = (type) => {
- if (type === types.bitcoin) return bc;
- else if (type === types.liquid) return lq;
- else fail("unrecognized transaction type");
-};
-
-let build = async (
- { amount, address, feeRate: fee_rate, type = types.bitcoin },
- user,
-) => {
- let node = getNode(type);
- amount = parseInt(amount);
-
- 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) }];
- let raw = await node.createRawTransaction([], outs, 0, replaceable);
-
- let tx = await node.fundRawTransaction(raw, {
- fee_rate,
- 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,
- replaceable,
- subtractFeeFromOutputs: [0],
- });
-
- fee = sats(tx.fee);
- }
-
- return { feeRate: fee_rate, ourfee, fee, hex: tx.hex };
-};
-
-let seen = await db.sMembers("missing");
-let catchUp = async () => {
- 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) {
- console.log(e.message);
- }
- }
- setTimeout(catchUp, 2000);
-};
-
-catchUp();
-
export default {
async create({ body, user }, res) {
let { amount, hash, maxfee, fund, memo, payreq, rate, username } = body;
@@ -349,7 +276,13 @@ export default {
try {
res.send(await build(body, user));
} catch (e) {
- warn("problem estimating fee", e.message, user.username, body.amount, body.address);
+ warn(
+ "problem estimating fee",
+ e.message,
+ user.username,
+ body.amount,
+ body.address,
+ );
bail(res, "problem estimating fee");
}
},
@@ -358,44 +291,80 @@ export default {
try {
await requirePin({ body, user });
- let { memo, hex, rate, type = types.bitcoin } = body;
- let node = getNode(type);
-
+ let { memo, hex, rate } = body;
if (!hex) ({ hex } = await build(body, user));
- if (config.bitcoin.walletpass)
- await node.walletPassphrase(config.bitcoin.walletpass, 300);
+ let type, tx;
+ try {
+ tx = await bc.decodeRawTransaction(hex);
+ type = types.bitcoin;
+ } catch (e) {
+ try {
+ tx = await lq.decodeRawTransaction(hex);
+ type = types.liquid;
+ } catch (e) {
+ fail("unrecognized tx");
+ }
+ }
- ({ hex } = await node.signRawTransactionWithWallet(hex));
+ let node = getNode(type);
+ let { txid } = tx;
- let r = await node.testMempoolAccept([hex]);
- if (!r[0].allowed) fail("transaction rejected");
+ if (config[type].walletpass)
+ await node.walletPassphrase(config[type].walletpass, 300);
- let tx = await node.decodeRawTransaction(hex);
- let { txid } = tx;
+ let { hex: signed } = await node.signRawTransactionWithWallet(
+ type === types.liquid ? await node.blindRawTransaction(hex) : hex,
+ );
+
+ let r = await node.testMempoolAccept([signed]);
+ if (!r[0].allowed) fail("transaction rejected");
let total = 0;
+ let fee = 0;
let change = 0;
- let totalIn = 0;
- for await (let { txid, vout } of tx.vin) {
- let hex = await node.getRawTransaction(txid);
- let tx = await node.decodeRawTransaction(hex);
- totalIn += sats(tx.vout[vout].value);
- }
+ if (type === types.liquid) {
+ for (let {
+ asset,
+ scriptPubKey: { address, type },
+ value,
+ } of tx.vout) {
+ if (asset !== config.liquid.btc) fail("only L-BTC supported");
+ if (type === "fee") fee = sats(value);
+ else {
+ total += sats(value);
- for (let {
- scriptPubKey: { address },
- value,
- } of tx.vout) {
- total += sats(value);
- if (await g(`invoice:${address}`))
- fail("Cannot send to internal address");
+ if (address) {
+ if ((await node.getAddressInfo(address)).ismine) {
+ change += sats(value);
+ }
+ }
+ }
+ }
+ } else {
+ let totalIn = 0;
+ for await (let { txid, vout } of tx.vin) {
+ let hex = await node.getRawTransaction(txid);
+ let tx = await node.decodeRawTransaction(hex);
+ totalIn += sats(tx.vout[vout].value);
+ }
+
+ for (let {
+ scriptPubKey: { address },
+ value,
+ } of tx.vout) {
+ total += sats(value);
+ if (await g(`invoice:${address}`))
+ fail("Cannot send to internal address");
+
+ if ((await node.getAddressInfo(address)).ismine)
+ change += sats(value);
+ }
- if ((await node.getAddressInfo(address)).ismine) change += sats(value);
+ fee = totalIn - total;
}
- let fee = totalIn - total;
let amount = total - change;
await debit({
@@ -407,7 +376,7 @@ export default {
type,
});
- await node.sendRawTransaction(hex);
+ await node.sendRawTransaction(signed);
res.send({ txid });
} catch (e) {