diff options
| author | Adam Soltys <[email protected]> | 2026-03-11 22:20:46 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-03-11 22:20:46 -0700 |
| commit | e6938fb6c4cf6e6f3b3c7aa5c7b1d8f28c745031 (patch) | |
| tree | 64c1d7e5e46b061d87e85b79f617005d3cd88ec9 /lib/payments.ts | |
| parent | 4007842de3cacd93db5df6222ddaa47ec6e557ce (diff) | |
| download | coinos-server-e6938fb6c4cf6e6f3b3c7aa5c7b1d8f28c745031.tar.gz coinos-server-e6938fb6c4cf6e6f3b3c7aa5c7b1d8f28c745031.zip | |
cpfp improvements
Diffstat (limited to 'lib/payments.ts')
| -rw-r--r-- | lib/payments.ts | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/lib/payments.ts b/lib/payments.ts index 0dacf035..cc5551ce 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -56,6 +56,7 @@ import { v4 } from "uuid"; import { PaymentType } from "$lib/types"; import { P2A_VALUE, getP2AAddress, isP2AOutput, calculateBumpReserve } from "$lib/p2a"; +import { buildCpfpSend } from "$lib/cpfp"; const bc = rpc(config.bitcoin); const lq = rpc(config.liquid); @@ -723,6 +724,61 @@ export const sendOnchain = async (params) => { hex = buildResult.hex; } + // CPFP+send path: already signed, needs package submission + if (buildResult?.signed) { + const { txid, parentHex, fee, bumpReserve, p2aVout, parentVsize, ourfee, cpfpParentId, cpfpSubsidy } = buildResult; + + try { + if (inflight[txid]) fail("payment in flight"); + inflight[txid] = true; + + // Submit as package (parent + child) since parent may not be in mempool + const r = await bc.testMempoolAccept([hex]); + if (r[0].allowed) { + await bc.sendRawTransaction(hex); + } else { + const pkg = await bc.submitPackage([parentHex, hex]); + if (!pkg["tx-results"]) fail("package submission failed"); + } + + // Decode to get the send amount + const decoded = await bc.decodeRawTransaction(hex); + let amount = 0; + for (const out of decoded.vout) { + if (isP2AOutput(out.scriptPubKey.hex)) continue; + const info = await bc.getAddressInfo(out.scriptPubKey.address); + if (!info.ismine) amount += sats(out.value); + } + + const p = await debit({ + aid, + hash: txid, + amount, + fee: fee + bumpReserve, + rate, + user, + type: PaymentType.bitcoin, + }); + + p.fee = fee; + p.hex = hex; + (p as any).bumpReserve = bumpReserve; + (p as any).p2aVout = p2aVout; + (p as any).parentVsize = parentVsize; + (p as any).cpfpParentId = cpfpParentId; + p.confirmed = false; + + await s(`payment:${p.id}`, p); + await db.sAdd("outgoing:unconfirmed", p.id); + + delete inflight[txid]; + return p; + } catch (e) { + delete inflight[txid]; + throw e; + } + } + const { tx, type } = await decode(hex); const node = rpc(config[type]); const isBitcoin = type === PaymentType.bitcoin; @@ -1163,6 +1219,21 @@ export const build = async ({ aid, amount, address, feeRate, subtract, user }) = let bumpReserve = 0; let parentVsize = 0; + // Check if confirmed UTXOs can cover this send — if not, fall back to CPFP+send + if (isBitcoin) { + const confirmedUtxos = await node.listUnspent(1); + const confirmedTotal = confirmedUtxos.reduce((sum, u) => sum + sats(u.amount), 0); + if (confirmedTotal < amount + P2A_VALUE) { + const balance = await getBalance(aid); + let ourfee = Math.round(amount * config.fee[type]); + const creditBal = await getCredit(aid, type); + const covered = Math.min(creditBal, ourfee); + ourfee -= covered; + const cpfp = await buildCpfpSend({ address, amount: amount - ourfee, feeRate, fees }); + return { ...cpfp, feeRate, ourfee, fees, inputs: [], subtract, signed: true }; + } + } + try { tx = await node.fundRawTransaction(raw, { fee_rate: feeRate, |
