diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 106 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | src/locales/ar.json | 3 | ||||
| -rw-r--r-- | src/locales/bn.json | 3 | ||||
| -rw-r--r-- | src/locales/de.json | 3 | ||||
| -rw-r--r-- | src/locales/es.json | 3 | ||||
| -rw-r--r-- | src/locales/fr.json | 3 | ||||
| -rw-r--r-- | src/locales/hi.json | 3 | ||||
| -rw-r--r-- | src/locales/id.json | 3 | ||||
| -rw-r--r-- | src/locales/ja.json | 3 | ||||
| -rw-r--r-- | src/locales/ko.json | 3 | ||||
| -rw-r--r-- | src/locales/mr.json | 3 | ||||
| -rw-r--r-- | src/locales/pt.json | 3 | ||||
| -rw-r--r-- | src/locales/ru.json | 3 | ||||
| -rw-r--r-- | src/locales/sw.json | 3 | ||||
| -rw-r--r-- | src/locales/ta.json | 3 | ||||
| -rw-r--r-- | src/locales/te.json | 3 | ||||
| -rw-r--r-- | src/locales/tr.json | 3 | ||||
| -rw-r--r-- | src/locales/ur.json | 3 | ||||
| -rw-r--r-- | src/locales/vi.json | 3 | ||||
| -rw-r--r-- | src/locales/zh.json | 3 | ||||
| -rw-r--r-- | src/wallet.js | 76 |
22 files changed, 176 insertions, 64 deletions
@@ -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) --------------------------------- |
