aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.js106
-rw-r--r--src/i18n.js1
-rw-r--r--src/locales/ar.json3
-rw-r--r--src/locales/bn.json3
-rw-r--r--src/locales/de.json3
-rw-r--r--src/locales/es.json3
-rw-r--r--src/locales/fr.json3
-rw-r--r--src/locales/hi.json3
-rw-r--r--src/locales/id.json3
-rw-r--r--src/locales/ja.json3
-rw-r--r--src/locales/ko.json3
-rw-r--r--src/locales/mr.json3
-rw-r--r--src/locales/pt.json3
-rw-r--r--src/locales/ru.json3
-rw-r--r--src/locales/sw.json3
-rw-r--r--src/locales/ta.json3
-rw-r--r--src/locales/te.json3
-rw-r--r--src/locales/tr.json3
-rw-r--r--src/locales/ur.json3
-rw-r--r--src/locales/vi.json3
-rw-r--r--src/locales/zh.json3
-rw-r--r--src/wallet.js76
22 files changed, 176 insertions, 64 deletions
diff --git a/src/app.js b/src/app.js
index 5f64e90..911bca0 100644
--- a/src/app.js
+++ b/src/app.js
@@ -43,6 +43,7 @@ const ui = {
send: blankSend(),
draft: null, // built tx summary awaiting review
broadcastTx: null, // scanned signed tx awaiting broadcast confirmation
+ bump: null, // RBF bump in progress: { prep, feeChoice, customFee }
sendError: '',
sendResult: null, // { txid } | { signedHex, txid }
busy: false,
@@ -600,6 +601,7 @@ function lock() {
ui.receiveSeenIndex = null;
ui.txDetail = null;
ui.broadcastTx = null;
+ ui.bump = null;
render();
}
@@ -869,6 +871,7 @@ function tabsBar() {
ui.tab = id;
ui.revealShown = false; // re-mask the recovery phrase whenever tabs change
ui.txDetail = null; // back to the history list when leaving/returning
+ ui.bump = null;
render();
})
)
@@ -1008,7 +1011,7 @@ function broadcastConfirmView() {
return h(
'div',
{ class: 'card col' },
- h('h3', {}, b.bump ? t('bumpConfirm') : t('broadcastScanned')),
+ h('h3', {}, t('broadcastScanned')),
h(
'div',
{ class: 'summary col', style: 'gap:0' },
@@ -1018,11 +1021,6 @@ function broadcastConfirmView() {
h('span', { class: 'v' }, fmtAmount(o.value), ' ', unitTag())
)
),
- b.bump
- ? h('div', { class: 'line' },
- h('span', { class: 'k' }, t('networkFee')),
- h('span', { class: 'v' }, fmtAmount(b.bump.oldFee) + ' → ' + fmtAmount(b.bump.newFee) + ' ' + unitLabel()))
- : null,
h('div', { class: 'line' },
h('span', { class: 'k' }, t('transactionId')),
h('span', { class: 'v mono break' }, shortTxid(b.txid))
@@ -1033,23 +1031,27 @@ function broadcastConfirmView() {
h('button', { class: 'btn-ghost', onClick: () => { ui.broadcastTx = null; ui.sendError = ''; render(); } }, t('back')),
ui.busy
? h('button', { class: 'btn-primary grow', disabled: true }, h('span', { class: 'spinner' }))
- : h('button', { class: 'btn-primary grow', onClick: broadcastScanned }, b.bump ? t('replaceTx') : t('broadcastNow'))
+ : h('button', { class: 'btn-primary grow', onClick: broadcastScanned }, t('broadcastNow'))
)
);
}
-// Build a higher-fee RBF replacement of an unconfirmed send and route it to the
-// broadcast confirmation. Bumps to the Priority rate.
+// --- RBF fee bump (with a fee-rate picker) ---------------------------------
+function bumpRate() {
+ const s = ui.bump;
+ if (s.feeChoice === 'custom') return Math.max(1, Math.round(Number(s.customFee) || 1));
+ const fr = wallet.feeRates;
+ return (fr && fr[s.feeChoice]) || 5;
+}
+
+// Open the bump screen: fetch + reconstruct the original, default to Priority.
async function bumpFee(txid) {
if (wallet.offline) { toast(t('scanOffline')); return; }
- const rate = (wallet.feeRates && wallet.feeRates.fastestFee) || 10;
ui.busy = true;
render();
try {
- const d = await wallet.bumpTx(txid, rate);
- ui.broadcastTx = { hex: d.hex, txid: d.txid, outputs: d.outputs, bump: { oldFee: d.oldFee, newFee: d.fee } };
- ui.txDetail = null;
- ui.tab = 'send';
+ const prep = await wallet.prepareBump(txid);
+ ui.bump = { prep, feeChoice: 'fastestFee', customFee: '' };
ui.sendError = '';
} catch (e) {
toast(e.message);
@@ -1058,6 +1060,81 @@ async function bumpFee(txid) {
render();
}
+function bumpView() {
+ const s = ui.bump;
+ const feeOpts = [
+ ['economyFee', t('feeEconomy')],
+ ['halfHourFee', t('feeNormal')],
+ ['fastestFee', t('feePriority')],
+ ['custom', t('feeCustom')],
+ ];
+ const rate = bumpRate();
+ let pl = null;
+ try { pl = wallet.planBump(s.prep, rate); } catch {}
+ const newFee = pl && pl.ok ? pl.fee : null;
+ const planErr = pl && !pl.ok ? t('bumpInsufficient') : '';
+ return h(
+ 'div',
+ { class: 'card col' },
+ h('h3', {}, t('bumpConfirm')),
+ h('div', { class: 'summary col', style: 'gap:0' },
+ ...s.prep.recipients.map((r) =>
+ h('div', { class: 'line' },
+ h('span', { class: 'k mono break' }, r.address ? shortAddr(r.address, 14, 8) : '—'),
+ h('span', { class: 'v' }, fmtAmount(r.value), ' ', unitTag())
+ )
+ ),
+ h('div', { class: 'line' },
+ h('span', { class: 'k' }, t('networkFee')),
+ h('span', { class: 'v' }, fmtAmount(s.prep.oldFee) + ' → ' + (newFee != null ? fmtAmount(newFee) : '—') + ' ' + unitLabel())
+ )
+ ),
+ h('div', { class: 'field' },
+ h('span', { class: 'lab' }, t('feeRate')),
+ h('div', { class: 'seg', style: 'display:flex;width:100%' },
+ feeOpts.map(([k, label]) =>
+ h('button', {
+ type: 'button', class: (s.feeChoice === k ? 'active ' : '') + 'grow',
+ onClick: () => { s.feeChoice = k; if (k === 'custom' && !s.customFee) s.customFee = String(rate); render(); },
+ }, label)
+ )
+ ),
+ s.feeChoice === 'custom'
+ ? h('div', { class: 'input-group mt8' },
+ h('input', { type: 'number', min: '1', placeholder: 'sat/vB', value: s.customFee,
+ onInput: (e) => (s.customFee = e.target.value), onChange: () => render() }),
+ h('span', { class: 'small muted', style: 'align-self:center' }, 'sat/vB'))
+ : h('div', { class: 'small faint mt8' }, t('selectedRate', { n: rate }))
+ ),
+ (ui.sendError || planErr) && h('div', { class: 'notice err' }, ui.sendError || planErr),
+ h('div', { class: 'row gap6' },
+ h('button', { class: 'btn-ghost', onClick: () => { ui.bump = null; ui.sendError = ''; render(); } }, t('back')),
+ ui.busy
+ ? h('button', { class: 'btn-primary grow', disabled: true }, h('span', { class: 'spinner' }))
+ : h('button', { class: 'btn-primary grow', disabled: !newFee, onClick: doBump }, t('replaceTx'))
+ )
+ );
+}
+
+async function doBump() {
+ ui.busy = true;
+ ui.sendError = '';
+ render();
+ try {
+ const d = wallet.buildBump(ui.bump.prep, bumpRate());
+ const txid = await wallet.broadcast(d.hex);
+ ui.sendResult = { txid };
+ ui.bump = null;
+ ui.txDetail = null;
+ ui.tab = 'send';
+ await wallet.scan().catch(() => {});
+ } catch (e) {
+ ui.sendError = t('broadcastFailed', { msg: e.message });
+ }
+ ui.busy = false;
+ render();
+}
+
async function broadcastScanned() {
if (wallet.offline) { ui.sendError = t('scanOffline'); render(); return; }
ui.busy = true;
@@ -1424,6 +1501,7 @@ function sendResultView() {
// ---------------------------------------------------------------- History
function historyTab() {
+ if (ui.bump) return bumpView();
if (ui.txDetail) {
const tx = wallet.txs.find((x) => x.txid === ui.txDetail);
if (tx) return txDetailView(tx);
diff --git a/src/i18n.js b/src/i18n.js
index 396c0f7..10dc74d 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -113,6 +113,7 @@ const STR = {
bumpFee: 'Bump fee',
bumpConfirm: 'Replace with a higher fee?',
replaceTx: 'Replace',
+ bumpInsufficient: 'Not enough funds to bump at this rate. Try a lower rate.',
scanOffline: 'You’re offline — reconnect to broadcast this transaction.',
broadcastFailed: 'Broadcast failed: {msg}',
signingFailed: 'Signing failed: {msg}',
diff --git a/src/locales/ar.json b/src/locales/ar.json
index 11cfd61..54a60c0 100644
--- a/src/locales/ar.json
+++ b/src/locales/ar.json
@@ -144,5 +144,6 @@
"footerSourceOn": "الكود المصدري على",
"bumpFee": "رفع الرسوم",
"bumpConfirm": "استبدال برسوم أعلى؟",
- "replaceTx": "استبدال"
+ "replaceTx": "استبدال",
+ "bumpInsufficient": "لا توجد أموال كافية للرفع بهذا المعدل. جرّب معدلاً أقل."
} \ No newline at end of file
diff --git a/src/locales/bn.json b/src/locales/bn.json
index 43785aa..8e0f100 100644
--- a/src/locales/bn.json
+++ b/src/locales/bn.json
@@ -144,5 +144,6 @@
"footerSourceOn": "সোর্স কোড",
"bumpFee": "ফি বাড়ান",
"bumpConfirm": "বেশি ফি দিয়ে প্রতিস্থাপন করবেন?",
- "replaceTx": "প্রতিস্থাপন"
+ "replaceTx": "প্রতিস্থাপন",
+ "bumpInsufficient": "এই হারে বাড়ানোর মতো যথেষ্ট তহবিল নেই। কম হার চেষ্টা করুন।"
} \ No newline at end of file
diff --git a/src/locales/de.json b/src/locales/de.json
index 3a59f78..6ad55a8 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -144,5 +144,6 @@
"footerSourceOn": "Quellcode auf",
"bumpFee": "Gebühr erhöhen",
"bumpConfirm": "Mit höherer Gebühr ersetzen?",
- "replaceTx": "Ersetzen"
+ "replaceTx": "Ersetzen",
+ "bumpInsufficient": "Nicht genug Guthaben für diese Gebührenrate. Versuche eine niedrigere Rate."
} \ No newline at end of file
diff --git a/src/locales/es.json b/src/locales/es.json
index b9bddab..5d98fb3 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -144,5 +144,6 @@
"footerSourceOn": "código disponible en",
"bumpFee": "Aumentar comisión",
"bumpConfirm": "¿Reemplazar con una comisión más alta?",
- "replaceTx": "Reemplazar"
+ "replaceTx": "Reemplazar",
+ "bumpInsufficient": "Fondos insuficientes para aumentar a esta tasa. Prueba una tasa más baja."
} \ No newline at end of file
diff --git a/src/locales/fr.json b/src/locales/fr.json
index c09408d..fd7f325 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -144,5 +144,6 @@
"footerSourceOn": "code source sur",
"bumpFee": "Augmenter les frais",
"bumpConfirm": "Remplacer avec des frais plus élevés ?",
- "replaceTx": "Remplacer"
+ "replaceTx": "Remplacer",
+ "bumpInsufficient": "Fonds insuffisants pour augmenter à ce taux. Essayez un taux plus bas."
} \ No newline at end of file
diff --git a/src/locales/hi.json b/src/locales/hi.json
index fd0d997..dfda450 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -144,5 +144,6 @@
"footerSourceOn": "सोर्स यहाँ",
"bumpFee": "फ़ीस बढ़ाएँ",
"bumpConfirm": "अधिक फ़ीस के साथ बदलें?",
- "replaceTx": "बदलें"
+ "replaceTx": "बदलें",
+ "bumpInsufficient": "इस दर पर बढ़ाने के लिए पर्याप्त धन नहीं। कम दर आज़माएँ।"
} \ No newline at end of file
diff --git a/src/locales/id.json b/src/locales/id.json
index 4edc234..6a215a6 100644
--- a/src/locales/id.json
+++ b/src/locales/id.json
@@ -144,5 +144,6 @@
"footerSourceOn": "sumber tersedia di",
"bumpFee": "Naikkan biaya",
"bumpConfirm": "Ganti dengan biaya lebih tinggi?",
- "replaceTx": "Ganti"
+ "replaceTx": "Ganti",
+ "bumpInsufficient": "Dana tidak cukup untuk menaikkan pada tarif ini. Coba tarif lebih rendah."
} \ No newline at end of file
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 6bc532b..1c6aef6 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -144,5 +144,6 @@
"footerSourceOn": "ソースは",
"bumpFee": "手数料を上げる",
"bumpConfirm": "より高い手数料で置き換えますか?",
- "replaceTx": "置き換える"
+ "replaceTx": "置き換える",
+ "bumpInsufficient": "この手数料率に上げる資金が足りません。低い率をお試しください。"
} \ No newline at end of file
diff --git a/src/locales/ko.json b/src/locales/ko.json
index e46b7c7..13d83d4 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -144,5 +144,6 @@
"footerSourceOn": "소스 공개:",
"bumpFee": "수수료 올리기",
"bumpConfirm": "더 높은 수수료로 교체할까요?",
- "replaceTx": "교체"
+ "replaceTx": "교체",
+ "bumpInsufficient": "이 수수료율로 올릴 자금이 부족합니다. 더 낮은 율을 시도하세요."
} \ No newline at end of file
diff --git a/src/locales/mr.json b/src/locales/mr.json
index 36dfd56..6717d9d 100644
--- a/src/locales/mr.json
+++ b/src/locales/mr.json
@@ -144,5 +144,6 @@
"footerSourceOn": "सोर्स येथे",
"bumpFee": "फी वाढवा",
"bumpConfirm": "जास्त फीसह बदला?",
- "replaceTx": "बदला"
+ "replaceTx": "बदला",
+ "bumpInsufficient": "या दराने वाढवण्यासाठी पुरेसा निधी नाही. कमी दर वापरून पहा."
} \ No newline at end of file
diff --git a/src/locales/pt.json b/src/locales/pt.json
index 01e07eb..12d07fc 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -144,5 +144,6 @@
"footerSourceOn": "código disponível no",
"bumpFee": "Aumentar taxa",
"bumpConfirm": "Substituir com uma taxa maior?",
- "replaceTx": "Substituir"
+ "replaceTx": "Substituir",
+ "bumpInsufficient": "Fundos insuficientes para aumentar nessa taxa. Tente uma taxa menor."
} \ No newline at end of file
diff --git a/src/locales/ru.json b/src/locales/ru.json
index dad4ba4..4f00e77 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -144,5 +144,6 @@
"footerSourceOn": "исходный код на",
"bumpFee": "Поднять комиссию",
"bumpConfirm": "Заменить с более высокой комиссией?",
- "replaceTx": "Заменить"
+ "replaceTx": "Заменить",
+ "bumpInsufficient": "Недостаточно средств для повышения по этой ставке. Попробуйте ниже."
} \ No newline at end of file
diff --git a/src/locales/sw.json b/src/locales/sw.json
index 6e5c0bc..73a2c37 100644
--- a/src/locales/sw.json
+++ b/src/locales/sw.json
@@ -144,5 +144,6 @@
"footerSourceOn": "msimbo unapatikana",
"bumpFee": "Ongeza ada",
"bumpConfirm": "Badilisha kwa ada ya juu zaidi?",
- "replaceTx": "Badilisha"
+ "replaceTx": "Badilisha",
+ "bumpInsufficient": "Fedha hazitoshi kuongeza kwa kiwango hiki. Jaribu kiwango cha chini."
} \ No newline at end of file
diff --git a/src/locales/ta.json b/src/locales/ta.json
index dadd598..2e0102b 100644
--- a/src/locales/ta.json
+++ b/src/locales/ta.json
@@ -144,5 +144,6 @@
"footerSourceOn": "மூலம்",
"bumpFee": "கட்டணத்தை உயர்த்து",
"bumpConfirm": "அதிக கட்டணத்துடன் மாற்றவா?",
- "replaceTx": "மாற்று"
+ "replaceTx": "மாற்று",
+ "bumpInsufficient": "இந்த கட்டண விகிதத்தில் உயர்த்த போதிய நிதி இல்லை. குறைந்த விகிதத்தை முயற்சிக்கவும்."
} \ No newline at end of file
diff --git a/src/locales/te.json b/src/locales/te.json
index 1380942..05790a0 100644
--- a/src/locales/te.json
+++ b/src/locales/te.json
@@ -144,5 +144,6 @@
"footerSourceOn": "సోర్స్ ఇక్కడ",
"bumpFee": "ఫీజు పెంచు",
"bumpConfirm": "ఎక్కువ ఫీజుతో భర్తీ చేయాలా?",
- "replaceTx": "భర్తీ చేయి"
+ "replaceTx": "భర్తీ చేయి",
+ "bumpInsufficient": "ఈ రేటుకు పెంచడానికి సరిపడా నిధులు లేవు. తక్కువ రేటు ప్రయత్నించండి."
} \ No newline at end of file
diff --git a/src/locales/tr.json b/src/locales/tr.json
index adb8d36..df74fbb 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -144,5 +144,6 @@
"footerSourceOn": "kaynak kodu",
"bumpFee": "Ücreti artır",
"bumpConfirm": "Daha yüksek ücretle değiştirilsin mi?",
- "replaceTx": "Değiştir"
+ "replaceTx": "Değiştir",
+ "bumpInsufficient": "Bu oranda artırmak için yeterli bakiye yok. Daha düşük bir oran deneyin."
} \ No newline at end of file
diff --git a/src/locales/ur.json b/src/locales/ur.json
index bb5c028..e08e4d3 100644
--- a/src/locales/ur.json
+++ b/src/locales/ur.json
@@ -144,5 +144,6 @@
"footerSourceOn": "سورس کوڈ یہاں",
"bumpFee": "فیس بڑھائیں",
"bumpConfirm": "زیادہ فیس کے ساتھ بدلیں؟",
- "replaceTx": "بدلیں"
+ "replaceTx": "بدلیں",
+ "bumpInsufficient": "اس ریٹ پر بڑھانے کے لیے کافی رقم نہیں۔ کم ریٹ آزمائیں۔"
} \ No newline at end of file
diff --git a/src/locales/vi.json b/src/locales/vi.json
index 2c17f7d..39b2b10 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -144,5 +144,6 @@
"footerSourceOn": "mã nguồn tại",
"bumpFee": "Tăng phí",
"bumpConfirm": "Thay thế với phí cao hơn?",
- "replaceTx": "Thay thế"
+ "replaceTx": "Thay thế",
+ "bumpInsufficient": "Không đủ tiền để tăng ở mức phí này. Hãy thử mức thấp hơn."
} \ No newline at end of file
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 0ac43c2..50958a8 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -144,5 +144,6 @@
"footerSourceOn": "源代码见",
"bumpFee": "提高手续费",
"bumpConfirm": "用更高手续费替换?",
- "replaceTx": "替换"
+ "replaceTx": "替换",
+ "bumpInsufficient": "资金不足,无法以此费率提升。请尝试更低的费率。"
} \ No newline at end of file
diff --git a/src/wallet.js b/src/wallet.js
index a107fdc..edacc05 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -668,15 +668,15 @@ export class Wallet {
return { txid: tx.id, total, outputs };
}
- // Build + sign an RBF replacement of an unconfirmed outgoing tx at a higher
- // fee rate. Reuses the original inputs and recipient outputs, taking the extra
- // fee from the change output (pulling in a confirmed coin if change can't
- // cover it). Returns a signed draft { hex, txid, fee, oldFee, feeRate, outputs }.
- async bumpTx(origTxid, feeRate) {
+ // RBF fee bumping, in three steps so the UI can preview a fee per rate before
+ // building: prepareBump() fetches + reconstructs the original (async), then
+ // planBump()/buildBump() are pure and reuse that prep.
+
+ // Fetch an unconfirmed outgoing tx and reconstruct what's needed to replace it.
+ async prepareBump(origTxid) {
const orig = await this.api.getTx(origTxid);
if (orig.status && orig.status.confirmed) throw new Error('Transaction already confirmed.');
- // Every input of our own send is ours; reconstruct them with values + paths.
const ins = [];
let totalIn = 0;
for (const vin of orig.vin || []) {
@@ -687,8 +687,8 @@ export class Wallet {
ins.push({ txid: vin.txid, vout: vin.vout, value: vin.prevout.value, chain: p.chain, index: p.index });
}
- // Split outputs: our first change-chain output is the change we'll shrink;
- // everything else is a recipient and stays fixed.
+ // Our first change-chain output is the change we'll shrink; the rest are
+ // recipients and stay fixed.
const recipients = [];
let outTotal = 0;
let changeSeen = false;
@@ -698,50 +698,64 @@ export class Wallet {
if (p && p.chain === 1 && !changeSeen) { changeSeen = true; continue; }
recipients.push({ address: o.scriptpubkey_address, value: o.value });
}
- const recipTotal = recipients.reduce((s, r) => s + r.value, 0);
- const oldFee = totalIn - outTotal;
+ const usedIds = new Set(ins.map((i) => `${i.txid}:${i.vout}`));
+ const spare = this.utxos.filter((u) => u.confirmed && !usedIds.has(utxoId(u)));
+ return {
+ txid: origTxid,
+ ins,
+ recipients,
+ totalIn,
+ recipTotal: recipients.reduce((s, r) => s + r.value, 0),
+ oldFee: totalIn - outTotal,
+ spare,
+ };
+ }
+ // Pure: pick the fee/change (and any extra inputs) for a given rate.
+ planBump(prep, feeRate) {
const rate = Math.max(1, Math.round(feeRate));
const DUST = 294; // p2wpkh dust
const vsizeOf = (nIn, nOut) => 11 + 68 * nIn + 31 * nOut;
- const usedIds = new Set(ins.map((i) => `${i.txid}:${i.vout}`));
- const spare = this.utxos.filter((u) => u.confirmed && !usedIds.has(utxoId(u)));
+ const spare = prep.spare.slice();
const extra = [];
-
- const plan = () => {
- const nIn = ins.length + extra.length;
- const inAmt = totalIn + extra.reduce((s, u) => s + u.value, 0);
- let nOut = recipients.length + 1;
+ const compute = () => {
+ const nIn = prep.ins.length + extra.length;
+ const inAmt = prep.totalIn + extra.reduce((s, u) => s + u.value, 0);
+ let nOut = prep.recipients.length + 1;
let fee = Math.ceil(vsizeOf(nIn, nOut) * rate);
- let change = inAmt - recipTotal - fee;
- if (change < DUST) { nOut = recipients.length; fee = inAmt - recipTotal; change = 0; }
- // BIP125: beat old fee by at least the replacement's vsize (incremental relay).
- const minFee = oldFee + vsizeOf(nIn, nOut);
+ let change = inAmt - prep.recipTotal - fee;
+ if (change < DUST) { nOut = prep.recipients.length; fee = inAmt - prep.recipTotal; change = 0; }
+ const minFee = prep.oldFee + vsizeOf(nIn, nOut); // BIP125 incremental relay
if (change > 0 && fee < minFee) {
- fee = minFee; change = inAmt - recipTotal - fee;
- if (change < DUST) { fee = inAmt - recipTotal; change = 0; }
+ fee = minFee; change = inAmt - prep.recipTotal - fee;
+ if (change < DUST) { fee = inAmt - prep.recipTotal; change = 0; }
}
- return { inAmt, fee, change, ok: inAmt - recipTotal >= fee && fee > oldFee };
+ return { fee, change, ok: inAmt - prep.recipTotal >= fee && fee > prep.oldFee };
};
- let pl = plan();
- while (!pl.ok && spare.length) { extra.push(spare.shift()); pl = plan(); }
- if (!pl.ok) throw new Error('Not enough funds to bump at this rate. Try a lower rate or CPFP.');
+ let pl = compute();
+ while (!pl.ok && spare.length) { extra.push(spare.shift()); pl = compute(); }
+ return { fee: pl.fee, change: pl.change, extra, ok: pl.ok, rate };
+ }
- const allIns = [...ins, ...extra.map((u) => ({ txid: u.txid, vout: u.vout, value: u.value, chain: u.chain, index: u.index }))];
+ // Build + sign the replacement at a rate. Returns { hex, txid, fee, oldFee, ... }.
+ buildBump(prep, feeRate) {
+ const pl = this.planBump(prep, feeRate);
+ if (!pl.ok) throw new Error('Not enough funds to bump at this rate. Try a lower rate or CPFP.');
+ const allIns = [...prep.ins, ...pl.extra.map((u) => ({ txid: u.txid, vout: u.vout, value: u.value, chain: u.chain, index: u.index }))];
const t = new btc.Transaction();
for (const i of allIns) {
const pay = p2wpkh(this.derive(i.chain, i.index).pubkey, this.netCfg.net);
t.addInput({ ...pay, txid: i.txid, index: i.vout, sequence: 0xfffffffd, witnessUtxo: { script: pay.script, amount: BigInt(i.value) } });
}
- for (const r of recipients) t.addOutputAddress(r.address, BigInt(r.value), this.netCfg.net);
+ for (const r of prep.recipients) t.addOutputAddress(r.address, BigInt(r.value), this.netCfg.net);
const changeAddr = this.freshChange().address;
if (pl.change > 0) t.addOutputAddress(changeAddr, BigInt(pl.change), this.netCfg.net);
for (let k = 0; k < allIns.length; k++) t.signIdx(this.node(allIns[k].chain, allIns[k].index).privateKey, k);
t.finalize();
- const outputs = recipients.map((r) => ({ address: r.address, value: r.value }));
+ const outputs = prep.recipients.map((r) => ({ address: r.address, value: r.value }));
if (pl.change > 0) outputs.push({ address: changeAddr, value: pl.change, change: true });
- return { hex: t.hex, txid: t.id, fee: pl.fee, oldFee, feeRate: rate, outputs, replaces: origTxid };
+ return { hex: t.hex, txid: t.id, fee: pl.fee, oldFee: prep.oldFee, feeRate: pl.rate, outputs, replaces: prep.txid };
}
// --- realtime (mempool.space WebSocket) ---------------------------------