From 144ffdb00f0ea88b808514a6fd3d49acb57a719c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 14 Feb 2024 12:48:55 -0800 Subject: fee bumping --- index.js | 1 + lib/api.js | 5 ++++ lib/payments.js | 19 +++++++++----- routes/nostr.js | 3 +++ routes/payments.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 lib/api.js diff --git a/index.js b/index.js index c2f70c4c..57825fe6 100644 --- a/index.js +++ b/index.js @@ -64,6 +64,7 @@ app.post("/take", auth, payments.take); app.post("/print", auth, payments.print); app.post("/send/:lnaddress/:amount", auth, payments.lnaddress); app.post("/gateway", payments.gateway); +app.post("/replace", auth, payments.replace); app.get("/encode", lnurl.encode); app.get("/decode", lnurl.decode); diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 00000000..3fa8c35b --- /dev/null +++ b/lib/api.js @@ -0,0 +1,5 @@ +export default { + bitcoin: "https://mempool.space/api/v1", + liquid: "https://liquid.network/api/v1", +}; + diff --git a/lib/payments.js b/lib/payments.js index 6c98c15e..2b2ed2af 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -11,6 +11,7 @@ import { callWebhook } from "$lib/webhooks"; import got from "got"; import { mqtt1, mqtt2 } from "$lib/mqtt"; import { mail, templates } from "$lib/mail"; +import api from "$lib/api"; import lq from "$lib/liquid"; import bc from "$lib/bitcoin"; @@ -18,11 +19,6 @@ import ln from "$lib/ln"; let { URL } = process.env; -let api = { - bitcoin: "https://mempool.space/api/v1", - liquid: "https://liquid.network/api/v1", -}; - export let types = { internal: "internal", bitcoin: "bitcoin", @@ -91,7 +87,7 @@ export let debit = async ({ memo, iid, uid, - confirmed: true, + confirmed: ![types.bitcoin, types.liquid].includes(type), rate, currency, type, @@ -100,6 +96,7 @@ export let debit = async ({ created: Date.now(), }; + await s(`payment:${hash}`, id); await s(`payment:${id}`, p); await db.lPush(`${uid}:payments`, id); @@ -267,12 +264,13 @@ let pay = async ({ amount, to, user }) => { pr = to; } + console.log("PAYING"); return pr ? await sendLightning({ user, pr, amount, maxfee }) : await sendOnchain({ amount, address: to, user, subtract: true }); }; -let decode = async (hex) => { +export let decode = async (hex) => { let type, tx; try { tx = await bc.decodeRawTransaction(hex); @@ -282,6 +280,7 @@ let decode = async (hex) => { tx = await lq.decodeRawTransaction(hex); type = types.liquid; } catch (e) { + err("invalid hex", hex); fail("unrecognized tx"); } } @@ -364,7 +363,11 @@ export let sendOnchain = async (params) => { type, }); + p.hex = signed; + await s(`payment:${p.id}`, p); + await node.sendRawTransaction(signed); + return p; }; @@ -517,6 +520,8 @@ export let build = async ({ amount, address, feeRate, user, subtract }) => { feeRate = fees.halfHourFee; } + if (feeRate < fees.economyFee) fail("fee rate too low"); + let replaceable = true; let outs = [{ [address]: btc(amount) }]; diff --git a/routes/nostr.js b/routes/nostr.js index 95186078..75c001a9 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -52,6 +52,7 @@ export default { }, async messages({ params: { pubkey, since = 0 } }, res) { + console.log("H") let params = { kinds: [4], authors: [pubkey], @@ -84,6 +85,8 @@ export default { }), ); + console.log(messages.length) + res.send(messages); }, diff --git a/routes/payments.js b/routes/payments.js index 12793495..f377b7e4 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -8,6 +8,7 @@ import { bail, fail, getInvoice, btc, sats, SATS } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { completePayment, + decode, debit, credit, types, @@ -18,6 +19,7 @@ import { } from "$lib/payments"; import { mqtt1, mqtt2 } from "$lib/mqtt"; import got from "got"; +import api from "$lib/api"; import lq from "$lib/liquid"; import bc from "$lib/bitcoin"; @@ -240,7 +242,15 @@ export default { for (let { address, amount, asset, category, vout } of details) { if (!address) continue; if (type === types.liquid && asset !== config.liquid.btc) continue; - if (category !== "receive") continue; + if (category === "send") { + let p = await g(`payment:${txid}`); + if (typeof p === "string") p = await g(`payment:${p}`); + + if (confirmations >= 1) p.confirmed = true; + await s(`payment:${p.id}`, p); + continue; + } + let p = await g(`payment:${txid}:${vout}`); if (typeof p === "string") p = await g(`payment:${p}`); @@ -272,7 +282,7 @@ export default { emit(p.uid, "payment", p); let user = await g(`user:${p.uid}`); - await completePayment(p, user); + await completePayment(p, user); } } } @@ -305,6 +315,8 @@ export default { try { await requirePin({ body, user }); let { hash: txid } = await sendOnchain({ ...body, user }); + let pid = await g(`payment:${txid}`); + let p = await g(`payment:${pid}`); res.send({ txid }); } catch (e) { @@ -385,4 +397,65 @@ export default { await s(short_channel_id, webhook); res.send({ ok: true }); }, + + async replace({ body: { id }, user }, res) { + try { + let { id: uid } = user; + let p = await g(`payment:${id}`); + if (!p) fail("Payment not found"); + if (p.uid !== user.id) fail("unauthorized"); + + let { tx, type } = await decode(p.hex); + let node = getNode(type); + + let fees = await fetch(`${api[type]}/fees/recommended`).then((r) => + r.json(), + ); + + let outputs = []; + for (let { + scriptPubKey: { address }, + value, + } of tx.vout) { + if (address && !(await node.getAddressInfo(address)).ismine) + outputs.push({ [address]: value }); + } + + let raw = await node.createRawTransaction(tx.vin, outputs); + + let newTx = await node.fundRawTransaction(raw, { + fee_rate: fees.fastestFee, + replaceable: true, + subtractFeeFromOutputs: [], + }); + + let diff = sats(newTx.fee) - p.fee; + if (diff < 0) fail("fee must increase"); + + let ourfee = Math.round(diff * config.fee) || 0; + + ourfee = await db.debit( + `balance:${uid}`, + `credit:${type}:${uid}`, + p.amount || 0, + 0, + diff, + ourfee, + ); + + p.hex = (await node.signRawTransactionWithWallet(newTx.hex)).hex; + let r = await node.testMempoolAccept([p.hex]); + if (!r[0].allowed) fail("transaction rejected"); + p.hash = await node.sendRawTransaction(p.hex); + p.fee = sats(newTx.fee); + await s(`payment:${id}`, p); + await s(`payment:${p.hash}`, id); + emit(uid, "payment", p); + + res.send({ ok: true }); + } catch (e) { + err("failed to bump payment", id, e.message); + bail(res, e.message); + } + }, }; -- cgit v1.2.3