aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/rates-test.js69
1 files changed, 69 insertions, 0 deletions
diff --git a/tools/rates-test.js b/tools/rates-test.js
new file mode 100644
index 0000000..88119f3
--- /dev/null
+++ b/tools/rates-test.js
@@ -0,0 +1,69 @@
+// What a payment was worth WHEN IT HAPPENED. The price is sampled as the app
+// runs and kept as a series; a payment's fiat value is read off the sample
+// nearest its timestamp, and a payment older than anything we sampled is
+// priced at today's number and SAID to be — never quietly.
+//
+// Run: bun tools/rates-test.js
+let fails = 0;
+const check = (name, ok, detail = '') => {
+ console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 90) : ''}`);
+ if (!ok) fails++;
+};
+globalThis.localStorage = {
+ _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); },
+ removeItem(k) { delete this._d[k]; }, key(i) { return Object.keys(this._d)[i] ?? null; },
+ get length() { return Object.keys(this._d).length; },
+};
+const HOUR = 3600;
+const now = Math.floor(Date.now() / 1000);
+// a week of hourly samples, climbing from 50k to 100k
+const series = [];
+for (let i = 7 * 24; i >= 0; i--) series.push([now - i * HOUR, 50_000 + (7 * 24 - i) * 300]);
+localStorage.setItem('btc-wallet-rate-series', JSON.stringify(series));
+localStorage.setItem('btc-wallet-rates', JSON.stringify({ USD: series[series.length - 1][1], EUR: 1234 }));
+
+const { rateAt, rateNow, fmtFiat, getCurrency, setCurrency, currencies, RATE_TOLERANCE } = await import('../src/rates.js');
+const { parseAmount, SATS } = await import('../src/format.js');
+
+check('the live price is the newest sample', rateNow() === series[series.length - 1][1], String(rateNow()));
+
+// a payment three days ago is priced at the price three days ago
+const threeDays = now - 3 * 24 * HOUR;
+const then = rateAt(threeDays);
+const want = series.find(([ts]) => ts === threeDays)[1];
+check('a payment is priced at the moment it happened', then.rate === want && !then.stale, `${then.rate} vs ${want}`);
+check("...which isn't today's price", then.rate !== rateNow(), `${then.rate} vs ${rateNow()}`);
+
+// a payment between samples takes the nearest one
+const between = rateAt(threeDays + 900);
+check('between samples, the nearest one wins', between.rate === want && !between.stale, String(between.rate));
+
+// older than anything we have: today's price, flagged
+const ancient = rateAt(now - 400 * 24 * HOUR);
+check('an older payment than we have samples for is flagged', ancient.stale === true && ancient.rate === rateNow(), JSON.stringify(ancient));
+check('...and the tolerance is a day', RATE_TOLERANCE === 24 * HOUR, String(RATE_TOLERANCE));
+
+// no timestamp at all (a balance, a form) is simply the live price
+check('an amount with no time is the live price', rateAt(null).rate === rateNow() && !rateAt(null).stale);
+
+// typing money
+check('typing 5 with the unit on fiat buys five dollars of bitcoin',
+ parseAmount('5', 'fiat', 100_000) === Math.round((5 / 100_000) * SATS), String(parseAmount('5', 'fiat', 100_000)));
+check('...and without a price it refuses rather than guessing', parseAmount('5', 'fiat', null) === null);
+check('sats and BTC are untouched', parseAmount('2100', 'sats') === 2100 && parseAmount('0.001', 'btc') === 100_000);
+
+// formatting
+check('an amount formats as money', /\$1[,.]/.test(fmtFiat(SATS / 100, 100_000)), fmtFiat(SATS / 100, 100_000));
+check('a small amount keeps more decimals', fmtFiat(100, 100_000).includes('0.10'), fmtFiat(100, 100_000));
+check('a currency Intl has never heard of still renders', /XYZ/.test(fmtFiat(SATS, 10, 'XYZ')), fmtFiat(SATS, 10, 'XYZ'));
+
+// changing currency drops the series rather than mislabelling it
+check('the currency list comes from the rates map', currencies().includes('EUR'), currencies().join(','));
+setCurrency('EUR');
+check('changing currency starts the history again', JSON.parse(localStorage.getItem('btc-wallet-rate-series')).length <= 1,
+ localStorage.getItem('btc-wallet-rate-series').slice(0, 40));
+check('...and the live price follows it', rateNow() === 1234, String(rateNow()));
+check('the choice sticks', getCurrency() === 'EUR');
+
+console.log(fails ? `\n❌ ${fails} failed` : '\n✅ prices are remembered, not invented');
+process.exit(fails ? 1 : 0);