From 7b523ea8a36e4623c8d8b06ceacbc6743bc42c60 Mon Sep 17 00:00:00 2001 From: Petr Knetl Date: Thu, 22 Feb 2024 15:23:39 +0100 Subject: [PATCH] feat(suite-native): token definitions redux logic Closes #8434 --- packages/suite-desktop/tsconfig.json | 4 +- .../TxDetailModal/TxDetailModal.tsx | 4 +- .../TransactionItem/TransactionItem.tsx | 2 +- suite-common/wallet-core/src/index.ts | 1 + .../tokenDefinitionsSelectors.ts | 28 ++++++- .../src/transactions/transactionsReducer.ts | 12 +-- .../fiat-rates/src/fiatRatesMiddleware.ts | 45 ++++++----- .../components/AccountImportSummaryForm.tsx | 24 ++++-- .../screens/AccountImportLoadingScreen.tsx | 20 +++-- suite-native/state/package.json | 1 + suite-native/state/src/reducers.ts | 3 + suite-native/state/src/store.ts | 2 + suite-native/state/tsconfig.json | 1 + suite-native/token-definitions/package.json | 18 +++++ suite-native/token-definitions/src/index.ts | 1 + .../src/tokenDefinitionsMiddleware.ts | 79 +++++++++++++++++++ suite-native/token-definitions/tsconfig.json | 15 ++++ .../TransactionDetailData.tsx | 10 ++- .../TransactionListItemContainer.tsx | 10 ++- tsconfig.json | 3 + yarn.lock | 12 +++ 21 files changed, 229 insertions(+), 66 deletions(-) create mode 100644 suite-native/token-definitions/package.json create mode 100644 suite-native/token-definitions/src/index.ts create mode 100644 suite-native/token-definitions/src/tokenDefinitionsMiddleware.ts create mode 100644 suite-native/token-definitions/tsconfig.json diff --git a/packages/suite-desktop/tsconfig.json b/packages/suite-desktop/tsconfig.json index e164b4a3626..646ed9c41d3 100644 --- a/packages/suite-desktop/tsconfig.json +++ b/packages/suite-desktop/tsconfig.json @@ -1,8 +1,6 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./libDev" - }, + "compilerOptions": { "outDir": "./libDev" }, "include": ["./src", "./e2e", "**/*.json"], "references": [] } diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx index 140db95df54..57149ef122a 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx @@ -103,7 +103,9 @@ export const TxDetailModal = ({ tx, rbfForm, onCancel }: TxDetailModalProps) => ); const account = useSelector(state => selectAccountByKey(state, accountKey)); const network = account && getAccountNetwork(account); - const isPhishingTransaction = useSelector(state => selectIsPhishingTransaction(state, tx)); + const isPhishingTransaction = useSelector(state => + selectIsPhishingTransaction(state, tx.txid, accountKey), + ); return ( - selectIsPhishingTransaction(state, transaction), + selectIsPhishingTransaction(state, transaction.txid, accountKey), ); const dataTestBase = `@transaction-item/${index}${ diff --git a/suite-common/wallet-core/src/index.ts b/suite-common/wallet-core/src/index.ts index cbdbe8a2f47..1fc2fc7c431 100644 --- a/suite-common/wallet-core/src/index.ts +++ b/suite-common/wallet-core/src/index.ts @@ -27,3 +27,4 @@ export * from './token-definitions/tokenDefinitionsSelectors'; export * from './token-definitions/tokenDefinitionsReducer'; export * from './token-definitions/tokenDefinitionsThunks'; export * from './token-definitions/tokenDefinitionsMiddleware'; +export * from './token-definitions/tokenDefinitionsTypes'; diff --git a/suite-common/wallet-core/src/token-definitions/tokenDefinitionsSelectors.ts b/suite-common/wallet-core/src/token-definitions/tokenDefinitionsSelectors.ts index 588a6455774..91cd5faefb8 100644 --- a/suite-common/wallet-core/src/token-definitions/tokenDefinitionsSelectors.ts +++ b/suite-common/wallet-core/src/token-definitions/tokenDefinitionsSelectors.ts @@ -1,4 +1,6 @@ -import { NetworkSymbol } from '@suite-common/wallet-config'; +import { D, pipe } from '@mobily/ts-belt'; + +import { NetworkSymbol, isEthereumBasedNetwork, networks } from '@suite-common/wallet-config'; import { TokenDefinitionsRootState } from './tokenDefinitionsTypes'; @@ -12,3 +14,27 @@ export const selectSpecificTokenDefinition = ( networkSymbol: NetworkSymbol, contractAddress: string, ) => state.wallet.tokenDefinitions?.[networkSymbol]?.[contractAddress]; + +export const selectShouldFetchTokenDefinition = ( + state: TokenDefinitionsRootState, + networkSymbol: NetworkSymbol, + contractAddress: string, +) => { + const tokenDefinition = selectSpecificTokenDefinition(state, networkSymbol, contractAddress); + const network = networks[networkSymbol]; + + return isEthereumBasedNetwork(network) && (!tokenDefinition || tokenDefinition.error); +}; + +export const selectKnownNetworkTokens = ( + state: TokenDefinitionsRootState, + networkSymbol: NetworkSymbol, +) => { + const networkTokenDefinitions = selectTokenDefinitions(state, networkSymbol); + + return pipe( + networkTokenDefinitions, + D.filter(tokenDefinition => !!tokenDefinition.isTokenKnown), + D.keys, + ); +}; diff --git a/suite-common/wallet-core/src/transactions/transactionsReducer.ts b/suite-common/wallet-core/src/transactions/transactionsReducer.ts index d76b266ae35..41320be4c65 100644 --- a/suite-common/wallet-core/src/transactions/transactionsReducer.ts +++ b/suite-common/wallet-core/src/transactions/transactionsReducer.ts @@ -5,7 +5,6 @@ import { findTransaction, getConfirmations, isPending, - getIsZeroValuePhishing, getIsPhishingTransaction, } from '@suite-common/wallet-utils'; import { createReducerWithExtraDeps } from '@suite-common/redux-utils'; @@ -288,8 +287,8 @@ export const selectTransactionConfirmations = ( return getConfirmations(transaction, blockchainHeight); }; -export const selectIsTransactionZeroValuePhishing = ( - state: TransactionsRootState, +export const selectIsPhishingTransaction = ( + state: TokenDefinitionsRootState & TransactionsRootState, txid: string, accountKey: AccountKey, ) => { @@ -297,13 +296,6 @@ export const selectIsTransactionZeroValuePhishing = ( if (!transaction) return false; - return getIsZeroValuePhishing(transaction); -}; - -export const selectIsPhishingTransaction = ( - state: TokenDefinitionsRootState, - transaction: WalletAccountTransaction, -) => { const tokenDefinitions = selectTokenDefinitions(state, transaction.symbol); if (!tokenDefinitions) return false; diff --git a/suite-native/fiat-rates/src/fiatRatesMiddleware.ts b/suite-native/fiat-rates/src/fiatRatesMiddleware.ts index d0a8d388f61..76ea5f80b7b 100644 --- a/suite-native/fiat-rates/src/fiatRatesMiddleware.ts +++ b/suite-native/fiat-rates/src/fiatRatesMiddleware.ts @@ -1,7 +1,12 @@ import { isAnyOf } from '@reduxjs/toolkit'; import { createMiddlewareWithExtraDeps } from '@suite-common/redux-utils'; -import { transactionsActions, accountsActions, blockchainActions } from '@suite-common/wallet-core'; +import { + transactionsActions, + accountsActions, + blockchainActions, + getTokenDefinitionThunk, +} from '@suite-common/wallet-core'; import { TokenAddress } from '@suite-common/wallet-types'; import { @@ -29,28 +34,6 @@ export const prepareFiatRatesMiddleware = createMiddlewareWithExtraDeps( ); } - // Fetch fiat rates for all tokens of newly discovered ETH account. - if ( - accountsActions.createIndexLabeledAccount.match(action) && - action.payload.symbol === 'eth' - ) { - const localCurrency = selectLocalCurrency(getState()); - - const { tokens } = action.payload; - tokens?.forEach(token => { - dispatch( - updateFiatRatesThunk({ - ticker: { - symbol: 'eth', - tokenAddress: token.contract as TokenAddress, - }, - rateType: 'current', - localCurrency, - }), - ); - }); - } - if (transactionsActions.addTransaction.match(action)) { // fetch historical rates for each added transaction const { account, transactions } = action.payload; @@ -83,6 +66,22 @@ export const prepareFiatRatesMiddleware = createMiddlewareWithExtraDeps( ); } + // If there is a new valid token present, download its fiat rates. + if (getTokenDefinitionThunk.fulfilled.match(action)) { + const { networkSymbol, contractAddress } = action.meta.arg; + const localCurrency = selectLocalCurrency(getState()); + dispatch( + updateFiatRatesThunk({ + ticker: { + symbol: networkSymbol, + tokenAddress: contractAddress as TokenAddress, + }, + rateType: 'current', + localCurrency, + }), + ); + } + return next(action); }, ); diff --git a/suite-native/module-accounts-import/src/components/AccountImportSummaryForm.tsx b/suite-native/module-accounts-import/src/components/AccountImportSummaryForm.tsx index 6659ea86463..8099890230b 100644 --- a/suite-native/module-accounts-import/src/components/AccountImportSummaryForm.tsx +++ b/suite-native/module-accounts-import/src/components/AccountImportSummaryForm.tsx @@ -7,6 +7,7 @@ import { AccountsRootState, selectAccountsByNetworkAndDeviceState, PORTFOLIO_TRACKER_DEVICE_STATE, + selectKnownNetworkTokens, } from '@suite-common/wallet-core'; import { Box, Button, Divider, VStack } from '@suite-native/atoms'; import { useAccountLabelForm, AccountFormValues } from '@suite-native/accounts'; @@ -26,6 +27,7 @@ import { TokenAddress, TokenInfoBranded, TokenSymbol } from '@suite-common/walle import { selectAnyOfTokensHasFiatRates } from '@suite-native/ethereum-tokens'; import { FiatRatesRootState } from '@suite-native/fiat-rates'; import { SettingsSliceRootState } from '@suite-native/module-settings'; +import { TokenDefinitionsRootState } from '@suite-common/wallet-core/src/token-definitions/tokenDefinitionsTypes'; import { importAccountThunk } from '../accountsImportThunks'; import { AccountImportOverview } from './AccountImportOverview'; @@ -56,6 +58,14 @@ export const AccountImportSummaryForm = ({ const navigation = useNavigation(); const showImportError = useShowImportError(networkSymbol, navigation); + const areTokensDisplayed = useSelector((state: SettingsSliceRootState & FiatRatesRootState) => + selectAnyOfTokensHasFiatRates(state, (accountInfo?.tokens as TokenInfoBranded[]) ?? []), + ); + + const knownTokens = useSelector((state: TokenDefinitionsRootState) => + selectKnownNetworkTokens(state, networkSymbol), + ); + const deviceNetworkAccounts = useSelector((state: AccountsRootState) => selectAccountsByNetworkAndDeviceState(state, PORTFOLIO_TRACKER_DEVICE_STATE, networkSymbol), ); @@ -80,14 +90,16 @@ export const AccountImportSummaryForm = ({ }), ).unwrap(); + // Report to analytics only those tokens that are known. + const validTokens = + accountInfo.tokens?.filter(({ contract }) => knownTokens.includes(contract)) ?? []; + analytics.report({ type: EventType.AssetsSync, payload: { assetSymbol: networkSymbol, - tokenSymbols: accountInfo?.tokens?.map(token => token.symbol as TokenSymbol), - tokenAddresses: accountInfo?.tokens?.map( - token => token.contract as TokenAddress, - ), + tokenSymbols: validTokens.map(token => token.symbol as TokenSymbol), + tokenAddresses: validTokens.map(token => token.contract as TokenAddress), }, }); @@ -109,10 +121,6 @@ export const AccountImportSummaryForm = ({ } }); - const areTokensDisplayed = useSelector((state: SettingsSliceRootState & FiatRatesRootState) => - selectAnyOfTokensHasFiatRates(state, (accountInfo?.tokens as TokenInfoBranded[]) ?? []), - ); - return (
diff --git a/suite-native/module-accounts-import/src/screens/AccountImportLoadingScreen.tsx b/suite-native/module-accounts-import/src/screens/AccountImportLoadingScreen.tsx index 040c958e7e7..af2e0c28f2d 100644 --- a/suite-native/module-accounts-import/src/screens/AccountImportLoadingScreen.tsx +++ b/suite-native/module-accounts-import/src/screens/AccountImportLoadingScreen.tsx @@ -11,7 +11,8 @@ import { StackToStackCompositeScreenProps, } from '@suite-native/navigation'; import TrezorConnect, { AccountInfo } from '@trezor/connect'; -import { TokenAddress } from '@suite-common/wallet-types'; +import { getTokenDefinitionThunk } from '@suite-common/wallet-core'; +import { networks } from '@suite-common/wallet-config'; import { AccountImportLoader } from '../components/AccountImportLoader'; import { useShowImportError } from '../useShowImportError'; @@ -85,18 +86,15 @@ export const AccountImportLoadingScreen = ({ if (!ignore) { if (fetchedAccountInfo?.success) { if (networkSymbol === 'eth') { - fetchedAccountInfo.payload.tokens?.forEach(token => { + fetchedAccountInfo.payload.tokens?.forEach(token => dispatch( - updateFiatRatesThunk({ - ticker: { - symbol: 'eth', - tokenAddress: token.contract as TokenAddress, - }, - rateType: 'current', - localCurrency: fiatCurrency, + getTokenDefinitionThunk({ + networkSymbol: 'eth', + chainId: networks.eth.chainId, + contractAddress: token.contract, }), - ); - }); + ), + ); } setAccountInfo(fetchedAccountInfo.payload); } else { diff --git a/suite-native/state/package.json b/suite-native/state/package.json index 43fdc82ef0f..c6a092a7767 100644 --- a/suite-native/state/package.json +++ b/suite-native/state/package.json @@ -30,6 +30,7 @@ "@suite-native/module-settings": "workspace:*", "@suite-native/storage": "workspace:*", "@suite-native/toasts": "workspace:*", + "@suite-native/token-definitions": "workspace:*", "@trezor/connect": "workspace:*", "@trezor/transport-native": "workspace:*", "@trezor/utils": "workspace:*", diff --git a/suite-native/state/src/reducers.ts b/suite-native/state/src/reducers.ts index ed2438f5199..def5ccc145c 100644 --- a/suite-native/state/src/reducers.ts +++ b/suite-native/state/src/reducers.ts @@ -5,6 +5,7 @@ import { prepareBlockchainReducer, prepareDeviceReducer, prepareDiscoveryReducer, + prepareTokenDefinitionsReducer, prepareTransactionsReducer, } from '@suite-common/wallet-core'; import { prepareFiatRatesReducer } from '@suite-native/fiat-rates'; @@ -36,6 +37,7 @@ const analyticsReducer = prepareAnalyticsReducer(extraDependencies); const messageSystemReducer = prepareMessageSystemReducer(extraDependencies); const deviceReducer = prepareDeviceReducer(extraDependencies); const discoveryReducer = prepareDiscoveryReducer(extraDependencies); +const tokenDefinitionsReducer = prepareTokenDefinitionsReducer(extraDependencies); export const prepareRootReducers = async () => { const appSettingsPersistedReducer = await preparePersistReducer({ @@ -51,6 +53,7 @@ export const prepareRootReducers = async () => { fiat: fiatRatesReducer, transactions: transactionsReducer, discovery: discoveryReducer, + tokenDefinitions: tokenDefinitionsReducer, }); const walletPersistedReducer = await preparePersistReducer({ diff --git a/suite-native/state/src/store.ts b/suite-native/state/src/store.ts index 533091d661e..9a20bb412aa 100644 --- a/suite-native/state/src/store.ts +++ b/suite-native/state/src/store.ts @@ -6,6 +6,7 @@ import { prepareButtonRequestMiddleware, prepareDeviceMiddleware } from '@suite- import { prepareDiscoveryMiddleware } from '@suite-native/discovery'; import { prepareTransactionCacheMiddleware } from '@suite-native/accounts'; import { blockchainMiddleware } from '@suite-native/blockchain'; +import { tokenDefinitionsMiddleware } from '@suite-native/token-definitions'; import { extraDependencies } from './extraDependencies'; import { prepareRootReducers } from './reducers'; @@ -18,6 +19,7 @@ const middlewares: Middleware[] = [ prepareButtonRequestMiddleware(extraDependencies), prepareDiscoveryMiddleware(extraDependencies), prepareTransactionCacheMiddleware(extraDependencies), + tokenDefinitionsMiddleware, ]; if (__DEV__) { diff --git a/suite-native/state/tsconfig.json b/suite-native/state/tsconfig.json index 1b4a6715db4..843033fc168 100644 --- a/suite-native/state/tsconfig.json +++ b/suite-native/state/tsconfig.json @@ -33,6 +33,7 @@ { "path": "../module-settings" }, { "path": "../storage" }, { "path": "../toasts" }, + { "path": "../token-definitions" }, { "path": "../../packages/connect" }, { "path": "../../packages/transport-native" diff --git a/suite-native/token-definitions/package.json b/suite-native/token-definitions/package.json new file mode 100644 index 00000000000..758f042c6ac --- /dev/null +++ b/suite-native/token-definitions/package.json @@ -0,0 +1,18 @@ +{ + "name": "@suite-native/token-definitions", + "version": "1.0.0", + "private": true, + "license": "See LICENSE.md in repo root", + "sideEffects": false, + "main": "src/index", + "scripts": { + "lint:js": "yarn g:eslint '**/*.{ts,tsx,js}'", + "type-check": "yarn g:tsc --build" + }, + "dependencies": { + "@reduxjs/toolkit": "1.9.5", + "@suite-common/redux-utils": "workspace:*", + "@suite-common/wallet-config": "workspace:*", + "@suite-common/wallet-core": "workspace:*" + } +} diff --git a/suite-native/token-definitions/src/index.ts b/suite-native/token-definitions/src/index.ts new file mode 100644 index 00000000000..f6399babd5f --- /dev/null +++ b/suite-native/token-definitions/src/index.ts @@ -0,0 +1 @@ +export * from './tokenDefinitionsMiddleware'; diff --git a/suite-native/token-definitions/src/tokenDefinitionsMiddleware.ts b/suite-native/token-definitions/src/tokenDefinitionsMiddleware.ts new file mode 100644 index 00000000000..0dbd034bb5b --- /dev/null +++ b/suite-native/token-definitions/src/tokenDefinitionsMiddleware.ts @@ -0,0 +1,79 @@ +import { isAnyOf } from '@reduxjs/toolkit'; + +import { createMiddleware } from '@suite-common/redux-utils'; +import { getNetworkFeatures, isEthereumBasedNetwork, networks } from '@suite-common/wallet-config'; +import { + accountsActions, + getTokenDefinitionThunk, + selectShouldFetchTokenDefinition, +} from '@suite-common/wallet-core'; + +const isAccountChangingAction = isAnyOf( + accountsActions.createAccount, + accountsActions.updateAccount, +); + +export const tokenDefinitionsMiddleware = createMiddleware( + (action, { dispatch, next, getState }) => { + // The action changes has to be stored before evaluated in this middleware, + // because it needs to check the latest state to decide if we should fetch token definitions. + next(action); + + if (isAccountChangingAction(action)) { + const { symbol } = action.payload; + + const networkFeatures = getNetworkFeatures(symbol); + + if (networkFeatures.includes('token-definitions')) { + action.payload.tokens?.forEach(token => { + const contractAddress = token.contract; + + const shouldFetchTokenDefinition = selectShouldFetchTokenDefinition( + getState(), + symbol, + contractAddress, + ); + + const network = networks[symbol]; + if (shouldFetchTokenDefinition && isEthereumBasedNetwork(network)) { + dispatch( + getTokenDefinitionThunk({ + networkSymbol: symbol, + chainId: network.chainId, + contractAddress, + }), + ); + } + }); + } + } + + // Fetch token definitions for each token of the suite-native discovery created account. + if (accountsActions.createIndexLabeledAccount.match(action)) { + const { tokens, symbol } = action.payload; + + const network = networks[symbol]; + if (isEthereumBasedNetwork(network)) { + const { chainId } = network; + tokens?.forEach(token => { + const shouldFetchTokenDefinition = selectShouldFetchTokenDefinition( + getState(), + symbol, + token.contract, + ); + + if (shouldFetchTokenDefinition) + dispatch( + getTokenDefinitionThunk({ + networkSymbol: symbol, + contractAddress: token.contract, + chainId, + }), + ); + }); + } + } + + return action; + }, +); diff --git a/suite-native/token-definitions/tsconfig.json b/suite-native/token-definitions/tsconfig.json new file mode 100644 index 00000000000..f48072426a5 --- /dev/null +++ b/suite-native/token-definitions/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "libDev" }, + "references": [ + { + "path": "../../suite-common/redux-utils" + }, + { + "path": "../../suite-common/wallet-config" + }, + { + "path": "../../suite-common/wallet-core" + } + ] +} diff --git a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx index 3dc53e44a40..524f84874b2 100644 --- a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx +++ b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx @@ -7,12 +7,13 @@ import { useFormatters } from '@suite-common/formatters'; import { CryptoAmountFormatter, CryptoToFiatAmountFormatter } from '@suite-native/formatters'; import { selectTransactionBlockTimeById, - selectIsTransactionZeroValuePhishing, TransactionsRootState, + selectIsPhishingTransaction, } from '@suite-common/wallet-core'; import { EthereumTokenTransfer, WalletAccountTransaction } from '@suite-native/ethereum-tokens'; import { Translation } from '@suite-native/intl'; import { Link } from '@suite-native/link'; +import { TokenDefinitionsRootState } from '@suite-common/wallet-core'; import { TransactionDetailSummary } from './TransactionDetailSummary'; import { TransactionDetailRow } from './TransactionDetailRow'; @@ -35,8 +36,9 @@ export const TransactionDetailData = ({ selectTransactionBlockTimeById(state, transaction.txid, accountKey), ); - const isZeroValuePhishing = useSelector((state: TransactionsRootState) => - selectIsTransactionZeroValuePhishing(state, transaction.txid, accountKey), + const isPhishingTransaction = useSelector( + (state: TokenDefinitionsRootState & TransactionsRootState) => + selectIsPhishingTransaction(state, transaction.txid, accountKey), ); const transactionTokensCount = transaction.tokens.length; @@ -50,7 +52,7 @@ export const TransactionDetailData = ({ return ( <> - {isZeroValuePhishing && ( + {isPhishingTransaction && ( - selectIsTransactionZeroValuePhishing(state, txid, accountKey), + const isPhishingTransaction = useSelector( + (state: TokenDefinitionsRootState & TransactionsRootState) => + selectIsPhishingTransaction(state, txid, accountKey), ); const iconColor: Color = isTransactionPending ? 'backgroundAlertYellowBold' : 'iconSubdued'; @@ -187,7 +189,7 @@ export const TransactionListItemContainer = ({ {transactionTitle} - {isZeroValuePhishing && ( + {isPhishingTransaction && (