aboutsummaryrefslogtreecommitdiffstats
path: root/routes
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-02 22:50:58 -0800
committerAdam Soltys <[email protected]>2026-03-02 22:51:02 -0800
commitc7300714c57652e68fcadcdbcfcce3df8dd7d37b (patch)
tree84bee888a8f3173257c6d2aa9670d41498f50c63 /routes
parent58e7f32f48c2fb094196f44f4c59a8975e5f93a9 (diff)
downloadcoinos-server-c7300714c57652e68fcadcdbcfcce3df8dd7d37b.tar.gz
coinos-server-c7300714c57652e68fcadcdbcfcce3df8dd7d37b.zip
fee bumping
Diffstat (limited to 'routes')
-rw-r--r--routes/payments.ts66
1 files changed, 35 insertions, 31 deletions
diff --git a/routes/payments.ts b/routes/payments.ts
index 91115fc2..043d1c47 100644
--- a/routes/payments.ts
+++ b/routes/payments.ts
@@ -9,6 +9,7 @@ import { replay } from "$lib/lightning";
import ln from "$lib/ln";
import { err, l, warn } from "$lib/logging";
import mqtt from "$lib/mqtt";
+import { createCpfpChild, estimateBumpCost } from "$lib/cpfp";
import {
acquireArkLock,
build,
@@ -16,7 +17,6 @@ import {
createArkPayment,
credit,
debit,
- decode,
getUserRate,
processWatchedTx,
reverse,
@@ -665,49 +665,53 @@ export default {
return c.json({ ok: true });
},
- async replace(c) {
+ async bump(c) {
const body = await c.req.json();
- const { id } = body;
+ const { id, targetFeeRate } = body;
const user = c.get("user");
try {
const p = await gf(`payment:${id}`);
if (!p) fail("Payment not found");
if (p.uid !== user.id) fail("unauthorized");
+ if (p.confirmed) fail("transaction already confirmed");
+ if (p.type !== PaymentType.bitcoin) fail("only bitcoin transactions can be bumped");
+ if (!p.bumpReserve || p.bumpReserve <= 0) fail("no bump reserve available");
+ if (p.childTxid) fail("transaction already bumped");
- const { tx, type } = await decode(p.hex);
- const node = rpc(config[type]);
-
- const fees: any = await fetch(`${api[type]}/fees/recommended`).then((r) => r.json());
-
- const outputs = [];
- for (const {
- scriptPubKey: { address },
- value,
- } of tx.vout) {
- if (address && !(await node.getAddressInfo(address)).ismine)
- outputs.push({ [address]: value });
- }
-
- const raw = await node.createRawTransaction(tx.vin, outputs);
+ const { txid, childFee } = await createCpfpChild(p, targetFeeRate);
- const newTx = await node.fundRawTransaction(raw, {
- fee_rate: fees.fastestFee + 50,
- replaceable: true,
- subtractFeeFromOutputs: [],
+ return c.json({
+ txid,
+ childFee,
+ remaining: p.bumpReserve - childFee,
});
+ } catch (e) {
+ err("failed to bump payment", id, e.message);
+ return bail(c, e.message);
+ }
+ },
- const diff = sats(newTx.fee) - p.fee;
- if (diff < 0) fail("fee must increase");
+ async bumpEstimate(c) {
+ const body = await c.req.json();
+ const { id, targetFeeRate } = body;
+ const user = c.get("user");
+ try {
+ const p = await gf(`payment:${id}`);
+ if (!p) fail("Payment not found");
+ if (p.uid !== user.id) fail("unauthorized");
- if (config[type].walletpass) await node.walletPassphrase(config[type].walletpass, 300);
- p.hex = (await node.signRawTransactionWithWallet(newTx.hex)).hex;
- const r = await node.testMempoolAccept([p.hex]);
- if (!r[0].allowed) fail(`transaction rejected ${p.hex}`);
- warn("bump", user.username, p.hex);
+ const cost = estimateBumpCost(p.fee, p.parentVsize, targetFeeRate);
- return c.json({ ok: true });
+ const fees: any = await fetch(`${api[PaymentType.bitcoin]}/fees/recommended`).then((r) =>
+ r.json(),
+ );
+
+ return c.json({
+ cost,
+ fees,
+ bumpReserve: p.bumpReserve || 0,
+ });
} catch (e) {
- err("failed to bump payment", id, e.message);
return bail(c, e.message);
}
},