Skip to content
Merged
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
248 changes: 154 additions & 94 deletions src/actions/ExchangeRateActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,23 @@ async function loadExchangeRateCache(): Promise<ExchangeRateCacheFile> {
const { cryptoPairs, fiatPairs, rates } = asExchangeRateCacheFile(json)

// Keep un-expired asset pairs:
const cryptoPairsMap = new Map<string, CryptoFiatPair>()
for (const pair of cryptoPairs) {
if (pair.expiration < now) continue
out.cryptoPairs.push(pair)
const tokenIdStr =
pair.asset.tokenId != null ? `_${pair.asset.tokenId}` : ''
const key = `${pair.asset.pluginId}${tokenIdStr}_${pair.targetFiat}`
cryptoPairsMap.set(key, { ...pair, isoDate: undefined })
}
out.cryptoPairs = Array.from(cryptoPairsMap.values())

const fiatPairsMap = new Map<string, FiatFiatPair>()
for (const pair of fiatPairs) {
if (pair.expiration < now) continue
out.fiatPairs.push(pair)
const key = `${pair.fiatCode}_${pair.targetFiat}`
fiatPairsMap.set(key, { ...pair, isoDate: undefined })
}
out.fiatPairs = Array.from(fiatPairsMap.values())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Historical Exchange Rate Data Integrity Compromised

The deduplication logic in loadExchangeRateCache uses a key that excludes isoDate, causing pairs with the same pluginId/tokenId/targetFiat but different isoDate values to collide and overwrite each other. Since the persisted cache can contain pairs with historical dates (e.g., isoDate: yesterday), only one per key is kept with isoDate cleared to undefined. This loses historical rate pair information that was previously persisted, preventing the app from accurately restoring its previous state after restart when daily rate updates were involved.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good thing


// Keep un-expired rates:
for (const [pluginId, tokenObj] of Object.entries(rates.crypto)) {
Expand Down Expand Up @@ -309,112 +318,163 @@ async function fetchExchangeRates(
}

const requests = convertToRatesParams(cryptoPairMap, fiatPairMap)
for (const query of requests) {
for (let attempt = 0; attempt < 5; ++attempt) {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(query)
}
try {
const response = await fetchRates('v3/rates', options)
if (response.ok) {
const json = await response.json()
const cleanedRates = asRatesParams(json)
const targetFiat = fixFiatCurrencyCode(cleanedRates.targetFiat)

for (const cryptoRate of cleanedRates.crypto) {
const { asset, isoDate, rate } = cryptoRate
if (rate == null) continue

const { pluginId, tokenId } = asset
const safeTokenId = tokenId ?? ''

rates.crypto[pluginId] ??= {}
rates.crypto[pluginId][safeTokenId] ??= {}
rates.crypto[pluginId][safeTokenId][targetFiat] ??= {
current: 0,
yesterday: 0,
yesterdayTimestamp: 0,
expiration: 0
}
const promises = requests.map(async query => {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(query)
}
try {
const response = await fetchRates('v3/rates', options)
if (response.ok) {
const json = await response.json()
const cleanedRates = asRatesParams(json)
const targetFiat = fixFiatCurrencyCode(cleanedRates.targetFiat)

for (const cryptoRate of cleanedRates.crypto) {
const { asset, isoDate, rate } = cryptoRate
if (rate == null) continue

const { pluginId, tokenId } = asset
const safeTokenId = tokenId ?? ''

rates.crypto[pluginId] ??= {}
rates.crypto[pluginId][safeTokenId] ??= {}
rates.crypto[pluginId][safeTokenId][targetFiat] ??= {
current: 0,
yesterday: 0,
yesterdayTimestamp: 0,
expiration: 0
}

const rateObj = rates.crypto[pluginId][safeTokenId][targetFiat]

const isHistorical =
isoDate != null && isoDate.getTime() < now - ONE_HOUR
if (isHistorical) {
const dateTimestamp = isoDate.getTime()
const yesterdayTargetTimestamp = Date.parse(yesterday)
const yesterdayRateTimestamp = rateObj.yesterdayTimestamp

// update yesterday rate if we find one closer than we have
if (
Math.abs(yesterdayTargetTimestamp - dateTimestamp) <
Math.abs(yesterdayTargetTimestamp - yesterdayRateTimestamp)
) {
rates.crypto[pluginId][safeTokenId][
targetFiat
].yesterdayTimestamp = yesterdayTimestamp
rateObj.yesterday = rate
}
} else {
rateObj.current = rate
const rateObj = rates.crypto[pluginId][safeTokenId][targetFiat]

const isHistorical =
isoDate != null && isoDate.getTime() < now - ONE_HOUR
if (isHistorical) {
const dateTimestamp = isoDate.getTime()
const yesterdayTargetTimestamp = Date.parse(yesterday)
const yesterdayRateTimestamp = rateObj.yesterdayTimestamp

// update yesterday rate if we find one closer than we have
if (
Math.abs(yesterdayTargetTimestamp - dateTimestamp) <
Math.abs(yesterdayTargetTimestamp - yesterdayRateTimestamp)
) {
rates.crypto[pluginId][safeTokenId][
targetFiat
].yesterdayTimestamp = yesterdayTimestamp
rateObj.yesterday = rate
}
} else {
rateObj.current = rate
}

rateObj.expiration = rateExpiration
rateObj.expiration = rateExpiration
}
for (const fiatRate of cleanedRates.fiat) {
const { isoDate, rate } = fiatRate
const fiatCode = fixFiatCurrencyCode(fiatRate.fiatCode)
if (rate == null) continue

rates.fiat[fiatCode] ??= {}
rates.fiat[fiatCode][targetFiat] ??= {
current: 0,
yesterday: 0,
yesterdayTimestamp: 0,
expiration: 0
}
for (const fiatRate of cleanedRates.fiat) {
const { isoDate, rate } = fiatRate
const fiatCode = fixFiatCurrencyCode(fiatRate.fiatCode)
if (rate == null) continue

rates.fiat[fiatCode] ??= {}
rates.fiat[fiatCode][targetFiat] ??= {
current: 0,
yesterday: 0,
yesterdayTimestamp: 0,
expiration: 0
}
const rateObj = rates.fiat[fiatCode][targetFiat]

const isHistorical =
isoDate != null && isoDate.getTime() < now - ONE_HOUR
if (isHistorical) {
const dateTimestamp = isoDate.getTime()
const yesterdayTargetTimestamp = Date.parse(yesterday)
const yesterdayRateTimestamp = rateObj.yesterdayTimestamp

// update yesterday rate if we find one closer than we have
if (
Math.abs(yesterdayTargetTimestamp - dateTimestamp) <
Math.abs(yesterdayTargetTimestamp - yesterdayRateTimestamp)
) {
rates.fiat[fiatCode][targetFiat].yesterdayTimestamp =
yesterdayTimestamp
rateObj.yesterday = rate
}
} else {
rateObj.current = rate
const rateObj = rates.fiat[fiatCode][targetFiat]

const isHistorical =
isoDate != null && isoDate.getTime() < now - ONE_HOUR
if (isHistorical) {
const dateTimestamp = isoDate.getTime()
const yesterdayTargetTimestamp = Date.parse(yesterday)
const yesterdayRateTimestamp = rateObj.yesterdayTimestamp

// update yesterday rate if we find one closer than we have
if (
Math.abs(yesterdayTargetTimestamp - dateTimestamp) <
Math.abs(yesterdayTargetTimestamp - yesterdayRateTimestamp)
) {
rates.fiat[fiatCode][targetFiat].yesterdayTimestamp =
yesterdayTimestamp
rateObj.yesterday = rate
}

rateObj.expiration = rateExpiration
} else {
rateObj.current = rate
}
break

rateObj.expiration = rateExpiration
}
} catch (error: unknown) {
console.log(
`buildExchangeRates error querying rates server ${String(error)}`
}
} catch (error: unknown) {
console.log(
`buildExchangeRates error querying rates server ${String(error)}`
)
}
})
await Promise.allSettled(promises)

// Merge successful rate responses into the pair cache
const cryptoPairCache = [...(exchangeRateCache?.cryptoPairs ?? [])]
const fiatPairCache = [...(exchangeRateCache?.fiatPairs ?? [])]
for (const [pluginId, tokenObj] of Object.entries(rates.crypto)) {
for (const [tokenId, rateObj] of Object.entries(tokenObj)) {
for (const targetFiat of Object.keys(rateObj)) {
const edgeTokenId = tokenId === '' ? null : tokenId
const cryptoPairIndex = cryptoPairCache.findIndex(
pair =>
pair.asset.pluginId === pluginId &&
pair.asset.tokenId === edgeTokenId &&
pair.targetFiat === targetFiat
)
if (cryptoPairIndex === -1) {
cryptoPairCache.push({
asset: { pluginId, tokenId: edgeTokenId },
targetFiat,
isoDate: undefined,
expiration: pairExpiration
})
} else {
cryptoPairCache[cryptoPairIndex] = {
asset: { pluginId, tokenId: edgeTokenId },
targetFiat,
isoDate: undefined,
expiration: pairExpiration
}
}
}
}
}
for (const [fiatCode, fiatObj] of Object.entries(rates.fiat)) {
for (const targetFiat of Object.keys(fiatObj)) {
const fiatPairIndex = fiatPairCache.findIndex(
pair => pair.fiatCode === fiatCode && pair.targetFiat === targetFiat
)
if (fiatPairIndex === -1) {
fiatPairCache.push({
fiatCode,
targetFiat,
isoDate: undefined,
expiration: pairExpiration
})
} else {
fiatPairCache[fiatPairIndex] = {
fiatCode,
targetFiat,
isoDate: undefined,
expiration: pairExpiration
}
}
}
}

// Update the in-memory cache:
exchangeRateCache = {
rates,
cryptoPairs: Array.from(cryptoPairMap.values()),
fiatPairs: Array.from(fiatPairMap.values())
cryptoPairs: cryptoPairCache,
fiatPairs: fiatPairCache
}

// Write the cache to disk:
Expand Down
Loading