diff options
| -rw-r--r-- | lib/payments.js | 128 | ||||
| -rw-r--r-- | routes/payments.js | 185 |
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) { |
