diff --git a/src/analytics/selectors.test.ts b/src/analytics/selectors.test.ts index 3662b47e0db..7234da60757 100644 --- a/src/analytics/selectors.test.ts +++ b/src/analytics/selectors.test.ts @@ -2,6 +2,7 @@ import { PincodeType } from 'src/account/reducer' import { getCurrentUserTraits } from 'src/analytics/selectors' import { getFeatureGate } from 'src/statsig' import { getMockStoreData } from 'test/utils' +import { NetworkId } from 'src/transactions/types' jest.mock('src/statsig') @@ -30,8 +31,10 @@ describe('getCurrentUserTraits', () => { }, tokens: { tokenBalances: { - '0xcusd': { + 'celo-alfajores:0xcusd': { name: 'Celo Dollars', + tokenId: 'celo-alfajores:0xcusd', + networkId: NetworkId['celo-alfajores'], address: '0xcusd', symbol: 'cUSD', decimals: 18, @@ -41,8 +44,10 @@ describe('getCurrentUserTraits', () => { priceFetchedAt: Date.now(), isCoreToken: true, }, - '0xceur': { + 'celo-alfajores:0xceur': { name: 'Celo Euros', + tokenId: 'celo-alfajores:0xceur', + networkId: NetworkId['celo-alfajores'], address: '0xceur', symbol: 'cEUR', decimals: 18, @@ -52,19 +57,24 @@ describe('getCurrentUserTraits', () => { priceFetchedAt: Date.now(), isCoreToken: true, }, - '0xcelo': { + 'celo-alfajores:native': { name: 'Celo', + tokenId: 'celo-alfajores:native', + networkId: NetworkId['celo-alfajores'], address: '0xcelo', symbol: 'CELO', decimals: 18, imageUrl: '', usdPrice: '5', balance: '0', + isNative: true, priceFetchedAt: Date.now(), isCoreToken: true, }, - '0xa': { + 'celo-alfajores:0xa': { name: 'a', + tokenId: 'celo-alfajores:0xa', + networkId: NetworkId['celo-alfajores'], address: '0xa', symbol: 'A', decimals: 18, @@ -73,8 +83,10 @@ describe('getCurrentUserTraits', () => { balance: '1', priceFetchedAt: Date.now(), }, - '0xb': { + 'celo-alfajores:0xb': { name: 'b', + tokenId: 'celo-alfajores:0xb', + networkId: NetworkId['celo-alfajores'], address: '0xb', symbol: 'B', decimals: 18, @@ -83,8 +95,10 @@ describe('getCurrentUserTraits', () => { balance: '3', priceFetchedAt: Date.now(), }, - '0xc': { + 'celo-alfajores:0xc': { name: 'c', + tokenId: 'celo-alfajores:0xc', + networkId: NetworkId['celo-alfajores'], address: '0xc', symbol: 'C', decimals: 18, @@ -93,8 +107,10 @@ describe('getCurrentUserTraits', () => { balance: '2', priceFetchedAt: Date.now(), }, - '0xd': { + 'celo-alfajores:0xd': { name: 'd', + tokenId: 'celo-alfajores:0xd', + networkId: NetworkId['celo-alfajores'], address: '0xd', symbol: 'D', decimals: 18, @@ -103,8 +119,10 @@ describe('getCurrentUserTraits', () => { balance: '0.01', priceFetchedAt: Date.now(), }, - '0xe': { + 'celo-alfajores:0xe': { name: 'e', + tokenId: 'celo-alfajores:0xe', + networkId: NetworkId['celo-alfajores'], address: '0xe', symbol: 'E', decimals: 18, @@ -113,8 +131,10 @@ describe('getCurrentUserTraits', () => { balance: '7', priceFetchedAt: Date.now(), }, - '0xf': { + 'celo-alfajores:0xf': { name: 'f', + tokenId: 'celo-alfajores:0xf', + networkId: NetworkId['celo-alfajores'], address: '0xf', symbol: 'F', decimals: 18, @@ -123,8 +143,10 @@ describe('getCurrentUserTraits', () => { balance: '6', priceFetchedAt: Date.now(), }, - '0xg': { + 'celo-alfajores:0xg': { name: 'g', + tokenId: 'celo-alfajores:0xg', + networkId: NetworkId['celo-alfajores'], address: '0xg', symbol: 'G', decimals: 18, @@ -133,8 +155,10 @@ describe('getCurrentUserTraits', () => { balance: '10', priceFetchedAt: Date.now(), }, - '0xh': { + 'celo-alfajores:0xh': { name: 'h', + tokenId: 'celo-alfajores:0xh', + networkId: NetworkId['celo-alfajores'], address: '0xh', symbol: 'H', decimals: 18, @@ -143,8 +167,10 @@ describe('getCurrentUserTraits', () => { balance: '9.123456789', priceFetchedAt: Date.now(), }, - '0xi': { + 'celo-alfajores:0xi': { name: 'i', + tokenId: 'celo-alfajores:0xi', + networkId: NetworkId['celo-alfajores'], address: '0xi', symbol: 'I', decimals: 18, @@ -153,9 +179,11 @@ describe('getCurrentUserTraits', () => { balance: '1000', priceFetchedAt: Date.now(), }, - '0xj': { + 'celo-alfajores:0xj': { name: 'j', - address: '0xi', + tokenId: 'celo-alfajores:0xj', + networkId: NetworkId['celo-alfajores'], + address: '0xj', symbol: '', // Empty on purpose, will end up using the address decimals: 18, imageUrl: '', @@ -163,8 +191,10 @@ describe('getCurrentUserTraits', () => { balance: '11.003', priceFetchedAt: Date.now(), }, - '0xk': { + 'celo-alfajores:0xk': { name: 'k', + tokenId: 'celo-alfajores:0xk', + networkId: NetworkId['celo-alfajores'], address: '0xk', symbol: 'K', decimals: 18, @@ -248,7 +278,7 @@ describe('getCurrentUserTraits', () => { language: 'es-419', localCurrencyCode: 'PHP', netWorthUsd: 5764.949123945, - otherTenTokens: 'I:1000,K:80,0xi:11.003,G:10,H:9.12345,E:7,F:6,B:3,C:2,A:1', + otherTenTokens: 'I:1000,K:80,0xj:11.003,G:10,H:9.12345,E:7,F:6,B:3,C:2,A:1', phoneCountryCallingCode: '+33', phoneCountryCodeAlpha2: 'FR', pincodeType: 'CustomPin', diff --git a/src/components/TokenBottomSheet.test.tsx b/src/components/TokenBottomSheet.test.tsx index 4d7a32b1c9a..078cb62c862 100644 --- a/src/components/TokenBottomSheet.test.tsx +++ b/src/components/TokenBottomSheet.test.tsx @@ -8,20 +8,30 @@ import TokenBottomSheet, { DEBOUCE_WAIT_TIME, TokenPickerOrigin, } from 'src/components/TokenBottomSheet' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' import { createMockStore, getElementText } from 'test/utils' -import { mockCeurAddress, mockCusdAddress, mockTestTokenAddress } from 'test/values' +import { + mockCeurAddress, + mockCusdAddress, + mockTestTokenAddress, + mockCeurTokenId, + mockCusdTokenId, + mockTestTokenTokenId, +} from 'test/values' +import { NetworkId } from 'src/transactions/types' jest.mock('src/components/useShowOrHideAnimation') jest.mock('src/analytics/ValoraAnalytics') -const tokens: TokenBalance[] = [ +const tokens: TokenBalanceWithAddress[] = [ { balance: new BigNumber('10'), usdPrice: new BigNumber('1'), lastKnownUsdPrice: new BigNumber('1'), symbol: 'cUSD', address: mockCusdAddress, + tokenId: mockCusdTokenId, + networkId: NetworkId['celo-alfajores'], isCoreToken: true, priceFetchedAt: Date.now(), decimals: 18, @@ -34,6 +44,8 @@ const tokens: TokenBalance[] = [ lastKnownUsdPrice: new BigNumber('1.2'), symbol: 'cEUR', address: mockCeurAddress, + tokenId: mockCeurTokenId, + networkId: NetworkId['celo-alfajores'], isCoreToken: true, priceFetchedAt: Date.now(), decimals: 18, @@ -46,6 +58,8 @@ const tokens: TokenBalance[] = [ usdPrice: null, lastKnownUsdPrice: new BigNumber('1'), address: mockTestTokenAddress, + tokenId: mockTestTokenTokenId, + networkId: NetworkId['celo-alfajores'], priceFetchedAt: Date.now(), decimals: 18, name: 'Test Token', @@ -56,28 +70,34 @@ const tokens: TokenBalance[] = [ const mockStore = createMockStore({ tokens: { tokenBalances: { - [mockCusdAddress]: { + [mockCusdTokenId]: { balance: '10', usdPrice: '1', symbol: 'cUSD', address: mockCusdAddress, + tokenId: mockCusdTokenId, + networkId: NetworkId['celo-alfajores'], isCoreToken: true, priceFetchedAt: Date.now(), name: 'Celo Dollar', }, - [mockCeurAddress]: { + [mockCeurTokenId]: { balance: '20', usdPrice: '1.2', symbol: 'cEUR', address: mockCeurAddress, + tokenId: mockCeurTokenId, + networkId: NetworkId['celo-alfajores'], isCoreToken: true, priceFetchedAt: Date.now(), name: 'Celo Euro', }, - [mockTestTokenAddress]: { + [mockTestTokenTokenId]: { balance: '10', symbol: 'TT', address: mockTestTokenAddress, + tokenId: mockTestTokenTokenId, + networkId: NetworkId['celo-alfajores'], priceFetchedAt: Date.now(), name: 'Test Token', }, diff --git a/src/components/TokenBottomSheet.tsx b/src/components/TokenBottomSheet.tsx index 5e861c32e2f..713c52988e0 100644 --- a/src/components/TokenBottomSheet.tsx +++ b/src/components/TokenBottomSheet.tsx @@ -13,7 +13,7 @@ import Times from 'src/icons/Times' import colors, { Colors } from 'src/styles/colors' import fontStyles from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' export enum TokenPickerOrigin { Send = 'Send', @@ -29,12 +29,18 @@ interface Props { origin: TokenPickerOrigin onTokenSelected: (tokenAddress: string) => void onClose: () => void - tokens: TokenBalance[] + tokens: TokenBalanceWithAddress[] searchEnabled?: boolean title: string } -function TokenOption({ tokenInfo, onPress }: { tokenInfo: TokenBalance; onPress: () => void }) { +function TokenOption({ + tokenInfo, + onPress, +}: { + tokenInfo: TokenBalanceWithAddress + onPress: () => void +}) { return ( diff --git a/src/components/TokenIcon.test.tsx b/src/components/TokenIcon.test.tsx index a41eb36316a..22378f02235 100644 --- a/src/components/TokenIcon.test.tsx +++ b/src/components/TokenIcon.test.tsx @@ -3,12 +3,12 @@ import React from 'react' import { Provider } from 'react-redux' import TokenIcon from 'src/components/TokenIcon' import { createMockStore } from 'test/utils' -import { mockCeloAddress, mockCusdAddress, mockTokenBalances } from 'test/values' +import { mockCeloTokenId, mockCusdTokenId, mockTokenBalances } from 'test/values' // Setting up the mock token balances with expected additional values -const CELO_TOKEN = mockTokenBalances[mockCeloAddress] +const CELO_TOKEN = mockTokenBalances[mockCeloTokenId] const CUSD_TOKEN = { - ...mockTokenBalances[mockCusdAddress], + ...mockTokenBalances[mockCusdTokenId], networkIconUrl: 'https://raw.githubusercontent.com/valora-inc/address-metadata/main/assets/tokens/CELO.png', } diff --git a/src/components/TokenTotalLineItem.test.tsx b/src/components/TokenTotalLineItem.test.tsx index e8928a26a8d..6dd8f38ae7d 100644 --- a/src/components/TokenTotalLineItem.test.tsx +++ b/src/components/TokenTotalLineItem.test.tsx @@ -5,11 +5,12 @@ import 'react-native' import { Provider } from 'react-redux' import TokenTotalLineItem from 'src/components/TokenTotalLineItem' import { LocalCurrencyCode } from 'src/localCurrency/consts' -import { LocalAmount } from 'src/transactions/types' +import { NetworkId, LocalAmount } from 'src/transactions/types' import { createMockStore, getElementText } from 'test/utils' -import { mockCusdAddress } from 'test/values' +import { mockCusdAddress, mockCusdTokenId } from 'test/values' const mockBtcAddress = '0xbtc' +const mockBtcTokenId = `celo-alfajores:${mockBtcAddress}` const defaultAmount = new BigNumber(10) const defaultTokenAddress = mockCusdAddress @@ -42,13 +43,19 @@ describe('TokenTotalLineItem', () => { }, tokens: { tokenBalances: { - [mockCusdAddress]: { + [mockCusdTokenId]: { + networkId: NetworkId['celo-alfajores'], + address: mockCusdAddress, + tokenId: mockCusdTokenId, symbol: 'cUSD', usdPrice: '1', balance: '10', priceFetchedAt: Date.now(), }, - [mockBtcAddress]: { + [mockBtcTokenId]: { + networkId: NetworkId['celo-alfajores'], + address: mockBtcAddress, + tokenId: mockBtcTokenId, symbol: 'WBTC', usdPrice: '65000', balance: '0.5', diff --git a/src/consumerIncentives/ConsumerIncentivesHomeScreen.test.tsx b/src/consumerIncentives/ConsumerIncentivesHomeScreen.test.tsx index 4fc130c9240..19a097b3e4e 100644 --- a/src/consumerIncentives/ConsumerIncentivesHomeScreen.test.tsx +++ b/src/consumerIncentives/ConsumerIncentivesHomeScreen.test.tsx @@ -17,7 +17,8 @@ import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' import { StoredTokenBalance } from 'src/tokens/slice' import { createMockStore } from 'test/utils' -import { mockCeurAddress, mockCusdAddress } from 'test/values' +import { mockCeurAddress, mockCusdAddress, mockCusdTokenId } from 'test/values' +import { NetworkId } from 'src/transactions/types' interface TokenBalances { [address: string]: StoredTokenBalance @@ -25,6 +26,8 @@ interface TokenBalances { const CUSD_TOKEN_BALANCE = { address: mockCusdAddress, + tokenId: mockCusdTokenId, + networkId: NetworkId['celo-alfajores'], balance: '50', usdPrice: '1', symbol: 'cUSD', @@ -36,10 +39,10 @@ const CUSD_TOKEN_BALANCE = { } const ONLY_CUSD_BALANCE: TokenBalances = { - [mockCusdAddress]: CUSD_TOKEN_BALANCE, + [mockCusdTokenId]: CUSD_TOKEN_BALANCE, } const NO_BALANCES: TokenBalances = { - [mockCusdAddress]: { + [mockCusdTokenId]: { ...CUSD_TOKEN_BALANCE, balance: '5', }, diff --git a/src/consumerIncentives/selectors.test.ts b/src/consumerIncentives/selectors.test.ts index 53621136860..1f39ad5b4b8 100644 --- a/src/consumerIncentives/selectors.test.ts +++ b/src/consumerIncentives/selectors.test.ts @@ -1,5 +1,6 @@ import { superchargeInfoSelector } from 'src/consumerIncentives/selectors' import { getMockStoreData } from 'test/utils' +import { NetworkId } from 'src/transactions/types' const DEFAULT_SUPERCHARGE_CONFIG = { minBalance: 10, @@ -8,6 +9,7 @@ const DEFAULT_SUPERCHARGE_CONFIG = { const DEFAULT_TOKEN_BALANCE_INFO = { name: 'Test token', + networkId: NetworkId['celo-alfajores'], decimals: 18, imageUrl: '', priceFetchedAt: Date.now(), @@ -26,30 +28,34 @@ describe('balanceInfoForSuperchargeSelector', () => { }, tokens: { tokenBalances: { - '0xa': { + 'celo-alfajores:0xa': { ...DEFAULT_TOKEN_BALANCE_INFO, address: '0xa', + tokenId: 'celo-alfajores:0xa', symbol: 'A', balance: '2000', usdPrice: '1', }, - '0xb': { + 'celo-alfajores:0xb': { ...DEFAULT_TOKEN_BALANCE_INFO, address: '0xb', + tokenId: 'celo-alfajores:0xb', symbol: 'B', balance: '800', usdPrice: '2', }, - '0xc': { + 'celo-alfajores:0xc': { ...DEFAULT_TOKEN_BALANCE_INFO, address: '0xc', + tokenId: 'celo-alfajores:0xc', symbol: 'C', balance: '1000', usdPrice: '1', }, - '0xd': { + 'celo-alfajores:0xd': { ...DEFAULT_TOKEN_BALANCE_INFO, address: '0xd', + tokenId: 'celo-alfajores:0xd', symbol: 'D', balance: '500', usdPrice: '5', @@ -82,20 +88,26 @@ describe('balanceInfoForSuperchargeSelector', () => { }, tokens: { tokenBalances: { - '0xa': { + 'celo-alfajores:0xa': { ...DEFAULT_TOKEN_BALANCE_INFO, + tokenId: 'celo-alfajores:0xa', + address: '0xa', symbol: 'A', balance: '8', usdPrice: '20', }, - '0xb': { + 'celo-alfajores:0xb': { ...DEFAULT_TOKEN_BALANCE_INFO, + tokenId: 'celo-alfajores:0xb', + address: '0xb', symbol: 'B', balance: '20', usdPrice: '2', }, - '0xc': { + 'celo-alfajores:0xc': { ...DEFAULT_TOKEN_BALANCE_INFO, + tokenId: 'celo-alfajores:0xc', + address: '0xc', symbol: 'C', balance: '30', usdPrice: '1', @@ -128,20 +140,26 @@ describe('balanceInfoForSuperchargeSelector', () => { }, tokens: { tokenBalances: { - '0xa': { + 'celo-alfajores:0xa': { ...DEFAULT_TOKEN_BALANCE_INFO, + tokenId: 'celo-alfajores:0xa', + address: '0xa', symbol: 'A', balance: '1200', usdPrice: '2', }, - '0xb': { + 'celo-alfajores:0xb': { ...DEFAULT_TOKEN_BALANCE_INFO, + tokenId: 'celo-alfajores:0xb', + address: '0xb', symbol: 'B', balance: '3000', usdPrice: '1', }, - '0xc': { + 'celo-alfajores:0xc': { ...DEFAULT_TOKEN_BALANCE_INFO, + tokenId: 'celo-alfajores:0xc', + address: '0xc', symbol: 'C', balance: '900', usdPrice: '1', diff --git a/src/exchange/WithdrawCeloScreen.test.tsx b/src/exchange/WithdrawCeloScreen.test.tsx index 4662705b3a4..f0f6eedffe2 100644 --- a/src/exchange/WithdrawCeloScreen.test.tsx +++ b/src/exchange/WithdrawCeloScreen.test.tsx @@ -8,7 +8,7 @@ import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { Currency } from 'src/utils/currencies' import { createMockStore, getMockStackScreenProps } from 'test/utils' -import { mockCeloAddress, mockTokenBalances } from 'test/values' +import { mockCeloTokenId, mockTokenBalances } from 'test/values' const SAMPLE_ADDRESS = '0xcc642068bdbbdeb91f348213492d2a80ab1ed23c' const SAMPLE_BALANCE = '55.00001' @@ -19,8 +19,8 @@ const mockScreenProps = getMockStackScreenProps(Screens.WithdrawCeloScreen, { is const store = createMockStore({ tokens: { tokenBalances: { - [mockCeloAddress]: { - ...mockTokenBalances[mockCeloAddress], + [mockCeloTokenId]: { + ...mockTokenBalances[mockCeloTokenId], balance: SAMPLE_BALANCE, }, }, diff --git a/src/fees/saga.ts b/src/fees/saga.ts index 7d8b9278a89..b305f3fced2 100644 --- a/src/fees/saga.ts +++ b/src/fees/saga.ts @@ -15,7 +15,7 @@ import { tokensByAddressSelector, tokensByUsdBalanceSelector, } from 'src/tokens/selectors' -import { TokenBalance, TokenBalances } from 'src/tokens/slice' +import { TokenBalanceWithAddress, TokenBalancesWithAddress } from 'src/tokens/slice' import Logger from 'src/utils/Logger' import { Currency } from 'src/utils/currencies' import { ensureError } from 'src/utils/ensureError' @@ -49,7 +49,7 @@ export function* estimateFeeSaga({ }: ReturnType) { Logger.debug(`${TAG}/estimateFeeSaga`, `updating for ${feeType} ${tokenAddress} `) - const tokenBalances: TokenBalances = yield* select(tokensByAddressSelector) + const tokenBalances: TokenBalancesWithAddress = yield* select(tokensByAddressSelector) const tokenInfo = tokenBalances[tokenAddress] if (!tokenInfo?.balance || tokenInfo.balance.isEqualTo(0)) { @@ -241,7 +241,7 @@ export function* fetchFeeCurrencySaga() { return fetchFeeCurrency(tokens) } -export function fetchFeeCurrency(tokens: TokenBalance[]) { +export function fetchFeeCurrency(tokens: TokenBalanceWithAddress[]) { for (const token of tokens) { if (!token.isCoreToken) { continue diff --git a/src/fiatExchanges/BidaliScreen.test.tsx b/src/fiatExchanges/BidaliScreen.test.tsx index 31dba559252..256c3984478 100644 --- a/src/fiatExchanges/BidaliScreen.test.tsx +++ b/src/fiatExchanges/BidaliScreen.test.tsx @@ -6,7 +6,7 @@ import BidaliScreen from 'src/fiatExchanges/BidaliScreen' import { Screens } from 'src/navigator/Screens' import { Currency } from 'src/utils/currencies' import { createMockStore, getMockStackScreenProps } from 'test/utils' -import { mockCeurAddress, mockCusdAddress, mockTokenBalances } from 'test/values' +import { mockCeurTokenId, mockCusdTokenId, mockTokenBalances } from 'test/values' const mockScreenProps = getMockStackScreenProps(Screens.BidaliScreen, { currency: Currency.Dollar, @@ -29,12 +29,12 @@ describe(BidaliScreen, () => { account: { e164PhoneNumber: null }, tokens: { tokenBalances: { - [mockCusdAddress]: { - ...mockTokenBalances[mockCusdAddress], + [mockCusdTokenId]: { + ...mockTokenBalances[mockCusdTokenId], balance: '10', }, - [mockCeurAddress]: { - ...mockTokenBalances[mockCeurAddress], + [mockCeurTokenId]: { + ...mockTokenBalances[mockCeurTokenId], balance: '5', }, }, @@ -70,12 +70,12 @@ describe(BidaliScreen, () => { account: { e164PhoneNumber: '+14155556666' }, tokens: { tokenBalances: { - [mockCusdAddress]: { - ...mockTokenBalances[mockCusdAddress], + [mockCusdTokenId]: { + ...mockTokenBalances[mockCusdTokenId], balance: '10', }, - [mockCeurAddress]: { - ...mockTokenBalances[mockCeurAddress], + [mockCeurTokenId]: { + ...mockTokenBalances[mockCeurTokenId], balance: '5', }, }, @@ -110,12 +110,12 @@ describe(BidaliScreen, () => { account: { e164PhoneNumber: '+14155556666' }, tokens: { tokenBalances: { - [mockCusdAddress]: { - ...mockTokenBalances[mockCusdAddress], + [mockCusdTokenId]: { + ...mockTokenBalances[mockCusdTokenId], balance: '10', }, - [mockCeurAddress]: { - ...mockTokenBalances[mockCeurAddress], + [mockCeurTokenId]: { + ...mockTokenBalances[mockCeurTokenId], balance: '9', }, }, diff --git a/src/fiatExchanges/quotes/ExternalQuote.test.ts b/src/fiatExchanges/quotes/ExternalQuote.test.ts index c54e2820d12..1b41927a437 100644 --- a/src/fiatExchanges/quotes/ExternalQuote.test.ts +++ b/src/fiatExchanges/quotes/ExternalQuote.test.ts @@ -7,7 +7,13 @@ import { navigate } from 'src/navigator/NavigationService' import { CiCoCurrency } from 'src/utils/currencies' import { navigateToURI } from 'src/utils/linking' import { createMockStore } from 'test/utils' -import { mockCusdAddress, mockProviders, mockProviderSelectionAnalyticsData } from 'test/values' +import { + mockCusdAddress, + mockProviders, + mockProviderSelectionAnalyticsData, + mockCusdTokenId, +} from 'test/values' +import { NetworkId } from 'src/transactions/types' jest.mock('src/analytics/ValoraAnalytics') @@ -19,6 +25,8 @@ const mockTokenInfo = { lastKnownUsdPrice: new BigNumber('1'), symbol: 'cUSD', address: mockCusdAddress, + tokenId: mockCusdTokenId, + networkId: NetworkId['celo-alfajores'], isCoreToken: true, priceFetchedAt: Date.now(), decimals: 18, diff --git a/src/fiatExchanges/quotes/FiatConnectQuote.test.ts b/src/fiatExchanges/quotes/FiatConnectQuote.test.ts index fa0816acb71..4ddb2029bb4 100644 --- a/src/fiatExchanges/quotes/FiatConnectQuote.test.ts +++ b/src/fiatExchanges/quotes/FiatConnectQuote.test.ts @@ -21,10 +21,12 @@ import { Currency } from 'src/utils/currencies' import { createMockStore } from 'test/utils' import { mockCusdAddress, + mockCusdTokenId, mockFiatConnectProviderInfo, mockFiatConnectQuotes, mockProviderSelectionAnalyticsData, } from 'test/values' +import { NetworkId } from 'src/transactions/types' jest.mock('src/analytics/ValoraAnalytics') jest.mock('src/web3/contracts', () => ({ @@ -54,6 +56,8 @@ const mockTokenInfo = { usdPrice: new BigNumber('1'), lastKnownUsdPrice: new BigNumber('1'), symbol: 'cUSD', + tokenId: mockCusdTokenId, + networkId: NetworkId['celo-alfajores'], address: mockCusdAddress, isCoreToken: true, priceFetchedAt: Date.now(), diff --git a/src/fiatconnect/saga.ts b/src/fiatconnect/saga.ts index 971c7a374db..d307df677bc 100644 --- a/src/fiatconnect/saga.ts +++ b/src/fiatconnect/saga.ts @@ -74,7 +74,7 @@ import { UserLocationData } from 'src/networkInfo/saga' import { userLocationDataSelector } from 'src/networkInfo/selectors' import { buildAndSendPayment } from 'src/send/saga' import { tokensListSelector } from 'src/tokens/selectors' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' import { isTxPossiblyPending } from 'src/transactions/send' import { Network, newTransactionContext } from 'src/transactions/types' import Logger from 'src/utils/Logger' @@ -956,7 +956,7 @@ export function* _initiateSendTxToProvider({ }) { Logger.info(TAG, 'Starting transfer out transaction..') - const tokenList: TokenBalance[] = yield* select(tokensListSelector) + const tokenList: TokenBalanceWithAddress[] = yield* select(tokensListSelector) const cryptoType = fiatConnectQuote.getCryptoTypeString() const tokenInfo = tokenList.find((token) => token.symbol === cryptoType) if (!tokenInfo) { diff --git a/src/localCurrency/hooks.test.tsx b/src/localCurrency/hooks.test.tsx index d728a4823f5..26544252470 100644 --- a/src/localCurrency/hooks.test.tsx +++ b/src/localCurrency/hooks.test.tsx @@ -5,6 +5,7 @@ import { MoneyAmount } from 'src/apollo/types' import * as localCurrencyHooks from 'src/localCurrency/hooks' import { Currency } from 'src/utils/currencies' import { createMockStore } from 'test/utils' +import { NetworkId } from 'src/transactions/types' const useLocalCurrencyToShowSpy = jest.spyOn(localCurrencyHooks, 'useLocalCurrencyToShow') @@ -18,25 +19,33 @@ function createStore(usdToLocalRate: string | null = '2') { return createMockStore({ tokens: { tokenBalances: { - '0xcUSD': { + 'celo-alfajores:0xcUSD': { + networkId: NetworkId['celo-alfajores'], + address: '0xcUSD', symbol: 'cUSD', balance: '0', usdPrice: '1', priceFetchedAt: Date.now(), }, - '0xCELO': { + 'celo-alfajores:native': { + networkId: NetworkId['celo-alfajores'], + address: '0xCELO', symbol: 'CELO', balance: '0', usdPrice: '5', priceFetchedAt: Date.now(), }, - '0xT1': { + 'celo-alfajores:0xT1': { + networkId: NetworkId['celo-alfajores'], + address: '0xT1', symbol: 'T1', balance: '0', usdPrice: '5', priceFetchedAt: Date.now(), }, - '0xT2': { + 'celo-alfajores:0xT2': { + networkId: NetworkId['celo-alfajores'], + address: '0xT2', symbol: 'T2', usdPrice: '5', balance: null, diff --git a/src/paymentRequest/IncomingPaymentRequestListItem.test.tsx b/src/paymentRequest/IncomingPaymentRequestListItem.test.tsx index 6f792066494..6db38556132 100644 --- a/src/paymentRequest/IncomingPaymentRequestListItem.test.tsx +++ b/src/paymentRequest/IncomingPaymentRequestListItem.test.tsx @@ -18,7 +18,9 @@ import { createMockStore, getElementText } from 'test/utils' import { mockAccount3, mockCeurAddress, + mockCeurTokenId, mockCusdAddress, + mockCusdTokenId, mockE164Number, mockPaymentRequests, mockPhoneRecipient, @@ -46,8 +48,8 @@ const mockPaymentRequest = mockPaymentRequests[1] const balances = { tokenBalances: { ...mockTokenBalances, - [mockCusdAddress]: { - ...mockTokenBalances[mockCusdAddress], + [mockCusdTokenId]: { + ...mockTokenBalances[mockCusdTokenId], balance: '200', }, }, @@ -173,8 +175,8 @@ describe('IncomingPaymentRequestListItem', () => { tokens: { tokenBalances: { ...mockTokenBalances, - [mockCeurAddress]: { - ...mockTokenBalances[mockCeurAddress], + [mockCeurTokenId]: { + ...mockTokenBalances[mockCeurTokenId], balance: '200', }, }, @@ -194,8 +196,8 @@ describe('IncomingPaymentRequestListItem', () => { tokens: { tokenBalances: { ...mockTokenBalances, - [mockCeurAddress]: { - ...mockTokenBalances[mockCeurAddress], + [mockCeurTokenId]: { + ...mockTokenBalances[mockCeurTokenId], balance: '200', }, }, diff --git a/src/paymentRequest/utils.test.ts b/src/paymentRequest/utils.test.ts index b573fe60b92..8429a96d6f7 100644 --- a/src/paymentRequest/utils.test.ts +++ b/src/paymentRequest/utils.test.ts @@ -14,6 +14,8 @@ import { mockAccount, mockAccount2, mockCeurAddress, + mockCeurTokenId, + mockCusdTokenId, mockCusdAddress, mockE164Number, mockName, @@ -145,13 +147,13 @@ describe('transactionDataFromPaymentRequest', () => { }): TokenBalance[] => { return [ { - ...mockTokenBalances[mockCusdAddress], + ...mockTokenBalances[mockCusdTokenId], balance: BigNumber(cusdBalance), usdPrice: BigNumber(1), lastKnownUsdPrice: null, }, { - ...mockTokenBalances[mockCeurAddress], + ...mockTokenBalances[mockCeurTokenId], balance: BigNumber(ceurBalance), usdPrice: BigNumber(1), lastKnownUsdPrice: null, diff --git a/src/redux/migrations.ts b/src/redux/migrations.ts index c00496f90ed..0aafd50057a 100644 --- a/src/redux/migrations.ts +++ b/src/redux/migrations.ts @@ -1291,4 +1291,11 @@ export const migrations = { : 'Main', // same as initial state. should be very rare, since removed screens were not present in prev app version. }, }), + 150: (state: any) => ({ + ...state, + tokens: { + ...state.tokens, + tokenBalances: {}, + }, + }), } diff --git a/src/redux/store.test.ts b/src/redux/store.test.ts index d70c5114136..69f8b716ea0 100644 --- a/src/redux/store.test.ts +++ b/src/redux/store.test.ts @@ -98,7 +98,7 @@ describe('store state', () => { { "_persist": { "rehydrated": true, - "version": 149, + "version": 150, }, "account": { "acceptedTerms": false, diff --git a/src/redux/store.ts b/src/redux/store.ts index f08edaab410..cb633a57ca3 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -23,7 +23,7 @@ const persistConfig: PersistConfig = { key: 'root', // default is -1, increment as we make migrations // See https://github.com/valora-inc/wallet/tree/main/WALLET.md#redux-state-migration - version: 149, + version: 150, keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems. storage: FSStorage(), blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup'], diff --git a/src/send/Send.test.tsx b/src/send/Send.test.tsx index b4c369bad0c..cff81ba5cdc 100644 --- a/src/send/Send.test.tsx +++ b/src/send/Send.test.tsx @@ -9,7 +9,9 @@ import Send from 'src/send/Send' import { createMockStore, getMockStackScreenProps } from 'test/utils' import { mockCeloAddress, + mockCeloTokenId, mockCusdAddress, + mockCusdTokenId, mockE164Number, mockE164NumberInvite, mockRecipient, @@ -43,12 +45,12 @@ const defaultStore = { }, tokens: { tokenBalances: { - [mockCeloAddress]: { - ...mockTokenBalances[mockCeloAddress], + [mockCeloTokenId]: { + ...mockTokenBalances[mockCeloTokenId], balance: '20.0', }, - [mockCusdAddress]: { - ...mockTokenBalances[mockCusdAddress], + [mockCusdTokenId]: { + ...mockTokenBalances[mockCusdTokenId], balance: '10.0', }, }, diff --git a/src/send/utils.ts b/src/send/utils.ts index 6c549911847..47b242e56a0 100644 --- a/src/send/utils.ts +++ b/src/send/utils.ts @@ -11,7 +11,7 @@ import { updateValoraRecipientCache } from 'src/recipients/reducer' import { canSendTokensSelector } from 'src/send/selectors' import { TransactionDataInput } from 'src/send/SendAmount' import { tokensListSelector } from 'src/tokens/selectors' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' import { Currency } from 'src/utils/currencies' import Logger from 'src/utils/Logger' import { call, put, select } from 'typed-redux-saga' @@ -39,7 +39,7 @@ export function* handleSendPaymentData( }) ) - const tokens: TokenBalance[] = yield* select(tokensListSelector) + const tokens: TokenBalanceWithAddress[] = yield* select(tokensListSelector) const tokenInfo = tokens.find((token) => token?.symbol === (data.token ?? Currency.Dollar)) if (!tokenInfo?.usdPrice) { diff --git a/src/statsig/constants.ts b/src/statsig/constants.ts index 4d68f884328..6ea8280d921 100644 --- a/src/statsig/constants.ts +++ b/src/statsig/constants.ts @@ -37,6 +37,7 @@ export const FeatureGates = { [StatsigFeatureGates.SHOW_MULTI_CHAIN_TRANSFERS]: false, [StatsigFeatureGates.SHOW_NATIVE_TOKENS]: false, [StatsigFeatureGates.SHOW_ETH_IN_CICO]: false, + [StatsigFeatureGates.FETCH_MULTI_CHAIN_BALANCES]: false, [StatsigFeatureGates.USE_VIEM_FOR_SEND]: false, } diff --git a/src/statsig/index.ts b/src/statsig/index.ts index 5ad5b247fbb..2c08cae7753 100644 --- a/src/statsig/index.ts +++ b/src/statsig/index.ts @@ -142,7 +142,7 @@ export function setupOverridesFromLaunchArgs() { const { statsigGateOverrides } = LaunchArguments.value() if (statsigGateOverrides) { Logger.debug(TAG, 'Setting up gate overrides', statsigGateOverrides) - statsigGateOverrides.split(',').forEach((gateOverride) => { + statsigGateOverrides.split(',').forEach((gateOverride: string) => { const [gate, value] = gateOverride.split('=') Statsig.overrideGate(gate, value === 'true') }) diff --git a/src/statsig/types.ts b/src/statsig/types.ts index 507827d5537..b5f22aedd23 100644 --- a/src/statsig/types.ts +++ b/src/statsig/types.ts @@ -32,6 +32,7 @@ export enum StatsigFeatureGates { SHOW_MULTI_CHAIN_TRANSFERS = 'show_multi_chain_transfers', SHOW_NATIVE_TOKENS = 'show_native_tokens', SHOW_ETH_IN_CICO = 'show_eth_in_cico', + FETCH_MULTI_CHAIN_BALANCES = 'fetch_multi_chain_balances', USE_VIEM_FOR_SEND = 'use_viem_for_send', } diff --git a/src/swap/SwapScreen.tsx b/src/swap/SwapScreen.tsx index 2eabec7d061..41c80aa42b9 100644 --- a/src/swap/SwapScreen.tsx +++ b/src/swap/SwapScreen.tsx @@ -35,7 +35,7 @@ import SwapAmountInput from 'src/swap/SwapAmountInput' import { Field, SwapAmount } from 'src/swap/types' import useSwapQuote from 'src/swap/useSwapQuote' import { swappableTokensSelector } from 'src/tokens/selectors' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' const FETCH_UPDATED_QUOTE_DEBOUNCE_TIME = 500 const DEFAULT_FROM_TOKEN_SYMBOL = 'CELO' @@ -86,8 +86,8 @@ export function SwapScreenSection({ showDrawerTopNav }: { showDrawerTopNav: bool return swappableTokens[0] ?? CELO }, [swappableTokens]) - const [fromToken, setFromToken] = useState(defaultFromToken) - const [toToken, setToToken] = useState() + const [fromToken, setFromToken] = useState(defaultFromToken) + const [toToken, setToToken] = useState() // Raw input values (can contain region specific decimal separators) const [swapAmount, setSwapAmount] = useState(DEFAULT_SWAP_AMOUNT) diff --git a/src/swap/saga.test.ts b/src/swap/saga.test.ts index 6786e2cd18b..22d304aa506 100644 --- a/src/swap/saga.test.ts +++ b/src/swap/saga.test.ts @@ -20,6 +20,7 @@ import { mockAccount, mockCeloAddress, mockCeurAddress, + mockCeurTokenId, mockContract, mockTokenBalances, } from 'test/values' @@ -101,7 +102,7 @@ describe(swapSubmitSaga, () => { select(swappableTokensSelector), [ { - ...mockTokenBalances[mockCeurAddress], + ...mockTokenBalances[mockCeurTokenId], usdPrice: new BigNumber('1'), balance: new BigNumber('10'), }, diff --git a/src/swap/useSwapQuote.ts b/src/swap/useSwapQuote.ts index 60925e049d0..99fdff76594 100644 --- a/src/swap/useSwapQuote.ts +++ b/src/swap/useSwapQuote.ts @@ -4,7 +4,7 @@ import { useAsyncCallback } from 'react-async-hook' import { useSelector } from 'react-redux' import { guaranteedSwapPriceEnabledSelector } from 'src/swap/selectors' import { FetchQuoteResponse, Field, ParsedSwapAmount } from 'src/swap/types' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' import Logger from 'src/utils/Logger' import networkConfig from 'src/web3/networkConfig' import { walletAddressSelector } from 'src/web3/selectors' @@ -30,8 +30,8 @@ const useSwapQuote = () => { const refreshQuote = useAsyncCallback( async ( - fromToken: TokenBalance, - toToken: TokenBalance, + fromToken: TokenBalanceWithAddress, + toToken: TokenBalanceWithAddress, swapAmount: ParsedSwapAmount, updatedField: Field ) => { diff --git a/src/tokens/AssetItem.test.tsx b/src/tokens/AssetItem.test.tsx index 1429ddc2e1f..4ff92bc5d60 100644 --- a/src/tokens/AssetItem.test.tsx +++ b/src/tokens/AssetItem.test.tsx @@ -7,7 +7,8 @@ import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { AppTokenPosition } from 'src/positions/types' import { PositionItem, TokenBalanceItem } from 'src/tokens/AssetItem' import { createMockStore } from 'test/utils' -import { mockCusdAddress, mockPositions } from 'test/values' +import { NetworkId } from 'src/transactions/types' +import { mockCusdAddress, mockCusdTokenId, mockPositions } from 'test/values' beforeEach(() => { jest.clearAllMocks() @@ -80,7 +81,9 @@ describe('PositionItem', () => { describe('TokenBalanceItem', () => { const mockTokenInfo = { balance: new BigNumber('10'), + tokenId: mockCusdTokenId, usdPrice: new BigNumber('1'), + networkId: NetworkId['celo-alfajores'], lastKnownUsdPrice: new BigNumber('1'), symbol: 'cUSD', address: mockCusdAddress, diff --git a/src/tokens/AssetItem.tsx b/src/tokens/AssetItem.tsx index 74a055c482c..312a8887974 100644 --- a/src/tokens/AssetItem.tsx +++ b/src/tokens/AssetItem.tsx @@ -11,7 +11,7 @@ import { Position } from 'src/positions/types' import Colors from 'src/styles/colors' import fontStyles from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' import { Currency } from 'src/utils/currencies' import { ONE_DAY_IN_MILLIS } from 'src/utils/time' @@ -77,7 +77,7 @@ export const TokenBalanceItem = ({ token, showPriceChangeIndicatorInBalances, }: { - token: TokenBalance + token: TokenBalanceWithAddress showPriceChangeIndicatorInBalances: boolean }) => { const isHistoricalPriceUpdated = () => { diff --git a/src/tokens/TokenBalances.test.tsx b/src/tokens/TokenBalances.test.tsx index 0b7fc7e59bd..cb520e7b07f 100644 --- a/src/tokens/TokenBalances.test.tsx +++ b/src/tokens/TokenBalances.test.tsx @@ -13,12 +13,16 @@ import MockedNavigator from 'test/MockedNavigator' import { createMockStore, getElementText, getMockStackScreenProps } from 'test/utils' import { mockCeurAddress, + mockCeurTokenId, mockCusdAddress, + mockCusdTokenId, mockPositions, mockTestTokenAddress, mockTokenBalances, + mockTestTokenTokenId, mockTokenBalancesWithHistoricalPrices, } from 'test/values' +import { NetworkId } from 'src/transactions/types' jest.mock('src/statsig', () => { return { @@ -33,8 +37,10 @@ const storeWithoutHistoricalPrices = { tokens: { tokenBalances: { ...mockTokenBalances, - [mockTestTokenAddress]: { + [mockTestTokenTokenId]: { address: mockTestTokenAddress, + tokenId: mockTestTokenTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'TT', balance: '50', }, @@ -49,8 +55,10 @@ const storeWithHistoricalPrices = { tokens: { tokenBalances: { ...mockTokenBalancesWithHistoricalPrices, - [mockTestTokenAddress]: { + [mockTestTokenTokenId]: { address: mockTestTokenAddress, + tokenId: mockTestTokenTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'TT', balance: '50', usdPrice: '2', @@ -72,9 +80,11 @@ const storeWithHistoricalPrices = { const storeWithPositions = { tokens: { tokenBalances: { - [mockCeurAddress]: { + [mockCeurTokenId]: { usdPrice: '1.16', address: mockCeurAddress, + tokenId: mockCeurTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'cEUR', imageUrl: 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cEUR.png', @@ -84,9 +94,11 @@ const storeWithPositions = { isCoreToken: true, priceFetchedAt: Date.now(), }, - [mockCusdAddress]: { + [mockCusdTokenId]: { usdPrice: '1.001', address: mockCusdAddress, + tokenId: mockCusdTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'cUSD', imageUrl: 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cUSD.png', diff --git a/src/tokens/TokenBalances.tsx b/src/tokens/TokenBalances.tsx index 98908526350..5b390029b90 100644 --- a/src/tokens/TokenBalances.tsx +++ b/src/tokens/TokenBalances.tsx @@ -53,7 +53,7 @@ import { totalTokenBalanceSelector, visualizeNFTsEnabledInHomeAssetsPageSelector, } from 'src/tokens/selectors' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' import { sortByUsdBalance } from 'src/tokens/utils' import networkConfig from 'src/web3/networkConfig' import { walletAddressSelector } from 'src/web3/selectors' @@ -64,11 +64,11 @@ interface SectionData { } const AnimatedSectionList = - Animated.createAnimatedComponent>( - SectionList - ) + Animated.createAnimatedComponent< + SectionListProps + >(SectionList) -const assetIsPosition = (asset: Position | TokenBalance): asset is Position => +const assetIsPosition = (asset: Position | TokenBalanceWithAddress): asset is Position => 'type' in asset && (asset.type === 'app-token' || asset.type === 'contract-position') export enum AssetViewType { @@ -241,7 +241,7 @@ function TokenBalancesScreen({ navigation, route }: Props) { } }) - const sections: SectionListData[] = [] + const sections: SectionListData[] = [] positionsByDapp.forEach((positions, appName) => { sections.push({ data: positions, @@ -257,7 +257,7 @@ function TokenBalancesScreen({ navigation, route }: Props) { const renderSectionHeader = ({ section, }: { - section: SectionListData + section: SectionListData }) => { if (section.appName) { return ( @@ -271,7 +271,7 @@ function TokenBalancesScreen({ navigation, route }: Props) { return null } - const renderAssetItem = ({ item }: { item: TokenBalance | Position }) => { + const renderAssetItem = ({ item }: { item: TokenBalanceWithAddress | Position }) => { if (assetIsPosition(item)) { return } diff --git a/src/tokens/e2eTokens.ts b/src/tokens/e2eTokens.ts index f553d74fd6d..ce9934c9e67 100644 --- a/src/tokens/e2eTokens.ts +++ b/src/tokens/e2eTokens.ts @@ -1,4 +1,5 @@ import { StoredTokenBalances } from 'src/tokens/slice' +import { NetworkId } from 'src/transactions/types' // alfajores addresses const cUSD = '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1'.toLowerCase() @@ -7,35 +8,41 @@ const CELO = '0xF194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9'.toLowerCase() export function e2eTokens(): StoredTokenBalances { return { - [cUSD]: { + [`celo-alfajores:${cUSD}`]: { + tokenId: `celo-alfajores:${cUSD}`, address: cUSD, decimals: 18, imageUrl: '', name: 'Celo Dollars', symbol: 'cUSD', usdPrice: '1', + networkId: NetworkId['celo-alfajores'], balance: null, isCoreToken: true, priceFetchedAt: Date.now(), }, - [cEUR]: { + [`celo-alfajores:${cEUR}`]: { + tokenId: `celo-alfajores:${cEUR}`, address: cEUR, decimals: 18, imageUrl: '', name: 'Celo Euros', symbol: 'cEUR', usdPrice: '1.18', + networkId: NetworkId['celo-alfajores'], balance: null, isCoreToken: true, priceFetchedAt: Date.now(), }, - [CELO]: { + 'celo-alfajores:native': { + tokenId: 'celo-alfajores:native', address: CELO, decimals: 18, imageUrl: '', name: 'Celo native token', symbol: 'CELO', usdPrice: '6.5', + networkId: NetworkId['celo-alfajores'], balance: null, isCoreToken: true, priceFetchedAt: Date.now(), diff --git a/src/tokens/hooks.test.tsx b/src/tokens/hooks.test.tsx index ee44a83b814..36b53421c35 100644 --- a/src/tokens/hooks.test.tsx +++ b/src/tokens/hooks.test.tsx @@ -5,9 +5,12 @@ import { Text, View } from 'react-native' import { Provider } from 'react-redux' import { useAmountAsUsd, useLocalToTokenAmount, useTokenToLocalAmount } from 'src/tokens/hooks' import { createMockStore } from 'test/utils' +import { NetworkId } from 'src/transactions/types' const tokenAddressWithPriceAndBalance = '0x001' +const tokenIdWithPriceAndBalance = `celo-alfajores:${tokenAddressWithPriceAndBalance}` const tokenAddressWithoutBalance = '0x002' +const tokenIdWithoutBalance = `celo-alfajores:${tokenAddressWithoutBalance}` function TestComponent({ tokenAddress }: { tokenAddress: string }) { const tokenAmount = useLocalToTokenAmount(new BigNumber(1), tokenAddress) @@ -27,13 +30,19 @@ const store = (usdToLocalRate: string | null = '2') => createMockStore({ tokens: { tokenBalances: { - [tokenAddressWithPriceAndBalance]: { + [tokenIdWithPriceAndBalance]: { + address: tokenAddressWithPriceAndBalance, + tokenId: tokenIdWithPriceAndBalance, + networkId: NetworkId['celo-alfajores'], symbol: 'T1', balance: '0', usdPrice: '5', priceFetchedAt: Date.now(), }, - [tokenAddressWithoutBalance]: { + [tokenIdWithoutBalance]: { + address: tokenAddressWithoutBalance, + tokenId: tokenIdWithoutBalance, + networkId: NetworkId['celo-alfajores'], symbol: 'T2', usdPrice: '5', balance: null, diff --git a/src/tokens/saga.test.ts b/src/tokens/saga.test.ts index f1bd68a83dd..b43a7ec56bd 100644 --- a/src/tokens/saga.test.ts +++ b/src/tokens/saga.test.ts @@ -4,12 +4,12 @@ import { dynamic, throwError } from 'redux-saga-test-plan/providers' import { call, select } from 'redux-saga/effects' import { AppEvents } from 'src/analytics/Events' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' -import { readOnceFromFirebase } from 'src/firebase/firebase' import { fetchTokenBalancesForAddress, fetchTokenBalancesSaga, tokenAmountInSmallestUnit, watchAccountFundedOrLiquidated, + getTokensInfo, } from 'src/tokens/saga' import { lastKnownTokenBalancesSelector } from 'src/tokens/selectors' import { @@ -23,22 +23,57 @@ import { createMockStore } from 'test/utils' import { mockAccount, mockCeurAddress, + mockCeurTokenId, mockCusdAddress, + mockCusdTokenId, mockPoofAddress, + mockPoofTokenId, mockTokenBalances, } from 'test/values' +import { FetchMock } from 'jest-fetch-mock' +import Logger from 'src/utils/Logger' +import { apolloClient } from 'src/apollo' +import { getFeatureGate } from 'src/statsig' +import { ApolloQueryResult } from 'apollo-client' -const mockFirebaseTokenInfo: StoredTokenBalances = { - [mockPoofAddress]: { - ...mockTokenBalances[mockPoofAddress], +jest.mock('src/statsig') +jest.mock('src/apollo', () => { + return { + apolloClient: { + query: jest.fn(), + }, + } +}) +jest.mock('src/web3/networkConfig', () => { + const originalModule = jest.requireActual('src/web3/networkConfig') + return { + ...originalModule, + __esModule: true, + default: { + ...originalModule.default, + networkToNetworkId: { + celo: 'celo-alfajores', + ethereum: 'ethereum-sepolia', + }, + defaultNetworkId: 'celo-alfajores', + }, + } +}) +jest.mock('src/utils/Logger') + +const mockFetch = fetch as FetchMock + +const mockBlockchainApiTokenInfo: StoredTokenBalances = { + [mockPoofTokenId]: { + ...mockTokenBalances[mockPoofTokenId], balance: null, }, - [mockCusdAddress]: { - ...mockTokenBalances[mockCusdAddress], + [mockCusdTokenId]: { + ...mockTokenBalances[mockCusdTokenId], balance: null, }, [mockCeurAddress]: { - ...mockTokenBalances[mockCeurAddress], + ...mockTokenBalances[mockCeurTokenId], balance: null, }, } @@ -46,33 +81,55 @@ const mockFirebaseTokenInfo: StoredTokenBalances = { const fetchBalancesResponse = [ { tokenAddress: mockPoofAddress, + tokenId: mockPoofTokenId, balance: (5 * Math.pow(10, 18)).toString(), decimals: '18', }, { tokenAddress: mockCusdAddress, + tokenId: mockCusdTokenId, balance: '0', decimals: '18', }, // cEUR intentionally missing ] +describe('getTokensInfo', () => { + beforeEach(() => { + mockFetch.resetMocks() + }) + it('returns payload if response OK', async () => { + mockFetch.mockResponseOnce('{"some": "data"}') + + const result = await getTokensInfo() + expect(result).toEqual({ + some: 'data', + }) + }) + it('throws if request does not complete within timeout', async () => { + mockFetch.mockResponseOnce('error!', { status: 500, statusText: 'some error' }) + await expect(getTokensInfo()).rejects.toEqual( + new Error('Failure response fetching token info. 500 some error') + ) + expect(Logger.error).toHaveBeenCalledTimes(1) + }) +}) describe(fetchTokenBalancesSaga, () => { const tokenBalancesAfterUpdate: StoredTokenBalances = { - ...mockFirebaseTokenInfo, - [mockPoofAddress]: { - ...(mockFirebaseTokenInfo[mockPoofAddress] as StoredTokenBalance), + ...mockBlockchainApiTokenInfo, + [mockPoofTokenId]: { + ...(mockBlockchainApiTokenInfo[mockPoofTokenId] as StoredTokenBalance), balance: '5', // should convert to ethers (rather than keep in wei) }, - [mockCusdAddress]: { - ...(mockFirebaseTokenInfo[mockCusdAddress] as StoredTokenBalance), + [mockCusdTokenId]: { + ...(mockBlockchainApiTokenInfo[mockCusdTokenId] as StoredTokenBalance), balance: '0', }, } it('get token info successfully', async () => { await expectSaga(fetchTokenBalancesSaga) .provide([ - [call(readOnceFromFirebase, 'tokensInfo'), mockFirebaseTokenInfo], + [call(getTokensInfo), mockBlockchainApiTokenInfo], [select(walletAddressSelector), mockAccount], [call(fetchTokenBalancesForAddress, mockAccount), fetchBalancesResponse], ]) @@ -84,10 +141,10 @@ describe(fetchTokenBalancesSaga, () => { await expectSaga(fetchTokenBalancesSaga) .provide([ [select(walletAddressSelector), null], - [call(readOnceFromFirebase, 'tokensInfo'), mockFirebaseTokenInfo], + [call(getTokensInfo), mockBlockchainApiTokenInfo], [call(fetchTokenBalancesForAddress, mockAccount), fetchBalancesResponse], ]) - .not.call(readOnceFromFirebase, 'tokensInfo') + .not.call(getTokensInfo) .not.put(setTokenBalances(tokenBalancesAfterUpdate)) .run() }) @@ -95,7 +152,7 @@ describe(fetchTokenBalancesSaga, () => { it("fires an event if there's an error", async () => { await expectSaga(fetchTokenBalancesSaga) .provide([ - [call(readOnceFromFirebase, 'tokensInfo'), mockFirebaseTokenInfo], + [call(getTokensInfo), mockBlockchainApiTokenInfo], [select(walletAddressSelector), mockAccount], [call(fetchTokenBalancesForAddress, mockAccount), throwError(new Error('Error message'))], ]) @@ -108,8 +165,48 @@ describe(fetchTokenBalancesSaga, () => { }) }) +describe(fetchTokenBalancesForAddress, () => { + it('returns token balances for a single chain', async () => { + jest.mocked(getFeatureGate).mockReturnValueOnce(false) + jest + .mocked(apolloClient.query) + .mockImplementation(async (payload: any): Promise> => { + return { + data: { + userBalances: { + balances: [`${payload.variables.networkId} balance`], + }, + }, + } as ApolloQueryResult + }) + const result = await fetchTokenBalancesForAddress('some-address') + expect(result).toHaveLength(1), + expect(result).toEqual(expect.arrayContaining(['celo_alfajores balance'])) + }) + it('returns token balances for multiple chains', async () => { + jest.mocked(getFeatureGate).mockReturnValueOnce(true) + jest + .mocked(apolloClient.query) + .mockImplementation(async (payload: any): Promise> => { + return { + data: { + userBalances: { + balances: [`${payload.variables.networkId} balance`], + }, + }, + } as ApolloQueryResult + }) + const result = await fetchTokenBalancesForAddress('some-address') + expect(result).toHaveLength(2), + expect(result).toEqual( + expect.arrayContaining(['celo_alfajores balance', 'ethereum_sepolia balance']) + ) + }) +}) + describe(tokenAmountInSmallestUnit, () => { const mockAddress = '0xMockAddress' + const mockTokenId = `celo-alfajores:${mockAddress}` it('map to token amount successfully', async () => { await expectSaga(tokenAmountInSmallestUnit, new BigNumber(10), mockAddress) @@ -117,8 +214,9 @@ describe(tokenAmountInSmallestUnit, () => { createMockStore({ tokens: { tokenBalances: { - [mockAddress]: { + [mockTokenId]: { address: mockAddress, + tokenId: mockTokenId, decimals: 5, }, }, diff --git a/src/tokens/saga.ts b/src/tokens/saga.ts index 1c41cb20819..cfe5aba3460 100644 --- a/src/tokens/saga.ts +++ b/src/tokens/saga.ts @@ -14,7 +14,6 @@ import { TokenTransactionType } from 'src/apollo/types' import { ErrorMessages } from 'src/app/ErrorMessages' import { DOLLAR_MIN_AMOUNT_ACCOUNT_FUNDED, isE2EEnv } from 'src/config' import { FeeInfo } from 'src/fees/saga' -import { readOnceFromFirebase } from 'src/firebase/firebase' import { SentryTransactionHub } from 'src/sentry/SentryTransactionHub' import { SentryTransaction } from 'src/sentry/SentryTransactions' import { Actions } from 'src/stableToken/actions' @@ -28,9 +27,11 @@ import { StoredTokenBalances, TokenBalance, } from 'src/tokens/slice' +import { fetchWithTimeout } from 'src/utils/fetchWithTimeout' import { addStandbyTransactionLegacy, removeStandbyTransaction } from 'src/transactions/actions' import { sendAndMonitorTransaction } from 'src/transactions/saga' import { TransactionContext, TransactionStatus } from 'src/transactions/types' +import networkConfig from 'src/web3/networkConfig' import { Currency } from 'src/utils/currencies' import { ensureError } from 'src/utils/ensureError' import Logger from 'src/utils/Logger' @@ -40,6 +41,9 @@ import { getContractKitAsync } from 'src/web3/contracts' import { getConnectedUnlockedAccount } from 'src/web3/saga' import { walletAddressSelector } from 'src/web3/selectors' import { call, put, select, spawn, take, takeEvery } from 'typed-redux-saga' +import { getFeatureGate } from 'src/statsig' +import { StatsigFeatureGates } from 'src/statsig/types' + import * as utf8 from 'utf8' const TAG = 'tokens/saga' @@ -220,7 +224,8 @@ export async function getStableTokenContract(tokenAddress: string) { } export interface FetchedTokenBalance { - tokenAddress: string + tokenId: string + tokenAddress?: string balance: string } @@ -233,25 +238,47 @@ interface UserBalancesResponse { export async function fetchTokenBalancesForAddress( address: string ): Promise { - const response = await apolloClient.query({ - query: gql` - query FetchUserBalances($address: Address!) { - userBalances(address: $address) { - balances { - tokenAddress - balance + const chainsToFetch = getFeatureGate(StatsigFeatureGates.FETCH_MULTI_CHAIN_BALANCES) + ? Object.values(networkConfig.networkToNetworkId) + : [networkConfig.defaultNetworkId] + const userBalances = await Promise.all( + chainsToFetch.map((networkId) => { + return apolloClient.query({ + query: gql` + query FetchUserBalances($address: Address!, $networkId: NetworkId) { + userBalances(address: $address, networkId: $networkId) { + balances { + tokenId + tokenAddress + balance + } + } } - } - } - `, - variables: { - address, - }, - fetchPolicy: 'network-only', - errorPolicy: 'all', - }) - - return response.data.userBalances.balances + `, + variables: { + address, + networkId: networkId.replaceAll('-', '_'), // GraphQL does not support hyphens in enum values + }, + fetchPolicy: 'network-only', + errorPolicy: 'all', + }) + }) + ) + return userBalances.reduce( + (acc, response) => acc.concat(response.data.userBalances.balances), + [] as FetchedTokenBalance[] + ) +} + +export async function getTokensInfo(): Promise { + const response = await fetchWithTimeout(networkConfig.getTokensInfoUrl) + if (!response.ok) { + Logger.error(TAG, `Failure response fetching token info: ${response}`) + throw new Error( + `Failure response fetching token info. ${response.status} ${response.statusText}` + ) + } + return await response.json() } export function* fetchTokenBalancesSaga() { @@ -263,14 +290,10 @@ export function* fetchTokenBalancesSaga() { } SentryTransactionHub.startTransaction(SentryTransaction.fetch_balances) // In e2e environment we use a static token list since we can't access Firebase. - const tokens: StoredTokenBalances = isE2EEnv - ? e2eTokens() - : yield* call(readOnceFromFirebase, 'tokensInfo') + const tokens: StoredTokenBalances = isE2EEnv ? e2eTokens() : yield* call(getTokensInfo) const tokenBalances: FetchedTokenBalance[] = yield* call(fetchTokenBalancesForAddress, address) for (const token of Object.values(tokens) as StoredTokenBalance[]) { - const tokenBalance = tokenBalances.find( - (t) => t.tokenAddress.toLowerCase() === token.address.toLowerCase() - ) + const tokenBalance = tokenBalances.find((t) => t.tokenId === token.tokenId) if (!tokenBalance) { token.balance = '0' } else { diff --git a/src/tokens/selectors.test.ts b/src/tokens/selectors.test.ts index bfbf504f074..e4f97b405b6 100644 --- a/src/tokens/selectors.test.ts +++ b/src/tokens/selectors.test.ts @@ -10,6 +10,7 @@ import { totalTokenBalanceSelector, } from 'src/tokens/selectors' import { ONE_DAY_IN_MILLIS } from 'src/utils/time' +import { NetworkId } from 'src/transactions/types' const mockDate = 1588200517518 @@ -24,7 +25,10 @@ beforeAll(() => { const state: any = { tokens: { tokenBalances: { - ['0xusd']: { + ['celo-alfajores:0xusd']: { + tokenId: 'celo-alfajores:0xusd', + networkId: NetworkId['celo-alfajores'], + name: 'cUSD', address: '0xusd', balance: '0', usdPrice: '1', @@ -32,7 +36,10 @@ const state: any = { priceFetchedAt: mockDate, isSwappable: true, }, - ['0xeur']: { + ['celo-alfajores:0xeur']: { + tokenId: 'celo-alfajores:0xeur', + networkId: NetworkId['celo-alfajores'], + name: 'cEUR', address: '0xeur', balance: '50', usdPrice: '0.5', @@ -41,20 +48,30 @@ const state: any = { priceFetchedAt: mockDate, minimumAppVersionToSwap: '1.0.0', }, - ['0x1']: { + ['celo-alfajores:0x1']: { + tokenId: 'celo-alfajores:0x1', + networkId: NetworkId['celo-alfajores'], + name: '0x1 token', + bridge: 'somebridge', address: '0x1', balance: '10', usdPrice: '10', priceFetchedAt: mockDate, minimumAppVersionToSwap: '1.20.0', }, - ['0x3']: { + ['celo-alfajores:0x2']: { + tokenId: 'celo-alfajores:0x2', + networkId: NetworkId['celo-alfajores'], + name: '0x2 token', address: '0x2', usdPrice: '100', balance: null, priceFetchedAt: mockDate, }, - ['0x4']: { + ['celo-alfajores:0x4']: { + tokenId: 'celo-alfajores:0x4', + networkId: NetworkId['celo-alfajores'], + name: '0x4 token', address: '0x4', symbol: 'TT', balance: '50', @@ -62,12 +79,22 @@ const state: any = { priceFetchedAt: mockDate, minimumAppVersionToSwap: '1.10.0', }, - ['0x5']: { + ['celo-alfajores:0x5']: { + tokenId: 'celo-alfajores:0x5', + networkId: NetworkId['celo-alfajores'], + name: '0x5 token', address: '0x5', balance: '50', usdPrice: '500', priceFetchedAt: mockDate - 2 * ONE_DAY_IN_MILLIS, }, + ['celo-alfajores:0x6']: { + tokenId: 'celo-alfajores:0x6', + networkId: NetworkId['celo-alfajores'], + balance: '50', + usdPrice: '500', + priceFetchedAt: mockDate - 2 * ONE_DAY_IN_MILLIS, + }, }, }, localCurrency: { @@ -85,6 +112,8 @@ describe(tokensByAddressSelector, () => { expect(tokensByAddress['0xusd']?.symbol).toEqual('cUSD') expect(tokensByAddress['0xeur']?.symbol).toEqual('cEUR') expect(tokensByAddress['0x4']?.symbol).toEqual('TT') + expect(tokensByAddress['0x1']?.name).toEqual('0x1 token (somebridge)') + expect(tokensByAddress['0x5']?.name).toEqual('0x5 token') }) }) }) @@ -109,9 +138,13 @@ describe('tokensByUsdBalanceSelector', () => { { "address": "0x1", "balance": "10", + "bridge": "somebridge", "lastKnownUsdPrice": "10", "minimumAppVersionToSwap": "1.20.0", + "name": "0x1 token (somebridge)", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, + "tokenId": "celo-alfajores:0x1", "usdPrice": "10", }, { @@ -120,8 +153,11 @@ describe('tokensByUsdBalanceSelector', () => { "isSupercharged": true, "lastKnownUsdPrice": "0.5", "minimumAppVersionToSwap": "1.0.0", + "name": "cEUR", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, "symbol": "cEUR", + "tokenId": "celo-alfajores:0xeur", "usdPrice": "0.5", }, { @@ -129,8 +165,11 @@ describe('tokensByUsdBalanceSelector', () => { "balance": "0", "isSwappable": true, "lastKnownUsdPrice": "1", + "name": "cUSD", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, "symbol": "cUSD", + "tokenId": "celo-alfajores:0xusd", "usdPrice": "1", }, { @@ -139,15 +178,21 @@ describe('tokensByUsdBalanceSelector', () => { "isSupercharged": true, "lastKnownUsdPrice": null, "minimumAppVersionToSwap": "1.10.0", + "name": "0x4 token", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, "symbol": "TT", + "tokenId": "celo-alfajores:0x4", "usdPrice": null, }, { "address": "0x5", "balance": "50", "lastKnownUsdPrice": "500", + "name": "0x5 token", + "networkId": "celo-alfajores", "priceFetchedAt": 1588027717518, + "tokenId": "celo-alfajores:0x5", "usdPrice": null, }, ] @@ -163,9 +208,13 @@ describe('tokensWithUsdValueSelector', () => { { "address": "0x1", "balance": "10", + "bridge": "somebridge", "lastKnownUsdPrice": "10", "minimumAppVersionToSwap": "1.20.0", + "name": "0x1 token (somebridge)", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, + "tokenId": "celo-alfajores:0x1", "usdPrice": "10", }, { @@ -174,8 +223,11 @@ describe('tokensWithUsdValueSelector', () => { "isSupercharged": true, "lastKnownUsdPrice": "0.5", "minimumAppVersionToSwap": "1.0.0", + "name": "cEUR", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, "symbol": "cEUR", + "tokenId": "celo-alfajores:0xeur", "usdPrice": "0.5", }, ] @@ -221,29 +273,38 @@ describe(totalTokenBalanceSelector, () => { "isSupercharged": true, "lastKnownUsdPrice": "0.5", "minimumAppVersionToSwap": "1.0.0", + "name": "cEUR", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, "symbol": "cEUR", + "tokenId": "celo-alfajores:0xeur", "usdPrice": "0.5", }, - { - "address": "0xusd", - "balance": "0", - "isSwappable": true, - "lastKnownUsdPrice": "1", - "priceFetchedAt": 1588200517518, - "symbol": "cUSD", - "usdPrice": "1", - }, { "address": "0x4", "balance": "50", "isSupercharged": true, "lastKnownUsdPrice": null, "minimumAppVersionToSwap": "1.10.0", + "name": "0x4 token", + "networkId": "celo-alfajores", "priceFetchedAt": 1588200517518, "symbol": "TT", + "tokenId": "celo-alfajores:0x4", "usdPrice": null, }, + { + "address": "0xusd", + "balance": "0", + "isSwappable": true, + "lastKnownUsdPrice": "1", + "name": "cUSD", + "networkId": "celo-alfajores", + "priceFetchedAt": 1588200517518, + "symbol": "cUSD", + "tokenId": "celo-alfajores:0xusd", + "usdPrice": "1", + }, ] `) }) diff --git a/src/tokens/selectors.ts b/src/tokens/selectors.ts index 3230d08f3ba..a596177b31b 100644 --- a/src/tokens/selectors.ts +++ b/src/tokens/selectors.ts @@ -8,17 +8,17 @@ import { } from 'src/config' import { usdToLocalCurrencyRateSelector } from 'src/localCurrency/selectors' import { RootState } from 'src/redux/reducers' -import { TokenBalance, TokenBalances } from 'src/tokens/slice' +import { TokenBalance, TokenBalancesWithAddress, TokenBalanceWithAddress } from 'src/tokens/slice' import { Currency } from 'src/utils/currencies' import { isVersionBelowMinimum } from 'src/utils/versionCheck' import { sortByUsdBalance, sortFirstStableThenCeloThenOthersByUsdBalance } from './utils' -type TokenBalanceWithUsdPrice = TokenBalance & { +type TokenBalanceWithUsdPrice = TokenBalanceWithAddress & { usdPrice: BigNumber } export type CurrencyTokens = { - [currency in Currency]: TokenBalance | undefined + [currency in Currency]: TokenBalanceWithAddress | undefined } export const tokenFetchLoadingSelector = (state: RootState) => state.tokens.loading @@ -28,17 +28,19 @@ export const tokenFetchErrorSelector = (state: RootState) => state.tokens.error export const tokensByAddressSelector = createSelector( (state: RootState) => state.tokens.tokenBalances, (storedBalances) => { - const tokenBalances: TokenBalances = {} - for (const [tokenAddress, storedState] of Object.entries(storedBalances)) { - if (!storedState || storedState.balance === null) { + const tokenBalances: TokenBalancesWithAddress = {} + for (const storedState of Object.values(storedBalances)) { + if (!storedState || storedState.balance === null || !storedState.address) { continue } const usdPrice = new BigNumber(storedState.usdPrice) const tokenUsdPriceIsStale = (storedState.priceFetchedAt ?? 0) < Date.now() - TIME_UNTIL_TOKEN_INFO_BECOMES_STALE - tokenBalances[tokenAddress] = { + tokenBalances[storedState.address] = { ...storedState, + address: storedState.address, // TS complains if this isn't explicitly included, despite it necessarily being non-null + name: storedState.bridge ? `${storedState.name} (${storedState.bridge})` : storedState.name, balance: new BigNumber(storedState.balance), usdPrice: usdPrice.isNaN() || tokenUsdPriceIsStale ? null : usdPrice, lastKnownUsdPrice: !usdPrice.isNaN() ? usdPrice : null, @@ -57,7 +59,7 @@ export const tokensBySymbolSelector = createSelector( ( tokens ): { - [symbol: string]: TokenBalance + [symbol: string]: TokenBalanceWithAddress } => { return tokens.reduce( (acc, token) => ({ diff --git a/src/tokens/slice.ts b/src/tokens/slice.ts index 68493701d19..c29007aa743 100644 --- a/src/tokens/slice.ts +++ b/src/tokens/slice.ts @@ -2,14 +2,18 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import BigNumber from 'bignumber.js' import { REHYDRATE, RehydrateAction } from 'redux-persist' import { getRehydratePayload } from 'src/redux/persist-helper' +import { NetworkId } from 'src/transactions/types' export interface BaseToken { - address: string + address: string | null + tokenId: string decimals: number imageUrl: string name: string symbol: string + networkId: NetworkId priceFetchedAt?: number + isNative?: boolean // This field is for tokens that are part of the core contracts that allow paying for fees and // making transfers with a comment. isCoreToken?: boolean @@ -17,6 +21,7 @@ export interface BaseToken { isSwappable?: boolean minimumAppVersionToSwap?: string networkIconUrl?: string + bridge?: string } interface HistoricalUsdPrices { @@ -33,6 +38,10 @@ export interface StoredTokenBalance extends BaseToken { historicalUsdPrices?: HistoricalUsdPrices } +export interface StoredTokenBalanceWithAddress extends StoredTokenBalance { + address: string +} + export interface TokenBalance extends BaseToken { balance: BigNumber usdPrice: BigNumber | null @@ -40,8 +49,26 @@ export interface TokenBalance extends BaseToken { historicalUsdPrices?: HistoricalUsdPrices } +// The "WithAddress" suffixed types are legacy types, for places in the wallet +// that require an address to be present. As we move to multichain, (where address +// is not guaranteed,) existing code should be updated to use the "address optional" types. + +/** + * @deprecated use `TokenBalance` for new code + */ +export interface TokenBalanceWithAddress extends TokenBalance { + address: string +} + export interface StoredTokenBalances { - [address: string]: StoredTokenBalance | undefined + [tokenId: string]: StoredTokenBalance | undefined +} + +/** + * @deprecated use `StoredTokenBalances` for new code + */ +export interface StoredTokenBalancesWithAddress { + [tokenId: string]: StoredTokenBalance | undefined } export interface TokenLoadingAction { @@ -49,7 +76,14 @@ export interface TokenLoadingAction { } export interface TokenBalances { - [address: string]: TokenBalance | undefined + [tokenId: string]: TokenBalance | undefined +} + +/** + * @deprecated use `TokenBalances` for new code + */ +export interface TokenBalancesWithAddress { + [tokenId: string]: TokenBalanceWithAddress | undefined } export interface State { diff --git a/src/transactions/send.ts b/src/transactions/send.ts index c270a6b098e..6dd0a8bb7e3 100644 --- a/src/transactions/send.ts +++ b/src/transactions/send.ts @@ -6,7 +6,7 @@ import { ErrorMessages } from 'src/app/ErrorMessages' import { STATIC_GAS_PADDING } from 'src/config' import { fetchFeeCurrencySaga } from 'src/fees/saga' import { coreTokensSelector } from 'src/tokens/selectors' -import { TokenBalance } from 'src/tokens/slice' +import { TokenBalanceWithAddress } from 'src/tokens/slice' import { SendTransactionLogEvent, SendTransactionLogEventType, @@ -124,7 +124,7 @@ export function* chooseTxFeeDetails( gas?: number, gasPrice?: BigNumber ) { - const coreTokens: TokenBalance[] = yield* select(coreTokensSelector) + const coreTokens: TokenBalanceWithAddress[] = yield* select(coreTokensSelector) const tokenInfo = coreTokens.find( (token) => token.address === preferredFeeCurrency || (token.symbol === 'CELO' && !preferredFeeCurrency) diff --git a/src/web3/networkConfig.ts b/src/web3/networkConfig.ts index 4b5fb0f285c..27e0d6c5b4f 100644 --- a/src/web3/networkConfig.ts +++ b/src/web3/networkConfig.ts @@ -62,6 +62,7 @@ interface NetworkConfig { cabStoreEncryptedMnemonicUrl: string networkToNetworkId: Record defaultNetworkId: NetworkId + getTokensInfoUrl: string viemChain: { [key in Network]: ViemChain } @@ -70,6 +71,9 @@ interface NetworkConfig { const CLOUD_FUNCTIONS_STAGING = 'https://api.alfajores.valora.xyz' const CLOUD_FUNCTIONS_MAINNET = 'https://api.mainnet.valora.xyz' +const BLOCKCHAIN_API_STAGING = 'https://blockchain-api-dot-celo-mobile-alfajores.appspot.com' +const BLOCKCHAIN_API_MAINNET = 'https://blockchain-api-dot-celo-mobile-mainnet.appspot.com' + const ALLOWED_MTW_IMPLEMENTATIONS_MAINNET: Address[] = [ '0x6511FB5DBfe95859d8759AdAd5503D656E2555d7', ] @@ -83,6 +87,9 @@ const CURRENT_MTW_IMPLEMENTATION_ADDRESS_MAINNET: Address = const CURRENT_MTW_IMPLEMENTATION_ADDRESS_STAGING: Address = '0x5C9a6E3c3E862eD306E2E3348EBC8b8310A99e5A' +const GET_TOKENS_INFO_URL_ALFAJORES = `${BLOCKCHAIN_API_STAGING}/tokensInfo` +const GET_TOKENS_INFO_URL_MAINNET = `${BLOCKCHAIN_API_MAINNET}/tokensInfo` + const FETCH_EXCHANGES_URL_ALFAJORES = `${CLOUD_FUNCTIONS_STAGING}/getExchanges` const FETCH_EXCHANGES_URL_MAINNET = `${CLOUD_FUNCTIONS_MAINNET}/getExchanges` @@ -178,14 +185,14 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = { }, defaultNetworkId: NetworkId['celo-alfajores'], // blockchainApiUrl: 'http://127.0.0.1:8080', - blockchainApiUrl: 'https://blockchain-api-dot-celo-mobile-alfajores.appspot.com', + blockchainApiUrl: BLOCKCHAIN_API_STAGING, cloudFunctionsUrl: CLOUD_FUNCTIONS_STAGING, hooksApiUrl: HOOKS_API_URL_ALFAJORES, odisUrl: OdisUtils.Query.ODIS_ALFAJORES_CONTEXT_PNP.odisUrl, odisPubKey: OdisUtils.Query.ODIS_ALFAJORES_CONTEXT_PNP.odisPubKey, sentryTracingUrls: [ DEFAULT_FORNO_URL, - 'https://blockchain-api-dot-celo-mobile-alfajores.appspot.com', + BLOCKCHAIN_API_STAGING, CLOUD_FUNCTIONS_STAGING, 'https://liquidity-dot-celo-mobile-alfajores.appspot.com', ], @@ -224,6 +231,7 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = { cabIssueSmsCodeUrl: CAB_ISSUE_SMS_CODE_ALFAJORES, cabIssueValoraKeyshareUrl: CAB_ISSUE_VALORA_KEYSHARE_ALFAJORES, cabStoreEncryptedMnemonicUrl: CAB_STORE_ENCRYPTED_MNEMONIC_ALFAJORES, + getTokensInfoUrl: GET_TOKENS_INFO_URL_ALFAJORES, viemChain: { [Network.Celo]: celoAlfajores, [Network.Ethereum]: ethereumSepolia, @@ -236,14 +244,14 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = { [Network.Ethereum]: NetworkId['ethereum-mainnet'], }, defaultNetworkId: NetworkId['celo-mainnet'], - blockchainApiUrl: 'https://blockchain-api-dot-celo-mobile-mainnet.appspot.com', + blockchainApiUrl: BLOCKCHAIN_API_MAINNET, cloudFunctionsUrl: CLOUD_FUNCTIONS_MAINNET, hooksApiUrl: HOOKS_API_URL_MAINNET, odisUrl: OdisUtils.Query.ODIS_MAINNET_CONTEXT_PNP.odisUrl, odisPubKey: OdisUtils.Query.ODIS_MAINNET_CONTEXT_PNP.odisPubKey, sentryTracingUrls: [ DEFAULT_FORNO_URL, - 'https://blockchain-api-dot-celo-mobile-mainnet.appspot.com', + BLOCKCHAIN_API_MAINNET, CLOUD_FUNCTIONS_MAINNET, 'https://liquidity-dot-celo-mobile-mainnet.appspot.com', ], @@ -282,6 +290,7 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = { cabIssueSmsCodeUrl: CAB_ISSUE_SMS_CODE_MAINNET, cabIssueValoraKeyshareUrl: CAB_ISSUE_VALORA_KEYSHARE_MAINNET, cabStoreEncryptedMnemonicUrl: CAB_STORE_ENCRYPTED_MNEMONIC_MAINNET, + getTokensInfoUrl: GET_TOKENS_INFO_URL_MAINNET, viemChain: { [Network.Celo]: celo, [Network.Ethereum]: ethereum, diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index 6ca39d17f3e..71c602761d6 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -4240,7 +4240,10 @@ "additionalProperties": false, "properties": { "address": { - "type": "string" + "type": [ + "null", + "string" + ] }, "balance": { "type": [ @@ -4248,6 +4251,9 @@ "string" ] }, + "bridge": { + "type": "string" + }, "decimals": { "type": "number" }, @@ -4260,6 +4266,9 @@ "isCoreToken": { "type": "boolean" }, + "isNative": { + "type": "boolean" + }, "isSwappable": { "type": "boolean" }, @@ -4272,12 +4281,18 @@ "networkIconUrl": { "type": "string" }, + "networkId": { + "$ref": "#/definitions/NetworkId" + }, "priceFetchedAt": { "type": "number" }, "symbol": { "type": "string" }, + "tokenId": { + "type": "string" + }, "usdPrice": { "type": "string" } @@ -4288,7 +4303,9 @@ "decimals", "imageUrl", "name", + "networkId", "symbol", + "tokenId", "usdPrice" ], "type": "object" diff --git a/test/schemas.ts b/test/schemas.ts index bfc291d7faa..ca204a6c740 100644 --- a/test/schemas.ts +++ b/test/schemas.ts @@ -24,9 +24,20 @@ import { mockPositions, mockTestTokenAddress, } from 'test/values' +import { NetworkId } from 'src/transactions/types' export const DEFAULT_DAILY_PAYMENT_LIMIT_CUSD_LEGACY = 1000 +function updateTestTokenInfo(tokenInfo: any): any { + const isNative = tokenInfo.symbol === 'CELO' + return { + ...tokenInfo, + tokenId: `celo-alfajores:${isNative ? 'native' : tokenInfo.address}`, + isNative, + networkId: NetworkId['celo-alfajores'], + } +} + // Default (version -1 schema) export const vNeg1Schema = { app: { @@ -2556,6 +2567,27 @@ export const v149Schema = { }, } +export const v150Schema = { + ...v149Schema, + _persist: { + ...v149Schema._persist, + version: 150, + }, + tokens: { + ...v149Schema.tokens, + tokenBalances: Object.values(v149Schema.tokens.tokenBalances).reduce( + (acc: Record, tokenInfo: any) => { + const newTokenInfo = updateTestTokenInfo(tokenInfo) + return { + ...acc, + [newTokenInfo.tokenId]: newTokenInfo, + } + }, + {} + ), + }, +} + export function getLatestSchema(): Partial { - return v149Schema as Partial + return v150Schema as Partial } diff --git a/test/values.ts b/test/values.ts index 3ab25cc33b1..e03e3dfcf48 100644 --- a/test/values.ts +++ b/test/values.ts @@ -53,6 +53,7 @@ import { TransactionDataInput } from 'src/send/SendAmount' import { StoredTokenBalance } from 'src/tokens/slice' import { CiCoCurrency, Currency } from 'src/utils/currencies' import { ONE_DAY_IN_MILLIS } from 'src/utils/time' +import { NetworkId } from 'src/transactions/types' export const nullAddress = '0x0' @@ -119,6 +120,14 @@ export const mockTestTokenAddress = '0x048F47d358EC521a6cf384461d674750a3cB58C8' export const mockCrealAddress = '0xE4D517785D091D3c54818832dB6094bcc2744545'.toLowerCase() export const mockWBTCAddress = '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'.toLowerCase() +export const mockCusdTokenId = `celo-alfajores:${mockCusdAddress}` +export const mockCeurTokenId = `celo-alfajores:${mockCeurAddress}` +export const mockCeloTokenId = `celo-alfajores:native` +export const mockPoofTokenId = `celo-alfajores:${mockPoofAddress}` +export const mockTestTokenTokenId = `celo-alfajores:${mockTestTokenAddress}` +export const mockCrealTokenId = `celo-alfajores:${mockCrealAddress}` +export const mockWBTCTokenId = `celo-alfajores:${mockWBTCAddress}` + export const mockQrCodeData2 = { address: mockAccount2Invite, e164PhoneNumber: mockE164Number2Invite, @@ -487,9 +496,11 @@ export const makeExchangeRates = ( export const mockTokenBalances: Record = { // NOTE: important to keep 'symbol' fields in this object matching their counterparts from here: https://github.com/valora-inc/address-metadata/blob/main/src/data/mainnet/tokens-info.json , // particularly for CICO currencies - [mockPoofAddress]: { + [mockPoofTokenId]: { usdPrice: '0.1', address: mockPoofAddress, + tokenId: mockPoofTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'POOF', imageUrl: 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_POOF.png', @@ -498,9 +509,11 @@ export const mockTokenBalances: Record = { balance: '5', priceFetchedAt: Date.now(), }, - [mockCeurAddress]: { + [mockCeurTokenId]: { usdPrice: '1.16', address: mockCeurAddress, + tokenId: mockCeurTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'cEUR', imageUrl: 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cEUR.png', @@ -510,9 +523,11 @@ export const mockTokenBalances: Record = { isCoreToken: true, priceFetchedAt: Date.now(), }, - [mockCusdAddress]: { + [mockCusdTokenId]: { usdPrice: '1.001', address: mockCusdAddress, + tokenId: mockCusdTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'cUSD', imageUrl: 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cUSD.png', @@ -522,9 +537,11 @@ export const mockTokenBalances: Record = { isCoreToken: true, priceFetchedAt: Date.now(), }, - [mockCeloAddress]: { + [mockCeloTokenId]: { usdPrice: '13.25085583155252100584', address: mockCeloAddress, + tokenId: mockCeloTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'CELO', // NOT cGLD, see https://github.com/valora-inc/address-metadata/blob/c84ef7056fa066ef86f9b4eb295ae248f363f67a/src/data/mainnet/tokens-info.json#L173 imageUrl: 'https://raw.githubusercontent.com/valora-inc/address-metadata/main/assets/tokens/CELO.png', @@ -534,9 +551,11 @@ export const mockTokenBalances: Record = { isCoreToken: true, priceFetchedAt: Date.now(), }, - [mockCrealAddress]: { + [mockCrealTokenId]: { usdPrice: '0.17', address: mockCrealAddress, + tokenId: mockCrealTokenId, + networkId: NetworkId['celo-alfajores'], symbol: 'cREAL', imageUrl: 'https://raw.githubusercontent.com/valora-inc/address-metadata/main/assets/tokens/cREAL.png', @@ -549,8 +568,8 @@ export const mockTokenBalances: Record = { } export const mockTokenBalancesWithHistoricalPrices = { - [mockPoofAddress]: { - ...mockTokenBalances[mockPoofAddress], + [mockPoofTokenId]: { + ...mockTokenBalances[mockPoofTokenId], historicalUsdPrices: { lastDay: { price: '0.15',