@@ -5,7 +5,7 @@ import { makeReactNativeDisklet } from 'disklet'
55import { RootState , ThunkAction } from '../types/reduxTypes'
66import { GuiExchangeRates } from '../types/types'
77import { fetchRates } from '../util/network'
8- import { datelog , DECIMAL_PRECISION , getYesterdayDateRoundDownHour } from '../util/utils'
8+ import { datelog , DECIMAL_PRECISION } from '../util/utils'
99
1010const disklet = makeReactNativeDisklet ( )
1111const EXCHANGE_RATES_FILENAME = 'exchangeRates.json'
@@ -14,8 +14,18 @@ const HOUR_MS = 1000 * 60 * 60
1414const ONE_DAY = 1000 * 60 * 60 * 24
1515const ONE_MONTH = 1000 * 60 * 60 * 24 * 30
1616
17- const asAssetPair = asObject ( { currency_pair : asString , date : asOptional ( asString ) , expiration : asNumber } )
18- const asExchangeRateCache = asObject ( asObject ( { expiration : asNumber , rate : asString } ) )
17+ const asAssetPair = asObject ( {
18+ currency_pair : asString ,
19+ date : asOptional ( asString ) , // Defaults to today if not specified
20+ expiration : asNumber
21+ } )
22+
23+ const asExchangeRateCache = asObject (
24+ asObject ( {
25+ expiration : asNumber ,
26+ rate : asString
27+ } )
28+ )
1929const asExchangeRateCacheFile = asObject ( {
2030 rates : asExchangeRateCache ,
2131 assetPairs : asArray ( asAssetPair )
@@ -25,7 +35,7 @@ type AssetPair = ReturnType<typeof asAssetPair>
2535type ExchangeRateCache = ReturnType < typeof asExchangeRateCache >
2636type ExchangeRateCacheFile = ReturnType < typeof asExchangeRateCacheFile >
2737
28- const exchangeRateCache : ExchangeRateCache = { }
38+ let exchangeRateCache : ExchangeRateCache = { }
2939
3040const asRatesResponse = asObject ( {
3141 data : asArray (
@@ -48,49 +58,48 @@ export function updateExchangeRates(): ThunkAction<Promise<void>> {
4858 }
4959}
5060
51- /**
52- * Remove duplicates and expired entries from the given array of AssetPair.
53- * If two items share the same currency_pair and date,
54- * only keep the one with the higher expiration.
55- */
56- function filterAssetPairs ( assetPairs : AssetPair [ ] ) : AssetPair [ ] {
57- const map = new Map < string , AssetPair > ( )
58- const now = Date . now ( )
59- for ( const asset of assetPairs ) {
60- if ( asset . expiration < now ) continue
61- // Construct a key based on currency_pair and date (including handling for empty/undefined date)
62- const key = `${ asset . currency_pair } _${ asset . date ?? '' } `
63-
64- const existing = map . get ( key )
65- if ( existing == null || asset . expiration > existing . expiration ) {
66- map . set ( key , asset )
67- }
68- }
69-
70- return [ ...map . values ( ) ]
71- }
72-
7361async function buildExchangeRates ( state : RootState ) : Promise < GuiExchangeRates > {
62+ const accountIsoFiat = state . ui . settings . defaultIsoFiat
7463 const { account } = state . core
7564 const { currencyWallets } = account
65+
66+ // Look up various dates:
7667 const now = Date . now ( )
77- const initialAssetPairs : AssetPair [ ] = [ ]
78- let numCacheEntries = 0
68+ const pairExpiration = now + ONE_MONTH
69+ const rateExpiration = now + ONE_DAY
70+ const yesterday = getYesterdayDateRoundDownHour ( now ) . toISOString ( )
7971
80- // Load exchange rate cache off disk
81- if ( Object . keys ( exchangeRateCache ) . length === 0 ) {
72+ // What we need to fetch from the server:
73+ const initialAssetPairs : AssetPair [ ] = [ ]
74+ let hasWallets = false
75+ let hasCachedRates = false
76+
77+ // If we have loaded the cache before, keep any un-expired entries:
78+ const rateCache : ExchangeRateCache = { }
79+ const cachedKeys = Object . keys ( exchangeRateCache )
80+ if ( cachedKeys . length > 0 ) {
81+ for ( const key of cachedKeys ) {
82+ if ( exchangeRateCache [ key ] . expiration > now ) {
83+ rateCache [ key ] = exchangeRateCache [ key ]
84+ hasCachedRates = true
85+ }
86+ }
87+ } else {
88+ // Load exchange rate cache off disk, since we haven't done that yet:
8289 try {
8390 const raw = await disklet . getText ( EXCHANGE_RATES_FILENAME )
8491 const json = JSON . parse ( raw )
85- const exchangeRateCacheFile = asExchangeRateCacheFile ( json )
86- const { assetPairs , rates } = exchangeRateCacheFile
87- // Prune expired rates
92+ const { assetPairs , rates } = asExchangeRateCacheFile ( json )
93+
94+ // Keep un- expired rates:
8895 for ( const key of Object . keys ( rates ) ) {
8996 if ( rates [ key ] . expiration > now ) {
90- exchangeRateCache [ key ] = rates [ key ]
91- numCacheEntries ++
97+ rateCache [ key ] = rates [ key ]
98+ hasCachedRates = true
9299 }
93100 }
101+
102+ // Keep un-expired asset pairs:
94103 for ( const pair of assetPairs ) {
95104 if ( pair . expiration > now ) {
96105 initialAssetPairs . push ( pair )
@@ -100,43 +109,70 @@ async function buildExchangeRates(state: RootState): Promise<GuiExchangeRates> {
100109 datelog ( 'Error loading exchange rate cache:' , String ( e ) )
101110 }
102111 }
103- const accountIsoFiat = state . ui . settings . defaultIsoFiat
104112
105- const expiration = now + ONE_MONTH
106- const yesterdayDate = getYesterdayDateRoundDownHour ( )
113+ // If the user's fiat isn't dollars, get it's price:
107114 if ( accountIsoFiat !== 'iso:USD' ) {
108- initialAssetPairs . push ( { currency_pair : `iso:USD_${ accountIsoFiat } ` , date : undefined , expiration } )
115+ initialAssetPairs . push ( {
116+ currency_pair : `iso:USD_${ accountIsoFiat } ` ,
117+ date : undefined ,
118+ expiration : pairExpiration
119+ } )
109120 }
110- const walletIds = Object . keys ( currencyWallets )
111- for ( const id of walletIds ) {
112- const wallet = currencyWallets [ id ]
113- const currencyCode = wallet . currencyInfo . currencyCode
114- // need to get both forward and backwards exchange rates for wallets & account fiats, for each parent currency AND each token
115- initialAssetPairs . push ( { currency_pair : `${ currencyCode } _${ accountIsoFiat } ` , date : undefined , expiration } )
116- initialAssetPairs . push ( { currency_pair : `${ currencyCode } _iso:USD` , date : `${ yesterdayDate } ` , expiration } )
117- // now add tokens, if they exist
118- if ( accountIsoFiat !== 'iso:USD' ) {
119- initialAssetPairs . push ( { currency_pair : `iso:USD_${ accountIsoFiat } ` , date : undefined , expiration } )
120- }
121+
122+ for ( const walletId of Object . keys ( currencyWallets ) ) {
123+ const wallet = currencyWallets [ walletId ]
124+ const { currencyCode } = wallet . currencyInfo
125+ hasWallets = true
126+
127+ // Get the primary asset's prices for today and yesterday,
128+ // but with yesterday's price in dollars:
129+ initialAssetPairs . push ( {
130+ currency_pair : `${ currencyCode } _${ accountIsoFiat } ` ,
131+ date : undefined ,
132+ expiration : pairExpiration
133+ } )
134+ initialAssetPairs . push ( {
135+ currency_pair : `${ currencyCode } _iso:USD` ,
136+ date : yesterday ,
137+ expiration : pairExpiration
138+ } )
139+
140+ // Do the same for any tokens:
121141 for ( const tokenId of wallet . enabledTokenIds ) {
122- if ( wallet . currencyConfig . allTokens [ tokenId ] == null ) continue
123- const { currencyCode : tokenCode } = wallet . currencyConfig . allTokens [ tokenId ]
124- if ( tokenCode !== currencyCode ) {
125- initialAssetPairs . push ( { currency_pair : `${ tokenCode } _${ accountIsoFiat } ` , date : undefined , expiration } )
126- initialAssetPairs . push ( { currency_pair : `${ tokenCode } _iso:USD` , date : `${ yesterdayDate } ` , expiration } )
127- }
142+ const token = wallet . currencyConfig . allTokens [ tokenId ]
143+ if ( token == null ) continue
144+ if ( token . currencyCode === currencyCode ) continue
145+ initialAssetPairs . push ( {
146+ currency_pair : `${ token . currencyCode } _${ accountIsoFiat } ` ,
147+ date : undefined ,
148+ expiration : pairExpiration
149+ } )
150+ initialAssetPairs . push ( {
151+ currency_pair : `${ token . currencyCode } _iso:USD` ,
152+ date : yesterday ,
153+ expiration : pairExpiration
154+ } )
128155 }
129156 }
130157
131- const filteredAssetPairs = filterAssetPairs ( initialAssetPairs )
132- const assetPairs = [ ...filteredAssetPairs ]
158+ // De-duplicate asset pairs:
159+ const assetMap = new Map < string , AssetPair > ( )
160+ for ( const asset of initialAssetPairs ) {
161+ const key = `${ asset . currency_pair } _${ asset . date ?? '' } `
162+
163+ const existing = assetMap . get ( key )
164+ if ( existing == null || asset . expiration > existing . expiration ) {
165+ assetMap . set ( key , asset )
166+ }
167+ }
168+ const filteredAssetPairs = [ ...assetMap . values ( ) ]
133169
134170 /**
135171 * On initial load, buildExchangeRates may get called before any wallets are
136172 * loaded. In this case, we can skip the rates fetch and use the cache to
137173 * save on the network delay.
138174 */
139- const skipRatesFetch = walletIds . length === 0 && numCacheEntries > 0
175+ const skipRatesFetch = hasCachedRates && ! hasWallets
140176
141177 while ( filteredAssetPairs . length > 0 ) {
142178 if ( skipRatesFetch ) break
@@ -155,22 +191,19 @@ async function buildExchangeRates(state: RootState): Promise<GuiExchangeRates> {
155191 const cleanedRates = asRatesResponse ( json )
156192 for ( const rate of cleanedRates . data ) {
157193 const { currency_pair : currencyPair , exchangeRate, date } = rate
158- const newDate = new Date ( date ) . valueOf ( )
194+ const isHistorical = now - new Date ( date ) . valueOf ( ) > HOUR_MS
195+ const key = isHistorical ? `${ currencyPair } _${ date } ` : currencyPair
159196
160- const key = now - newDate > HOUR_MS ? `${ currencyPair } _${ date } ` : currencyPair
161- const cachedRate = exchangeRateCache [ key ] ?? { expiration : 0 , rate : '0' }
162197 if ( exchangeRate != null ) {
163- cachedRate . rate = exchangeRate
164- cachedRate . expiration = now + ONE_DAY
165- }
166- exchangeRateCache [ key ] = cachedRate
167-
168- const codes = key . split ( '_' )
169- const reverseExchangeRateKey = `${ codes [ 1 ] } _${ codes [ 0 ] } ${ codes [ 2 ] ? '_' + codes [ 2 ] : '' } `
170- if ( exchangeRateCache [ reverseExchangeRateKey ] == null ) {
171- exchangeRateCache [ reverseExchangeRateKey ] = { expiration : cachedRate . expiration , rate : '0' }
172- if ( ! eq ( cachedRate . rate , '0' ) ) {
173- exchangeRateCache [ reverseExchangeRateKey ] . rate = div ( '1' , cachedRate . rate , DECIMAL_PRECISION )
198+ rateCache [ key ] = {
199+ expiration : rateExpiration ,
200+ rate : exchangeRate
201+ }
202+ } else if ( rateCache [ key ] == null ) {
203+ // We at least need a placeholder:
204+ rateCache [ key ] = {
205+ expiration : 0 ,
206+ rate : '0'
174207 }
175208 }
176209 }
@@ -182,23 +215,37 @@ async function buildExchangeRates(state: RootState): Promise<GuiExchangeRates> {
182215 } while ( -- tries > 0 )
183216 }
184217
185- // Save exchange rate cache to disk
218+ // Save exchange rate cache to disk:
186219 try {
187- const exchangeRateCacheFile : ExchangeRateCacheFile = { rates : exchangeRateCache , assetPairs }
220+ const exchangeRateCacheFile : ExchangeRateCacheFile = {
221+ rates : rateCache ,
222+ assetPairs : filteredAssetPairs
223+ }
188224 await disklet . setText ( EXCHANGE_RATES_FILENAME , JSON . stringify ( exchangeRateCacheFile ) )
189225 } catch ( e ) {
190226 datelog ( 'Error saving exchange rate cache:' , String ( e ) )
191227 }
228+ exchangeRateCache = rateCache
192229
230+ // Build the GUI rate structure:
193231 const serverRates : GuiExchangeRates = { 'iso:USD_iso:USD' : '1' }
194- for ( const key of Object . keys ( exchangeRateCache ) ) {
195- const rate = exchangeRateCache [ key ]
196- if ( rate . expiration > now ) {
197- serverRates [ key ] = rate . rate
198- } else {
199- delete exchangeRateCache [ key ]
200- }
232+ for ( const key of Object . keys ( rateCache ) ) {
233+ const { rate } = rateCache [ key ]
234+ serverRates [ key ] = rate
235+
236+ // Include reverse rates:
237+ const codes = key . split ( '_' )
238+ const reverseKey = `${ codes [ 1 ] } _${ codes [ 0 ] } ${ codes [ 2 ] ? '_' + codes [ 2 ] : '' } `
239+ serverRates [ reverseKey ] = eq ( rate , '0' ) ? '0' : div ( '1' , rate , DECIMAL_PRECISION )
201240 }
202-
203241 return serverRates
204242}
243+
244+ const getYesterdayDateRoundDownHour = ( now ?: Date | number ) : Date => {
245+ const yesterday = now == null ? new Date ( ) : new Date ( now )
246+ yesterday . setMinutes ( 0 )
247+ yesterday . setSeconds ( 0 )
248+ yesterday . setMilliseconds ( 0 )
249+ yesterday . setDate ( yesterday . getDate ( ) - 1 )
250+ return yesterday
251+ }
0 commit comments