aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js88
1 files changed, 76 insertions, 12 deletions
diff --git a/src/app.js b/src/app.js
index 1c8f2f8..6b481a5 100644
--- a/src/app.js
+++ b/src/app.js
@@ -15,6 +15,7 @@ import { dataSources, getSource, setSource, getNetwork, setNetwork, NETWORKS } f
import { STAGING } from './build-flags.js';
import { buildFeatures, loadDeferredFeatures } from './features/index.js';
import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js';
+import { rateNow, rateAt, refreshRates, haveRate, getCurrency, setCurrency, currencies, fmtFiat } from './rates.js';
import {
fmtBtc,
fmtSats,
@@ -790,24 +791,56 @@ function pasteInto(ta, apply) {
// logouts. Every unit label on the site is clickable to toggle it.
const UNIT_KEY = 'btc-wallet-unit';
let unit = (() => {
- // Default to sats for first-time users; only an explicit 'btc' choice sticks.
+ // Default to sats for first-time users; only an explicit choice sticks.
try {
- return localStorage.getItem(UNIT_KEY) === 'btc' ? 'btc' : 'sats';
+ const v = localStorage.getItem(UNIT_KEY);
+ return v === 'btc' || v === 'fiat' ? v : 'sats';
} catch {
return 'sats';
}
})();
+// sats → BTC → your money → sats. Fiat is a display unit like the other two
+// rather than a second number beside them: the balance is one figure, and
+// which figure is your choice. It's skipped entirely when no price has ever
+// been fetched — a third position showing blanks is worse than two that work.
function toggleUnit() {
- unit = unit === 'btc' ? 'sats' : 'btc';
+ const ring = haveRate() ? ['sats', 'btc', 'fiat'] : ['sats', 'btc'];
+ unit = ring[(ring.indexOf(unit) + 1) % ring.length];
try {
localStorage.setItem(UNIT_KEY, unit);
} catch {}
render();
}
-const unitLabel = () => (unit === 'sats' ? 'sats' : 'BTC');
-const fmtAmount = (sats) => (unit === 'sats' ? fmtSats(sats) : fmtBtc(sats));
+const unitLabel = () => (unit === 'sats' ? 'sats' : unit === 'fiat' ? getCurrency() : 'BTC');
+
+// "Worth X then · Y now" for a payment's detail page — the one place both
+// numbers earn their keep, because that page is where the question is asked.
+// Absent when there's no price, and when the two would say the same thing.
+function worthLine(sats, at, row = null) {
+ const then = rateAt(at);
+ const now = rateNow();
+ if (!now || !at) return null;
+ const line = row || ((k, v) => h('div', { class: 'line' }, h('span', { class: 'k' }, k), h('span', { class: 'v' }, v)));
+ const thenStr = then.rate && !then.stale ? fmtFiat(sats, then.rate) : null;
+ const nowStr = fmtFiat(sats, now);
+ if (!thenStr) return line(t('worthNow'), nowStr);
+ return line(t('worthThen'), thenStr + (thenStr === nowStr ? '' : ' · ' + t('worthNowInline', { v: nowStr })));
+}
+// `at` is when the money moved. In fiat it picks the price recorded at that
+// moment, so a payment is shown for what it was worth then — the number the
+// rest of the world remembers it by. Without it (a balance, a form) the live
+// price is used. A '~' means we had no sample near that time and had to price
+// it at today's, which is a different claim and shouldn't look like the same
+// one.
+const fmtAmount = (sats, at) => {
+ if (unit === 'sats') return fmtSats(sats);
+ if (unit === 'btc') return fmtBtc(sats);
+ const { rate, stale } = rateAt(at);
+ if (!rate) return fmtSats(sats);
+ return (stale && at ? '~' : '') + fmtFiat(sats, rate);
+};
// A clickable unit label. cls lets callers inherit surrounding sizing.
function unitTag(cls = '') {
@@ -2296,7 +2329,7 @@ function settingsTab() {
switch (ui.settingsPage) {
case 'wallet': return page(a ? [walletNameCard(a), pubkeyCard(a), recoveryCard(a)] : []);
case 'payments': return page(featureAll('settingsCards').reverse());
- case 'network': return page([networkCard(), explorerCard()]);
+ case 'network': return page([networkCard(), currencyCard(), explorerCard()]);
case 'nostr': return nostrSettingsView();
case 'notifications': return page(featureAll('notifySettingsCards').reverse());
case 'advanced': return advancedSettingsView();
@@ -2455,6 +2488,26 @@ function explorerCard() {
}
+// Which money the third tap on an amount shows. The list is whatever the
+// rates endpoint carries, so it's every currency coinos knows a price for.
+// Changing it starts the recorded price history again: the samples behind
+// "worth then" are denominated in the currency they were taken in, and
+// relabelling them would be a lie.
+function currencyCard() {
+ const codes = currencies();
+ return h(
+ 'div',
+ { class: 'card col' },
+ h('h3', {}, t('currencyLabel')),
+ h('select', {
+ value: getCurrency(),
+ onChange: (e) => { setCurrency(e.target.value); refreshRates({ force: true }).then(render); render(); },
+ }, codes.map((c) => h('option', { value: c, selected: c === getCurrency() }, c))),
+ h('div', { class: 'small faint' }, t('currencyHint')),
+ rateNow() ? h('div', { class: 'small muted' }, '1 BTC = ' + fmtFiat(SATS, rateNow())) : null
+ );
+}
+
// Language selector. Changing it persists the choice, flips text direction for
// RTL languages, and re-renders the whole app in the new language.
function languagePicker() {
@@ -4572,7 +4625,7 @@ function reviewSend() {
recipients = s.recipients.map((r, i) => {
const addr = r.address.trim();
if (!addr) throw new Error(t('enterAddrForN', { n: i + 1 }));
- const sats = parseAmount(r.amount, unit);
+ const sats = parseAmount(r.amount, unit, rateNow());
if (!sats || sats <= 0) throw new Error(t('enterValidAmtForN', { n: i + 1 }));
return { address: addr, amount: sats };
});
@@ -4746,8 +4799,9 @@ function txHistoryItem(tx) {
h('div', { class: 'small faint' }, tx.confirmed ? timeAgo(tx.blockTime) : stuck ? t('stuckNote') : t('awaitingConfirmation'))
),
h('div', { style: 'text-align:right' },
- h('div', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(tx.net)),
- !incoming && tx.fee ? h('div', { class: 'small faint' }, t('feeShort', { x: fmtAmount(tx.fee) })) : null
+ // priced at the moment it happened, not at today's number
+ h('div', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(tx.net, tx.blockTime)),
+ !incoming && tx.fee ? h('div', { class: 'small faint' }, t('feeShort', { x: fmtAmount(tx.fee, tx.blockTime) })) : null
)
);
}
@@ -4864,14 +4918,17 @@ function txDetailView(tx) {
h('span', { class: `tag ${tx.confirmed ? 'conf' : 'pending'}` }, tx.confirmed ? t('confirmedTag') : t('pendingTag'))
),
h('div', { class: 'amt', style: 'font-size:30px' },
- h('span', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(tx.net)),
+ h('span', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(tx.net, tx.blockTime)),
' ', unitTag('unit')
),
h('div', { class: 'summary col', style: 'gap:0' },
line(t('status'), tx.confirmed ? t('confirmed') : t('pending')),
tx.confirmed ? line(t('block'), String(tx.blockHeight || '—')) : null,
tx.confirmed && tx.blockTime ? line(t('date'), new Date(tx.blockTime * 1000).toLocaleString()) : null,
- tx.fee ? line(t('networkFee'), fmtAmount(tx.fee) + ' ' + unitLabel()) : null
+ // what it was worth then, beside what it is now — the one place both
+ // numbers earn their keep, because this is the page you open to ask
+ worthLine(tx.net, tx.blockTime),
+ tx.fee ? line(t('networkFee'), fmtAmount(tx.fee, tx.blockTime) + ' ' + unitLabel()) : null
),
...FEATURES.map((f) => (f.txDetailSection ? f.txDetailSection(tx) : null)),
!tx.confirmed && wallet.isStuck(tx)
@@ -4944,7 +5001,10 @@ async function importSnapshotFile(e) {
// only invoked at runtime (first render happens after loadLocale below).
const ctx = {
h, ui, render, wallet, toast, copy, copyBtn, pasteBtn, blankSend, goBack, goHome, openExternal,
- fmtAmount, unitLabel, unitTag, parseAmount, getUnit: () => unit, toggleUnit, download,
+ fmtAmount, unitLabel, unitTag, getUnit: () => unit, toggleUnit, download,
+ // in fiat the typed figure is money, so it needs today's price to become sats
+ parseAmount: (v, u) => parseAmount(v, u, rateNow()),
+ worthLine,
// "The next balance isn't a change, it's the first real value" — state
// arriving late (the ark store opening past boot's patience) must not read
// as money landing: without this the balance counts up from 0 in green,
@@ -5204,6 +5264,10 @@ loadLocale(getLang()).finally(async () => {
// counted itself up in green as if money had just come in. The shell is on
// screen either way; waiting shows it a moment longer rather than showing a
// wrong balance. The hook caps its own wait.
+ // The price, early: the unit toggle only offers fiat once we've had one,
+ // and a wallet opening in fiat should open with a number in it.
+ refreshRates().then(() => { if (unit === 'fiat') render(); }).catch(() => {});
+ setInterval(() => { refreshRates().then(() => { if (unit === 'fiat') render(); }).catch(() => {}); }, 10 * 60_000);
const storeReady = featureHook('arkStoreReady');
if (storeReady) await storeReady;
_bootDeciding = false; // from here on the screen is a decision, not a guess