Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions src/__tests__/actions/ExchangeRateActions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from '@jest/globals'

import {
convertToRatesParams,
type ExchangeRateCache,
mergePairCache
} from '../../actions/ExchangeRateActions'
Expand Down Expand Up @@ -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<Record<string, unknown>> = JSON.parse(
JSON.stringify(request)
).crypto
const wireCurrent = wireCrypto.find(entry => entry.isoDate == null)
expect(Object.prototype.hasOwnProperty.call(wireCurrent, 'isoDate')).toBe(
false
)
})
})
13 changes: 9 additions & 4 deletions src/actions/ExchangeRateActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ function fiatRateLogKey(
/**
* Convert maps to an array of RatesParams objects grouped by targetFiat.
*/
function convertToRatesParams(
export function convertToRatesParams(
cryptoPairMap: Map<string, CryptoFiatPair>,
fiatPairMap: Map<string, FiatFiatPair>
): RatesParams[] {
Expand Down Expand Up @@ -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
}))
Expand Down
Loading