From 80f67a648137a8ea2ef3715b3f80af290f384bb2 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 2 Jan 2024 15:19:12 -0800 Subject: liquid payments --- lib/payments.js | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 7 deletions(-) (limited to 'lib') 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(); -- cgit v1.2.3