diff options
| author | Adam Soltys <[email protected]> | 2026-06-24 09:15:45 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-24 09:15:45 -0700 |
| commit | 85eca26f9141d339c2e58a1022d49e4007eb638f (patch) | |
| tree | 755e852934528e0c8fb114545f6b39a803ce6c90 /src/app.js | |
| parent | 17acf5ec80a0c0b0acc44a6a8373e154c68d4748 (diff) | |
| download | coinosv3-85eca26f9141d339c2e58a1022d49e4007eb638f.tar.gz coinosv3-85eca26f9141d339c2e58a1022d49e4007eb638f.zip | |
Add RBF fee bumping for unconfirmed sends
- Signal opt-in RBF (BIP125, sequence 0xfffffffd) on all new sends so
they're replaceable.
- "Bump fee" on an unconfirmed sent tx (its detail view): rebuilds the
same inputs + recipient outputs at the Priority rate, taking the extra
fee from the change output (pulling in a confirmed coin if change can't
cover it), re-signs, and routes to a "Replace with a higher fee?"
confirmation showing old → new fee before broadcasting.
- New keys translated into all 19 locales.
Honest scope: P2A/TRUC/package-relay are L2/pre-signed-tx + node-relay
features that don't apply to a single-sig wallet broadcasting single txs
via public explorers, so they're intentionally out. CPFP (for receives /
unbumpable sends) is a possible follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 35 |
1 files changed, 33 insertions, 2 deletions
@@ -1008,7 +1008,7 @@ function broadcastConfirmView() { return h( 'div', { class: 'card col' }, - h('h3', {}, t('broadcastScanned')), + h('h3', {}, b.bump ? t('bumpConfirm') : t('broadcastScanned')), h( 'div', { class: 'summary col', style: 'gap:0' }, @@ -1018,6 +1018,11 @@ 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)) @@ -1028,11 +1033,31 @@ 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 }, t('broadcastNow')) + : h('button', { class: 'btn-primary grow', onClick: broadcastScanned }, b.bump ? t('replaceTx') : t('broadcastNow')) ) ); } +// Build a higher-fee RBF replacement of an unconfirmed send and route it to the +// broadcast confirmation. Bumps to the Priority rate. +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'; + ui.sendError = ''; + } catch (e) { + toast(e.message); + } + ui.busy = false; + render(); +} + async function broadcastScanned() { if (wallet.offline) { ui.sendError = t('scanOffline'); render(); return; } ui.busy = true; @@ -1480,6 +1505,12 @@ function txDetailView(tx) { copyBtn(tx.txid, t('copyId')), h('a', { class: 'btn btn-sm', href: wallet.api.explorerTx(tx.txid), target: '_blank', rel: 'noopener' }, t('viewOnMempool')) ), + // RBF: an unconfirmed send can be rebroadcast at a higher fee. + !tx.confirmed && !incoming && !wallet.offline + ? (ui.busy + ? h('button', { class: 'btn-primary btn-block', disabled: true }, h('span', { class: 'spinner' })) + : h('button', { class: 'btn-primary btn-block', onClick: () => bumpFee(tx.txid) }, t('bumpFee'))) + : null, h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.txDetail = null; render(); } }, t('backToHistory')) ); } |
