aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app.js12
-rw-r--r--src/features/ark.js4
-rw-r--r--src/features/gifts.js2
-rw-r--r--src/features/zaps.js8
-rw-r--r--tools/amount-field-unit-test.js50
5 files changed, 68 insertions, 8 deletions
diff --git a/src/app.js b/src/app.js
index 1e77849..122ccfe 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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); }