diff options
| author | Adam Soltys <[email protected]> | 2026-09-11 22:55:14 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-11 22:55:14 -0700 |
| commit | e9a0aa2ca4d3aa659b579295dd983decd3729b6f (patch) | |
| tree | b237a05408a451aedd2d86be0fdda04040a37aaf /tools/rates-test.js | |
| parent | efaa9a6d0525e5fab18bb1c97da5685107506eff (diff) | |
| download | coinosv3-e9a0aa2ca4d3aa659b579295dd983decd3729b6f.tar.gz coinosv3-e9a0aa2ca4d3aa659b579295dd983decd3729b6f.zip | |
Every payment remembers what it was worth.
A payment is worth what it was worth WHEN IT HAPPENED, and that number is
gone by the time you look at it unless someone wrote it down. So every price
the app fetches is kept as a sample — one an hour, a few months back — and a
payment's fiat value is read off the sample nearest its own timestamp. The
history rows and both detail pages (on-chain and Spending) pass the time the
money moved; balances and forms use the live price, as they should.
A payment older than any sample we hold is priced at today's number and says
so with a '~', because that is a different claim and shouldn't look like the
same one. The detail page spells it out: "Worth then $X · $Y now".
Fiat is a third position on the unit toggle rather than a second number
beside the first: sats → BTC → your money → sats. One figure, and which
figure is your choice. The ring skips fiat entirely until a price has been
fetched — a third position showing blanks is worse than two that work. Typing
an amount with the unit on USD means dollars, priced now.
The rates are coinos' own endpoint, a map of every currency it knows, so
there's no third party watching what the wallet asks about; the currency
picker under Settings → Network lists all of them. Changing it starts the
recorded history again rather than relabelling samples taken in another
currency.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'tools/rates-test.js')
| -rw-r--r-- | tools/rates-test.js | 69 |
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); |
