From b19032c5b5c6f73262e1959078400c405ae09e0e Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 23:57:55 -0700 Subject: [PATCH 1/7] Remove some unused types --- src/types/types.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/types/types.ts b/src/types/types.ts index 6a6d95ac957..aad6acfc3c7 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -13,13 +13,11 @@ import { } from 'cleaners' import type { EdgeCurrencyWallet, - EdgeMetadata, EdgeToken, EdgeTokenId } from 'edge-core-js/types' import type { LocaleStringKey } from '../locales/en_US' -import type { RootState } from './reduxTypes' import type { Theme } from './Theme' /** @deprecated Only to be used for payloads that still allow undefined for @@ -87,32 +85,6 @@ export interface FlatListItem { item: T } -export interface DeviceDimensions { - keyboardHeight: number -} - -export interface GuiTouchIdInfo { - isTouchEnabled: boolean - isTouchSupported: boolean -} - -export interface GuiReceiveAddress { - metadata: EdgeMetadata - publicAddress: string - legacyAddress?: string - segwitAddress?: string - nativeAmount: string -} - -export interface CurrencyConverter { - convertCurrency: ( - state: RootState, - currencyCode: string, - isoFiatCurrencyCode: string, - balanceInCryptoDisplay: string - ) => number -} - const asPasswordReminder = asObject({ needsPasswordCheck: asMaybe(asBoolean, false), lastLoginDate: asMaybe(asNumber, 0), @@ -135,9 +107,6 @@ export const asSpendingLimits = asObject({ const asAccountNotifDismissInfo = asObject({ ip2FaNotifShown: asMaybe(asBoolean, false) }) -export type AccountNotifDismissInfo = ReturnType< - typeof asAccountNotifDismissInfo -> const asTokenWarningsShown = asArray(asString) From 2ed49954ea4988a79adef175c84f689592dae97c Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 16:23:12 -0700 Subject: [PATCH 2/7] Don't use convertTransactionFeeToDisplayFee if the transaction is null --- src/components/scenes/SendScene2.tsx | 34 +++++++++++++++------------- src/util/utils.ts | 11 ++++----- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 57d04ec17bb..86147f62173 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -675,6 +675,10 @@ const SendComponent = (props: Props) => { const { noChangeMiningFee } = getSpecialCurrencyInfo(pluginId) let feeDisplayDenomination: EdgeDenomination let feeExchangeDenomination: EdgeDenomination + + let fiatAmount = '0' + let feeSyntax = ` 0 (${fiatAmount})` + let feeSyntaxStyle: string | undefined if (edgeTransaction?.parentNetworkFee != null) { feeDisplayDenomination = parentDisplayDenom feeExchangeDenomination = parentExchangeDenom @@ -682,23 +686,21 @@ const SendComponent = (props: Props) => { feeDisplayDenomination = cryptoDisplayDenomination feeExchangeDenomination = cryptoExchangeDenomination } - const transactionFee = convertTransactionFeeToDisplayFee( - coreWallet.currencyInfo.currencyCode, - defaultIsoFiat, - exchangeRates, - edgeTransaction, - feeDisplayDenomination, - feeExchangeDenomination - ) - const fiatAmount = - transactionFee.fiatAmount === '0' - ? '0' - : ` ${transactionFee.fiatAmount}` - const feeSyntax = `${transactionFee.cryptoSymbol ?? ''} ${ - transactionFee.cryptoAmount - } (${transactionFee.fiatSymbol ?? ''}${fiatAmount})` - const feeSyntaxStyle = transactionFee.fiatStyle + if (edgeTransaction != null) { + const transactionFee = convertTransactionFeeToDisplayFee( + coreWallet.currencyInfo.currencyCode, + defaultIsoFiat, + exchangeRates, + edgeTransaction, + feeDisplayDenomination, + feeExchangeDenomination + ) + + fiatAmount = ` ${transactionFee.fiatAmount}` + feeSyntax = `${transactionFee.cryptoSymbol} ${transactionFee.cryptoAmount} (${transactionFee.fiatSymbol}${fiatAmount})` + feeSyntaxStyle = transactionFee.fiatStyle + } return ( Date: Wed, 6 Aug 2025 17:09:10 -0700 Subject: [PATCH 3/7] Replace currencyCode with pluginId/tokenId in convertCurrencyFromExchangeRates This breaks the snapshots until we upgrade exchangeRateActions to fix the keys in the exchangeRates object --- .../__snapshots__/SendScene2.ui.test.tsx.snap | 28 +++++++++---------- .../TransactionDetailsScene.test.tsx.snap | 27 ++---------------- src/actions/WalletActions.tsx | 3 +- src/components/modals/AccelerateTxModal.tsx | 3 +- .../scenes/Fio/FioStakingChangeScene.tsx | 3 +- .../scenes/MigrateWalletCalculateFeeScene.tsx | 3 +- src/components/scenes/SendScene2.tsx | 9 ++++-- .../scenes/Staking/StakeModifyScene.tsx | 3 +- .../scenes/SwapConfirmationScene.tsx | 3 +- .../SweepPrivateKeyCalculateFeeScene.tsx | 3 +- .../scenes/TransactionDetailsScene.tsx | 3 +- .../services/AccountCallbackManager.tsx | 3 +- src/components/tiles/EditableAmountTile.tsx | 11 +++++--- src/selectors/WalletSelectors.ts | 3 +- src/util/ActionProgramUtils.ts | 9 ++++-- src/util/utils.ts | 15 ++++++---- 16 files changed, 66 insertions(+), 63 deletions(-) diff --git a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap index 1cebea41b08..5c05ca162ef 100644 --- a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap @@ -896,7 +896,7 @@ exports[`SendScene2 1 spendTarget 1`] = ` ] } > - € 0.23 + € 0.00 - € 0.23 + € 0.00 - Amount: 0.00001234 BTC (€ 0.23) + Amount: 0.00001234 BTC (€ 0.00) - € 2.26 + € 0.00 - Amount: 0.00001234 BTC (€ 0.23) + Amount: 0.00001234 BTC (€ 0.00) - € 2.26 + € 0.00 - Amount: 0.00001234 BTC (€ 0.23) + Amount: 0.00001234 BTC (€ 0.00) - Amount: 0.00001234 BTC (€ 0.23) + Amount: 0.00001234 BTC (€ 0.00) - Amount: 0.00001234 BTC (€ 0.23) + Amount: 0.00001234 BTC (€ 0.00) - € 2.26 + € 0.00 - Amount: 0.00001234 BTC (€ 0.23) + Amount: 0.00001234 BTC (€ 0.00) @@ -13091,7 +13091,7 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` ] } > - € 2.26 + € 0.00 @@ -14400,7 +14400,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` ] } > - Amount: 0.00001234 BTC (€ 0.23) + Amount: 0.00001234 BTC (€ 0.00) @@ -14728,7 +14728,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` ] } > - € 2.26 + € 0.00 diff --git a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap index c5f619978a2..32c98709252 100644 --- a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap @@ -984,28 +984,7 @@ exports[`TransactionDetailsScene should render 1`] = ` ] } > - 1230.00 - - - (0%) + 0.00 @@ -3812,7 +3791,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi ] } > - 1230.00 + 0.00 - (-80.75%) + (-,100%) diff --git a/src/actions/WalletActions.tsx b/src/actions/WalletActions.tsx index edc61bab2dd..1df40928ae8 100644 --- a/src/actions/WalletActions.tsx +++ b/src/actions/WalletActions.tsx @@ -276,7 +276,8 @@ export function activateWalletTokens( ) let fiatFee = convertCurrencyFromExchangeRates( state.exchangeRates, - paymentCurrencyCode, + pluginId, + feeTokenId, defaultIsoFiat, exchangeNetworkFee ) diff --git a/src/components/modals/AccelerateTxModal.tsx b/src/components/modals/AccelerateTxModal.tsx index 81a17572864..6eba55395fa 100644 --- a/src/components/modals/AccelerateTxModal.tsx +++ b/src/components/modals/AccelerateTxModal.tsx @@ -106,7 +106,8 @@ export class AccelerateTxModalComponent extends PureComponent { const feeDefaultDenomination = getExchangeDenom(wallet.currencyConfig, null) const transactionFee = convertTransactionFeeToDisplayFee( - wallet.currencyInfo.currencyCode, + wallet.currencyInfo.pluginId, + null, isoFiatCurrencyCode, exchangeRates, edgeTransaction, diff --git a/src/components/scenes/Fio/FioStakingChangeScene.tsx b/src/components/scenes/Fio/FioStakingChangeScene.tsx index a860328d8f9..1d4d0b328c0 100644 --- a/src/components/scenes/Fio/FioStakingChangeScene.tsx +++ b/src/components/scenes/Fio/FioStakingChangeScene.tsx @@ -141,7 +141,8 @@ export const FioStakingChangeScene = withWallet((props: Props) => { )(stakingNativeAmount) const stakingFiatBalance = convertCurrencyFromExchangeRates( exchangeRates, - currencyCode, + pluginId, + tokenId, defaultIsoFiat, stakingDefaultCryptoAmount ) diff --git a/src/components/scenes/MigrateWalletCalculateFeeScene.tsx b/src/components/scenes/MigrateWalletCalculateFeeScene.tsx index 534139e4002..642951c1f7d 100644 --- a/src/components/scenes/MigrateWalletCalculateFeeScene.tsx +++ b/src/components/scenes/MigrateWalletCalculateFeeScene.tsx @@ -134,7 +134,8 @@ const MigrateWalletCalculateFeeComponent = (props: Props) => { displayDenominations[pluginId]?.[currencyCode] ?? exchangeDenom const transactionFee = convertTransactionFeeToDisplayFee( - wallet.currencyInfo.currencyCode, + wallet.currencyInfo.pluginId, + null, isoFiatCurrencyCode, exchangeRates, fakeEdgeTransaction, diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 86147f62173..74bc4f0fcb2 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -404,7 +404,8 @@ const SendComponent = (props: Props) => { title={title} exchangeRates={exchangeRates} nativeAmount={nativeAmount ?? ''} - currencyCode={currencyCode} + pluginId={pluginId} + tokenId={tokenId} exchangeDenomination={cryptoExchangeDenomination} displayDenomination={cryptoDisplayDenomination} lockInputs={lockTilesMap.amount ?? false} @@ -535,7 +536,8 @@ const SendComponent = (props: Props) => { title={title} exchangeRates={exchangeRates} nativeAmount={nativeAmount ?? ''} - currencyCode={currencyCode} + pluginId={pluginId} + tokenId={tokenId} exchangeDenomination={cryptoExchangeDenomination} displayDenomination={cryptoDisplayDenomination} lockInputs={lockTilesMap.amount ?? false} @@ -689,7 +691,8 @@ const SendComponent = (props: Props) => { if (edgeTransaction != null) { const transactionFee = convertTransactionFeeToDisplayFee( - coreWallet.currencyInfo.currencyCode, + coreWallet.currencyInfo.pluginId, + null, defaultIsoFiat, exchangeRates, edgeTransaction, diff --git a/src/components/scenes/Staking/StakeModifyScene.tsx b/src/components/scenes/Staking/StakeModifyScene.tsx index 61712ccfb85..e94b3a80f06 100644 --- a/src/components/scenes/Staking/StakeModifyScene.tsx +++ b/src/components/scenes/Staking/StakeModifyScene.tsx @@ -478,7 +478,8 @@ const StakeModifySceneComponent = (props: Props) => { key={allocationType + pluginId + currencyCode} exchangeRates={guiExchangeRates} nativeAmount={nativeAmount} - currencyCode={quoteCurrencyCode} + pluginId={pluginId} + tokenId={tokenId} exchangeDenomination={quoteDenom} displayDenomination={quoteDenom} lockInputs={ diff --git a/src/components/scenes/SwapConfirmationScene.tsx b/src/components/scenes/SwapConfirmationScene.tsx index c16887f44b3..e3ab21ef288 100644 --- a/src/components/scenes/SwapConfirmationScene.tsx +++ b/src/components/scenes/SwapConfirmationScene.tsx @@ -90,7 +90,8 @@ export const SwapConfirmationScene = (props: Props) => { ? '0' : convertCurrencyFromExchangeRates( state.exchangeRates, - selectedQuote.networkFee.currencyCode, + selectedQuote.pluginId, + selectedQuote.networkFee.tokenId, state.ui.settings.defaultIsoFiat, selectedQuote.networkFee.nativeAmount ) diff --git a/src/components/scenes/SweepPrivateKeyCalculateFeeScene.tsx b/src/components/scenes/SweepPrivateKeyCalculateFeeScene.tsx index c1e18fe10e7..7c4058a2db7 100644 --- a/src/components/scenes/SweepPrivateKeyCalculateFeeScene.tsx +++ b/src/components/scenes/SweepPrivateKeyCalculateFeeScene.tsx @@ -120,7 +120,8 @@ const SweepPrivateKeyCalculateFeeComponent = (props: Props) => { displayDenominations[pluginId]?.[currencyCode] ?? exchangeDenom const transactionFee = convertTransactionFeeToDisplayFee( - currencyCode, + receivingWallet.currencyInfo.pluginId, + null, defaultFiat, exchangeRates, tx, diff --git a/src/components/scenes/TransactionDetailsScene.tsx b/src/components/scenes/TransactionDetailsScene.tsx index 31e65916f12..04690298270 100644 --- a/src/components/scenes/TransactionDetailsScene.tsx +++ b/src/components/scenes/TransactionDetailsScene.tsx @@ -136,7 +136,8 @@ const TransactionDetailsComponent = (props: Props) => { parseFloat( convertCurrencyFromExchangeRates( state.exchangeRates, - currencyCode, + currencyInfo.pluginId, + tokenId, defaultIsoFiat, absExchangeAmount ) diff --git a/src/components/services/AccountCallbackManager.tsx b/src/components/services/AccountCallbackManager.tsx index d27664cceda..9b2d0ea0bfd 100644 --- a/src/components/services/AccountCallbackManager.tsx +++ b/src/components/services/AccountCallbackManager.tsx @@ -158,7 +158,8 @@ export function AccountCallbackManager(props: Props) { const usdAmount = parseFloat( convertCurrencyFromExchangeRates( exchangeRates, - tx.currencyCode, + wallet.currencyInfo.pluginId, + tx.tokenId, 'iso:USD', String(cryptoAmount) ) diff --git a/src/components/tiles/EditableAmountTile.tsx b/src/components/tiles/EditableAmountTile.tsx index e6ead4009ff..14f20d83ea1 100644 --- a/src/components/tiles/EditableAmountTile.tsx +++ b/src/components/tiles/EditableAmountTile.tsx @@ -1,5 +1,5 @@ import { div, round, toFixed } from 'biggystring' -import type { EdgeDenomination } from 'edge-core-js' +import type { EdgeDenomination, EdgeTokenId } from 'edge-core-js' import * as React from 'react' import { lstrings } from '../../locales/strings' @@ -20,7 +20,8 @@ interface Props { title: string exchangeRates: GuiExchangeRates nativeAmount: string - currencyCode: string + pluginId: string + tokenId: EdgeTokenId exchangeDenomination: EdgeDenomination displayDenomination: EdgeDenomination lockInputs: boolean @@ -36,7 +37,8 @@ export const EditableAmountTile = (props: Props) => { title, exchangeRates, nativeAmount, - currencyCode, + pluginId, + tokenId, exchangeDenomination, displayDenomination, lockInputs, @@ -66,7 +68,8 @@ export const EditableAmountTile = (props: Props) => { ) const fiatAmount = convertCurrencyFromExchangeRates( exchangeRates, - currencyCode, + pluginId, + tokenId, isoFiatCurrencyCode, exchangeAmount ) diff --git a/src/selectors/WalletSelectors.ts b/src/selectors/WalletSelectors.ts index 4e9ac32099a..7dc827bdbf7 100644 --- a/src/selectors/WalletSelectors.ts +++ b/src/selectors/WalletSelectors.ts @@ -95,7 +95,8 @@ export const calculateFiatBalance = ( ) const fiatValue = convertCurrencyFromExchangeRates( exchangeRates, - currencyCode, + wallet.currencyInfo.pluginId, + null, isoFiatCurrencyCode, cryptoAmount ) diff --git a/src/util/ActionProgramUtils.ts b/src/util/ActionProgramUtils.ts index 1a12eae6c5e..979bddf9dbc 100644 --- a/src/util/ActionProgramUtils.ts +++ b/src/util/ActionProgramUtils.ts @@ -341,7 +341,8 @@ export const makeAaveCloseAction = async ({ const collateralDenom = collateralDenoms[0] const collateralFiat = convertCurrencyFromExchangeRates( exchangeRates, - collateralCurrencyCode, + wallet.currencyInfo.pluginId, + collateralTokenId, defaultIsoFiat, convertNativeToExchange(collateralDenom.multiplier)( collateral.nativeAmount @@ -350,14 +351,16 @@ export const makeAaveCloseAction = async ({ const debtDenom = debtDenoms[0] const principalFiat = convertCurrencyFromExchangeRates( exchangeRates, - debtCurrencyCode, + wallet.currencyInfo.pluginId, + debtTokenId, defaultIsoFiat, convertNativeToExchange(debtDenom.multiplier)(debt.nativeAmount) ) const debtBalanceNativeAmount = wallet.balanceMap.get(debtTokenId) ?? '0' const debtBalanceFiat = convertCurrencyFromExchangeRates( exchangeRates, - debtCurrencyCode, + wallet.currencyInfo.pluginId, + debtTokenId, defaultIsoFiat, convertNativeToExchange(debtDenom.multiplier)(debtBalanceNativeAmount) ) diff --git a/src/util/utils.ts b/src/util/utils.ts index c398f0eb8fa..8410ca980dd 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -5,6 +5,7 @@ import type { EdgeDenomination, EdgePluginMap, EdgeToken, + EdgeTokenId, EdgeTokenMap, EdgeTransaction } from 'edge-core-js' @@ -179,11 +180,12 @@ export const roundedFee = ( export const convertCurrencyFromExchangeRates = ( exchangeRates: GuiExchangeRates, - fromCurrencyCode: string, + fromPluginId: string, + fromTokenId: EdgeTokenId, toCurrencyCode: string, amount: string ): string => { - const rateKey = `${fromCurrencyCode}_${toCurrencyCode}` + const rateKey = `${fromPluginId}_${String(fromTokenId)}_${toCurrencyCode}` const rate = exchangeRates[rateKey] ?? '0' const convertedAmount = mul(amount, rate) return convertedAmount @@ -503,7 +505,8 @@ export const feeStyle = { } export const convertTransactionFeeToDisplayFee = ( - currencyCode: string, + pluginId: string, + tokenId: EdgeTokenId, isoFiatCurrencyCode: string, exchangeRates: GuiExchangeRates, transaction: EdgeTransaction, @@ -550,13 +553,15 @@ export const convertTransactionFeeToDisplayFee = ( convertNativeToExchange(exchangeMultiplier)(feeNativeAmount) const fiatFeeAmount = convertCurrencyFromExchangeRates( exchangeRates, - currencyCode, + pluginId, + tokenId, isoFiatCurrencyCode, cryptoFeeExchangeAmount ) const feeAmountInUSD = convertCurrencyFromExchangeRates( exchangeRates, - currencyCode, + pluginId, + tokenId, 'iso:USD', cryptoFeeExchangeAmount ) From c28056f9c7e7e11128ade7595bea02469b632f76 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 11:35:58 -0700 Subject: [PATCH 4/7] Fix eslint warning in ExchangeRateActions.ts --- src/actions/ExchangeRateActions.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/actions/ExchangeRateActions.ts b/src/actions/ExchangeRateActions.ts index 77cd78da331..ac3f3b66f6d 100644 --- a/src/actions/ExchangeRateActions.ts +++ b/src/actions/ExchangeRateActions.ts @@ -67,10 +67,12 @@ export function updateExchangeRates(): ThunkAction> { // If this is the first run, immediately use whatever we have on disk // before moving on to the potentially slow network: if (state.exchangeRatesMap.size === 0 || exchangeRateCache == null) { - exchangeRateCache = await loadExchangeRateCache().catch(error => { - datelog('Error loading exchange rate cache:', String(error)) - return { assetPairs: [], rates: {} } - }) + exchangeRateCache = await loadExchangeRateCache().catch( + (error: unknown) => { + datelog('Error loading exchange rate cache:', String(error)) + return { assetPairs: [], rates: {} } + } + ) const { exchangeRates, exchangeRatesMap } = buildGuiRates( exchangeRateCache.rates, yesterday @@ -153,7 +155,7 @@ async function fetchExchangeRates( // Maintain a map of the unique asset pairs we need: const assetPairMap = new Map() - function addAssetPair(assetPair: AssetPair) { + function addAssetPair(assetPair: AssetPair): void { const key = `${assetPair.currency_pair}_${assetPair.date ?? ''}` assetPairMap.set(key, assetPair) } @@ -216,7 +218,7 @@ async function fetchExchangeRates( } } - const assetPairs = [...assetPairMap.values()] + const assetPairs = Array.from(assetPairMap.values()) for (let i = 0; i < assetPairs.length; i += RATES_SERVER_MAX_QUERY_SIZE) { const query = assetPairs.slice(i, i + RATES_SERVER_MAX_QUERY_SIZE) @@ -241,9 +243,9 @@ async function fetchExchangeRates( expiration: rateExpiration, rate: parseFloat(exchangeRate) } - } else if (rates[key] == null) { + } else { // We at least need a placeholder: - rates[key] = { + rates[key] ??= { expiration: 0, rate: 0 } @@ -265,7 +267,7 @@ async function fetchExchangeRates( // Write the cache to disk: await disklet .setText(EXCHANGE_RATES_FILENAME, JSON.stringify(exchangeRateCache)) - .catch(error => { + .catch((error: unknown) => { datelog('Error saving exchange rate cache:', String(error)) }) } @@ -291,7 +293,7 @@ function buildGuiRates( string, string | undefined ] - const reverseKey = `${codeB}_${codeA}${date ? '_' + date : ''}` + const reverseKey = `${codeB}_${codeA}${date != null ? '_' + date : ''}` out[reverseKey] = rate === 0 ? 0 : 1 / rate // Set up exchange rate map. This nest map is keyed This map will hold current rate and 24 hour rate, if available. From b2041b1a47781ba4292cd7b7548fc47cca3c45d8 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 12:58:01 -0700 Subject: [PATCH 5/7] Fix exchange rate test expected values --- src/__tests__/exchangeRates.test.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/__tests__/exchangeRates.test.ts b/src/__tests__/exchangeRates.test.ts index c7ae093dffa..53b255ec37d 100644 --- a/src/__tests__/exchangeRates.test.ts +++ b/src/__tests__/exchangeRates.test.ts @@ -36,7 +36,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`31616 === ${Math.floor(v)}`) - expect(31616).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(31616) }) ) @@ -48,7 +48,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1814 === ${Math.floor(v)}`) - expect(1814).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1814) }) ) @@ -60,7 +60,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`184 === ${Math.floor(v)}`) - expect(184).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(184) }) ) @@ -72,7 +72,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1553326 === ${Math.floor(v)}`) - expect(1553326).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1553326) }) ) @@ -84,7 +84,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1 === ${Math.floor(v)}`) - expect(1).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1) }) ) @@ -98,7 +98,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1553326 === ${Math.floor(v)}`) - expect(1553326).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1553326) }) ) @@ -110,7 +110,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1 === ${Math.floor(v)}`) - expect(1).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1) }) ) @@ -124,7 +124,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1814 === ${Math.floor(v)}`) - expect(1814).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1814) }) ) @@ -136,7 +136,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`184 === ${Math.floor(v)}`) - expect(184).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(184) }) ) @@ -148,7 +148,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1553326 === ${Math.floor(v)}`) - expect(1553326).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1553326) }) ) @@ -160,7 +160,7 @@ it('get bulk rates', async () => { fetch ).then(v => { console.log(`1 === ${Math.floor(v)}`) - expect(1).toEqual(Math.floor(v)) + expect(Math.floor(v)).toEqual(1) }) ) From aa64328f85902e557c257a6a4ff5433a5698cb77 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 22:51:16 -0700 Subject: [PATCH 6/7] Use rates v3 in historical rate functions --- .../v3/rates/bf6a2039b07c447cd11554af546f00a3 | 69 ++++ src/__tests__/exchangeRates.test.ts | 100 +++-- src/actions/TransactionExportActions.tsx | 9 +- .../scenes/TransactionDetailsScene.tsx | 7 +- src/components/themed/TransactionListRow.tsx | 4 +- src/hooks/useHistoricalRate.ts | 19 +- src/plugins/gui/amountQuotePlugin.ts | 9 +- src/plugins/gui/fiatPlugin.tsx | 8 +- src/plugins/gui/fiatPluginTypes.ts | 7 +- src/plugins/gui/providers/paybisProvider.ts | 17 +- .../thorchainSavers/tcSaversPlugin.tsx | 18 +- .../thorchainSavers/tcSaversPluginSegwit.tsx | 18 +- src/util/exchangeRates.ts | 356 ++++++++++++------ src/util/network.ts | 2 +- 14 files changed, 467 insertions(+), 176 deletions(-) create mode 100644 src/__mocks__/snapshots/default/POST/http:/127.0.0.1:8087/v3/rates/bf6a2039b07c447cd11554af546f00a3 diff --git a/src/__mocks__/snapshots/default/POST/http:/127.0.0.1:8087/v3/rates/bf6a2039b07c447cd11554af546f00a3 b/src/__mocks__/snapshots/default/POST/http:/127.0.0.1:8087/v3/rates/bf6a2039b07c447cd11554af546f00a3 new file mode 100644 index 00000000000..e2db9647d57 --- /dev/null +++ b/src/__mocks__/snapshots/default/POST/http:/127.0.0.1:8087/v3/rates/bf6a2039b07c447cd11554af546f00a3 @@ -0,0 +1,69 @@ +{ + "request": { + "method": "POST", + "url": "https://rates3.edge.app/v3/rates", + "body": "{\"targetFiat\":\"USD\",\"crypto\":[{\"isoDate\":\"2022-06-01T04:00:00.000Z\",\"asset\":{\"pluginId\":\"bitcoin\",\"tokenId\":null}},{\"isoDate\":\"2022-06-02T04:00:00.000Z\",\"asset\":{\"pluginId\":\"ethereum\",\"tokenId\":null}},{\"isoDate\":\"2022-06-03T04:00:00.000Z\",\"asset\":{\"pluginId\":\"monero\",\"tokenId\":null}},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"asset\":{\"pluginId\":\"bitcoin\",\"tokenId\":null}},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"asset\":{\"pluginId\":\"dogecoin\",\"tokenId\":null}}],\"fiat\":[{\"isoDate\":\"2022-06-01T04:00:00.000Z\",\"fiatCode\":\"USD\"},{\"isoDate\":\"2022-06-02T04:00:00.000Z\",\"fiatCode\":\"USD\"},{\"isoDate\":\"2022-06-03T04:00:00.000Z\",\"fiatCode\":\"EUR\"},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"fiatCode\":\"PHP\"},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"fiatCode\":\"MXN\"}]}", + "headers": [ + [ + "accept", + "*/*" + ], + [ + "accept-encoding", + "gzip,deflate" + ], + [ + "connection", + "close" + ], + [ + "content-length", + "746" + ], + [ + "content-type", + "application/json" + ], + [ + "host", + "127.0.0.1:8087" + ], + [ + "user-agent", + "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" + ] + ], + "cookies": [] + }, + "response": { + "status": 200, + "statusText": "OK", + "body": "{\"targetFiat\":\"USD\",\"crypto\":[{\"isoDate\":\"2022-06-01T04:00:00.000Z\",\"asset\":{\"pluginId\":\"bitcoin\"},\"rate\":31865.749621173167},{\"isoDate\":\"2022-06-02T04:00:00.000Z\",\"asset\":{\"pluginId\":\"ethereum\"},\"rate\":1828.8926546074292},{\"isoDate\":\"2022-06-03T04:00:00.000Z\",\"asset\":{\"pluginId\":\"monero\"},\"rate\":193.59642765955266},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"asset\":{\"pluginId\":\"bitcoin\"},\"rate\":29836.29247906769},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"asset\":{\"pluginId\":\"dogecoin\"},\"rate\":0.08186741002113526}],\"fiat\":[{\"isoDate\":\"2022-06-01T04:00:00.000Z\",\"fiatCode\":\"USD\",\"rate\":1},{\"isoDate\":\"2022-06-02T04:00:00.000Z\",\"fiatCode\":\"USD\",\"rate\":1},{\"isoDate\":\"2022-06-03T04:00:00.000Z\",\"fiatCode\":\"EUR\",\"rate\":1.071983},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"fiatCode\":\"PHP\",\"rate\":0.0189},{\"isoDate\":\"2022-06-04T04:00:00.000Z\",\"fiatCode\":\"MXN\",\"rate\":0.051164}]}", + "headers": [ + [ + "connection", + "close" + ], + [ + "content-length", + "865" + ], + [ + "content-type", + "application/json; charset=utf-8" + ], + [ + "date", + "Fri, 15 Aug 2025 06:17:36 GMT" + ], + [ + "etag", + "W/\"361-NyuozaijAY3BRKA3B5FAo6hJHXM\"" + ], + [ + "x-powered-by", + "Express" + ] + ] + } +} \ No newline at end of file diff --git a/src/__tests__/exchangeRates.test.ts b/src/__tests__/exchangeRates.test.ts index 53b255ec37d..6f49020551c 100644 --- a/src/__tests__/exchangeRates.test.ts +++ b/src/__tests__/exchangeRates.test.ts @@ -5,7 +5,7 @@ import { closestRateForTimestamp, type ExchangeRateCache } from '../actions/ExchangeRateActions' -import { getHistoricalRate } from '../util/exchangeRates' +import { getHistoricalCryptoRate } from '../util/exchangeRates' import { mswServer } from '../util/mswServer' import { snooze } from '../util/utils' @@ -29,56 +29,66 @@ it('get bulk rates', async () => { // async function main(): Promise { const promises: Array> = [] promises.push( - getHistoricalRate( - 'BTC_iso:USD', + getHistoricalCryptoRate( + 'bitcoin', + null, + 'iso:USD', '2022-06-01T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`31616 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(31616) + console.log(`31865 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(31865) }) ) promises.push( - getHistoricalRate( - 'ETH_iso:USD', + getHistoricalCryptoRate( + 'ethereum', + null, + 'iso:USD', '2022-06-02T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`1814 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(1814) + console.log(`1828 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(1828) }) ) promises.push( - getHistoricalRate( - 'XMR_iso:EUR', + getHistoricalCryptoRate( + 'monero', + null, + 'iso:EUR', '2022-06-03T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`184 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(184) + console.log(`180 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(180) }) ) promises.push( - getHistoricalRate( - 'BTC_iso:PHP', + getHistoricalCryptoRate( + 'bitcoin', + null, + 'iso:PHP', '2022-06-04T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`1553326 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(1553326) + console.log(`1578639 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(1578639) }) ) promises.push( - getHistoricalRate( - 'DOGE_iso:MXN', + getHistoricalCryptoRate( + 'dogecoin', + null, + 'iso:MXN', '2022-06-04T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch @@ -91,20 +101,24 @@ it('get bulk rates', async () => { await snooze(2000) promises.push( - getHistoricalRate( - 'BTC_iso:PHP', + getHistoricalCryptoRate( + 'bitcoin', + null, + 'iso:PHP', '2022-06-04T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`1553326 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(1553326) + console.log(`1578639 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(1578639) }) ) promises.push( - getHistoricalRate( - 'DOGE_iso:MXN', + getHistoricalCryptoRate( + 'dogecoin', + null, + 'iso:MXN', '2022-06-04T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch @@ -117,44 +131,52 @@ it('get bulk rates', async () => { await snooze(1000) promises.push( - getHistoricalRate( - 'ETH_iso:USD', + getHistoricalCryptoRate( + 'ethereum', + null, + 'iso:USD', '2022-06-02T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`1814 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(1814) + console.log(`1828 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(1828) }) ) promises.push( - getHistoricalRate( - 'XMR_iso:EUR', + getHistoricalCryptoRate( + 'monero', + null, + 'iso:EUR', '2022-06-03T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`184 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(184) + console.log(`180 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(180) }) ) promises.push( - getHistoricalRate( - 'BTC_iso:PHP', + getHistoricalCryptoRate( + 'bitcoin', + null, + 'iso:PHP', '2022-06-04T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch ).then(v => { - console.log(`1553326 === ${Math.floor(v)}`) - expect(Math.floor(v)).toEqual(1553326) + console.log(`1578639 === ${Math.floor(v)}`) + expect(Math.floor(v)).toEqual(1578639) }) ) promises.push( - getHistoricalRate( - 'DOGE_iso:MXN', + getHistoricalCryptoRate( + 'dogecoin', + null, + 'iso:MXN', '2022-06-04T04:00:00.000Z', TEST_MAX_QUERY_SIZE, fetch diff --git a/src/actions/TransactionExportActions.tsx b/src/actions/TransactionExportActions.tsx index 3acdf43153e..9f62ae02a46 100644 --- a/src/actions/TransactionExportActions.tsx +++ b/src/actions/TransactionExportActions.tsx @@ -9,7 +9,7 @@ import shajs from 'sha.js' import { getExchangeDenom } from '../selectors/DenominationSelectors' import type { ThunkAction } from '../types/reduxTypes' -import { getHistoricalRate } from '../util/exchangeRates' +import { getHistoricalCryptoRate } from '../util/exchangeRates' import { DECIMAL_PRECISION } from '../util/utils' const UPDATE_TXS_MAX_PROMISES = 10 @@ -56,7 +56,12 @@ export function updateTxsFiat( if (amountFiat === 0) { const date = new Date(tx.date * 1000).toISOString() promises.push( - getHistoricalRate(`${currencyCode}_${defaultIsoFiat}`, date) + getHistoricalCryptoRate( + wallet.currencyInfo.pluginId, + tokenId, + wallet.fiatCurrencyCode, + date + ) .then(rate => { tx.metadata = { ...tx.metadata, diff --git a/src/components/scenes/TransactionDetailsScene.tsx b/src/components/scenes/TransactionDetailsScene.tsx index 04690298270..1d54302e405 100644 --- a/src/components/scenes/TransactionDetailsScene.tsx +++ b/src/components/scenes/TransactionDetailsScene.tsx @@ -77,8 +77,7 @@ export interface TransactionDetailsParams { const TransactionDetailsComponent = (props: Props) => { const { navigation, route, wallet } = props const { edgeTransaction: transaction, walletId, onDone } = route.params - const { currencyCode, metadata, nativeAmount, date, txid, tokenId } = - transaction + const { metadata, nativeAmount, date, txid, tokenId } = transaction const { currencyInfo } = wallet const theme = useTheme() @@ -149,7 +148,9 @@ const TransactionDetailsComponent = (props: Props) => { const dateString = dateObj.toLocaleString() const isoDate = dateObj.toISOString() const historicRate = useHistoricalRate( - `${currencyCode}_${defaultIsoFiat}`, + currencyInfo.pluginId, + tokenId, + defaultIsoFiat, isoDate ) const historicFiat = historicRate * Number(absExchangeAmount) diff --git a/src/components/themed/TransactionListRow.tsx b/src/components/themed/TransactionListRow.tsx index 84246318c82..40b24d7e792 100644 --- a/src/components/themed/TransactionListRow.tsx +++ b/src/components/themed/TransactionListRow.tsx @@ -131,7 +131,9 @@ function TransactionViewInner(props: TransactionViewInnerProps) { // Fiat Amount const isoDate = new Date(transaction.date * 1000).toISOString() const historicalRate = useHistoricalRate( - `${currencyCode}_${defaultIsoFiat}`, + currencyInfo.pluginId, + tokenId, + defaultIsoFiat, isoDate ) const amountFiat = diff --git a/src/hooks/useHistoricalRate.ts b/src/hooks/useHistoricalRate.ts index a86e7092d1f..d6471c1b977 100644 --- a/src/hooks/useHistoricalRate.ts +++ b/src/hooks/useHistoricalRate.ts @@ -1,17 +1,26 @@ -import type { EdgeFetchFunction } from 'edge-core-js' +import type { EdgeFetchFunction, EdgeTokenId } from 'edge-core-js' -import { getHistoricalRate } from '../util/exchangeRates' +import { getHistoricalCryptoRate } from '../util/exchangeRates' import { useAsyncValue } from './useAsyncValue' export const useHistoricalRate = ( - codePair: string, + pluginId: string, + tokenId: EdgeTokenId, + targetFiat: string, date: string, maxQuerySize?: number, doFetch?: EdgeFetchFunction ) => { const [historicalRate = 0] = useAsyncValue(async (): Promise => { - return await getHistoricalRate(codePair, date, maxQuerySize, doFetch) - }, [codePair, date, maxQuerySize, doFetch]) + return await getHistoricalCryptoRate( + pluginId, + tokenId, + targetFiat, + date, + maxQuerySize, + doFetch + ) + }, [pluginId, tokenId, targetFiat, date, maxQuerySize, doFetch]) return historicalRate } diff --git a/src/plugins/gui/amountQuotePlugin.ts b/src/plugins/gui/amountQuotePlugin.ts index 1b8d5a2a668..d9b288c586b 100644 --- a/src/plugins/gui/amountQuotePlugin.ts +++ b/src/plugins/gui/amountQuotePlugin.ts @@ -11,7 +11,7 @@ import { getCurrencyCode, getCurrencyCodeMultiplier } from '../../util/CurrencyInfoHelpers' -import { getHistoricalRate } from '../../util/exchangeRates' +import { getHistoricalFiatRate } from '../../util/exchangeRates' import { infoServerData } from '../../util/network' import { logEvent } from '../../util/tracking' import { @@ -113,8 +113,11 @@ async function getInitialFiatValue( ) { let initialValue1: string | undefined if (isoFiatCurrencyCode !== 'iso:USD') { - const ratePair = `${isoFiatCurrencyCode}_iso:USD` - const rate = await getHistoricalRate(ratePair, date) + const rate = await getHistoricalFiatRate( + isoFiatCurrencyCode, + 'iso:USD', + date + ) initialValue1 = div(startingFiatAmount, String(rate), DECIMAL_PRECISION) // Round out all decimals diff --git a/src/plugins/gui/fiatPlugin.tsx b/src/plugins/gui/fiatPlugin.tsx index eabde0b5d24..98ad77b981f 100644 --- a/src/plugins/gui/fiatPlugin.tsx +++ b/src/plugins/gui/fiatPlugin.tsx @@ -41,7 +41,10 @@ import type { NavigationBase, SellTabSceneProps } from '../../types/routerTypes' -import { getHistoricalRate } from '../../util/exchangeRates' +import { + getHistoricalCryptoRate, + getHistoricalFiatRate +} from '../../util/exchangeRates' import { getNavigationAbsolutePath } from '../../util/routerUtils' import type { BuyConversionValues, @@ -432,7 +435,8 @@ export const executePlugin = async (params: { } const pluginUtils: FiatPluginUtils = { - getHistoricalRate + getHistoricalCryptoRate, + getHistoricalFiatRate } if (guiPlugin.nativePlugin == null) { diff --git a/src/plugins/gui/fiatPluginTypes.ts b/src/plugins/gui/fiatPluginTypes.ts index b22fb0da264..d407ce038a0 100644 --- a/src/plugins/gui/fiatPluginTypes.ts +++ b/src/plugins/gui/fiatPluginTypes.ts @@ -22,6 +22,10 @@ import type { HomeAddress, SepaInfo } from '../../types/FormTypes' import type { GuiPlugin } from '../../types/GuiPluginTypes' import type { AppParamList } from '../../types/routerTypes' import type { EdgeAsset } from '../../types/types' +import type { + getHistoricalCryptoRate, + getHistoricalFiatRate +} from '../../util/exchangeRates' import type { BuyConversionValues, SellConversionValues, @@ -198,7 +202,8 @@ export interface FiatPluginUi { } export interface FiatPluginUtils { - getHistoricalRate: (codePair: string, date: string) => Promise + getHistoricalCryptoRate: typeof getHistoricalCryptoRate + getHistoricalFiatRate: typeof getHistoricalFiatRate } export interface FiatPluginFactoryArgs { diff --git a/src/plugins/gui/providers/paybisProvider.ts b/src/plugins/gui/providers/paybisProvider.ts index 861fe3e4a1b..5d5d6573c0b 100644 --- a/src/plugins/gui/providers/paybisProvider.ts +++ b/src/plugins/gui/providers/paybisProvider.ts @@ -616,15 +616,26 @@ export const paybisProvider: FiatProviderFactory = { let promoCode: string | undefined if (maybePromoCode != null) { + const isoNow = new Date().toISOString() let amountUsd: string const convertFromCc = amountType === 'fiat' ? fiatCurrencyCode : displayCurrencyCode if (convertFromCc === 'iso:USD') { amountUsd = exchangeAmount + } else if (convertFromCc.startsWith('iso:')) { + const rate = await pluginUtils.getHistoricalFiatRate( + convertFromCc, + 'iso:USD', + isoNow + ) + amountUsd = mul(exchangeAmount, String(rate)) } else { - const isoNow = new Date().toISOString() - const ratePair = `${convertFromCc}_iso:USD` - const rate = await pluginUtils.getHistoricalRate(ratePair, isoNow) + const rate = await pluginUtils.getHistoricalCryptoRate( + currencyPluginId, + tokenId, + 'iso:USD', + isoNow + ) amountUsd = mul(exchangeAmount, String(rate)) } // Only use the promo code if the user is requesting $1000 USD or less diff --git a/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.tsx b/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.tsx index fb042374de5..d46f76e7b91 100644 --- a/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.tsx +++ b/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.tsx @@ -31,7 +31,7 @@ import { getTokenId, getWalletTokenId } from '../../../util/CurrencyInfoHelpers' -import { getHistoricalRate } from '../../../util/exchangeRates' +import { getHistoricalCryptoRate } from '../../../util/exchangeRates' import { cleanMultiFetch, fetchInfo, @@ -636,10 +636,20 @@ const stakeRequest = async ( const parentCurrencyCode = wallet.currencyInfo.currencyCode let parentToTokenRate: number = 1 if (currencyCode !== parentCurrencyCode) { - parentToTokenRate = await getHistoricalRate( - `${parentCurrencyCode}_${currencyCode}`, - new Date().toISOString() + const isoDate = new Date().toISOString() + const parentRate = await getHistoricalCryptoRate( + pluginId, + null, + 'iso:USD', + isoDate + ) + const tokenRate = await getHistoricalCryptoRate( + pluginId, + tokenId, + 'iso:USD', + isoDate ) + parentToTokenRate = parentRate / tokenRate } const parentMultiplier = getCurrencyCodeMultiplier( wallet.currencyConfig, diff --git a/src/plugins/stake-plugins/thorchainSavers/tcSaversPluginSegwit.tsx b/src/plugins/stake-plugins/thorchainSavers/tcSaversPluginSegwit.tsx index 93bfa411d02..a15b880c9b0 100644 --- a/src/plugins/stake-plugins/thorchainSavers/tcSaversPluginSegwit.tsx +++ b/src/plugins/stake-plugins/thorchainSavers/tcSaversPluginSegwit.tsx @@ -31,7 +31,7 @@ import { getTokenId, getWalletTokenId } from '../../../util/CurrencyInfoHelpers' -import { getHistoricalRate } from '../../../util/exchangeRates' +import { getHistoricalCryptoRate } from '../../../util/exchangeRates' import { cleanMultiFetch, fetchInfo, @@ -631,10 +631,20 @@ const stakeRequest = async ( const parentCurrencyCode = wallet.currencyInfo.currencyCode let parentToTokenRate: number = 1 if (currencyCode !== parentCurrencyCode) { - parentToTokenRate = await getHistoricalRate( - `${parentCurrencyCode}_${currencyCode}`, - new Date().toISOString() + const isoDate = new Date().toISOString() + const parentRate = await getHistoricalCryptoRate( + pluginId, + null, + 'iso:USD', + isoDate + ) + const tokenRate = await getHistoricalCryptoRate( + pluginId, + tokenId, + 'iso:USD', + isoDate ) + parentToTokenRate = parentRate / tokenRate } const parentMultiplier = getCurrencyCodeMultiplier( wallet.currencyConfig, diff --git a/src/util/exchangeRates.ts b/src/util/exchangeRates.ts index 0825cf4bcac..dc5e9e694a4 100644 --- a/src/util/exchangeRates.ts +++ b/src/util/exchangeRates.ts @@ -1,121 +1,191 @@ -import { asArray, asEither, asNull, asObject, asString } from 'cleaners' -import type { EdgeFetchFunction } from 'edge-core-js' +import { + asArray, + asDate, + asEither, + asNull, + asNumber, + asObject, + asOptional, + asString +} from 'cleaners' +import type { EdgeFetchFunction, EdgeTokenId } from 'edge-core-js' import { fetchRates } from './network' +import { removeIsoPrefix } from './utils' const RATES_SERVER_MAX_QUERY_SIZE = 100 const FETCH_FREQUENCY = 1000 const SHOW_LOGS = false const clog = SHOW_LOGS ? console.log : (...args: any) => undefined -const asRatesResponse = asObject({ - data: asArray( - asObject({ - currency_pair: asString, - date: asString, - exchangeRate: asEither(asString, asNull) - }) - ) + +// From rates server: +const asCryptoAsset = asObject({ + pluginId: asString, + tokenId: asOptional(asEither(asString, asNull)) +}) +const asCryptoRate = asObject({ + isoDate: asOptional(asDate), + asset: asCryptoAsset, + rate: asOptional(asNumber) // Return undefined if unable to get rate +}) +const asFiatRate = asObject({ + isoDate: asOptional(asDate), + fiatCode: asString, + rate: asOptional(asNumber) // Return undefined if unable to get rate }) +const asRatesParams = asObject({ + targetFiat: asString, + crypto: asArray(asCryptoRate), + fiat: asArray(asFiatRate) +}) +type RatesParams = ReturnType type RateMap = Record -interface RateQueueEntry { - currency_pair: string - date: string -} - const rateMap: RateMap = {} const resolverMap: Record< string, { resolvers: Function[] - rateQueueEntry: RateQueueEntry + rateQueueEntry: RatesParams } > = {} let inQuery = false -const makePairDate = (currencyPair: string, date: string) => - `${currencyPair}_${date}` - -export const roundHalfMinute = (dateStr: string) => { - const date = new Date(dateStr) - let seconds = date.getSeconds() - seconds = seconds > 30 ? 30 : 0 - - date.setMinutes(0) - date.setSeconds(seconds) - date.setMilliseconds(0) - return date.toISOString() -} - let numDoQuery = 0 const doQuery = async (doFetch?: EdgeFetchFunction): Promise => { const n = numDoQuery++ clog(`${n} doQuery enter`) - const data: RateQueueEntry[] = [] + const groupedParams = new Map() // Fill the query up to RATES_SERVER_MAX_QUERY_SIZE entries const values = Object.values(resolverMap) for (const value of values) { - data.push(value.rateQueueEntry) - if (data.length === RATES_SERVER_MAX_QUERY_SIZE) break - } + const map = groupedParams.get(value.rateQueueEntry.targetFiat) + if (map == null) { + groupedParams.set(value.rateQueueEntry.targetFiat, value.rateQueueEntry) + } else { + const combinedMap: RatesParams = { + targetFiat: map.targetFiat, + crypto: [...map.crypto, ...value.rateQueueEntry.crypto], + fiat: [...map.fiat, ...value.rateQueueEntry.fiat] + } + groupedParams.set(value.rateQueueEntry.targetFiat, combinedMap) + } - clog(`${n} fetching ${JSON.stringify(data, null, 2)}`) - const options = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data }) + if (map?.crypto.length === RATES_SERVER_MAX_QUERY_SIZE) break } - try { - const response = await fetchRates( - 'v2/exchangeRates', - options, - 5000, - doFetch - ) - if (response.ok) { - const json = await response.json() - const cleanedRates = asRatesResponse(json) - for (const rateObj of cleanedRates.data) { - const { currency_pair: currencyPair, date } = rateObj - let { exchangeRate } = rateObj - if (exchangeRate == null) { - console.log(`${n} doQuery: ${currencyPair} ${date} exchangeRate=null`) - exchangeRate = '0' - } - const rate = parseFloat(exchangeRate) - const pairDate = makePairDate(currencyPair, date) - rateMap[pairDate] = rate - if (resolverMap[pairDate] == null) { - clog(`${n} oops`) - continue - } - const { resolvers } = resolverMap[pairDate] - clog(`${n} deleting ${pairDate}`) - delete resolverMap[pairDate] - if (resolvers.length) { - resolvers.forEach((r, i) => { - r(rate) - }) + + // clog(`${n} fetching ${JSON.stringify(data, null, 2)}`) + for (const data of groupedParams.values()) { + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + } + try { + const response = await fetchRates('v3/rates', options, 5000, doFetch) + if (response.ok) { + const json = await response.json() + const cleanedRates = asRatesParams(json) + + // Since the requests are USD only, we'll match up the original requests to what we've received + for (const [key, { rateQueueEntry, resolvers }] of Object.entries( + resolverMap + )) { + // Match crypto/fiat requests + if (rateQueueEntry.crypto.length === 1) { + const cryptoToMatch = rateQueueEntry.crypto[0] + const fiatToMatch = rateQueueEntry.fiat[0] + const crypto = cleanedRates.crypto.find(cr => { + return ( + cr.isoDate?.getTime() === cryptoToMatch.isoDate?.getTime() && + cr.asset.pluginId === cryptoToMatch.asset.pluginId && + (cr.asset.tokenId === cryptoToMatch.asset.tokenId || + (cr.asset.tokenId == null && + cryptoToMatch.asset.tokenId == null)) + ) + }) + + const fiat = cleanedRates.fiat.find(fr => { + return ( + fr.isoDate?.getTime() === fiatToMatch.isoDate?.getTime() && + fr.fiatCode === fiatToMatch.fiatCode + ) + }) + + if (crypto?.rate != null && fiat?.rate != null) { + const rate = crypto.rate / fiat.rate + rateMap[key] = rate + if (resolverMap[key] == null) { + clog(`${n} oops`) + continue + } + + clog(`${n} deleting ${key}`) + delete resolverMap[key] + if (resolvers.length) { + resolvers.forEach((r, i) => { + r(rate) + }) + } + } + } + + // Match fiat/fiat requests + if (rateQueueEntry.fiat.length === 2) { + const fromFiatToMatch = rateQueueEntry.fiat[0] + const fiatToMatch = rateQueueEntry.fiat[1] + const fromFiat = cleanedRates.fiat.find(fr => { + return ( + fr.isoDate?.getTime() === fromFiatToMatch.isoDate?.getTime() && + fr.fiatCode === fromFiatToMatch.fiatCode + ) + }) + + const toFiat = cleanedRates.fiat.find(fr => { + return ( + fr.isoDate?.getTime() === fiatToMatch.isoDate?.getTime() && + fr.fiatCode === fiatToMatch.fiatCode + ) + }) + + if (fromFiat?.rate != null && toFiat?.rate != null) { + const rate = fromFiat.rate / toFiat.rate + rateMap[key] = rate + if (resolverMap[key] == null) { + clog(`${n} oops`) + continue + } + + clog(`${n} deleting ${key}`) + delete resolverMap[key] + if (resolvers.length) { + resolvers.forEach((r, i) => { + r(rate) + }) + } + } + } } + } else { + const text = await response.text() + throw new Error(text) } - } else { - const text = await response.text() - throw new Error(text) + } catch (e: any) { + console.warn(`Error querying rates server ${e.message}`) + // Resolve all the promises with value 0 + Object.entries(resolverMap).forEach(entry => { + const [pairDate, value] = entry + clog(`${n} throw deleting ${pairDate}`) + delete resolverMap[pairDate] + value.resolvers.forEach(resolve => resolve(0)) + }) } - } catch (e: any) { - console.warn(`Error querying rates server ${e.message}`) - // Resolve all the promises with value 0 - Object.entries(resolverMap).forEach(entry => { - const [pairDate, value] = entry - clog(`${n} throw deleting ${pairDate}`) - delete resolverMap[pairDate] - value.resolvers.forEach(resolve => resolve(0)) - }) } + if (Object.keys(resolverMap).length > 0) { clog(`${n} Calling doQuery again`) await doQuery(doFetch) @@ -126,24 +196,22 @@ const doQuery = async (doFetch?: EdgeFetchFunction): Promise => { } const addToQueue = ( - entry: RateQueueEntry, + entry: RatesParams, + rateKey: string, resolve: Function, maxQuerySize: number, doFetch?: EdgeFetchFunction ) => { - const { currency_pair: pair, date } = entry - const pairDate = makePairDate(pair, date) - - if (resolverMap[pairDate] == null) { + if (resolverMap[rateKey] == null) { // Create a new entry in the map for this pair/date - clog(`adding ${pairDate}`) - resolverMap[pairDate] = { + clog(`adding ${rateKey}`) + resolverMap[rateKey] = { resolvers: [resolve], rateQueueEntry: entry } } else { // Add a resolver to existing pair/date entry - resolverMap[pairDate].resolvers.push(resolve) + resolverMap[rateKey].resolvers.push(resolve) return } if (!inQuery) { @@ -154,32 +222,104 @@ const addToQueue = ( } } -export const getHistoricalRate = async ( - codePair: string, +const createRateKey = ( + asset: { pluginId: string; tokenId?: EdgeTokenId } | string, + targetFiat: string, + date?: string +): string => { + let dateString = '' + if (date != null) { + dateString = `_${date}` + } + + if (typeof asset === 'object') { + let tokenIdString = '' + if (asset.tokenId != null) { + tokenIdString = `_${asset.tokenId}` + } + + return `${asset.pluginId}${tokenIdString}_${targetFiat}${dateString}` + } + + return `${asset}_${targetFiat}${dateString}` +} + +export const getHistoricalCryptoRate = async ( + pluginId: string, + tokenId: EdgeTokenId, + targetFiat: string, + date: string, + maxQuerySize: number = RATES_SERVER_MAX_QUERY_SIZE, + doFetch?: EdgeFetchFunction +): Promise => { + const rateKey = createRateKey({ pluginId, tokenId }, targetFiat, date) + + return await getHistoricalRate( + { + targetFiat: 'USD', + crypto: [ + { + isoDate: new Date(date), + asset: { pluginId, tokenId }, + rate: undefined + } + ], + fiat: [ + { + isoDate: new Date(date), + fiatCode: removeIsoPrefix(targetFiat), + rate: undefined + } + ] + }, + rateKey, + maxQuerySize, + doFetch + ) +} +export const getHistoricalFiatRate = async ( + fiatCode: string, + targetFiat: string, date: string, maxQuerySize: number = RATES_SERVER_MAX_QUERY_SIZE, doFetch?: EdgeFetchFunction ): Promise => { - const roundDate = roundHalfMinute(date) + return await getHistoricalRate( + { + targetFiat: 'USD', + crypto: [], + fiat: [ + { + isoDate: new Date(date), + fiatCode: removeIsoPrefix(fiatCode), + rate: undefined + }, + { + isoDate: new Date(date), + fiatCode: removeIsoPrefix(targetFiat), + rate: undefined + } + ] + }, + createRateKey(fiatCode, targetFiat, date), + maxQuerySize, + doFetch + ) +} + +const getHistoricalRate = async ( + RatesParams: RatesParams, + rateKey: string, + maxQuerySize: number = RATES_SERVER_MAX_QUERY_SIZE, + doFetch?: EdgeFetchFunction +): Promise => { return await new Promise((resolve, reject) => { - const [code1, code2] = codePair.split('_').sort() - const pair = `${code1}_${code2}` - const reverse = pair !== codePair - const rate = rateMap[makePairDate(pair, roundDate)] + const rate = rateMap[rateKey] if (rate == null) { - addToQueue( - { currency_pair: pair, date: roundDate }, - resolve, - maxQuerySize, - doFetch - ) + addToQueue(RatesParams, rateKey, resolve, maxQuerySize, doFetch) return } - let out = rate - if (reverse && rate !== 0) { - out = 1 / rate - } - resolve(out) + resolve(rate) }) } diff --git a/src/util/network.ts b/src/util/network.ts index d926c9c3385..70cb4063bfc 100644 --- a/src/util/network.ts +++ b/src/util/network.ts @@ -12,7 +12,7 @@ import { config } from '../theme/appConfig' import { asyncWaterfall, getOsVersion, shuffleArray } from './utils' import { checkAppVersion } from './versionCheck' const INFO_SERVERS = ['https://info1.edge.app', 'https://info2.edge.app'] -const RATES_SERVERS = ['https://rates1.edge.app', 'https://rates2.edge.app'] +const RATES_SERVERS = ['https://rates3.edge.app', 'https://rates4.edge.app'] const INFO_FETCH_INTERVAL = 5 * 60 * 1000 // 5 minutes From e5aecacf23f3277c6dca5d7e6ec1ae97ccec3611 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 15 Aug 2025 04:18:17 -0700 Subject: [PATCH 7/7] Use rates v3 and replace currency codes with pluginId/tokenId --- .../ExchangeQuoteComponent.test.tsx.snap | 2 +- .../__snapshots__/SendScene2.ui.test.tsx.snap | 28 +- .../SwapConfirmationScene.test.tsx.snap | 2 +- src/actions/ExchangeRateActions.ts | 373 +++++++++++++----- src/actions/ReceiveDropdown.tsx | 5 +- src/actions/ScanActions.tsx | 3 +- src/actions/SettingsActions.tsx | 4 +- src/components/cards/BalanceCard.tsx | 9 +- .../Fio/FioRequestConfirmationScene.tsx | 7 +- .../scenes/Fio/FioSentRequestDetailsScene.tsx | 10 +- .../scenes/Fio/FioStakingOverviewScene.tsx | 4 +- .../Loans/LoanCreateConfirmationScene.tsx | 32 +- .../scenes/Loans/LoanCreateScene.tsx | 70 ++-- .../scenes/Loans/LoanDetailsScene.tsx | 8 +- src/components/scenes/RequestScene.tsx | 7 +- src/components/scenes/SendScene2.tsx | 8 +- .../scenes/SwapConfirmationScene.tsx | 16 +- .../scenes/TransactionListScene.tsx | 6 +- src/components/scenes/WalletDetailsScene.tsx | 2 +- src/components/services/LoanManagerService.ts | 40 +- src/components/services/SortedWalletList.ts | 9 +- src/components/text/FiatText.tsx | 12 +- .../themed/ExchangeQuoteComponent.tsx | 18 +- src/components/themed/ExchangedFlipInput2.tsx | 38 +- src/components/themed/FioRequestRow.tsx | 7 +- src/components/themed/SwapInput.tsx | 43 +- src/components/themed/TransactionListRow.tsx | 9 +- src/components/themed/TransactionListTop.tsx | 7 +- .../tiles/InterestRateChangeTile.tsx | 21 +- src/components/tiles/NetworkFeeTile.tsx | 10 +- src/hooks/useFiatText.ts | 10 +- src/hooks/useTokenDisplayData.ts | 4 +- src/selectors/WalletSelectors.ts | 49 ++- src/util/ActionProgramUtils.ts | 21 +- src/util/CryptoAmount.ts | 9 +- src/util/borrowUtils.ts | 8 +- src/util/exchangeRates.ts | 8 +- src/util/fake/fakeRootState.ts | 26 +- src/util/network.ts | 4 +- src/util/utils.ts | 13 +- 40 files changed, 640 insertions(+), 322 deletions(-) diff --git a/src/__tests__/components/__snapshots__/ExchangeQuoteComponent.test.tsx.snap b/src/__tests__/components/__snapshots__/ExchangeQuoteComponent.test.tsx.snap index 61bc512cfcc..23aadefc915 100644 --- a/src/__tests__/components/__snapshots__/ExchangeQuoteComponent.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/ExchangeQuoteComponent.test.tsx.snap @@ -445,7 +445,7 @@ exports[`ExchangeQuote should render with loading props 1`] = ` ] } > - 0.00000001 BTC (<0.01 EUR) + 0 BTC (<0.01 EUR) diff --git a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap index 5c05ca162ef..1cebea41b08 100644 --- a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap @@ -896,7 +896,7 @@ exports[`SendScene2 1 spendTarget 1`] = ` ] } > - € 0.00 + € 0.23 - € 0.00 + € 0.23 - Amount: 0.00001234 BTC (€ 0.00) + Amount: 0.00001234 BTC (€ 0.23) - € 0.00 + € 2.26 - Amount: 0.00001234 BTC (€ 0.00) + Amount: 0.00001234 BTC (€ 0.23) - € 0.00 + € 2.26 - Amount: 0.00001234 BTC (€ 0.00) + Amount: 0.00001234 BTC (€ 0.23) - Amount: 0.00001234 BTC (€ 0.00) + Amount: 0.00001234 BTC (€ 0.23) - Amount: 0.00001234 BTC (€ 0.00) + Amount: 0.00001234 BTC (€ 0.23) - € 0.00 + € 2.26 - Amount: 0.00001234 BTC (€ 0.00) + Amount: 0.00001234 BTC (€ 0.23) @@ -13091,7 +13091,7 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` ] } > - € 0.00 + € 2.26 @@ -14400,7 +14400,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` ] } > - Amount: 0.00001234 BTC (€ 0.00) + Amount: 0.00001234 BTC (€ 0.23) @@ -14728,7 +14728,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` ] } > - € 0.00 + € 2.26 diff --git a/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap index 2a944516151..93ac84cbfcc 100644 --- a/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap @@ -896,7 +896,7 @@ exports[`SwapConfirmationScene should render with loading props 1`] = ` ] } > - 0.00000001 BTC (<0.01 EUR) + 0 BTC (<0.01 EUR) diff --git a/src/actions/ExchangeRateActions.ts b/src/actions/ExchangeRateActions.ts index ac3f3b66f6d..1ec65ed8dd6 100644 --- a/src/actions/ExchangeRateActions.ts +++ b/src/actions/ExchangeRateActions.ts @@ -1,19 +1,19 @@ -import { - asArray, - asEither, - asNull, - asNumber, - asObject, - asOptional, - asString -} from 'cleaners' +import { asArray, asNumber, asObject, asOptional, asString } from 'cleaners' import { makeReactNativeDisklet } from 'disklet' -import type { EdgeAccount } from 'edge-core-js' +import type { EdgeAccount, EdgeTokenId } from 'edge-core-js' +import { FIAT_CODES_SYMBOLS } from '../constants/WalletAndCurrencyConstants' import type { ThunkAction } from '../types/reduxTypes' import type { GuiExchangeRates, GuiExchangeRatesMap } from '../types/types' +import { currencyPlugins } from '../util/corePlugins' +import { + asCryptoAsset, + asRatesParams, + createRateKey, + type RatesParams +} from '../util/exchangeRates' import { fetchRates } from '../util/network' -import { datelog } from '../util/utils' +import { datelog, fixFiatCurrencyCode, removeIsoPrefix } from '../util/utils' const disklet = makeReactNativeDisklet() const EXCHANGE_RATES_FILENAME = 'exchangeRates.json' @@ -22,11 +22,20 @@ const HOUR_MS = 1000 * 60 * 60 const ONE_DAY = 1000 * 60 * 60 * 24 const ONE_MONTH = 1000 * 60 * 60 * 24 * 30 -const asAssetPair = asObject({ - currency_pair: asString, - date: asOptional(asString), // Defaults to today if not specified +const asCryptoFiatPair = asObject({ + asset: asCryptoAsset, + targetFiat: asString, + isoDate: asOptional(asString), // Defaults to today if not specified + expiration: asNumber +}) +type CryptoFiatPair = ReturnType +const asFiatFiatPair = asObject({ + fiatCode: asString, + targetFiat: asString, + isoDate: asOptional(asString), // Defaults to today if not specified expiration: asNumber }) +type FiatFiatPair = ReturnType const asExchangeRateCache = asObject( asObject({ @@ -36,26 +45,17 @@ const asExchangeRateCache = asObject( ) const asExchangeRateCacheFile = asObject({ rates: asExchangeRateCache, - assetPairs: asArray(asAssetPair) + cryptoPairs: asArray(asCryptoFiatPair), + fiatPairs: asArray(asFiatFiatPair) }) -type AssetPair = ReturnType +// type AssetPair = ReturnType // Exported for unit tests export type ExchangeRateCache = ReturnType type ExchangeRateCacheFile = ReturnType let exchangeRateCache: ExchangeRateCacheFile | undefined -const asRatesResponse = asObject({ - data: asArray( - asObject({ - currency_pair: asString, - date: asString, - exchangeRate: asEither(asString, asNull) - }) - ) -}) - export function updateExchangeRates(): ThunkAction> { return async (dispatch, getState) => { const state = getState() @@ -70,7 +70,7 @@ export function updateExchangeRates(): ThunkAction> { exchangeRateCache = await loadExchangeRateCache().catch( (error: unknown) => { datelog('Error loading exchange rate cache:', String(error)) - return { assetPairs: [], rates: {} } + return { rates: {}, cryptoPairs: [], fiatPairs: [] } } ) const { exchangeRates, exchangeRatesMap } = buildGuiRates( @@ -114,18 +114,23 @@ export function updateExchangeRates(): ThunkAction> { async function loadExchangeRateCache(): Promise { const now = Date.now() const out: ExchangeRateCacheFile = { - assetPairs: [], + cryptoPairs: [], + fiatPairs: [], rates: {} } const raw = await disklet.getText(EXCHANGE_RATES_FILENAME) const json = JSON.parse(raw) - const { assetPairs, rates } = asExchangeRateCacheFile(json) + const { cryptoPairs, fiatPairs, rates } = asExchangeRateCacheFile(json) // Keep un-expired asset pairs: - for (const pair of assetPairs) { + for (const pair of cryptoPairs) { if (pair.expiration < now) continue - out.assetPairs.push(pair) + out.cryptoPairs.push(pair) + } + for (const pair of fiatPairs) { + if (pair.expiration < now) continue + out.fiatPairs.push(pair) } // Keep un-expired rates: @@ -142,7 +147,7 @@ async function loadExchangeRateCache(): Promise { */ async function fetchExchangeRates( account: EdgeAccount, - accountIsoFiat: string, + accountFiat: string, cache: ExchangeRateCacheFile, now: number, yesterday: string @@ -154,16 +159,39 @@ async function fetchExchangeRates( const rateExpiration = now + ONE_DAY // Maintain a map of the unique asset pairs we need: - const assetPairMap = new Map() - function addAssetPair(assetPair: AssetPair): void { - const key = `${assetPair.currency_pair}_${assetPair.date ?? ''}` - assetPairMap.set(key, assetPair) + const cryptoPairMap = new Map() + const fiatPairMap = new Map() + + function addCryptoPair(pair: CryptoFiatPair): void { + let dateStr = '' + if (pair.isoDate != null) { + dateStr = `_${pair.isoDate}` + } + + let tokenIdStr = '' + if (pair.asset.tokenId != null) { + tokenIdStr = `_${tokenIdStr}` + } + const key = `${pair.asset.pluginId}${tokenIdStr}_${pair.targetFiat}${dateStr}` + cryptoPairMap.set(key, pair) + } + function addFiatPair(pair: FiatFiatPair): void { + let dateStr = '' + if (pair.isoDate != null) { + dateStr = `_${pair.isoDate}` + } + const key = `${pair.fiatCode}_${pair.targetFiat}${dateStr}` + fiatPairMap.set(key, pair) } // Keep the cached asset list, in case any wallets are still loading: - for (const assetPair of cache.assetPairs) { - if (assetPair.expiration < now) continue - addAssetPair(assetPair) + for (const pair of cache.cryptoPairs) { + if (pair.expiration < now) continue + addCryptoPair(pair) + } + for (const pair of cache.fiatPairs) { + if (pair.expiration < now) continue + addFiatPair(pair) } // Keep any un-expired rates, although they are likely to be stomped: @@ -174,10 +202,11 @@ async function fetchExchangeRates( } // If the user's fiat isn't dollars, get it's price: - if (accountIsoFiat !== 'iso:USD') { - addAssetPair({ - currency_pair: `iso:USD_${accountIsoFiat}`, - date: undefined, + if (accountFiat !== 'iso:USD') { + addFiatPair({ + isoDate: undefined, + fiatCode: accountFiat, + targetFiat: 'iso:USD', expiration: pairExpiration }) } @@ -185,18 +214,20 @@ async function fetchExchangeRates( // Grab the assets from all wallets: for (const walletId of Object.keys(currencyWallets)) { const wallet = currencyWallets[walletId] - const { currencyCode } = wallet.currencyInfo - + const { currencyCode, pluginId } = wallet.currencyInfo + const targetFiat = wallet.fiatCurrencyCode // Get the primary asset's prices for today and yesterday, // but with yesterday's price in dollars: - addAssetPair({ - currency_pair: `${currencyCode}_${accountIsoFiat}`, - date: undefined, + addCryptoPair({ + asset: { pluginId, tokenId: null }, + targetFiat, + isoDate: undefined, expiration: pairExpiration }) - addAssetPair({ - currency_pair: `${currencyCode}_iso:USD`, - date: yesterday, + addCryptoPair({ + asset: { pluginId, tokenId: null }, + targetFiat: 'iso:USD', + isoDate: yesterday, expiration: pairExpiration }) @@ -205,43 +236,54 @@ async function fetchExchangeRates( const token = wallet.currencyConfig.allTokens[tokenId] if (token == null) continue if (token.currencyCode === currencyCode) continue - addAssetPair({ - currency_pair: `${token.currencyCode}_${accountIsoFiat}`, - date: undefined, + addCryptoPair({ + asset: { pluginId, tokenId }, + targetFiat, + isoDate: undefined, expiration: pairExpiration }) - addAssetPair({ - currency_pair: `${token.currencyCode}_iso:USD`, - date: yesterday, + addCryptoPair({ + asset: { pluginId, tokenId }, + targetFiat: 'iso:USD', + isoDate: yesterday, expiration: pairExpiration }) } } - const assetPairs = Array.from(assetPairMap.values()) - for (let i = 0; i < assetPairs.length; i += RATES_SERVER_MAX_QUERY_SIZE) { - const query = assetPairs.slice(i, i + RATES_SERVER_MAX_QUERY_SIZE) - + const requests = convertToRatesParams(cryptoPairMap, fiatPairMap) + for (const query of requests) { + // const query = assetPairs.slice(i, i + RATES_SERVER_MAX_QUERY_SIZE) for (let attempt = 0; attempt < 5; ++attempt) { const options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data: query }) + body: JSON.stringify(query) } try { - const response = await fetchRates('v2/exchangeRates', options) + const response = await fetchRates('v3/rates', options) if (response.ok) { const json = await response.json() - const cleanedRates = asRatesResponse(json) - for (const rate of cleanedRates.data) { - const { currency_pair: currencyPair, exchangeRate, date } = rate - const isHistorical = now - new Date(date).valueOf() > HOUR_MS - const key = isHistorical ? `${currencyPair}_${date}` : currencyPair - - if (exchangeRate != null) { + const cleanedRates = asRatesParams(json) + + const allRates = [...cleanedRates.crypto, ...cleanedRates.fiat] + for (const exchangeRate of allRates) { + const date = exchangeRate.isoDate?.toISOString() + const isHistorical = + date != null && now - new Date(date).valueOf() > HOUR_MS + + const key = createRateKey( + 'asset' in exchangeRate + ? exchangeRate.asset + : exchangeRate.fiatCode, + fixFiatCurrencyCode(cleanedRates.targetFiat), + isHistorical ? date : undefined + ) + + if (exchangeRate.rate != null) { rates[key] = { expiration: rateExpiration, - rate: parseFloat(exchangeRate) + rate: exchangeRate.rate } } else { // We at least need a placeholder: @@ -262,7 +304,11 @@ async function fetchExchangeRates( } // Update the in-memory cache: - exchangeRateCache = { rates, assetPairs } + exchangeRateCache = { + rates, + cryptoPairs: Array.from(cryptoPairMap.values()), + fiatPairs: Array.from(fiatPairMap.values()) + } // Write the cache to disk: await disklet @@ -272,6 +318,77 @@ async function fetchExchangeRates( }) } +export const decodeRateKey = ( + rateKey: string +): + | { + asset: { pluginId: string; tokenId?: EdgeTokenId } | string + targetFiat: string + date?: string + } + | undefined => { + const args = rateKey.split('_') + if (args.length === 2) { + // fiat_fiat or crypto_fiat + const asset = args[0] + const targetFiat = args[1] + + if (FIAT_CODES_SYMBOLS[removeIsoPrefix(asset)] != null) { + return { + asset, + targetFiat, + date: undefined + } + } else { + return { + asset: { pluginId: asset }, + targetFiat, + date: undefined + } + } + } else if (args.length === 3) { + // fiat_fiat_date or pluginId_fiat_date or pluginId_tokenId_fiat + const [codeA, codeB, codeC] = args + if (FIAT_CODES_SYMBOLS[removeIsoPrefix(codeA)] != null) { + return { + asset: codeA, + targetFiat: codeB, + date: codeC + } + } else if ( + FIAT_CODES_SYMBOLS[removeIsoPrefix(codeB)] != null && + currencyPlugins[codeA] != null + ) { + return { + asset: { pluginId: codeA }, + targetFiat: codeB, + date: codeC + } + } else if ( + FIAT_CODES_SYMBOLS[removeIsoPrefix(codeC)] != null && + currencyPlugins[codeA] != null + ) { + return { + asset: { pluginId: codeA, tokenId: codeB }, + targetFiat: codeC, + date: undefined + } + } + } else if (args.length === 4) { + // pluginId/tokenId/fiat/date + const [codeA, codeB, codeC, codeD] = args + if ( + FIAT_CODES_SYMBOLS[removeIsoPrefix(codeC)] != null && + currencyPlugins[codeA] != null + ) + return { + asset: { pluginId: codeA, tokenId: codeB }, + targetFiat: codeC, + date: codeD + } + } +} + /** * Converts rates from the cache format to the GUI's in-memory format. */ @@ -287,39 +404,40 @@ function buildGuiRates( const { rate } = rateCache[key] out[key] = rate - // Include reverse rates: - const [codeA, codeB, date] = key.split('_') as [ - string, - string, - string | undefined - ] - const reverseKey = `${codeB}_${codeA}${date != null ? '_' + date : ''}` - out[reverseKey] = rate === 0 ? 0 : 1 / rate + const assetObj = decodeRateKey(key) + if (assetObj == null) continue + + const rateKeyWithoutDate = createRateKey( + assetObj.asset, + assetObj.targetFiat + ) + + const args = rateKeyWithoutDate.split('_') + + const targetFiat: string = args.pop() ?? '' + + const assetString = args.join('_') // Set up exchange rate map. This nest map is keyed This map will hold current rate and 24 hour rate, if available. - if (outMap.get(codeA)?.get(codeB) != null || date != null) { + if ( + outMap.get(assetString)?.get(targetFiat) != null || + assetObj.date != null + ) { continue } let yesterdayRate: number | undefined // We only look up yesterday's rate for USD pairs - if (codeB === 'iso:USD') { + if (targetFiat === 'iso:USD') { yesterdayRate = - rateCache[`${codeA}_${codeB}_${yesterday}`]?.rate ?? - closestRateForTimestamp(rateCache, codeA, yesterdayTimestamp) + rateCache[rateKeyWithoutDate]?.rate ?? + closestRateForTimestamp(rateCache, assetString, yesterdayTimestamp) } - const codeAMap = outMap.get(codeA) ?? new Map() - outMap.set(codeA, codeAMap.set(codeB, { currentRate: rate, yesterdayRate })) - - const codeBMap = outMap.get(codeB) ?? new Map() + const codeAMap = outMap.get(assetString) ?? new Map() outMap.set( - codeB, - codeBMap.set(codeA, { - currentRate: out[reverseKey], - yesterdayRate: - yesterdayRate === 0 || yesterdayRate == null ? 0 : 1 / yesterdayRate - }) + assetString, + codeAMap.set(targetFiat, { currentRate: rate, yesterdayRate }) ) } @@ -340,18 +458,21 @@ const getYesterdayDateRoundDownHour = (now?: Date | number): Date => { export const closestRateForTimestamp = ( exchangeRates: ExchangeRateCache, - currencyCode: string, + assetString: string, timestamp: number ): number | undefined => { // The extra _ at the end means there is a date string at the end of the key const filteredPairs = Object.keys(exchangeRates).filter(pair => - pair.startsWith(`${currencyCode}_iso:USD_`) + pair.startsWith(`${assetString}_iso:USD_`) ) let bestRate: number | undefined let bestDistance = Infinity for (const pair of filteredPairs) { - const [, , date] = pair.split('_') + const args = pair.split('_') + const date = args.pop() ?? '' + if (isNaN(Date.parse(date))) continue + const ms = Date.parse(date).valueOf() const distance = Math.abs(ms - timestamp) if (distance < bestDistance) { @@ -361,3 +482,61 @@ export const closestRateForTimestamp = ( } return bestRate } + +/** + * Convert maps to an array of RatesParams objects grouped by targetFiat. + */ +function convertToRatesParams( + cryptoPairMap: Map, + fiatPairMap: Map +): RatesParams[] { + const resultMap = new Map< + string, + { crypto: CryptoFiatPair[]; fiat: FiatFiatPair[] } + >() + + // Group CryptoPairs by targetFiat + for (const pair of cryptoPairMap.values()) { + const targetFiat = pair.targetFiat + if (!resultMap.has(targetFiat)) { + resultMap.set(targetFiat, { crypto: [], fiat: [] }) + } + resultMap.get(targetFiat)!.crypto.push(pair) + } + + // Group FiatPairs by targetFiat + for (const pair of fiatPairMap.values()) { + const targetFiat = pair.targetFiat + if (!resultMap.has(targetFiat)) { + resultMap.set(targetFiat, { crypto: [], fiat: [] }) + } + resultMap.get(targetFiat)!.fiat.push(pair) + } + + // 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) + + requests.push({ + targetFiat, + crypto: cryptoChunk.map(pair => ({ + isoDate: pair.isoDate == null ? newDate : new Date(pair.isoDate), + asset: pair.asset, + rate: undefined + })), + fiat: fiatChunk.map(pair => ({ + isoDate: pair.isoDate == null ? newDate : new Date(pair.isoDate), + fiatCode: pair.fiatCode, + rate: undefined + })) + }) + } + } + + return requests +} diff --git a/src/actions/ReceiveDropdown.tsx b/src/actions/ReceiveDropdown.tsx index 93328b4e6de..76368ecdc1f 100644 --- a/src/actions/ReceiveDropdown.tsx +++ b/src/actions/ReceiveDropdown.tsx @@ -27,7 +27,7 @@ export function showReceiveDropdown( transaction: EdgeTransaction ): ThunkAction { return (dispatch, getState) => { - const { currencyCode, nativeAmount, tokenId, walletId } = transaction + const { nativeAmount, tokenId, walletId } = transaction // Grab the matching wallet: const state = getState() @@ -44,7 +44,8 @@ export function showReceiveDropdown( const { spamFilterOn } = state.ui.settings const exchangeRate = getExchangeRate( state, - currencyCode, + wallet.currencyInfo.pluginId, + tokenId, isoFiatCurrencyCode ) const exchangeDenom = getExchangeDenom(wallet.currencyConfig, tokenId) diff --git a/src/actions/ScanActions.tsx b/src/actions/ScanActions.tsx index 40907e7a049..ce79c7b2af9 100644 --- a/src/actions/ScanActions.tsx +++ b/src/actions/ScanActions.tsx @@ -406,7 +406,8 @@ async function sweepPrivateKeys( ) const exchangeRate = getExchangeRate( state, - wallet.currencyInfo.currencyCode, + wallet.currencyInfo.pluginId, + null, 'iso:USD' ) const sweepAmountFiat = mul(sendExchangeAmount, exchangeRate) diff --git a/src/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx index 6d9c1e7a324..78a9765bbd2 100644 --- a/src/actions/SettingsActions.tsx +++ b/src/actions/SettingsActions.tsx @@ -29,7 +29,7 @@ import { } from '../components/services/AirshipInstance' import { lstrings } from '../locales/strings' import type { SettingsState } from '../reducers/scenes/SettingsReducer' -import { convertCurrency } from '../selectors/WalletSelectors' +import { convertFiatCurrency } from '../selectors/WalletSelectors' import type { ThunkAction } from '../types/reduxTypes' import { asMostRecentWallet, type MostRecentWallet } from '../types/types' import { DECIMAL_PRECISION } from '../util/utils' @@ -120,7 +120,7 @@ export function setDefaultFiatRequest( }) const nextDefaultIsoFiat = getState().ui.settings.defaultIsoFiat // convert from previous fiat to next fiat - const fiatString = convertCurrency( + const fiatString = convertFiatCurrency( state, previousDefaultIsoFiat, nextDefaultIsoFiat, diff --git a/src/components/cards/BalanceCard.tsx b/src/components/cards/BalanceCard.tsx index e822d90b325..f820361c01c 100644 --- a/src/components/cards/BalanceCard.tsx +++ b/src/components/cards/BalanceCard.tsx @@ -12,6 +12,7 @@ import { formatNumber } from '../../locales/intl' import { lstrings } from '../../locales/strings' import { useDispatch, useSelector } from '../../types/reactRedux' import type { NavigationBase } from '../../types/routerTypes' +import { createRateKey } from '../../util/exchangeRates' import { getTotalFiatAmountFromExchangeRates, removeIsoPrefix, @@ -76,7 +77,13 @@ export const BalanceCard = (props: Props) => { return ( wallet != null && exchangeRates[ - `${wallet.currencyInfo.currencyCode}_${defaultIsoFiat}` + createRateKey( + { + pluginId: wallet.currencyInfo.pluginId, + tokenId: null + }, + defaultIsoFiat + ) ] != null ) }), diff --git a/src/components/scenes/Fio/FioRequestConfirmationScene.tsx b/src/components/scenes/Fio/FioRequestConfirmationScene.tsx index 1a11b80d8b9..8111a625bdb 100644 --- a/src/components/scenes/Fio/FioRequestConfirmationScene.tsx +++ b/src/components/scenes/Fio/FioRequestConfirmationScene.tsx @@ -514,7 +514,12 @@ export const FioRequestConfirmationScene = withWallet((ownProps: OwnProps) => { selectDisplayDenom(state, wallet.currencyConfig, tokenId) ) const exchangeSecondaryToPrimaryRatio = useSelector(state => - getExchangeRate(state, currencyCode, defaultIsoFiat) + getExchangeRate( + state, + wallet.currencyInfo.pluginId, + tokenId, + defaultIsoFiat + ) ) const fioWallets = useSelector(state => state.ui.wallets.fioWallets) const isConnected = useSelector(state => state.network.isConnected) diff --git a/src/components/scenes/Fio/FioSentRequestDetailsScene.tsx b/src/components/scenes/Fio/FioSentRequestDetailsScene.tsx index 6a2fc3db219..a4f2791b7ab 100644 --- a/src/components/scenes/Fio/FioSentRequestDetailsScene.tsx +++ b/src/components/scenes/Fio/FioSentRequestDetailsScene.tsx @@ -1,4 +1,5 @@ import { mul } from 'biggystring' +import type { EdgeTokenId } from 'edge-core-js' import * as React from 'react' import { View } from 'react-native' @@ -12,6 +13,7 @@ import type { FioRequestStatus, GuiExchangeRates } from '../../../types/types' +import { createRateKey } from '../../../util/exchangeRates' import { EdgeCard } from '../../cards/EdgeCard' import { SceneWrapper } from '../../common/SceneWrapper' import { EdgeRow } from '../../rows/EdgeRow' @@ -39,9 +41,13 @@ interface StateProps { type Props = StateProps & OwnProps & ThemeProps class FioSentRequestDetailsComponent extends React.PureComponent { - fiatAmount = (currencyCode: string, amount: string = '0') => { + fiatAmount = ( + pluginId: string, + tokenId: EdgeTokenId, + amount: string = '0' + ) => { const { exchangeRates, isoFiatCurrencyCode } = this.props - const rateKey = `${currencyCode}_${isoFiatCurrencyCode}` + const rateKey = createRateKey({ pluginId, tokenId }, isoFiatCurrencyCode) const fiatPerCrypto = exchangeRates[rateKey] ?? '0' const fiatAmount = mul(fiatPerCrypto, amount) diff --git a/src/components/scenes/Fio/FioStakingOverviewScene.tsx b/src/components/scenes/Fio/FioStakingOverviewScene.tsx index 712505511ff..6e31f9b287d 100644 --- a/src/components/scenes/Fio/FioStakingOverviewScene.tsx +++ b/src/components/scenes/Fio/FioStakingOverviewScene.tsx @@ -232,7 +232,6 @@ export const FioStakingOverviewScene = connect< const { account } = state.core const { defaultIsoFiat, defaultFiat } = state.ui.settings const currencyWallet = account.currencyWallets[walletId] - const currencyCode = getCurrencyCode(currencyWallet, tokenId) const { staked } = getFioStakingBalances(currencyWallet.stakingStatus) const stakedNativeAmount = staked @@ -260,7 +259,8 @@ export const FioStakingOverviewScene = connect< )(stakedNativeAmount ?? '0') const stakingFiatBalance = convertCurrency( state, - currencyCode, + currencyWallet.currencyInfo.pluginId, + tokenId, defaultIsoFiat, stakingDefaultCryptoAmount ) diff --git a/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx b/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx index b6b4041b59f..5474841113f 100644 --- a/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx +++ b/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx @@ -105,19 +105,16 @@ export const LoanCreateConfirmationScene = (props: Props) => { // TODO: Extend to dynamically grab minimums from providers for fee and collateral quotes const borrowPluginCurrencyPluginId = borrowPlugin.borrowInfo.currencyPluginId const minFeeSwapAmount = useSelector(state => { + const polygonPrice = convertCurrency( + state, + 'polygon', + null, + 'iso:USD', + destWallet.currencyInfo.denominations[0].multiplier + ) + if (polygonPrice === '0') return '0' return borrowPluginCurrencyPluginId === 'polygon' && isCrossChainSrc - ? truncateDecimals( - mul( - '5.05', - convertCurrency( - state, - 'iso:USD', - 'MATIC', - destWallet.currencyInfo.denominations[0].multiplier - ) - ), - 0 - ) + ? truncateDecimals(mul('5.05', div('1', polygonPrice)), 0) : '0' }) @@ -274,7 +271,6 @@ export const LoanCreateConfirmationScene = (props: Props) => { // HACK: Interim solution before implementing a robust multi-asset fee aggregator for Action Programs const { - currencyCode: srcCurrencyCode, denomination: srcDenom, isoFiatCurrencyCode: srcIsoFiatCurrencyCode } = useTokenDisplayData({ @@ -282,7 +278,6 @@ export const LoanCreateConfirmationScene = (props: Props) => { currencyConfig: srcWallet.currencyConfig }) const { - currencyCode: feeCurrencyCode, denomination: feeDenom, isoFiatCurrencyCode: feeIsoFiatCurrencyCode } = useTokenDisplayData({ @@ -298,7 +293,8 @@ export const LoanCreateConfirmationScene = (props: Props) => { ) return convertCurrency( state, - srcCurrencyCode, + srcWallet.currencyInfo.pluginId, + srcTokenId, srcIsoFiatCurrencyCode, cryptoAmount ) @@ -311,7 +307,8 @@ export const LoanCreateConfirmationScene = (props: Props) => { ) return convertCurrency( state, - srcCurrencyCode, + srcWallet.currencyInfo.pluginId, + srcTokenId, srcIsoFiatCurrencyCode, cryptoAmount ) @@ -324,7 +321,8 @@ export const LoanCreateConfirmationScene = (props: Props) => { ) return convertCurrency( state, - feeCurrencyCode, + borrowEngineWallet.currencyInfo.pluginId, + null, feeIsoFiatCurrencyCode, cryptoAmount ) diff --git a/src/components/scenes/Loans/LoanCreateScene.tsx b/src/components/scenes/Loans/LoanCreateScene.tsx index 5b1cbd1c3ba..015fe22a4dc 100644 --- a/src/components/scenes/Loans/LoanCreateScene.tsx +++ b/src/components/scenes/Loans/LoanCreateScene.tsx @@ -39,6 +39,7 @@ import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' import { enableTokenCurrencyCode } from '../../../util/CurrencyWalletHelpers' +import { createRateKey } from '../../../util/exchangeRates' import { DECIMAL_PRECISION, removeIsoPrefix, @@ -280,7 +281,8 @@ export const LoanCreateScene = (props: Props) => { // regardless of what srcWallet's isoFiatCurrencyCode is, so all these // conversions have the same quote asset. const collateralToFiatRate = useCurrencyFiatRate({ - currencyCode: srcCurrencyCode, + pluginId: borrowEnginePluginId, + tokenId: collateralTokenId ?? null, isoFiatCurrencyCode: defaultIsoFiat }) @@ -313,33 +315,47 @@ export const LoanCreateScene = (props: Props) => { // Calculate how much collateral asset we can obtain after swapping from src asset const srcToCollateralExchangeRate = useSelector(state => { + if (srcWallet == null) return '1' + + const sourceRate = convertCurrency( + state, + srcWallet.currencyInfo.pluginId, + srcTokenId, + defaultIsoFiat + ) + const hardCollateralRate = convertCurrency( + state, + borrowEnginePluginId, + '1bfd67037b42cf73acf2047067bd4f2c47d9bfd6', // WETH + defaultIsoFiat + ) + if (hardCollateralRate === '0') return '1' + const exchangeRate = isRequiresSwap - ? convertCurrency(state, srcCurrencyCode, hardCollateralCurrencyCode) + ? div(sourceRate, hardCollateralRate, DECIMAL_PRECISION) : '1' // HACK: We don't have BTC->WBTC exchange rates for now, but this selector // will be needed in the future for supporting non-like-kind swaps for collateral return zeroString(exchangeRate) ? '1' : exchangeRate }) - const minSwapInputNativeAmount = useSelector(state => - isRequiresSwap && - borrowPlugin.borrowInfo.currencyPluginId === 'polygon' && - srcCurrencyCode != null && - srcExchangeMultiplier != null - ? truncateDecimals( - mul( - '30', - convertCurrency( - state, - 'iso:USD', - srcCurrencyCode, - srcExchangeMultiplier - ) - ), - 0 - ) + const minSwapInputNativeAmount = useSelector(state => { + if (srcWallet == null) return '0' + const rate = convertCurrency( + state, + srcWallet.currencyInfo.pluginId, + srcTokenId, + defaultIsoFiat + ) + if (rate === '0') return '0' + + return isRequiresSwap && + borrowPlugin.borrowInfo.currencyPluginId === 'polygon' && + srcCurrencyCode != null && + srcExchangeMultiplier != null + ? truncateDecimals(mul('30', div('1', rate, DECIMAL_PRECISION)), 0) : '0' - ) + }) if (isRequiresSwap) { // Calculate how much src asset we need to swap @@ -656,15 +672,21 @@ const getStyles = cacheStyles((theme: Theme) => ({ })) const useCurrencyFiatRate = ({ - currencyCode, + pluginId, + tokenId, isoFiatCurrencyCode }: { - currencyCode?: string + pluginId: string + tokenId: EdgeTokenId isoFiatCurrencyCode?: string }): number => { return useSelector(state => { - if (currencyCode == null || isoFiatCurrencyCode == null) return 0 + if (isoFiatCurrencyCode == null) return 0 else - return state.exchangeRates[`${currencyCode}_${isoFiatCurrencyCode}`] ?? 0 + return ( + state.exchangeRates[ + createRateKey({ pluginId, tokenId }, isoFiatCurrencyCode) + ] ?? 0 + ) }) } diff --git a/src/components/scenes/Loans/LoanDetailsScene.tsx b/src/components/scenes/Loans/LoanDetailsScene.tsx index 301ed64e60d..77f49a64cf8 100644 --- a/src/components/scenes/Loans/LoanDetailsScene.tsx +++ b/src/components/scenes/Loans/LoanDetailsScene.tsx @@ -25,6 +25,7 @@ import { useSelector } from '../../../types/reactRedux' import type { EdgeAppSceneProps } from '../../../types/routerTypes' import type { GuiExchangeRates } from '../../../types/types' import { getToken } from '../../../util/CurrencyInfoHelpers' +import { createRateKey } from '../../../util/exchangeRates' import { DECIMAL_PRECISION, removeIsoPrefix, @@ -464,8 +465,11 @@ export const calculateFiatAmount = ( const token = getToken(wallet, tokenId) if (token == null) return '0' - const { currencyCode, denominations } = token - const key = `${currencyCode}_${isoFiatCurrencyCode}` + const { denominations } = token + const key = createRateKey( + { pluginId: wallet.currencyInfo.pluginId, tokenId }, + isoFiatCurrencyCode + ) const assetFiatPrice = exchangeRates[key] ?? '0' if (assetFiatPrice === 0) { return '0' diff --git a/src/components/scenes/RequestScene.tsx b/src/components/scenes/RequestScene.tsx index 441106db123..59d2f3be166 100644 --- a/src/components/scenes/RequestScene.tsx +++ b/src/components/scenes/RequestScene.tsx @@ -857,7 +857,12 @@ export const RequestScene = withWallet((props: OwnProps) => { const account = useSelector(state => state.core.account) const exchangeSecondaryToPrimaryRatio = useSelector(state => - getExchangeRate(state, currencyCode, isoFiatCurrencyCode) + getExchangeRate( + state, + wallet.currencyInfo.pluginId, + tokenId, + isoFiatCurrencyCode + ) ) const fioAddresses = useSelector(state => state.ui.fioAddress.fioAddresses) const isConnected = useSelector(state => state.network.isConnected) diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 74bc4f0fcb2..fef083f8c75 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -43,6 +43,7 @@ import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes' import type { FioRequest, GuiExchangeRates } from '../../types/types' import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' +import { createRateKey } from '../../util/exchangeRates' import { addToFioAddressCache, checkRecordSendFee, @@ -1299,7 +1300,12 @@ const SendComponent = (props: Props) => { } if (pinSpendingLimitsEnabled) { const rate = - exchangeRates[`${currencyCode}_${defaultIsoFiat}`] ?? + exchangeRates[ + createRateKey( + { pluginId: coreWallet.currencyInfo.pluginId, tokenId }, + defaultIsoFiat + ) + ] ?? // TODO: INFINITY_STRING const totalNativeAmount = spendInfo.spendTargets.reduce( (prev, target) => add(target.nativeAmount ?? '0', prev), diff --git a/src/components/scenes/SwapConfirmationScene.tsx b/src/components/scenes/SwapConfirmationScene.tsx index e3ab21ef288..e975ca0b639 100644 --- a/src/components/scenes/SwapConfirmationScene.tsx +++ b/src/components/scenes/SwapConfirmationScene.tsx @@ -24,10 +24,7 @@ import type { SwapTabSceneProps } from '../../types/routerTypes' import type { GuiSwapInfo } from '../../types/types' import { getSwapPluginIconUri } from '../../util/CdnUris' import { CryptoAmount } from '../../util/CryptoAmount' -import { - getCurrencyCode, - getCurrencyCodeMultiplier -} from '../../util/CurrencyInfoHelpers' +import { getCurrencyCodeMultiplier } from '../../util/CurrencyInfoHelpers' import { logActivity } from '../../util/logger' import { logEvent } from '../../util/tracking' import { @@ -477,8 +474,6 @@ const getSwapInfo = ( // Both fromCurrencyCode and toCurrencyCode will exist, since we set them: const { request } = quote const { fromWallet, toWallet, fromTokenId, toTokenId } = request - const fromCurrencyCode = getCurrencyCode(fromWallet, fromTokenId) - const toCurrencyCode = getCurrencyCode(toWallet, toTokenId) // Format from amount: const fromDisplayDenomination = selectDisplayDenom( @@ -504,7 +499,8 @@ const getSwapInfo = ( const fromBalanceInFiatRaw = parseFloat( convertCurrency( state, - fromCurrencyCode, + fromWallet.currencyInfo.pluginId, + fromTokenId, defaultIsoFiat, fromBalanceInCryptoDisplay ) @@ -538,7 +534,8 @@ const getSwapInfo = ( const feeFiatAmountRaw = parseFloat( convertCurrency( state, - request.fromWallet.currencyInfo.currencyCode, + request.fromWallet.currencyInfo.pluginId, + quote.networkFee.tokenId, defaultIsoFiat, feeDenominatedAmount ) @@ -577,7 +574,8 @@ const getSwapInfo = ( const toBalanceInFiatRaw = parseFloat( convertCurrency( state, - toCurrencyCode, + toWallet.currencyInfo.pluginId, + toTokenId, defaultIsoFiat, toBalanceInCryptoDisplay ) diff --git a/src/components/scenes/TransactionListScene.tsx b/src/components/scenes/TransactionListScene.tsx index 1734089c22a..0c605341d6c 100644 --- a/src/components/scenes/TransactionListScene.tsx +++ b/src/components/scenes/TransactionListScene.tsx @@ -85,12 +85,8 @@ function TransactionListComponent(props: Props) { // Spam filter const exchangeDenom = getExchangeDenom(wallet.currencyConfig, tokenId) const defaultIsoFiat = useSelector(state => state.ui.settings.defaultIsoFiat) - const currencyCode = - tokenId == null - ? wallet.currencyInfo.currencyCode - : wallet.currencyConfig.allTokens[tokenId].currencyCode const exchangeRate = useSelector(state => - getExchangeRate(state, currencyCode, defaultIsoFiat) + getExchangeRate(state, pluginId, tokenId, defaultIsoFiat) ) const spamFilterOn = useSelector(state => state.ui.settings.spamFilterOn) const spamThreshold = React.useMemo(() => { diff --git a/src/components/scenes/WalletDetailsScene.tsx b/src/components/scenes/WalletDetailsScene.tsx index 56e4d1f34d9..2e5f396901d 100644 --- a/src/components/scenes/WalletDetailsScene.tsx +++ b/src/components/scenes/WalletDetailsScene.tsx @@ -103,7 +103,7 @@ function WalletDetailsComponent(props: Props) { const defaultIsoFiat = useSelector(state => state.ui.settings.defaultIsoFiat) const defaultFiat = defaultIsoFiat.replace('iso:', '') const exchangeRate = useSelector(state => - getExchangeRate(state, currencyCode, defaultIsoFiat) + getExchangeRate(state, pluginId, tokenId, defaultIsoFiat) ) const spamFilterOn = useSelector(state => state.ui.settings.spamFilterOn) const activeUsername = useSelector(state => state.core.account.username) diff --git a/src/components/services/LoanManagerService.ts b/src/components/services/LoanManagerService.ts index 822a1de0067..1a3d56834e1 100644 --- a/src/components/services/LoanManagerService.ts +++ b/src/components/services/LoanManagerService.ts @@ -20,9 +20,9 @@ import { waitForBorrowEngineSync } from '../../controllers/loan-manager/util/wai import { useAllTokens } from '../../hooks/useAllTokens' import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { lstrings } from '../../locales/strings' -import type { BorrowEngine } from '../../plugins/borrow-plugins/types' import { useState } from '../../types/reactHooks' import { useDispatch, useSelector } from '../../types/reactRedux' +import { createRateKey } from '../../util/exchangeRates' import { makePeriodicTask } from '../../util/PeriodicTask' import { makePushClient } from '../../util/PushClient/PushClient' import { DECIMAL_PRECISION, zeroString } from '../../util/utils' @@ -100,22 +100,6 @@ export const LoanManagerService = (props: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, loanAccountMap]) - // - // Update Liquidation Threshold Push Event - // - const getCurrencyCode = React.useCallback( - (borrowEngine: BorrowEngine, tokenId: EdgeTokenId): string => { - const tokens = - allTokens[borrowEngine.currencyWallet.currencyInfo.pluginId] - const token = tokenId != null ? tokens[tokenId] : undefined - return ( - token?.currencyCode ?? - borrowEngine.currencyWallet.currencyInfo.currencyCode - ) - }, - [allTokens] - ) - // Cache changes to specific watched properties of the loan accounts to detect // deltas useAsyncEffect( @@ -154,7 +138,14 @@ export const LoanManagerService = (props: Props) => { tokenId => tokenId == null || exchangeRates[ - `${getCurrencyCode(borrowEngine, tokenId)}_iso:USD` + createRateKey( + { + pluginId: + borrowEngine.currencyWallet.currencyInfo.pluginId, + tokenId + }, + 'iso:USD' + ) ] != null ) ) { @@ -274,10 +265,6 @@ export const LoanManagerService = (props: Props) => { ) const collateral = filteredCollaterals[0] - const collateralCurrencyCode = getCurrencyCode( - borrowEngine, - collateral.tokenId - ) const collateralExchangeAmount = getExchangeAmount( collateral.tokenId, collateral.nativeAmount @@ -290,7 +277,13 @@ export const LoanManagerService = (props: Props) => { DECIMAL_PRECISION ) ) - const currencyPair = `${collateralCurrencyCode}_iso:USD` + const currencyPair = createRateKey( + { + pluginId: borrowEngine.currencyWallet.currencyInfo.pluginId, + tokenId: collateral.tokenId + }, + 'iso:USD' + ) if (isSkipPriceCheck || exchangeRates[currencyPair] > thresholdRate) { await uploadLiquidationEvent(currencyPair, thresholdRate) @@ -303,7 +296,6 @@ export const LoanManagerService = (props: Props) => { cachedLoanAssetsMap, clientId, exchangeRates, - getCurrencyCode, loanAccountMap ] ) diff --git a/src/components/services/SortedWalletList.ts b/src/components/services/SortedWalletList.ts index c2d86bb4108..39a96ca84b1 100644 --- a/src/components/services/SortedWalletList.ts +++ b/src/components/services/SortedWalletList.ts @@ -12,6 +12,7 @@ import type { } from '../../types/types' import { getWalletTokenId } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' +import { createRateKey } from '../../util/exchangeRates' import { normalizeForSearch } from '../../util/utils' interface Props { @@ -241,7 +242,13 @@ function getFiat( const nativeBalance = wallet.balanceMap.get(tokenId) ?? '0' // Find the rate: - const rate = exchangeRates[`${currencyCode}_${isoFiatCurrencyCode}`] ?? '0' + const rate = + exchangeRates[ + createRateKey( + { pluginId: wallet.currencyInfo.pluginId, tokenId }, + isoFiatCurrencyCode + ) + ] ?? '0' // Do the conversion: return ( diff --git a/src/components/text/FiatText.tsx b/src/components/text/FiatText.tsx index 2d00c03d4de..865da45a9d8 100644 --- a/src/components/text/FiatText.tsx +++ b/src/components/text/FiatText.tsx @@ -39,16 +39,16 @@ export const FiatText = ({ tokenId, currencyConfig }: Props) => { - const { currencyCode, denomination, isoFiatCurrencyCode } = - useTokenDisplayData({ - tokenId, - currencyConfig - }) + const { denomination, isoFiatCurrencyCode } = useTokenDisplayData({ + tokenId, + currencyConfig + }) const text = useFiatText({ appendFiatCurrencyCode, autoPrecision, - cryptoCurrencyCode: currencyCode, + pluginId: currencyConfig.currencyInfo.pluginId, + tokenId, cryptoExchangeMultiplier: denomination.multiplier, fiatSymbolSpace, hideFiatSymbol, diff --git a/src/components/themed/ExchangeQuoteComponent.tsx b/src/components/themed/ExchangeQuoteComponent.tsx index 739722f2563..0a98a405d4f 100644 --- a/src/components/themed/ExchangeQuoteComponent.tsx +++ b/src/components/themed/ExchangeQuoteComponent.tsx @@ -50,15 +50,15 @@ export const ExchangeQuote = (props: Props) => { tokenId: feeTokenId }) - const { currencyCode: fromCurrencyCode, denomination: fromDenomination } = - useTokenDisplayData({ - currencyConfig: fromWallet.currencyConfig, - tokenId: fromTokenId - }) + const { denomination: fromDenomination } = useTokenDisplayData({ + currencyConfig: fromWallet.currencyConfig, + tokenId: fromTokenId + }) const feeFiatText = useFiatText({ autoPrecision: true, - cryptoCurrencyCode: networkFee.currencyCode, + pluginId: fromWallet.currencyInfo.pluginId, + tokenId: feeTokenId, cryptoExchangeMultiplier: feeDenomination.multiplier, isoFiatCurrencyCode, nativeCryptoAmount: feeNativeAmount, @@ -74,7 +74,8 @@ export const ExchangeQuote = (props: Props) => { ) return convertCurrency( state, - networkFee.currencyCode, + fromWallet.currencyInfo.pluginId, + networkFee.tokenId, isoFiatCurrencyCode, cryptoAmount ) @@ -88,7 +89,8 @@ export const ExchangeQuote = (props: Props) => { ) return convertCurrency( state, - fromCurrencyCode, + fromWallet.currencyInfo.pluginId, + fromTokenId, isoFiatCurrencyCode, cryptoAmount ) diff --git a/src/components/themed/ExchangedFlipInput2.tsx b/src/components/themed/ExchangedFlipInput2.tsx index a34ed03201c..e6ee9593d82 100644 --- a/src/components/themed/ExchangedFlipInput2.tsx +++ b/src/components/themed/ExchangedFlipInput2.tsx @@ -10,7 +10,9 @@ import { lstrings } from '../../locales/strings' import { getExchangeDenom } from '../../selectors/DenominationSelectors' import { useSelector } from '../../types/reactRedux' import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' +import { createRateKey } from '../../util/exchangeRates' import { + convertCurrencyFromExchangeRates, DECIMAL_PRECISION, getDenomFromIsoCode, maxPrimaryCurrencyConversionDecimals, @@ -109,7 +111,7 @@ const ExchangedFlipInput2Component = React.forwardRef< primaryExchangeMultiplier: cryptoExchangeDenom.multiplier, secondaryExchangeMultiplier: fiatDenom.multiplier, exchangeSecondaryToPrimaryRatio: - exchangeRates[`${cryptoCurrencyCode}_${defaultIsoFiat}`] + exchangeRates[createRateKey({ pluginId, tokenId }, defaultIsoFiat)] }) const cryptoMaxPrecision = maxPrimaryCurrencyConversionDecimals( log10(cryptoDisplayDenom.multiplier), @@ -129,12 +131,17 @@ const ExchangedFlipInput2Component = React.forwardRef< const convertCurrency = useHandler( ( amount: string, - fromCurrencyCode: string, - toCurrencyCode: string + pluginId: string, + tokenId: EdgeTokenId, + isoFiatCode: string ): string => { - const rateKey = `${fromCurrencyCode}_${toCurrencyCode}` - const rate = exchangeRates[rateKey] ?? '0' - return mul(amount, rate) + return convertCurrencyFromExchangeRates( + exchangeRates, + pluginId, + tokenId, + isoFiatCode, + amount + ) } ) @@ -153,7 +160,8 @@ const ExchangedFlipInput2Component = React.forwardRef< ) const fiatAmountLong = convertCurrency( exchangeAmount, - cryptoCurrencyCode, + pluginId, + tokenId, defaultIsoFiat ) const fiatAmount = round(fiatAmountLong, -2) @@ -165,8 +173,9 @@ const ExchangedFlipInput2Component = React.forwardRef< return { nativeAmount: '', exchangeAmount: '', displayAmount: '' } const exchangeAmountLong = convertCurrency( fiatAmount, - defaultIsoFiat, - cryptoCurrencyCode + pluginId, + tokenId, + defaultIsoFiat ) const nativeAmountLong = mul( exchangeAmountLong, @@ -243,7 +252,8 @@ const ExchangedFlipInput2Component = React.forwardRef< ) const initFiat = convertCurrency( exchangeAmount, - cryptoCurrencyCode, + pluginId, + tokenId, defaultIsoFiat ) setRenderDisplayAmount(displayAmount) @@ -253,7 +263,9 @@ const ExchangedFlipInput2Component = React.forwardRef< convertFromCryptoNative, cryptoCurrencyCode, defaultIsoFiat, - startNativeAmount + pluginId, + startNativeAmount, + tokenId ]) React.useImperativeHandle(ref, () => ({ @@ -276,10 +288,10 @@ const ExchangedFlipInput2Component = React.forwardRef< */ const overrideForceField = useMemo( () => - convertCurrency('100', cryptoCurrencyCode, defaultIsoFiat) === '0' + convertCurrency('100', pluginId, tokenId, defaultIsoFiat) === '0' ? 'crypto' : forceField, - [convertCurrency, cryptoCurrencyCode, defaultIsoFiat, forceField] + [convertCurrency, defaultIsoFiat, forceField, pluginId, tokenId] ) const pluginInfo = getSpecialCurrencyInfo(pluginId) diff --git a/src/components/themed/FioRequestRow.tsx b/src/components/themed/FioRequestRow.tsx index a4c7b05896e..e5a52703ac4 100644 --- a/src/components/themed/FioRequestRow.tsx +++ b/src/components/themed/FioRequestRow.tsx @@ -17,7 +17,6 @@ import { getExchangeRate } from '../../selectors/WalletSelectors' import { connect } from '../../types/reactRedux' import type { FioRequest, FioRequestStatus } from '../../types/types' import { getCryptoText } from '../../util/cryptoTextUtils' -import { getCurrencyCodeWithAccount } from '../../util/CurrencyInfoHelpers' import { removeIsoPrefix } from '../../util/utils' import { EdgeCard } from '../cards/EdgeCard' import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' @@ -272,21 +271,19 @@ export const FioRequestRow = connect( let displayDenomination = emptyDenomination let exchangeDenomination = emptyDenomination - let currencyCode = '' + let fiatPerCrypto = 0 if (edgeAsset != null) { const { pluginId, tokenId } = edgeAsset const config = account.currencyConfig[pluginId] displayDenomination = selectDisplayDenom(state, config, tokenId) exchangeDenomination = getExchangeDenom(config, tokenId) - currencyCode = - getCurrencyCodeWithAccount(account, pluginId, tokenId) ?? '' + fiatPerCrypto = getExchangeRate(state, pluginId, tokenId, defaultIsoFiat) } else { console.log(`Cannot find asset ${fioTokenCode} on chain ${fioChainCode}`) } const fiatSymbol = getFiatSymbol(removeIsoPrefix(defaultIsoFiat)) - const fiatPerCrypto = getExchangeRate(state, currencyCode, defaultIsoFiat) const fiatAmount = formatNumber(mul(fiatPerCrypto, fioRequest.content.amount), { toFixed: 2 diff --git a/src/components/themed/SwapInput.tsx b/src/components/themed/SwapInput.tsx index 66e70993030..cbee926f6e3 100644 --- a/src/components/themed/SwapInput.tsx +++ b/src/components/themed/SwapInput.tsx @@ -9,8 +9,9 @@ import { selectDisplayDenom } from '../../selectors/DenominationSelectors' import { useSelector } from '../../types/reactRedux' -import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' +import { createRateKey } from '../../util/exchangeRates' import { + convertCurrencyFromExchangeRates, DECIMAL_PRECISION, getDenomFromIsoCode, maxPrimaryCurrencyConversionDecimals, @@ -111,12 +112,17 @@ const SwapInputComponent = React.forwardRef( const convertCurrency = useHandler( ( amount: string, - fromCurrencyCode: string, - toCurrencyCode: string + pluginId: string, + tokenId: EdgeTokenId, + isoFiatCode: string ): string => { - const rateKey = `${fromCurrencyCode}_${toCurrencyCode}` - const rate = exchangeRates[rateKey] ?? '0' - return mul(amount, rate) + return convertCurrencyFromExchangeRates( + exchangeRates, + pluginId, + tokenId, + isoFiatCode, + amount + ) } ) @@ -124,7 +130,6 @@ const SwapInputComponent = React.forwardRef( if (nativeAmount === '') return { fiatAmount: '', exchangeAmount: '', displayAmount: '' } - const cryptoCurrencyCode = getCurrencyCode(wallet, tokenId) const cryptoExchangeDenom = getExchangeDenom( wallet.currencyConfig, tokenId @@ -141,7 +146,8 @@ const SwapInputComponent = React.forwardRef( ) const fiatAmountLong = convertCurrency( exchangeAmount, - cryptoCurrencyCode, + wallet.currencyInfo.pluginId, + tokenId, defaultIsoFiat ) const fiatAmount = round(fiatAmountLong, -2) @@ -152,15 +158,15 @@ const SwapInputComponent = React.forwardRef( if (fiatAmount === '') return { nativeAmount: '', exchangeAmount: '', displayAmount: '' } - const cryptoCurrencyCode = getCurrencyCode(wallet, tokenId) const cryptoExchangeDenom = getExchangeDenom( wallet.currencyConfig, tokenId ) const exchangeAmountLong = convertCurrency( fiatAmount, - defaultIsoFiat, - cryptoCurrencyCode + wallet.currencyInfo.pluginId, + tokenId, + defaultIsoFiat ) const nativeAmountLong = mul( exchangeAmountLong, @@ -176,7 +182,12 @@ const SwapInputComponent = React.forwardRef( primaryExchangeMultiplier: cryptoExchangeDenom.multiplier, secondaryExchangeMultiplier: fiatDenom.multiplier, exchangeSecondaryToPrimaryRatio: - exchangeRates[`${cryptoCurrencyCode}_${defaultIsoFiat}`] + exchangeRates[ + createRateKey( + { pluginId: wallet.currencyInfo.pluginId, tokenId }, + defaultIsoFiat + ) + ] }) const cryptoMaxPrecision = maxPrimaryCurrencyConversionDecimals( log10(cryptoDisplayDenom.multiplier), @@ -249,10 +260,10 @@ const SwapInputComponent = React.forwardRef( }, [convertFromCryptoNative, startNativeAmount]) const initialFiatAmount = React.useMemo(() => { - const cryptoCurrencyCode = getCurrencyCode(wallet, tokenId) const fiatAmount = convertCurrency( initialExchangeAmount, - cryptoCurrencyCode, + wallet.currencyInfo.pluginId, + tokenId, defaultIsoFiat ) return fiatAmount @@ -286,10 +297,10 @@ const SwapInputComponent = React.forwardRef( * to initialize the focused flip input field with fiat. */ const overrideForceField = useMemo(() => { - const cryptoCurrencyCode = getCurrencyCode(wallet, tokenId) const fiatValue = convertCurrency( '100', - cryptoCurrencyCode, + wallet.currencyInfo.pluginId, + tokenId, defaultIsoFiat ) return fiatValue === '0' ? 'crypto' : forceField diff --git a/src/components/themed/TransactionListRow.tsx b/src/components/themed/TransactionListRow.tsx index 40b24d7e792..4b3b98c44e5 100644 --- a/src/components/themed/TransactionListRow.tsx +++ b/src/components/themed/TransactionListRow.tsx @@ -70,7 +70,7 @@ function TransactionViewInner(props: TransactionViewInnerProps) { const styles = getStyles(theme) const { navigation, wallet, transaction, isCard } = props - const { metadata = {}, currencyCode, tokenId } = transaction + const { metadata = {}, tokenId } = transaction const currencyInfo = wallet.currencyInfo const defaultIsoFiat = useSelector(state => state.ui.settings.defaultIsoFiat) @@ -84,7 +84,12 @@ function TransactionViewInner(props: TransactionViewInnerProps) { // CryptoAmount const exchangeRate = useSelector(state => - getExchangeRate(state, currencyCode, defaultIsoFiat) + getExchangeRate( + state, + wallet.currencyInfo.pluginId, + tokenId, + defaultIsoFiat + ) ) let maxConversionDecimals = DEFAULT_TRUNCATE_PRECISION if (exchangeRate != null && gt(exchangeRate, '0')) { diff --git a/src/components/themed/TransactionListTop.tsx b/src/components/themed/TransactionListTop.tsx index 14e07ce1769..3121101c089 100644 --- a/src/components/themed/TransactionListTop.tsx +++ b/src/components/themed/TransactionListTop.tsx @@ -968,7 +968,12 @@ export function TransactionListTop(props: OwnProps) { ) const exchangeDenomination = getExchangeDenom(wallet.currencyConfig, tokenId) const exchangeRate = useSelector(state => - getExchangeRate(state, currencyCode, defaultIsoFiat) + getExchangeRate( + state, + wallet.currencyInfo.pluginId, + tokenId, + defaultIsoFiat + ) ) const isAccountBalanceVisible = useSelector( state => state.ui.settings.isAccountBalanceVisible diff --git a/src/components/tiles/InterestRateChangeTile.tsx b/src/components/tiles/InterestRateChangeTile.tsx index 1be6d3eb340..56311fce18e 100644 --- a/src/components/tiles/InterestRateChangeTile.tsx +++ b/src/components/tiles/InterestRateChangeTile.tsx @@ -10,6 +10,7 @@ import type { } from '../../plugins/borrow-plugins/types' import { useSelector } from '../../types/reactRedux' import type { GuiExchangeRates } from '../../types/types' +import { createRateKey } from '../../util/exchangeRates' import { mulToPrecision } from '../../util/utils' import { PercentageChangeArrowTile } from './PercentageChangeArrowTile' @@ -44,13 +45,19 @@ const InterestRateChangeTileComponent = (props: Props) => { } = currencyWallet // Define exchange rates - const necessaryExchangeRates = [...debts, newDebt].reduce((pairs, obj) => { - const { tokenId } = obj - const { currencyCode } = tokenId == null ? currencyInfo : allTokens[tokenId] - // @ts-expect-error - pairs.push(`${currencyCode}_${defaultIsoFiat}`) - return pairs - }, []) + const necessaryExchangeRates = [...debts, newDebt].reduce( + (pairs: string[], obj) => { + const { tokenId } = obj + pairs.push( + createRateKey( + { pluginId: currencyWallet.currencyInfo.pluginId, tokenId }, + defaultIsoFiat + ) + ) // TODO: + return pairs + }, + [] + ) const exchangeRateMap = React.useRef({}) const exchangeRates = useHandler( diff --git a/src/components/tiles/NetworkFeeTile.tsx b/src/components/tiles/NetworkFeeTile.tsx index 9e2eaa284a8..58e02e90d1b 100644 --- a/src/components/tiles/NetworkFeeTile.tsx +++ b/src/components/tiles/NetworkFeeTile.tsx @@ -19,16 +19,13 @@ interface Props { // TODO: Integrate into SendScene, FlipInputModal, and AdvancedDetailsModal export const NetworkFeeTile = (props: Props) => { const { wallet, nativeAmount } = props - const { - currencyConfig, - currencyInfo: { currencyCode } - } = wallet + const { currencyConfig } = wallet const defaultIsoFiat = useSelector(state => state.ui.settings.defaultIsoFiat) const fiatDenomination = getDenomFromIsoCode(defaultIsoFiat) const exchangeRate = useSelector(state => - getExchangeRate(state, currencyCode, defaultIsoFiat) + getExchangeRate(state, wallet.currencyInfo.pluginId, null, defaultIsoFiat) ) const exchangeDenominationMultiplier = getExchangeDenom( @@ -66,7 +63,8 @@ export const NetworkFeeTile = (props: Props) => { const feeFiatAmount = useFiatText({ appendFiatCurrencyCode: false, autoPrecision: true, - cryptoCurrencyCode: currencyCode, + pluginId: wallet.currencyInfo.pluginId, + tokenId: null, cryptoExchangeMultiplier: exchangeDenominationMultiplier, fiatSymbolSpace: true, isoFiatCurrencyCode: defaultIsoFiat, diff --git a/src/hooks/useFiatText.ts b/src/hooks/useFiatText.ts index 563b62d313d..ea43ba0d6da 100644 --- a/src/hooks/useFiatText.ts +++ b/src/hooks/useFiatText.ts @@ -1,4 +1,5 @@ import { abs, add, div, lt, toFixed } from 'biggystring' +import type { EdgeTokenId } from 'edge-core-js' import { getFiatSymbol, @@ -14,7 +15,8 @@ const defaultMultiplier = Math.pow(10, DECIMAL_PRECISION).toString() interface Props { appendFiatCurrencyCode?: boolean autoPrecision?: boolean - cryptoCurrencyCode: string + pluginId: string + tokenId: EdgeTokenId cryptoExchangeMultiplier?: string fiatSymbolSpace?: boolean hideFiatSymbol?: boolean @@ -31,7 +33,8 @@ export const useFiatText = (props: Props): string => { const { appendFiatCurrencyCode, autoPrecision, - cryptoCurrencyCode, + pluginId, + tokenId, cryptoExchangeMultiplier = defaultMultiplier, fiatSymbolSpace, hideFiatSymbol, @@ -55,7 +58,8 @@ export const useFiatText = (props: Props): string => { ) return convertCurrency( state, - cryptoCurrencyCode, + pluginId, + tokenId, isoFiatCurrencyCode, cryptoAmount ) diff --git a/src/hooks/useTokenDisplayData.ts b/src/hooks/useTokenDisplayData.ts index 73e48a209dc..d92b5dce7d1 100644 --- a/src/hooks/useTokenDisplayData.ts +++ b/src/hooks/useTokenDisplayData.ts @@ -1,6 +1,6 @@ import type { EdgeCurrencyConfig, EdgeTokenId } from 'edge-core-js' -import { getExchangeRate } from '../selectors/WalletSelectors' +import { getFiatExchangeRate } from '../selectors/WalletSelectors' import { useSelector } from '../types/reactRedux' import { getDenomFromIsoCode } from '../util/utils' @@ -34,7 +34,7 @@ export const useTokenDisplayData = (props: { // - 'Fiat' is the QUOTE, defined by the wallet's fiatCurrencyCode // - 'Yest' is an index for a historical price from 24 hours ago. const usdToWalletFiatRate = useSelector(state => - getExchangeRate(state, 'iso:USD', isoFiatCurrencyCode) + getFiatExchangeRate(state, 'iso:USD', isoFiatCurrencyCode) ) const assetFiatPrice = useSelector( state => diff --git a/src/selectors/WalletSelectors.ts b/src/selectors/WalletSelectors.ts index 7dc827bdbf7..3b9cabed2d7 100644 --- a/src/selectors/WalletSelectors.ts +++ b/src/selectors/WalletSelectors.ts @@ -2,12 +2,14 @@ import { mul } from 'biggystring' import type { EdgeCurrencyInfo, EdgeCurrencyWallet, - EdgeDenomination + EdgeDenomination, + EdgeTokenId } from 'edge-core-js' -import type { RootState, ThunkAction } from '../types/reduxTypes' +import type { RootState } from '../types/reduxTypes' import type { GuiExchangeRates } from '../types/types' import { getWalletTokenId } from '../util/CurrencyInfoHelpers' +import { createRateKey } from '../util/exchangeRates' import { convertCurrencyFromExchangeRates, convertNativeToExchange, @@ -42,41 +44,48 @@ export const getActiveWalletCurrencyInfos = ( export const getExchangeRate = ( state: RootState, - fromCurrencyCode: string, + pluginId: string, + tokenId: EdgeTokenId, toCurrencyCode: string ): number => { const exchangeRates = state.exchangeRates - const rateKey = `${fromCurrencyCode}_${toCurrencyCode}` + + const rateKey = createRateKey({ pluginId, tokenId }, toCurrencyCode) + const rate = exchangeRates[rateKey] ?? 0 + return rate +} +export const getFiatExchangeRate = ( + state: RootState, + fromIsoCode: string, + toIsoCode: string +): number => { + const exchangeRates = state.exchangeRates + const rateKey = createRateKey(fromIsoCode, toIsoCode) const rate = exchangeRates[rateKey] ?? 0 return rate } export const convertCurrency = ( state: RootState, - fromCurrencyCode: string, + pluginId: string, + tokenId: EdgeTokenId, toCurrencyCode: string, amount: string = '1' ): string => { - const exchangeRate = getExchangeRate(state, fromCurrencyCode, toCurrencyCode) + const exchangeRate = getExchangeRate(state, pluginId, tokenId, toCurrencyCode) const convertedAmount = mul(amount, exchangeRate) return convertedAmount } -export function convertCurrencyFromState( - fromCurrencyCode: string, - toCurrencyCode: string, +export const convertFiatCurrency = ( + state: RootState, + fromFiatCode: string, + toFiatCode: string, amount: string = '1' -): ThunkAction { - return (dispatch, getState): string => { - const state = getState() - const exchangeRate = getExchangeRate( - state, - fromCurrencyCode, - toCurrencyCode - ) - const convertedAmount = mul(amount, exchangeRate) - return convertedAmount - } +): string => { + const exchangeRate = getFiatExchangeRate(state, fromFiatCode, toFiatCode) + const convertedAmount = mul(amount, exchangeRate) + return convertedAmount } export const calculateFiatBalance = ( diff --git a/src/util/ActionProgramUtils.ts b/src/util/ActionProgramUtils.ts index 979bddf9dbc..1dc2d798d22 100644 --- a/src/util/ActionProgramUtils.ts +++ b/src/util/ActionProgramUtils.ts @@ -20,6 +20,7 @@ import { config } from '../theme/appConfig' import type { GuiExchangeRates } from '../types/types' import { getToken } from './CurrencyInfoHelpers' import { enableTokenCurrencyCode } from './CurrencyWalletHelpers' +import { createRateKey } from './exchangeRates' import { convertCurrencyFromExchangeRates, convertNativeToExchange, @@ -395,7 +396,15 @@ export const makeAaveCloseAction = async ({ ) const collateralDeficitAmount = div( collateralDeficitFiat, - exchangeRates[`${collateralCurrencyCode}_${defaultIsoFiat}`], + exchangeRates[ + createRateKey( + { + pluginId: wallet.currencyInfo.pluginId, + tokenId: collateralTokenId + }, + defaultIsoFiat + ) + ], // TODO: DECIMAL_PRECISION ) @@ -404,7 +413,15 @@ export const makeAaveCloseAction = async ({ secondaryExchangeMultiplier: getDenomFromIsoCode(defaultIsoFiat).multiplier, exchangeSecondaryToPrimaryRatio: - exchangeRates[`${collateralCurrencyCode}_${defaultIsoFiat}`] + exchangeRates[ + createRateKey( + { + pluginId: wallet.currencyInfo.pluginId, + tokenId: collateralTokenId + }, + defaultIsoFiat + ) + ] // TODO: }) const collateralMaxPrecision = maxPrimaryCurrencyConversionDecimals( log10(collateralDenom.multiplier), diff --git a/src/util/CryptoAmount.ts b/src/util/CryptoAmount.ts index e1a29c5cf01..fe56548776b 100644 --- a/src/util/CryptoAmount.ts +++ b/src/util/CryptoAmount.ts @@ -8,6 +8,7 @@ import type { import type { GuiExchangeRates } from '../types/types' import { asBiggystring } from './cleaners' import { getTokenId } from './CurrencyInfoHelpers' +import { createRateKey } from './exchangeRates' import { DECIMAL_PRECISION, mulToPrecision } from './utils' /** @@ -174,7 +175,13 @@ export class CryptoAmount { * Unrounded numeric fiat value. */ fiatValue(exchangeRates: GuiExchangeRates, isoFiatCode: string): number { - const exchangeRateKey = `${this.currencyCode}_${isoFiatCode}` + const exchangeRateKey = createRateKey( + { + pluginId: this.currencyConfig.currencyInfo.pluginId, + tokenId: this.tokenId + }, + isoFiatCode + ) const exchangeRate = exchangeRates[exchangeRateKey] ?? '0' const convertedAmount = mul(this.exchangeAmount, exchangeRate) return parseFloat(convertedAmount) diff --git a/src/util/borrowUtils.ts b/src/util/borrowUtils.ts index 688c92b8fbd..83eeab3f628 100644 --- a/src/util/borrowUtils.ts +++ b/src/util/borrowUtils.ts @@ -9,6 +9,7 @@ import type { BorrowDebt } from '../plugins/borrow-plugins/types' import { useSelector } from '../types/reactRedux' +import { createRateKey } from './exchangeRates' import { mulToPrecision } from './utils' export const useTotalFiatAmount = ( @@ -35,7 +36,12 @@ export const useTotalFiatAmount = ( total, mul( div(obj.nativeAmount, multiplier, mulToPrecision(multiplier)), - getExchangeRate(`${currencyCode}_${defaultIsoFiat}`) + getExchangeRate( + createRateKey( + { pluginId: currencyInfo.pluginId, tokenId: obj.tokenId }, + defaultIsoFiat + ) + ) ) ) }, '0') diff --git a/src/util/exchangeRates.ts b/src/util/exchangeRates.ts index dc5e9e694a4..4500389f055 100644 --- a/src/util/exchangeRates.ts +++ b/src/util/exchangeRates.ts @@ -20,7 +20,7 @@ const SHOW_LOGS = false const clog = SHOW_LOGS ? console.log : (...args: any) => undefined // From rates server: -const asCryptoAsset = asObject({ +export const asCryptoAsset = asObject({ pluginId: asString, tokenId: asOptional(asEither(asString, asNull)) }) @@ -34,12 +34,12 @@ const asFiatRate = asObject({ fiatCode: asString, rate: asOptional(asNumber) // Return undefined if unable to get rate }) -const asRatesParams = asObject({ +export const asRatesParams = asObject({ targetFiat: asString, crypto: asArray(asCryptoRate), fiat: asArray(asFiatRate) }) -type RatesParams = ReturnType +export type RatesParams = ReturnType type RateMap = Record @@ -222,7 +222,7 @@ const addToQueue = ( } } -const createRateKey = ( +export const createRateKey = ( asset: { pluginId: string; tokenId?: EdgeTokenId } | string, targetFiat: string, date?: string diff --git a/src/util/fake/fakeRootState.ts b/src/util/fake/fakeRootState.ts index b2de30e1772..06ae3314175 100644 --- a/src/util/fake/fakeRootState.ts +++ b/src/util/fake/fakeRootState.ts @@ -118,24 +118,24 @@ export const fakeRootState: FakeState = { }, exchangeRates: { 'iso:USD_iso:USD': 1, - 'BTC_iso:EUR': 18306.89774564676, - 'iso:EUR_BTC': 0.000054624219455084, - 'BTC_iso:USD': 18429.44411915612363372929, - 'iso:USD_BTC': 0.000054260996345547, + 'bitcoin_iso:EUR': 18306.89774564676, + 'iso:EUR_bitcoin': 0.000054624219455084, + 'bitcoin_iso:USD': 18429.44411915612363372929, + 'iso:USD_bitcoin': 0.000054260996345547, 'iso:USD_iso:EUR': 0.993350511674848, 'iso:EUR_iso:USD': 1.006693999999999, - 'ETH_iso:EUR': 1317.3772770501655, - 'iso:EUR_ETH': 0.000759083990152898, - 'ETH_iso:USD': 1326.19580054273933455988, - 'iso:USD_ETH': 0.000754036470022567, - 'BTC_iso:USD_2022-11-08T01:00:00.000Z': 20531.75733529252829612233, - 'iso:USD_BTC_2022-11-08T01:00:00.000Z': 0.000048705036966371, - 'ETH_iso:USD_2022-11-08T01:00:00.000Z': 1562.74372528628146028495, - 'iso:USD_ETH_2022-11-08T01:00:00.000Z': 0.000639900185692192 + 'ethereum_iso:EUR': 1317.3772770501655, + 'iso:EUR_ethereum': 0.000759083990152898, + 'ethereum_iso:USD': 1326.19580054273933455988, + 'iso:USD_ethereum': 0.000754036470022567, + 'bitcoin_iso:USD_2022-11-08T01:00:00.000Z': 20531.75733529252829612233, + 'iso:USD_bitcoin_2022-11-08T01:00:00.000Z': 0.000048705036966371, + 'ethereum_iso:USD_2022-11-08T01:00:00.000Z': 1562.74372528628146028495, + 'iso:USD_ethereum_2022-11-08T01:00:00.000Z': 0.000639900185692192 }, exchangeRatesMap: new Map([ [ - 'BTC', + 'bitcoin', new Map([['iso:EUR', { currentRate: 1000000, yesterdayRate: 10000000 }]]) ] ]), diff --git a/src/util/network.ts b/src/util/network.ts index 70cb4063bfc..2260c908178 100644 --- a/src/util/network.ts +++ b/src/util/network.ts @@ -12,7 +12,9 @@ import { config } from '../theme/appConfig' import { asyncWaterfall, getOsVersion, shuffleArray } from './utils' import { checkAppVersion } from './versionCheck' const INFO_SERVERS = ['https://info1.edge.app', 'https://info2.edge.app'] -const RATES_SERVERS = ['https://rates3.edge.app', 'https://rates4.edge.app'] +// const RATES_SERVERS = ['https://rates3.edge.app', 'https://rates4.edge.app'] +// const RATES_SERVERS = ['http://10.0.2.2:8087'] +const RATES_SERVERS = ['http://127.0.0.1:8087'] const INFO_FETCH_INTERVAL = 5 * 60 * 1000 // 5 minutes diff --git a/src/util/utils.ts b/src/util/utils.ts index 8410ca980dd..a373794a313 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -33,6 +33,7 @@ import type { RootState } from '../types/reduxTypes' import type { GuiExchangeRates, GuiFiatType } from '../types/types' import { getCurrencyCode, getTokenId } from './CurrencyInfoHelpers' import { base58 } from './encoding' +import { createRateKey } from './exchangeRates' export const DECIMAL_PRECISION = 18 export const DEFAULT_TRUNCATE_PRECISION = 6 @@ -185,7 +186,10 @@ export const convertCurrencyFromExchangeRates = ( toCurrencyCode: string, amount: string ): string => { - const rateKey = `${fromPluginId}_${String(fromTokenId)}_${toCurrencyCode}` + const rateKey = createRateKey( + { pluginId: fromPluginId, tokenId: fromTokenId }, + toCurrencyCode + ) const rate = exchangeRates[rateKey] ?? '0' const convertedAmount = mul(amount, rate) return convertedAmount @@ -373,7 +377,12 @@ export const getTotalFiatAmountFromExchangeRates = ( const nativeBalance = wallet.balanceMap.get(tokenId) ?? '0' const currencyCode = getCurrencyCode(wallet, tokenId) const rate = - exchangeRates[`${currencyCode}_${isoFiatCurrencyCode}`] ?? '0' + exchangeRates[ + createRateKey( + { pluginId: wallet.currencyInfo.pluginId, tokenId }, + isoFiatCurrencyCode + ) + ] ?? '0' log.push( `\nLogTot: code=${currencyCode} rate=${rate} nb=${nativeBalance}` )