diff options
| -rw-r--r-- | src/app.js | 13 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | src/wallet.js | 20 |
3 files changed, 21 insertions, 13 deletions
@@ -1288,8 +1288,12 @@ function claimScreen() { ); } const pv = previewGift(ui.claimCode); - const estFee = Math.ceil((11 + 68 + 31 * 2) * ((wallet.feeRates && wallet.feeRates.halfHourFee) || 5)); - const approx = pv ? Math.max(0, pv.room - estFee) : 0; + // The headline is the full amount received (inputs minus the sender's change). + // The network fee is determined now, at claim time, and comes out of that + // amount โ we surface the estimate on the Claim button so it isn't a surprise. + const rate = Math.max(1, Math.round((wallet.feeRates && wallet.feeRates.halfHourFee) || 5)); + const estFee = pv ? Math.ceil((11 + 68 * pv.inputs + 31 * 2) * rate) : 0; + const total = pv ? pv.room : 0; return h( 'div', { class: 'col', style: 'gap:16px' }, @@ -1298,14 +1302,15 @@ function claimScreen() { h('div', { class: 'check-badge', style: 'background:var(--accent)' }, '๐'), h('h2', { style: 'margin:0' }, t('giftWelcome')), h('div', { class: 'amt', style: 'font-size:30px' }, - h('span', { class: 'amount-pos' }, '~' + fmtAmount(approx)), ' ', unitTag('unit') + h('span', { class: 'amount-pos' }, fmtAmount(total)), ' ', unitTag('unit') ), h('p', { class: 'muted', style: 'margin:0' }, t('claimBody')) ), ui.claimError && h('div', { class: 'notice err' }, ui.claimError), ui.busy ? h('button', { class: 'btn-primary btn-block', disabled: true }, h('span', { class: 'spinner' })) - : h('button', { class: 'btn-primary btn-block', onClick: doClaim }, t('claimBtn')) + : h('button', { class: 'btn-primary btn-block', onClick: doClaim }, + t('claimBtn') + ' ยท ' + t('claimFeeNote', { n: fmtAmount(estFee) + ' ' + unitLabel() })) ); } diff --git a/src/i18n.js b/src/i18n.js index e427733..42fd34b 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -206,6 +206,7 @@ const STR = { copyLink: 'Copy link', claimBody: 'Claim it into a brand-new wallet that only you control.', claimBtn: 'Claim it', + claimFeeNote: '~{n} fee', claimFailed: 'This gift has already been claimed or is no longer available.', claimedTitle: 'Claimed!', claimedBody: 'Your Bitcoin is on its way. Back up your recovery phrase to keep it safe.', diff --git a/src/wallet.js b/src/wallet.js index 194b9dc..72d33be 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -878,10 +878,10 @@ export class Wallet { if (gift < BigInt(giftMinimum(rate))) throw new Error('Gift amount is too small.'); // Select confirmed coins (largest first, to minimize inputs) until they - // cover the gift + the claimer's fee (which grows with input count) + a dust - // change. The change output goes back to us and is protected; the rest is - // the claimer's to direct. - const claimFeeFor = (n) => BigInt(Math.ceil((11 + 68 * n + 31 * 2) * rate)); + // cover the gift plus a dust change. The change output goes back to us and + // is protected; the rest โ the full gift amount โ is the claimer's to + // direct. No fee is reserved here: the claimer's wallet looks up the fee + // rate at claim time and subtracts it from this amount. const pool = this.utxos .filter((u) => u.confirmed && !this.isReserved(utxoId(u))) .sort((a, b) => b.value - a.value); @@ -890,12 +890,11 @@ export class Wallet { for (const u of pool) { sel.push(u); sum += BigInt(u.value); - if (sum >= gift + claimFeeFor(sel.length) + DUST) break; + if (sum >= gift + DUST) break; } - const claimFee = claimFeeFor(sel.length); - if (sum < gift + claimFee + DUST) throw new Error('Not enough confirmed funds for that gift amount.'); + if (sum < gift + DUST) throw new Error('Not enough confirmed funds for that gift amount.'); - const change = sum - gift - claimFee; // back to us, >= DUST by construction + const change = sum - gift; // back to us, >= DUST by construction const t = new btc.Transaction({ allowUnknownOutputs: true }); // First input commits the change output (SIGHASH_SINGLE on output 0); the // rest commit only the inputs (SIGHASH_NONE). Together: inputs and our @@ -1423,7 +1422,10 @@ function _sumInputs(tx) { export function previewGift(code) { try { const tx = btc.Transaction.fromPSBT(base64urlnopad.decode(code)); - return { room: Number(_sumInputs(tx) - tx.getOutput(0).amount) }; // room minus the claimer fee + // room is the full amount the claimer receives (inputs minus our change); + // the claim fee is subtracted from it at claim time, so report inputs too + // so the caller can size that fee for this PSBT. + return { room: Number(_sumInputs(tx) - tx.getOutput(0).amount), inputs: tx.inputsLength }; } catch { return null; } |
