aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-24 14:12:46 -0700
committerAdam Soltys <[email protected]>2026-06-24 14:12:46 -0700
commit3463af1d81c2712d657bbf30ecba98ffe5873729 (patch)
tree897aa78d3aa3f9227b6a3de00132ded16607033a /src/app.js
parent1a92d81b792f9e6d2ecb284aab979985a36d34c7 (diff)
downloadcoinosv3-3463af1d81c2712d657bbf30ecba98ffe5873729.tar.gz
coinosv3-3463af1d81c2712d657bbf30ecba98ffe5873729.zip
Fee-aware gift minimum; move gift entrypoint to Send page
Minimum: the flat 546-sat floor is replaced by a fee-aware minimum (dust + one claim fee of headroom, floored at 546) via giftMinimum(), so the recipient clears dust comfortably even if fees rise before they claim — e.g. ~1000 sats at 5 sat/vB, ~7300 at 50. The create form shows the live minimum and rejects below it; buildClaimTx keeps the hard 294-sat dust guard as a final backstop (claim fails rather than handing over dust). UI: the gift entrypoint moves from Settings to a "Gift some Bitcoin" link on the Send page, opening the gift view as a send sub-view with a back button. New strings translated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js33
1 files changed, 26 insertions, 7 deletions
diff --git a/src/app.js b/src/app.js
index 07fe347..98b5d3a 100644
--- a/src/app.js
+++ b/src/app.js
@@ -4,7 +4,7 @@
// which rebuilds the active screen. Text inputs write back into `ui` on `input`
// (without re-rendering) so their values survive structural re-renders.
-import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, buildClaimTx } from './wallet.js';
+import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, buildClaimTx, giftMinimum } from './wallet.js';
import { qrSvg } from './qr.js';
import { scanQr } from './scan.js';
import { getSyncConfig, setSyncConfig } from './nostr.js';
@@ -35,7 +35,8 @@ const ui = {
passphrase: '',
showPass: false,
revealShown: false, // recovery phrase unmasked on the Backup tab (after the warning)
- giftAmount: '', // gift-create amount input (Settings)
+ giftMode: false, // gift sub-view active on the Send page
+ giftAmount: '', // gift-create amount input
giftCode: null, // last-created gift PSBT code
giftError: '',
revokeId: null, // outpoint of a gift being revoked (confirm state)
@@ -612,6 +613,7 @@ function lock() {
ui.passphrase = '';
ui.confirm = [];
ui.revealShown = false;
+ ui.giftMode = false;
ui.giftAmount = '';
ui.giftCode = null;
ui.giftError = '';
@@ -693,7 +695,6 @@ function settingsTab() {
wallet.scanning ? t('scanning') : t('rescanWallet'))
),
explorerCard(),
- giftCard(),
syncCard(),
h(
'div',
@@ -709,10 +710,14 @@ function settingsTab() {
function giftUrl() {
return `${location.origin}/#gift=${ui.giftCode}`;
}
+function giftRate() {
+ return (wallet.feeRates && wallet.feeRates.halfHourFee) || 5;
+}
function createGiftLink() {
+ const rate = giftRate();
+ const min = giftMinimum(rate);
const amt = Math.round(Number(ui.giftAmount));
- if (!amt || amt < 546) { ui.giftError = t('giftAmountInvalid'); render(); return; }
- const rate = (wallet.feeRates && wallet.feeRates.halfHourFee) || 5;
+ if (!amt || amt < min) { ui.giftError = t('giftAmountInvalid', { n: fmtAmount(min) + ' ' + unitLabel() }); render(); return; }
try {
ui.giftCode = wallet.createGift(amt, rate).code;
ui.giftError = '';
@@ -740,10 +745,11 @@ function giftCard() {
)
: h('div', { class: 'col gap6' },
h('div', { class: 'input-group' },
- h('input', { type: 'number', min: '546', inputmode: 'numeric', placeholder: t('giftAmountLabel'),
+ h('input', { type: 'number', min: String(giftMinimum(giftRate())), inputmode: 'numeric', placeholder: t('giftAmountLabel'),
value: ui.giftAmount, onInput: (e) => (ui.giftAmount = e.target.value) }),
h('span', { class: 'small muted', style: 'align-self:center' }, unitLabel())
),
+ h('div', { class: 'small faint' }, t('giftMinNote', { n: fmtAmount(giftMinimum(giftRate())) + ' ' + unitLabel() })),
ui.giftError && h('div', { class: 'notice err' }, ui.giftError),
h('button', { class: 'btn-block', onClick: createGiftLink }, t('giftLinkReveal'))
),
@@ -1073,6 +1079,7 @@ function tabsBar() {
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;
+ ui.giftMode = false;
render();
})
)
@@ -1130,9 +1137,20 @@ function sendTab() {
if (ui.sendResult) return sendResultView();
if (ui.broadcastTx) return broadcastConfirmView();
if (ui.draft) return reviewView();
+ if (ui.giftMode) return giftView();
return sendForm();
}
+// The gift UI as a send-page sub-view (entered from a link on the send form).
+function giftView() {
+ return h(
+ 'div',
+ { class: 'col', style: 'gap:12px' },
+ giftCard(),
+ h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.giftMode = false; ui.giftCode = null; ui.giftError = ''; ui.revokeId = null; render(); } }, t('back'))
+ );
+}
+
// QR scanning is only possible in a secure context with a camera.
const canScan = () =>
typeof navigator !== 'undefined' && !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
@@ -1480,7 +1498,8 @@ function sendForm() {
),
coinControl(),
ui.sendError && h('div', { class: 'notice err' }, ui.sendError),
- h('button', { class: 'btn-primary btn-block', onClick: reviewSend }, t('reviewTx'))
+ h('button', { class: 'btn-primary btn-block', onClick: reviewSend }, t('reviewTx')),
+ h('button', { class: 'linklike small', style: 'align-self:center;margin-top:2px', onClick: () => { ui.giftMode = true; ui.sendError = ''; render(); } }, t('giftLink'))
);
}