aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-11 23:51:53 -0700
committerAdam Soltys <[email protected]>2026-03-11 23:51:53 -0700
commit5347d890c4dca651500d6bdc31d4ae5d4ea20440 (patch)
treee7c49474ac0bfdb08f953b6702f5e7aa7be87fdf /lib
parent79252576d1bd24bc74fe1b7fc9cc3d4d68a1c8a0 (diff)
downloadcoinos-server-5347d890c4dca651500d6bdc31d4ae5d4ea20440.tar.gz
coinos-server-5347d890c4dca651500d6bdc31d4ae5d4ea20440.zip
tx recalc
Diffstat (limited to 'lib')
-rw-r--r--lib/cpfp.ts2
-rw-r--r--lib/p2a.ts4
-rw-r--r--lib/payments.ts57
3 files changed, 41 insertions, 22 deletions
diff --git a/lib/cpfp.ts b/lib/cpfp.ts
index 20171acc..cb1bbcbf 100644
--- a/lib/cpfp.ts
+++ b/lib/cpfp.ts
@@ -293,7 +293,7 @@ export async function buildCpfpSend({
"payments:", paymentOutputs.length);
// Calculate User B's bump reserve for this child
- const fastestFee = Math.ceil(fees.fastestFee * 1.5);
+ const fastestFee = Math.max(Math.ceil(fees.fastestFee * 1.5), 4);
const childBumpReserve = calculateBumpReserve(feeRate, fastestFee, decoded.vsize);
return {
diff --git a/lib/p2a.ts b/lib/p2a.ts
index 30e977a1..21a96713 100644
--- a/lib/p2a.ts
+++ b/lib/p2a.ts
@@ -37,6 +37,6 @@ export function calculateBumpReserve(
fastestRate: number,
parentVsize: number,
): number {
- if (userRate >= fastestRate) return 0;
- return Math.ceil((fastestRate - userRate) * parentVsize + fastestRate * CHILD_VSIZE);
+ const subsidy = Math.max(0, (fastestRate - userRate) * parentVsize);
+ return Math.ceil(subsidy + fastestRate * CHILD_VSIZE);
}
diff --git a/lib/payments.ts b/lib/payments.ts
index d936314b..417c0b8d 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -856,14 +856,32 @@ export const sendOnchain = async (params) => {
let bumpReserve = buildResult?.bumpReserve || 0;
let parentVsize = buildResult?.parentVsize || 0;
- // When hex is pre-built (custodial form submit), recalculate bump reserve
- if (isBitcoin && p2aVout >= 0 && !buildResult) {
+ // When hex is pre-built, check if the fee rate is still acceptable
+ if (isBitcoin && !buildResult) {
+ const currentFees: any = await fetch(`${api[type]}/fees/recommended`).then((r) => r.json());
+ const minAcceptable = currentFees.hourFee;
const decoded = await node.decodeRawTransaction(hex);
- parentVsize = decoded.vsize;
- const fees: any = await fetch(`${api[type]}/fees/recommended`).then((r) => r.json());
- const fastestFee = Math.ceil(fees.fastestFee * 1.5);
- const userRate = parseFloat(feeRate) || fees.halfHourFee;
- bumpReserve = calculateBumpReserve(userRate, fastestFee, parentVsize);
+ const originalRate = fee / decoded.vsize;
+
+ if (originalRate < minAcceptable) {
+ // Fee rate is stale — rebuild the tx from scratch
+ buildResult = await build(params);
+ hex = buildResult.hex;
+
+ ({ hex } = await node.signRawTransactionWithWallet(hex));
+ ({ txid } = await node.decodeRawTransaction(hex));
+
+ const r2 = await node.testMempoolAccept([hex]);
+ if (!r2[0].allowed) fail("rebuilt transaction rejected");
+
+ bumpReserve = buildResult.bumpReserve || 0;
+ parentVsize = buildResult.parentVsize || 0;
+ fee = buildResult.fee || 0;
+ } else {
+ // Original rate still acceptable — honor what was shown to the user
+ bumpReserve = Number(params.bumpReserve) || 0;
+ parentVsize = Number(params.parentVsize) || 0;
+ }
}
const p = await debit({
@@ -1059,7 +1077,7 @@ const buildNonCustodial = async ({ aid, amount, address, feeRate, subtract, user
fees.hourFee = fees.halfHourFee;
fees.halfHourFee = fees.fastestFee;
- fees.fastestFee = Math.ceil(fees.fastestFee * 1.5);
+ fees.fastestFee = Math.max(Math.ceil(fees.fastestFee * 1.5), 4);
if (!feeRate) feeRate = fees.halfHourFee;
@@ -1188,10 +1206,12 @@ export const build = async ({ aid, amount, address, feeRate, subtract, user }) =
? { fastestFee: 0.1, halfHourFee: 0.1, hourFee: 0.1 }
: await fetch(`${api[type]}/fees/recommended`).then((r) => r.json());
+ const rawFastestFee = fees.fastestFee;
+
if (isBitcoin) {
fees.hourFee = fees.halfHourFee;
fees.halfHourFee = fees.fastestFee;
- fees.fastestFee = Math.ceil(fees.fastestFee * 1.5);
+ fees.fastestFee = Math.max(Math.ceil(fees.fastestFee * 1.5), 4);
}
if (!feeRate) {
@@ -1256,20 +1276,19 @@ export const build = async ({ aid, amount, address, feeRate, subtract, user }) =
const covered = Math.min(creditBal, ourfee);
ourfee -= covered;
- if (subtract || amount + fee + ourfee + bumpReserve > balance) {
- subtract = true;
+ // Full balance withdrawal — skip reserve and P2A, nothing to refund to
+ const fullWithdrawal = subtract || amount + fee + ourfee + bumpReserve > balance;
- if (isBitcoin && parentVsize === 0) {
- parentVsize = 200;
- bumpReserve = calculateBumpReserve(feeRate, fees.fastestFee, parentVsize);
- }
+ if (fullWithdrawal) {
+ subtract = true;
+ bumpReserve = 0;
+ feeRate = Math.max(rawFastestFee, 1);
- if (amount <= fee + ourfee + bumpReserve + dust) {
- fail(`insufficient funds ⚡️${balance} of ⚡️${amount + fee + ourfee + bumpReserve + dust}`);
+ if (amount <= fee + ourfee + dust) {
+ fail(`insufficient funds ⚡️${balance} of ⚡️${amount + fee + ourfee + dust}`);
}
- outs = [{ [address]: btc(amount - ourfee - bumpReserve) }];
- if (p2aAddress) outs.push({ [p2aAddress]: btc(P2A_VALUE) });
+ outs = [{ [address]: btc(amount - ourfee) }];
raw = await node.createRawTransaction([], outs, 0, replaceable);
if (isBitcoin) raw = "03000000" + raw.substring(8);