diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b4211d5b7..cfba92d8c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - fixed: Bitwave account ids are no longer capitalized by the keyboard or padded with whitespace when entered, so exports import without hand-editing the account id. - fixed: NYM max swaps from EVM wallets now report the correct limit error instead of an unsupported-route error (edge-exchange-plugins 2.52.1). - fixed: Exchange rate queries no longer request each chain's own asset twice, which had been inflating every rate query with duplicate pairs. +- fixed: Current exchange rates no longer show $0.00 on devices whose clock runs a few minutes fast. Current-rate requests now omit the device timestamp so the rates server uses its own clock, instead of asking for a future date the server has no rate for. - fixed: XRP minimum balance warning copy to clarify the reserve is met once the address balance reaches 1 XRP, not on top of it. - fixed: Round fiat balances to cents in the Wallets list, matching the wallet detail scene - fixed: Notification center cards no longer shrink their text to fit. Long titles and messages now truncate with an ellipsis so every card renders at the same size. diff --git a/src/__tests__/actions/ExchangeRateActions.test.ts b/src/__tests__/actions/ExchangeRateActions.test.ts index 0149d1e4bda..1a099d0167c 100644 --- a/src/__tests__/actions/ExchangeRateActions.test.ts +++ b/src/__tests__/actions/ExchangeRateActions.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from '@jest/globals' import { + convertToRatesParams, type ExchangeRateCache, mergePairCache } from '../../actions/ExchangeRateActions' @@ -118,3 +119,63 @@ describe('mergePairCache', () => { expect(out.cryptoPairs).toHaveLength(2) }) }) + +describe('convertToRatesParams', () => { + it('omits isoDate for current pairs but keeps it for historical pairs', () => { + // A device with a fast clock used to stamp "current" pairs with a future + // timestamp, which the rates server answers with no rate. Current pairs + // must go out with no isoDate so the server uses its own clock. + const cryptoPairs = new Map([ + [ + 'current', + { + asset: { pluginId: 'bitcoin', tokenId: null }, + targetFiat: 'iso:USD', + isoDate: undefined, + expiration: PAIR_EXPIRATION + } + ], + [ + 'historical', + { + asset: { pluginId: 'bitcoin', tokenId: null }, + targetFiat: 'iso:USD', + isoDate: '2026-08-12T13:00:00.000Z', + expiration: PAIR_EXPIRATION + } + ] + ]) + const fiatPairs = new Map([ + [ + 'current', + { + fiatCode: 'iso:USD', + targetFiat: 'iso:USD', + isoDate: undefined, + expiration: PAIR_EXPIRATION + } + ] + ]) + + const requests = convertToRatesParams(cryptoPairs, fiatPairs) + expect(requests).toHaveLength(1) + const [request] = requests + + const current = request.crypto.find(entry => entry.isoDate == null) + const historical = request.crypto.find(entry => entry.isoDate != null) + expect(current).toBeDefined() + expect(current?.isoDate).toBeUndefined() + expect(historical?.isoDate?.toISOString()).toBe('2026-08-12T13:00:00.000Z') + expect(request.fiat[0].isoDate).toBeUndefined() + + // On the wire an undefined isoDate drops out entirely, so the server falls + // back to its own clock; the historical date is still sent. + const wireCrypto: Array> = JSON.parse( + JSON.stringify(request) + ).crypto + const wireCurrent = wireCrypto.find(entry => entry.isoDate == null) + expect(Object.prototype.hasOwnProperty.call(wireCurrent, 'isoDate')).toBe( + false + ) + }) +}) diff --git a/src/actions/ExchangeRateActions.ts b/src/actions/ExchangeRateActions.ts index 0f6995118d9..7f638a00fe0 100644 --- a/src/actions/ExchangeRateActions.ts +++ b/src/actions/ExchangeRateActions.ts @@ -614,7 +614,7 @@ function fiatRateLogKey( /** * Convert maps to an array of RatesParams objects grouped by targetFiat. */ -function convertToRatesParams( +export function convertToRatesParams( cryptoPairMap: Map, fiatPairMap: Map ): RatesParams[] { @@ -644,21 +644,26 @@ function convertToRatesParams( // Convert to RatesParams[] const requests: RatesParams[] = [] - const newDate = new Date() for (const [targetFiat, { crypto, fiat }] of resultMap.entries()) { while (crypto.length > 0 || fiat.length > 0) { const cryptoChunk = crypto.splice(0, RATES_SERVER_MAX_QUERY_SIZE) const fiatChunk = fiat.splice(0, RATES_SERVER_MAX_QUERY_SIZE) + // Leave `isoDate` off of "current" pairs (those with no date) so the + // rates server timestamps them with its own clock. Stamping the device + // clock here asked the server for a future date whenever the device ran + // fast, and the server returns no rate for future dates, which left the + // current rate at 0 and fiat balances stuck at $0.00. Historical pairs + // keep their explicit date. requests.push({ targetFiat: removeIsoPrefix(targetFiat), crypto: cryptoChunk.map(pair => ({ - isoDate: pair.isoDate == null ? newDate : new Date(pair.isoDate), + isoDate: pair.isoDate == null ? undefined : new Date(pair.isoDate), asset: pair.asset, rate: undefined })), fiat: fiatChunk.map(pair => ({ - isoDate: pair.isoDate == null ? newDate : new Date(pair.isoDate), + isoDate: pair.isoDate == null ? undefined : new Date(pair.isoDate), fiatCode: removeIsoPrefix(pair.fiatCode), rate: undefined }))