diff options
| -rw-r--r-- | src/app.js | 12 | ||||
| -rw-r--r-- | src/features/ark.js | 4 | ||||
| -rw-r--r-- | src/features/gifts.js | 2 | ||||
| -rw-r--r-- | src/features/zaps.js | 8 | ||||
| -rw-r--r-- | tools/amount-field-unit-test.js | 50 |
5 files changed, 68 insertions, 8 deletions
@@ -1151,6 +1151,16 @@ function toggleUnit() { render(); } +// A sats figure as the amount field types it in the current unit — what a +// Max button or a fixed-amount invoice fills in. Fiat is floored to the +// cent so a filled-in maximum never parses back to more than was offered. +function amountField(sat) { + if (unit === 'sats') return String(sat); + const r = unit === 'fiat' && rateNow(); + if (r) return (Math.floor((sat / 1e8) * r * 100) / 100).toFixed(2); + return (sat / 1e8).toFixed(8); +} + const unitLabel = () => (unit === 'sats' ? 'sats' : unit === 'fiat' ? getCurrency() : 'BTC'); // "Worth X then · Y now" for a payment's detail page — the one place both @@ -5951,7 +5961,7 @@ const ctx = { openWalletSettings: (view) => openWalletSettings(view), // one page of the Settings hub, from anywhere (Notifications' gear) openSettingsPage: (page) => { clearFeatureNav(); ui.screen = 'wallet'; ui.tab = 'settings'; ui.settingsPage = page; render(); try { window.scrollTo(0, 0); } catch {} }, - fmtAmount, unitLabel, unitTag, getUnit: () => unit, toggleUnit, download, + fmtAmount, unitLabel, unitTag, getUnit: () => unit, toggleUnit, download, amountField, // in fiat the typed figure is money, so it needs today's price to become sats parseAmount: (v, u) => parseAmount(v, u, rateNow()), worthLine, diff --git a/src/features/ark.js b/src/features/ark.js index 90b2a01..d5b6e7f 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1223,7 +1223,7 @@ export function arkFeature(ctx) { } return Math.max(0, amt); } - const amountFill = (sat) => (ctx.getUnit() === 'sats' ? String(sat) : (sat / 1e8).toFixed(8)); + const amountFill = (sat) => ctx.amountField(sat); // Auto-pay progress + success as ONE structurally identical card, so the // paying → paid step is a class flip (spinner ring settles into the green @@ -3651,7 +3651,7 @@ export function arkFeature(ctx) { ? (arkBalance()?.spendableSat || 0) : maxBoardSat(); if (!sat) return null; - return ctx.getUnit() === 'sats' ? String(sat) : (sat / 1e8).toFixed(8); + return ctx.amountField(sat); }, sendFormNote(a) { if (!isArkAddress(a)) return null; diff --git a/src/features/gifts.js b/src/features/gifts.js index 052a107..4d68a50 100644 --- a/src/features/gifts.js +++ b/src/features/gifts.js @@ -675,7 +675,7 @@ export function giftsFeature(ctx) { return h('div', { class: 'input-group' }, h('input', { type: 'number', step: getUnit() === 'sats' ? '1' : '0.00000001', min: '0', inputmode: 'decimal', placeholder: t('giftAmountLabel'), disabled: ui.giftMax, - value: ui.giftMax ? (getUnit() === 'sats' ? String(maxVal) : fmtBtc(maxVal)) : ui.giftAmount, + value: ui.giftMax ? ctx.amountField(maxVal) : ui.giftAmount, onInput: (e) => (ui.giftAmount = e.target.value) }), h('button', { type: 'button', class: ui.giftMax ? 'btn-primary' : '', onClick: () => { ui.giftMax = !ui.giftMax; ui.giftError = ''; render(); } }, t('max')), h('div', { style: 'display:flex;align-items:center' }, unitTag()) diff --git a/src/features/zaps.js b/src/features/zaps.js index a23ecdd..d327f9d 100644 --- a/src/features/zaps.js +++ b/src/features/zaps.js @@ -94,7 +94,7 @@ export function zapsFeature(ctx) { const off = target.offer; if (off.currency) throw new Error(t('bolt12Currency', { currency: off.currency })); const fixedSat = off.amountMsat ? Number((off.amountMsat + 999n) / 1000n) : null; - if (fixedSat) z.amount = getUnit() === 'sats' ? String(fixedSat) : (fixedSat / 1e8).toFixed(8); + if (fixedSat) z.amount = ctx.amountField(fixedSat); z.params = { minSendable: 1000, maxSendable: 21e15, commentAllowed: 0, allowsNostr: false, fixedSat }; z.status = 'ready'; render(); @@ -118,7 +118,7 @@ export function zapsFeature(ctx) { return; } const fixedSat = n.priceType === 0 && n.priceSats ? n.priceSats : null; - if (fixedSat) z.amount = getUnit() === 'sats' ? String(fixedSat) : (fixedSat / 1e8).toFixed(8); + if (fixedSat) z.amount = ctx.amountField(fixedSat); z.params = { minSendable: 1000, maxSendable: 21e15, commentAllowed: 100, allowsNostr: false, fixedSat }; z.status = 'ready'; render(); @@ -165,7 +165,7 @@ export function zapsFeature(ctx) { // exactly like a fixed-amount BOLT 12 offer. if (params.minSendable === params.maxSendable && params.minSendable >= 1000) { params.fixedSat = Math.ceil(params.minSendable / 1000); - z.amount = getUnit() === 'sats' ? String(params.fixedSat) : (params.fixedSat / 1e8).toFixed(8); + z.amount = ctx.amountField(params.fixedSat); } z.params = params; z.status = 'ready'; @@ -363,7 +363,7 @@ export function zapsFeature(ctx) { !p.fixedSat && (hook('lnMaxSendSat') || 0) > 0 ? h('button', { type: 'button', onClick: () => { const sat = hook('lnMaxSendSat'); - z.amount = getUnit() === 'sats' ? String(sat) : (sat / 1e8).toFixed(8); + z.amount = ctx.amountField(sat); render(); } }, t('max')) : null, diff --git a/tools/amount-field-unit-test.js b/tools/amount-field-unit-test.js new file mode 100644 index 0000000..26c5357 --- /dev/null +++ b/tools/amount-field-unit-test.js @@ -0,0 +1,50 @@ +// Max (and fixed-amount invoices) fill the amount field in the unit it is +// showing: in fiat that is money to the cent, floored so a filled-in maximum +// never parses back to more sats than were offered — not a BTC figure under a +// CAD label (2026-10-08: Max on an ark recipient filled 0.00004978 'CAD'). +// Run: bun tools/amount-field-unit-test.js +import assert from 'node:assert/strict'; +import puppeteer from './puppeteer.js'; + +const bundle = await Bun.build({ entrypoints: ['src/app.js'], target: 'browser', minify: true, + plugins: [{ name: 'amount-field-test', setup(build) { + build.onLoad({ filter: /src\/app\.js$/ }, async ({ path }) => ({ loader: 'js', contents: await Bun.file(path).text() + ` +window.amtApp = { ctx, toggleUnit, get unit() { return unit; } }; +` })); + build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; }' })); + } }] }); +assert(bundle.success, bundle.logs.join('\n')); +const html = `<!doctype html><meta charset="utf-8"><div id="app"></div><script type="module">${await bundle.outputs[0].text()}</script>`; +const server = Bun.serve({ port: 0, fetch: (req) => new URL(req.url).pathname === '/' + ? new Response(html, { headers: { 'Content-Type': 'text/html' } }) : new Response(null, { status: 404 }) }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] }); +try { + const page = await browser.newPage(), errors = []; + page.on('pageerror', (e) => errors.push(e.message)); + await page.setRequestInterception(true); + page.on('request', (r) => r.url().startsWith(server.url.origin) ? r.continue() : r.abort()); + await page.evaluateOnNewDocument(() => { + localStorage.setItem('btc-wallet-network', 'regtest'); + localStorage.setItem('btc-wallet-unit', 'fiat'); + localStorage.setItem('btc-wallet-currency', 'CAD'); + localStorage.setItem('btc-wallet-rates', JSON.stringify({ CAD: 150000, USD: 110000 })); + }); + await page.goto(server.url.href, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => window.amtApp); + const r = await page.evaluate(() => { + const { ctx } = amtApp; + const out = { unit: amtApp.unit, fiat: ctx.amountField(62212), back: ctx.parseAmount(ctx.amountField(62212), 'fiat') }; + while (amtApp.unit !== 'sats') amtApp.toggleUnit(); + out.sats = ctx.amountField(62212); + amtApp.toggleUnit(); out.btc = ctx.amountField(62212); + return out; + }); + console.log(r); + assert.equal(r.unit, 'fiat'); + assert.equal(r.fiat, '93.31', '62,212 sats at 150,000 CAD/BTC = 93.318 → floored to 93.31'); + assert(r.back <= 62212 && r.back > 62200, 'parses back to at most the maximum'); + assert.equal(r.sats, '62212'); + assert.equal(r.btc, '0.00062212'); + assert.deepEqual(errors, []); + console.log('✅ amount fills follow the display unit'); +} finally { await browser.close(); server.stop(true); } |
