diff --git a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts index 5e496435a6de..750931d9dfac 100644 --- a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts +++ b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts @@ -23,26 +23,29 @@ import type WalletApiProxy from '../../wallet_api_proxy' // utils import { getCoinFromTxDataUnion } from '../../../utils/network-utils' import { deserializeTransaction } from '../../../utils/model-serialization-utils' +import { getAssetIdKey } from '../../../utils/asset-utils' // mocks import { mockWalletState } from '../../../stories/mock-data/mock-wallet-state' import { mockedMnemonic } from '../../../stories/mock-data/user-accounts' import { + NativeAssetBalanceRegistry, + TokenBalanceRegistry, mockAccount, - mockErc721Token, mockEthAccountInfo, mockFilecoinAccountInfo, mockFilecoinMainnetNetwork, mockOnRampCurrencies, mockSolanaAccountInfo, mockSolanaMainnetNetwork, - mockSplNft } from '../../constants/mocks' import { mockEthMainnet, mockNetworks } from '../../../stories/mock-data/mock-networks' import { mockAccountAssetOptions, mockBasicAttentionToken, mockErc20TokensList, + mockErc721Token, + mockSplNft, } from '../../../stories/mock-data/mock-asset-options' import { mockFilSendTransaction, @@ -74,23 +77,6 @@ export const makeMockedStoreWithSpy = () => { return { store } } -type NativeAssetBalanceRegistry = Record< - string, // account address - | Record< - string, // chainId - string // balance - > - | undefined -> - -type TokenBalanceRegistry = Record< - string, // account address - Record< - string, // asset identifier - string // balance - > -> - export interface WalletApiDataOverrides { selectedCoin?: BraveWallet.CoinType selectedAccountId?: BraveWallet.AccountId @@ -235,6 +221,9 @@ export class MockedWalletApiProxy { deserializeTransaction(mockedErc20ApprovalTransaction) ] + // name service lookups + requireOffchainConsent: number = BraveWallet.ResolveMethod.kAsk + constructor(overrides?: WalletApiDataOverrides | undefined) { this.applyOverrides(overrides) } @@ -359,7 +348,48 @@ export class MockedWalletApiProxy { }, getNetworkForSelectedAccountOnActiveOrigin: async () => { return { network: this.selectedNetwork } - } + }, + isBase58EncodedSolanaPubkey: async (key) => { + return { + result: true + } + }, + getBalanceScannerSupportedChains: async () => { + return { + chainIds: this.networks.map((n) => n.chainId) + } + }, + ensureSelectedAccountForChain: async (coin, chainId) => { + const foundAccount = findAccountByUniqueKey( + this.accountInfos, + this.selectedAccountId.uniqueKey, + ) + + return { + accountId: + foundAccount?.accountId.coin === coin + ? foundAccount.accountId + : this.accountInfos.find((a) => a.accountId.coin === coin) + ?.accountId ?? null + } + }, + setNetworkForSelectedAccountOnActiveOrigin: async (chainId) => { + if (this.selectedNetwork.chainId === chainId) { + return { + success: true + } + } + + const net = this.networks.find(n => n.chainId === chainId) + + if (net) { + this.selectedNetwork = net + } + + return { + success: !!net + } + }, } swapService: Partial> = @@ -434,7 +464,27 @@ export class MockedWalletApiProxy { return password === 'password' ? { mnemonic: mockedMnemonic } : { mnemonic: '' } - } + }, + getChecksumEthAddress: async (address) => { + return { + checksumAddress: address.toLocaleLowerCase() + } + }, + setSelectedAccount: async (accountId) => { + const validId = !!this.accountInfos.find( + (a) => a.accountId.uniqueKey === accountId.uniqueKey + ) + + if (validId) { + this.selectedAccountId = accountId + } else { + console.log('invalid id: ' + accountId.uniqueKey) + } + + return { + success: validId + } + }, } ethTxManagerProxy: Partial< @@ -523,16 +573,18 @@ export class MockedWalletApiProxy { } }, getSolanaBalance: async (pubkey: string, chainId: string) => { + const balance = BigInt(this.nativeBalanceRegistry[pubkey]?.[chainId] ?? 0) return { - balance: BigInt(this.nativeBalanceRegistry[pubkey]?.[chainId] || 0), + balance, error: 0, errorMessage: '' } }, + // Token balances getERC20TokenBalance: async (contract, address, chainId) => { return { balance: - this.nativeBalanceRegistry[address]?.[ + this.tokenBalanceRegistry[address]?.[ blockchainTokenEntityAdaptor.selectId({ coin: BraveWallet.CoinType.ETH, chainId, @@ -554,7 +606,7 @@ export class MockedWalletApiProxy { ) => { return { balance: - this.nativeBalanceRegistry[accountAddress]?.[ + this.tokenBalanceRegistry[accountAddress]?.[ blockchainTokenEntityAdaptor.selectId({ coin: BraveWallet.CoinType.ETH, chainId, @@ -576,7 +628,7 @@ export class MockedWalletApiProxy { ) => { return { balance: - this.nativeBalanceRegistry[accountAddress]?.[ + this.tokenBalanceRegistry[accountAddress]?.[ blockchainTokenEntityAdaptor.selectId({ coin: BraveWallet.CoinType.ETH, chainId, @@ -597,7 +649,7 @@ export class MockedWalletApiProxy { ) => { return { amount: - this.nativeBalanceRegistry[walletAddress]?.[ + this.tokenBalanceRegistry[walletAddress]?.[ blockchainTokenEntityAdaptor.selectId({ coin: BraveWallet.CoinType.ETH, chainId, @@ -613,6 +665,53 @@ export class MockedWalletApiProxy { errorMessage: '' } }, + getSPLTokenBalances: async (pubkey, chainId) => { + const balances = Object.keys(this.tokenBalanceRegistry?.[pubkey]) + .filter((tokenId) => tokenId.includes(chainId)) + .map((tokenIdentifier) => { + const token = this.blockchainTokens.find( + (t) => getAssetIdKey(t) === tokenIdentifier + ) + + const amount = + this.tokenBalanceRegistry[pubkey][tokenIdentifier] || '0' + + return { + amount: this.tokenBalanceRegistry[pubkey][tokenIdentifier] || '0', + decimals: token?.decimals ?? 1, + mint: token?.contractAddress ?? '', + uiAmount: amount + } + }) + return { + balances, + error: 0, + errorMessage: '', + } + }, + getERC20TokenBalances: async (contracts, address, chainId) => { + const balances = Object.keys(this.tokenBalanceRegistry?.[address]) + .filter((tokenId) => tokenId.includes(chainId)) + .map((tokenIdentifier) => { + const token = this.blockchainTokens.find( + (t) => getAssetIdKey(t) === tokenIdentifier + ) + + const amount = + this.tokenBalanceRegistry[address][tokenIdentifier] || '0' + + return { + balance: amount, + contractAddress: token?.contractAddress || '' + } + }) + return { + balances, + error: 0, + errorMessage: '' + } + }, + // NFT Metadata getERC721Metadata: async (contract, tokenId, chainId) => { const mockedMetadata = mockNFTMetadata.find( @@ -662,6 +761,33 @@ export class MockedWalletApiProxy { name: mockedMetadata.contractInformation.name } as CommonNftMetadata) } + }, + // name service lookups + setEnsOffchainLookupResolveMethod(method) { + this.requireOffchainConsent = method + }, + ensGetEthAddr: async (domain) => { + return { + address: `0x1234abcd1234${domain}`, + error: 0, + errorMessage: '', + requireOffchainConsent: + this.requireOffchainConsent !== BraveWallet.ResolveMethod.kEnabled + } + }, + snsGetSolAddr: async (domain) => { + return { + address: `s1abcd1234567890${domain}`, + error: 0, + errorMessage: '' + } + }, + unstoppableDomainsGetWalletAddr: async (domain, token) => { + return { + address: `0x${token?.chainId}abcd${domain}`, + error: 0, + errorMessage: '' + } } } diff --git a/components/brave_wallet_ui/common/constants/mocks.ts b/components/brave_wallet_ui/common/constants/mocks.ts index 0b770ebd5712..c2b06f4d6f9f 100644 --- a/components/brave_wallet_ui/common/constants/mocks.ts +++ b/components/brave_wallet_ui/common/constants/mocks.ts @@ -35,9 +35,21 @@ import { getPriceIdForToken } from '../../utils/api-utils' // mocks import { + mockAlgorandErc20TokenId, mockBasicAttentionToken, + mockBasicAttentionTokenId, + mockBinanceCoinErc20TokenId, + mockBitcoinErc20TokenId, + mockDaiTokenId, + mockErc721Token, mockEthToken, - mockMoonCatNFT + mockMoonCatNFT, + mockSplBasicAttentionTokenId, + mockSplNft, + mockSplNftId, + mockSplUSDCoinId, + mockUSDCoinId, + mockZrxErc20TokenId } from '../../stories/mock-data/mock-asset-options' import { mockNFTMetadata } from '../../stories/mock-data/mock-nft-metadata' import { mockEthMainnet } from '../../stories/mock-data/mock-networks' @@ -167,61 +179,6 @@ export const mockSolanaTestnetNetwork: BraveWallet.NetworkInfo = { isEip1559: false } -export const mockERC20Token: BraveWallet.BlockchainToken = { - contractAddress: 'mockContractAddress', - name: 'Dog Coin', - symbol: 'DOG', - logo: '', - isErc20: true, - isErc721: false, - isErc1155: false, - isNft: false, - isSpam: false, - decimals: 18, - visible: true, - tokenId: '', - coingeckoId: '', - coin: BraveWallet.CoinType.ETH, - chainId: BraveWallet.MAINNET_CHAIN_ID -} - -export const mockErc721Token: BraveWallet.BlockchainToken = { - contractAddress: '0x59468516a8259058bad1ca5f8f4bff190d30e066', - name: 'Invisible Friends', - symbol: 'INVSBLE', - logo: 'https://ipfs.io/ipfs/QmX4nfgA35MiW5APoc4P815hMcH8hAt7edi5H3wXkFm485/2D/2585.gif', - isErc20: false, - isErc721: true, - isErc1155: false, - isNft: true, - isSpam: false, - decimals: 18, - visible: true, - tokenId: '0x0a19', - coingeckoId: '', - coin: BraveWallet.CoinType.ETH, - chainId: BraveWallet.MAINNET_CHAIN_ID -} - -export const mockSplNft: BraveWallet.BlockchainToken = { - contractAddress: 'wt1PhURTzRSgmWKHBEJgSX8hN9TdkdNoKhPAnwCmnZE', - name: 'The Degen #2314', - symbol: 'BNFT', - logo: 'https://shdw-drive.genesysgo.net/FR3sEzyAmQMooUYhcPPnN4TmVLSZWi3cEwAWpB4nJvYJ/image-2.png', - isErc20: false, - isErc721: false, - isErc1155: false, - isNft: true, - isSpam: false, - decimals: 1, - visible: true, - tokenId: 'wt1PhURTzRSgmWKHBEJgSX8hN9TdkdNoKhPAnwCmnZE', - coingeckoId: '', - coin: BraveWallet.CoinType.SOL, - chainId: BraveWallet.SOLANA_MAINNET, - -} - export const mockNftPinningStatus = { [getAssetIdKey(mockErc721Token)]: { code: BraveWallet.TokenPinStatusCode.STATUS_PINNED, @@ -1406,3 +1363,158 @@ export const mockOnRampCurrencies: BraveWallet.OnRampCurrency[] = [ providers: [] } ] + +export type NativeAssetBalanceRegistry = Record< + string, // account address + | Record< + string, // chainId + string // balance + > + | undefined +> + +export type TokenBalanceRegistry = Record< + string, // account address + Record< + string, // asset identifier + string // balance + > +> + +export const mockNativeBalanceRegistry: NativeAssetBalanceRegistry = { + [mockAccount.address]: { + [BraveWallet.BITCOIN_MAINNET]: '0', + [BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID]: '836', + [BraveWallet.FILECOIN_MAINNET]: '0', + [BraveWallet.MAINNET_CHAIN_ID]: '12312', + [BraveWallet.SOLANA_MAINNET]: '0', + // Secondary Networks + [BraveWallet.ARBITRUM_MAINNET_CHAIN_ID]: '2322', + [BraveWallet.ARBITRUM_NOVA_CHAIN_ID]: '45100002', + [BraveWallet.AURORA_MAINNET_CHAIN_ID]: '4326', + [BraveWallet.AVALANCHE_MAINNET_CHAIN_ID]: '345', + [BraveWallet.BASE_MAINNET_CHAIN_ID]: '56453455', + [BraveWallet.BINANCE_SMART_CHAIN_MAINNET_CHAIN_ID]: '444', + [BraveWallet.CELO_MAINNET_CHAIN_ID]: '55851', + [BraveWallet.FANTOM_MAINNET_CHAIN_ID]: '1', + [BraveWallet.GNOSIS_CHAIN_ID]: '440502', + [BraveWallet.NEON_EVM_MAINNET_CHAIN_ID]: '222', + [BraveWallet.OPTIMISM_MAINNET_CHAIN_ID]: '567', + [BraveWallet.POLYGON_MAINNET_CHAIN_ID]: '111', + [BraveWallet.POLYGON_ZKEVM_CHAIN_ID]: '98094343', + [BraveWallet.ZK_SYNC_ERA_CHAIN_ID]: '2621', + // Test Networks + [BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID]: '0', + [BraveWallet.GOERLI_CHAIN_ID]: '67', + [BraveWallet.LOCALHOST_CHAIN_ID]: '133', + [BraveWallet.SEPOLIA_CHAIN_ID]: '7798', + // Other + [BraveWallet.GODWOKEN_CHAIN_ID]: '777', + [BraveWallet.PALM_CHAIN_ID]: '2', + }, + [mockEthAccountInfo.address]: { + [BraveWallet.BITCOIN_MAINNET]: '0', + [BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID]: '22', + [BraveWallet.FILECOIN_MAINNET]: '3111', + [BraveWallet.MAINNET_CHAIN_ID]: '33214', + [BraveWallet.SOLANA_MAINNET]: '0', + // Secondary Networks + [BraveWallet.ARBITRUM_MAINNET_CHAIN_ID]: '1221', + [BraveWallet.ARBITRUM_NOVA_CHAIN_ID]: '251002', + [BraveWallet.AURORA_MAINNET_CHAIN_ID]: '1111', + [BraveWallet.AVALANCHE_MAINNET_CHAIN_ID]: '565', + [BraveWallet.BASE_MAINNET_CHAIN_ID]: '4444', + [BraveWallet.BINANCE_SMART_CHAIN_MAINNET_CHAIN_ID]: '2122', + [BraveWallet.CELO_MAINNET_CHAIN_ID]: '1', + [BraveWallet.FANTOM_MAINNET_CHAIN_ID]: '0', + [BraveWallet.GNOSIS_CHAIN_ID]: '2', + [BraveWallet.NEON_EVM_MAINNET_CHAIN_ID]: '0', + [BraveWallet.OPTIMISM_MAINNET_CHAIN_ID]: '2', + [BraveWallet.POLYGON_MAINNET_CHAIN_ID]: '55', + [BraveWallet.POLYGON_ZKEVM_CHAIN_ID]: '666', + [BraveWallet.ZK_SYNC_ERA_CHAIN_ID]: '5377', + // Test Networks + [BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID]: '1', + [BraveWallet.GOERLI_CHAIN_ID]: '7', + [BraveWallet.LOCALHOST_CHAIN_ID]: '3', + [BraveWallet.SEPOLIA_CHAIN_ID]: '9', + // Other + [BraveWallet.GODWOKEN_CHAIN_ID]: '727', + [BraveWallet.PALM_CHAIN_ID]: '1', + }, + [mockSolanaAccount.address]: { + [BraveWallet.SOLANA_MAINNET]: '7432', + }, + [mockSolanaAccountInfo.address]: { + [BraveWallet.SOLANA_MAINNET]: '45434545435', + }, + [mockFilecoinAccount.address]: { + [BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID]: '34598722', + [BraveWallet.FILECOIN_MAINNET]: '345545', + [BraveWallet.MAINNET_CHAIN_ID]: '1000', + // Secondary Networks + [BraveWallet.ARBITRUM_MAINNET_CHAIN_ID]: '1000', + [BraveWallet.ARBITRUM_NOVA_CHAIN_ID]: '3000', + [BraveWallet.POLYGON_MAINNET_CHAIN_ID]: '330', + // Test Networks + [BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID]: '220', + [BraveWallet.GOERLI_CHAIN_ID]: '30', + [BraveWallet.LOCALHOST_CHAIN_ID]: '11110', + [BraveWallet.SEPOLIA_CHAIN_ID]: '5550', + // Other + [BraveWallet.GODWOKEN_CHAIN_ID]: '40', + [BraveWallet.PALM_CHAIN_ID]: '70', + }, + [mockFilecoinAccountInfo.address]: { + [BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID]: '2334', + [BraveWallet.FILECOIN_MAINNET]: '35', + [BraveWallet.MAINNET_CHAIN_ID]: '220', + // Secondary Networks + [BraveWallet.ARBITRUM_MAINNET_CHAIN_ID]: '600', + [BraveWallet.ARBITRUM_NOVA_CHAIN_ID]: '400', + [BraveWallet.POLYGON_MAINNET_CHAIN_ID]: '30', + // Test Networks + [BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID]: '20', + [BraveWallet.GOERLI_CHAIN_ID]: '3', + [BraveWallet.LOCALHOST_CHAIN_ID]: '110', + [BraveWallet.SEPOLIA_CHAIN_ID]: '50', + // Other + [BraveWallet.GODWOKEN_CHAIN_ID]: '4', + [BraveWallet.PALM_CHAIN_ID]: '7', + }, +} + +export const mockTokenBalanceRegistry: TokenBalanceRegistry = { + [mockAccount.address]: { + [mockBasicAttentionTokenId]: '111', + [mockBinanceCoinErc20TokenId]: '222', + [mockBitcoinErc20TokenId]: '333', + [mockAlgorandErc20TokenId]: '444', + [mockZrxErc20TokenId]: '555', + [mockDaiTokenId]: '666', + [mockUSDCoinId]: '777', + }, + [mockEthAccountInfo.address]: { + [mockBasicAttentionTokenId]: '11', + [mockBinanceCoinErc20TokenId]: '22', + [mockBitcoinErc20TokenId]: '33', + [mockAlgorandErc20TokenId]: '44', + [mockZrxErc20TokenId]: '55', + [mockDaiTokenId]: '66', + [mockUSDCoinId]: '77', + }, + [mockSolanaAccount.address]: { + [mockSplNftId]: '1', + [mockSplUSDCoinId]: '14444', + [mockSplBasicAttentionTokenId]: '99999', + }, + [mockSolanaAccountInfo.address]: { + [mockSplNftId]: '0', + [mockSplUSDCoinId]: '3333', + [mockSplBasicAttentionTokenId]: '3421', + }, + [mockFilecoinAccount.address]: {}, + [mockFilecoinAccountInfo.address]: {}, +} + + diff --git a/components/brave_wallet_ui/common/hooks/assets.test.tsx b/components/brave_wallet_ui/common/hooks/assets.test.tsx deleted file mode 100644 index cc88ed85775b..000000000000 --- a/components/brave_wallet_ui/common/hooks/assets.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// you can obtain one at https://mozilla.org/MPL/2.0/. -import * as React from 'react' -import { Provider } from 'react-redux' -import { TextEncoder, TextDecoder } from 'util' -// @ts-expect-error -global.TextDecoder = TextDecoder -global.TextEncoder = TextEncoder -import { renderHook } from '@testing-library/react-hooks' -import { mockAccount } from '../constants/mocks' -import useAssets from './assets' -import * as MockedLib from '../async/__mocks__/lib' -import { LibContext } from '../context/lib.context' -import { mockWalletState } from '../../stories/mock-data/mock-wallet-state' -import { mockBasicAttentionToken, mockEthToken } from '../../stories/mock-data/mock-asset-options' -import { createMockStore } from '../../utils/test-utils' - -const mockVisibleList = [ - mockEthToken, - mockBasicAttentionToken -] - -const renderHookOptionsWithCustomStore = (store: any) => ({ - wrapper: ({ children }: { children?: React.ReactChildren }) => - - - {children} - - -}) - -describe('useAssets hook', () => { - it('should return assets by value & network', async () => { - const { result, waitForNextUpdate } = renderHook( - () => useAssets(), - renderHookOptionsWithCustomStore( - createMockStore({ - walletStateOverride: { - ...mockWalletState, - userVisibleTokensInfo: mockVisibleList, - accounts: [mockAccount] - } - }, - { selectedAccountId: mockAccount.accountId } - ) - ) - ) - - await waitForNextUpdate() - - expect(result.current).toEqual(mockVisibleList) - }) - - it('should return empty array for assets if visible assets is empty', () => { - const { result } = renderHook( - () => useAssets(), - renderHookOptionsWithCustomStore( - createMockStore({ - walletStateOverride: { - ...mockWalletState, - userVisibleTokensInfo: [], - accounts: [mockAccount] - } - }, - { selectedAccountId: mockAccount.accountId } - ) - ) - ) - expect(result.current).toEqual([]) - }) -}) diff --git a/components/brave_wallet_ui/common/hooks/assets.ts b/components/brave_wallet_ui/common/hooks/assets.ts deleted file mode 100644 index 41a873181978..000000000000 --- a/components/brave_wallet_ui/common/hooks/assets.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2021 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// you can obtain one at https://mozilla.org/MPL/2.0/. - -import * as React from 'react' - -// utils -import { WalletSelectors } from '../selectors' - -// hooks -import { useUnsafeWalletSelector } from './use-safe-selector' -import { - useGetSelectedChainQuery -} from '../slices/api.slice' - -export function useAssets () { - // redux - const userVisibleTokensInfo = useUnsafeWalletSelector( - WalletSelectors.userVisibleTokensInfo - ) - - // queries - const { data: selectedNetwork } = useGetSelectedChainQuery() - - // memos - const assetsByNetwork = React.useMemo(() => { - if (!userVisibleTokensInfo || !selectedNetwork) { - return [] - } - // We also filter by coinType here because localhost - // networks share the same chainId. - return userVisibleTokensInfo.filter((token) => - token.chainId === selectedNetwork.chainId && - token.coin === selectedNetwork.coin - ) - }, [userVisibleTokensInfo, selectedNetwork]) - - return assetsByNetwork -} - -export default useAssets diff --git a/components/brave_wallet_ui/common/hooks/send.ts b/components/brave_wallet_ui/common/hooks/send.ts deleted file mode 100644 index 23dc8a6c83c6..000000000000 --- a/components/brave_wallet_ui/common/hooks/send.ts +++ /dev/null @@ -1,577 +0,0 @@ -// Copyright (c) 2021 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// you can obtain one at https://mozilla.org/MPL/2.0/. - -import { assertNotReached } from 'chrome://resources/js/assert_ts.js'; -import * as React from 'react' -import { useDispatch } from 'react-redux' -import { skipToken } from '@reduxjs/toolkit/query' - -// Types -import { - BraveWallet, - GetEthAddrReturnInfo, - GetUnstoppableDomainsWalletAddrReturnInfo, - IsBase58EncodedSolanaPubkeyReturnInfo, - AmountValidationErrorType, - GetSolAddrReturnInfo, - CoinTypesMap, - BaseTransactionParams -} from '../../constants/types' - -// Utils -import { getLocale } from '../../../common/locale' -import { isValidAddress, isValidFilAddress } from '../../utils/address-utils' -import { endsWithAny } from '../../utils/string-utils' -import Amount from '../../utils/amount' -import { WalletSelectors } from '../selectors' - -// hooks -import { useLib } from './useLib' -import { useAssets } from './assets' -import { useGetFVMAddressQuery, useGetSelectedChainQuery, walletApi } from '../slices/api.slice' -import { useUnsafeWalletSelector } from './use-safe-selector' -import { useSelectedAccountQuery } from '../slices/api.slice.extra' - -// constants -import { - supportedENSExtensions, - supportedSNSExtensions, - supportedUDExtensions -} from '../constants/domain-extensions' -import { getChecksumEthAddress } from '../async/lib' - -export function useSend () { - // redux - const dispatch = useDispatch() - const fullTokenList = useUnsafeWalletSelector(WalletSelectors.fullTokenList) - - // queries - const { data: selectedNetwork } = useGetSelectedChainQuery() - const { data: selectedAccount } = useSelectedAccountQuery() - - // custom hooks - const { - enableEnsOffchainLookup, - findENSAddress, - findSNSAddress, - findUnstoppableDomainAddress, - isBase58EncodedSolanaPubkey - } = useLib() - const sendAssetOptions = useAssets() - - // State - const [searchingForDomain, setSearchingForDomain] = React.useState(false) - const [showEnsOffchainWarning, setShowEnsOffchainWarning] = React.useState(false) - const [toAddressOrUrl, setToAddressOrUrl] = React.useState('') - const [toAddress, setToAddress] = React.useState('') - const [sendAmount, setSendAmount] = React.useState('') - const [addressError, setAddressError] = React.useState(undefined) - const [addressWarning, setAddressWarning] = React.useState(undefined) - const [selectedSendAsset, setSelectedSendAsset] = React.useState(undefined) - const [showFilecoinFEVMWarning, setShowFilecoinFEVMWarning] = React.useState(false) - const { data: fevmTranslatedAddresses } = useGetFVMAddressQuery( - selectedSendAsset?.coin === BraveWallet.CoinType.FIL - ? { - coin: selectedSendAsset?.coin, - addresses: [toAddressOrUrl], - isMainNet: selectedSendAsset?.chainId === BraveWallet.FILECOIN_MAINNET - } - : skipToken - ) - - const selectSendAsset = (asset: BraveWallet.BlockchainToken | undefined) => { - if (asset?.isErc721 || asset?.isNft) { - setSendAmount('1') - } else { - setSendAmount('') - } - setToAddress('') - setToAddressOrUrl('') - setAddressError(undefined) - setAddressWarning(undefined) - setShowEnsOffchainWarning(false) - setSearchingForDomain(false) - setSelectedSendAsset(asset) - setShowFilecoinFEVMWarning(false) - } - - const validateETHAddress = React.useCallback(async (address: string) => { - if (!isValidAddress(address, 20)) { - setAddressWarning('') - setAddressError(getLocale('braveWalletInvalidRecipientAddress')) - return false - } - - const { checksumAddress } = (await getChecksumEthAddress(address)) - if (checksumAddress === address) { - setAddressWarning('') - setAddressError('') - return true - } - - if ([address.toLowerCase(), address.toUpperCase()].includes(address)) { - setAddressError('') - setAddressWarning(getLocale('braveWalletAddressMissingChecksumInfoWarning')) - return false - } - setAddressWarning('') - setAddressError(getLocale('braveWalletNotValidChecksumAddressError')) - return false - }, [setAddressWarning, setAddressError]) - - const setNotRegisteredError = React.useCallback(() => { - setAddressError(getLocale('braveWalletNotDomain').replace('$1', CoinTypesMap[selectedNetwork?.coin ?? 0])) - }, [selectedNetwork?.coin]) - - const handleDomainLookupResponse = React.useCallback((addressOrUrl: string, error: BraveWallet.ProviderError, requireOffchainConsent: boolean) => { - if (requireOffchainConsent) { - setAddressError('') - setAddressWarning('') - setShowEnsOffchainWarning(true) - setSearchingForDomain(false) - return - } - if (addressOrUrl && error === BraveWallet.ProviderError.kSuccess) { - setAddressError('') - setAddressWarning('') - setToAddress(addressOrUrl) - // If found address is the same as the selectedAccounts Wallet Address - if (addressOrUrl.toLowerCase() === selectedAccount?.address?.toLowerCase()) { - setAddressError(getLocale('braveWalletSameAddressError')) - } - setSearchingForDomain(false) - return - } - setShowEnsOffchainWarning(false) - setNotRegisteredError() - setSearchingForDomain(false) - }, [selectedAccount?.address, setShowEnsOffchainWarning, setNotRegisteredError]) - - const handleUDAddressLookUp = React.useCallback((addressOrUrl: string) => { - setSearchingForDomain(true) - setToAddress('') - findUnstoppableDomainAddress(addressOrUrl, selectedSendAsset ?? null).then((value: GetUnstoppableDomainsWalletAddrReturnInfo) => { - handleDomainLookupResponse(value.address, value.error, false) - }).catch(e => console.log(e)) - }, [findUnstoppableDomainAddress, handleDomainLookupResponse, selectedSendAsset, selectedAccount]) - - const processEthereumAddress = React.useCallback((addressOrUrl: string) => { - const valueToLowerCase = addressOrUrl.toLowerCase() - - // If value ends with a supported ENS extension, will call findENSAddress. - // If success true, will set toAddress else will return error message. - if (endsWithAny(supportedENSExtensions, valueToLowerCase)) { - setSearchingForDomain(true) - setToAddress('') - findENSAddress(addressOrUrl).then((value: GetEthAddrReturnInfo) => { - handleDomainLookupResponse(value.address, value.error, value.requireOffchainConsent) - }).catch(e => console.log(e)) - return - } - - setShowEnsOffchainWarning(false) - - // If value ends with a supported UD extension, will call findUnstoppableDomainAddress. - // If success true, will set toAddress else will return error message. - if (endsWithAny(supportedUDExtensions, valueToLowerCase)) { - handleUDAddressLookUp(addressOrUrl) - return - } - - // If value is the same as the selectedAccounts Wallet Address - if (valueToLowerCase === selectedAccount?.address?.toLowerCase()) { - setToAddress(addressOrUrl) - setAddressWarning('') - setAddressError(getLocale('braveWalletSameAddressError')) - return - } - - // If value is a Tokens Contract Address - if (fullTokenList.some(token => token.contractAddress.toLowerCase() === valueToLowerCase)) { - setToAddress(addressOrUrl) - setAddressWarning('') - setAddressError(getLocale('braveWalletInvalidRecipientAddress')) - return - } - - if (selectedAccount && selectedSendAsset && - selectedAccount.accountId.coin === BraveWallet.CoinType.ETH && - (selectedSendAsset.chainId === BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID || - selectedSendAsset.chainId === BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID) && - isValidFilAddress(addressOrUrl)) { - setToAddress(addressOrUrl); - setAddressWarning('') - setAddressError('') - return; - } - - // If value starts with 0x, will check if it's a valid address - if (valueToLowerCase.startsWith('0x')) { - setToAddress(addressOrUrl) - validateETHAddress(addressOrUrl) - return - } - - // Resets State - if (valueToLowerCase === '') { - setAddressError('') - setAddressWarning('') - setToAddress('') - setShowEnsOffchainWarning(false) - return - } - - // Fallback error state - setAddressWarning('') - setAddressError(getLocale('braveWalletInvalidRecipientAddress')) - }, [selectedAccount, - selectedSendAsset, - handleUDAddressLookUp, - handleDomainLookupResponse, - setShowEnsOffchainWarning]) - - const processFilecoinAddress = React.useCallback(async (addressOrUrl: string) => { - const valueToLowerCase = addressOrUrl.toLowerCase() - - // If value ends with a supported UD extension, will call findUnstoppableDomainAddress. - // If success true, will set toAddress else will return error message. - if (endsWithAny(supportedUDExtensions, valueToLowerCase)) { - handleUDAddressLookUp(addressOrUrl) - return - } - - // If value is the same as the selectedAccounts Wallet Address - if (valueToLowerCase === selectedAccount?.address.toLowerCase()) { - setAddressWarning('') - setAddressError(getLocale('braveWalletSameAddressError')) - return - } - - // Do nothing if value is an empty string - if (addressOrUrl === '') { - setAddressWarning('') - setAddressError('') - setToAddress('') - return - } - - // If value starts with 0x, will check if it's a valid address - if (valueToLowerCase.startsWith('0x')) { - setToAddress(addressOrUrl) - const v = (await validateETHAddress(addressOrUrl)) - setShowFilecoinFEVMWarning(v) - return - } else { - setToAddress(valueToLowerCase) - if (!isValidFilAddress(valueToLowerCase)) { - setAddressWarning('') - setAddressError(getLocale('braveWalletInvalidRecipientAddress')) - return - } - } - // Reset error and warning state back to normal - setAddressWarning('') - setAddressError('') - }, [selectedAccount?.address, fevmTranslatedAddresses, validateETHAddress, handleUDAddressLookUp]) - - const processSolanaAddress = React.useCallback((addressOrUrl: string) => { - const valueToLowerCase = addressOrUrl.toLowerCase() - - // If value ends with a supported UD extension, will call findUnstoppableDomainAddress. - // If success true, will set toAddress else will return error message. - if (endsWithAny(supportedUDExtensions, valueToLowerCase)) { - handleUDAddressLookUp(addressOrUrl) - return - } - - // If value ends with a supported SNS extension, will call findSNSAddress. - // If success true, will set toAddress else will return error message. - if (endsWithAny(supportedSNSExtensions, valueToLowerCase)) { - setSearchingForDomain(true) - setToAddress('') - findSNSAddress(addressOrUrl).then((value: GetSolAddrReturnInfo) => { - handleDomainLookupResponse(value.address, value.error, false) - }).catch(e => console.log(e)) - return - } - - setToAddress(addressOrUrl) - - // Do nothing if value is an empty string - if (addressOrUrl === '') { - setAddressWarning('') - setAddressError('') - return - } - - // Check if value is the same as the sending address - if (addressOrUrl.toLowerCase() === selectedAccount?.address?.toLowerCase()) { - setAddressError(getLocale('braveWalletSameAddressError')) - setAddressWarning('') - return - } - - // Check if value is a Tokens Contract Address - if (fullTokenList.some(token => token.contractAddress.toLowerCase() === addressOrUrl.toLowerCase())) { - setAddressError(getLocale('braveWalletInvalidRecipientAddress')) - setAddressWarning('') - return - } - - // Check if value is a Base58 Encoded Solana Pubkey - isBase58EncodedSolanaPubkey(addressOrUrl).then((value: IsBase58EncodedSolanaPubkeyReturnInfo) => { - const { result } = value - - // If result is false we show address error - if (!result) { - setAddressWarning('') - setAddressError(getLocale('braveWalletInvalidRecipientAddress')) - return - } - setAddressWarning('') - setAddressError('') - }).catch(e => { - console.log(e) - // Reset state back to normal - setAddressWarning('') - setAddressError('') - }) - }, [selectedAccount?.address, fullTokenList, handleUDAddressLookUp, handleDomainLookupResponse]) - - const processBitcoinAddress = React.useCallback((addressOrUrl: string) => { - // TODO(apaymyshev): support btc address aliases(UD, SNS) - - setToAddress(addressOrUrl) - - // Do nothing if value is an empty string - if (addressOrUrl === '') { - setAddressWarning('') - setAddressError('') - // eslint-disable-next-line no-useless-return - return - } - - // Check if value is the same as the sending address - // TODO(apaymyshev): should prohibit self transfers? - - // TODO(apaymyshev): validate address format. - }, []) - - const processAddressOrUrl = React.useCallback((addressOrUrl: string) => { - if (!selectedAccount) { - return - } - - setShowFilecoinFEVMWarning(false) - - if (selectedAccount.accountId.coin === BraveWallet.CoinType.ETH) { - processEthereumAddress(addressOrUrl) - } else if (selectedAccount.accountId.coin === BraveWallet.CoinType.FIL) { - processFilecoinAddress(addressOrUrl) - } else if (selectedAccount.accountId.coin === BraveWallet.CoinType.SOL) { - processSolanaAddress(addressOrUrl) - } else if (selectedAccount.accountId.coin === BraveWallet.CoinType.BTC) { - processBitcoinAddress(addressOrUrl) - } else { - assertNotReached(`Unknown coin ${selectedAccount.accountId.coin}`) - } - }, [ - selectedAccount, - processEthereumAddress, - processFilecoinAddress, - processSolanaAddress, - processBitcoinAddress - ]) - - const updateToAddressOrUrl = React.useCallback((addressOrUrl: string) => { - setToAddressOrUrl(addressOrUrl) - processAddressOrUrl(addressOrUrl) - }, [processAddressOrUrl]) - - const resetSendFields = React.useCallback(() => { - selectSendAsset(undefined) - setToAddressOrUrl('') - setSendAmount('') - setShowFilecoinFEVMWarning(false) - }, [selectSendAsset]) - - const submitSend = React.useCallback(() => { - if (!selectedSendAsset) { - console.log('Failed to submit Send transaction: no send asset selected') - return - } - - if (!selectedAccount) { - console.log('Failed to submit Send transaction: no account selected') - return - } - - if (!selectedNetwork) { - console.log('Failed to submit Send transaction: no network selected') - return - } - - const fromAccount: BaseTransactionParams['fromAccount'] = { - accountId: selectedAccount.accountId, - address: selectedAccount.address, - hardware: selectedAccount.hardware, - } - - selectedSendAsset.isErc20 && - dispatch( - walletApi.endpoints.sendERC20Transfer.initiate({ - network: selectedNetwork, - fromAccount, - to: toAddress, - value: new Amount(sendAmount) - // ETH → Wei conversion - .multiplyByDecimals(selectedSendAsset.decimals) - .toHex(), - contractAddress: selectedSendAsset.contractAddress, - }) - ) - - selectedSendAsset.isErc721 && - dispatch( - walletApi.endpoints.sendERC721TransferFrom.initiate({ - network: selectedNetwork, - fromAccount, - to: toAddress, - value: '', - contractAddress: selectedSendAsset.contractAddress, - tokenId: selectedSendAsset.tokenId ?? '', - }) - ) - - if ( - selectedAccount.accountId.coin === BraveWallet.CoinType.SOL && - selectedSendAsset.contractAddress !== '' && - !selectedSendAsset.isErc20 && - !selectedSendAsset.isErc721 - ) { - dispatch( - walletApi.endpoints.sendSPLTransfer.initiate({ - network: selectedNetwork, - fromAccount, - to: toAddress, - value: !selectedSendAsset.isNft - ? new Amount(sendAmount) - .multiplyByDecimals(selectedSendAsset.decimals) - .toHex() - : new Amount(sendAmount).toHex(), - splTokenMintAddress: selectedSendAsset.contractAddress - }) - ) - resetSendFields() - return - } - - if (selectedAccount.accountId.coin === BraveWallet.CoinType.FIL) { - dispatch( - walletApi.endpoints.sendTransaction.initiate({ - network: selectedNetwork, - fromAccount, - to: toAddress, - value: new Amount(sendAmount) - .multiplyByDecimals(selectedSendAsset.decimals) - .toNumber() - .toString(), - }) - ) - resetSendFields() - return - } - - if (selectedSendAsset.isErc721 || selectedSendAsset.isErc20) { - resetSendFields() - return - } - - if (selectedAccount.accountId.coin === BraveWallet.CoinType.ETH && - (selectedSendAsset.chainId === BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID || - selectedSendAsset.chainId === BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID) && - isValidFilAddress(toAddress)) { - - dispatch( - walletApi.endpoints.sendETHFilForwarderTransfer.initiate({ - network: selectedNetwork, - fromAccount, - to: toAddress, - value: new Amount(sendAmount) - // ETH → Wei conversion - .multiplyByDecimals(selectedSendAsset.decimals) - .toHex(), - contractAddress: "0x2b3ef6906429b580b7b2080de5ca893bc282c225" - }) - ) - resetSendFields() - return - } - - dispatch( - walletApi.endpoints.sendTransaction.initiate({ - network: selectedNetwork, - fromAccount, - to: toAddress, - value: - selectedAccount.accountId.coin === BraveWallet.CoinType.FIL - ? new Amount(sendAmount) - .multiplyByDecimals(selectedSendAsset.decimals) - .toString() - : new Amount(sendAmount) - .multiplyByDecimals(selectedSendAsset.decimals) - .toHex() - }) - ) - - resetSendFields() - }, [ - selectedSendAsset, - selectedAccount, - selectedNetwork, - sendAmount, - toAddress, - resetSendFields - ]) - - // memos - const sendAmountValidationError: AmountValidationErrorType | undefined = React.useMemo(() => { - if (!sendAmount || !selectedSendAsset) { - return - } - - const amountBN = new Amount(sendAmount) - .multiplyByDecimals(selectedSendAsset.decimals) // ETH → Wei conversion - .value // extract BigNumber object wrapped by Amount - - const amountDP = amountBN && amountBN.decimalPlaces() - return amountDP && amountDP > 0 - ? 'fromAmountDecimalsOverflow' - : undefined - }, [sendAmount, selectedSendAsset]) - - return { - setSendAmount, - updateToAddressOrUrl, - submitSend, - selectSendAsset, - toAddressOrUrl, - toAddress, - sendAmount, - addressError, - addressWarning, - selectedSendAsset, - sendAmountValidationError, - showFilecoinFEVMWarning, - fevmTranslatedAddresses, - showEnsOffchainWarning, - setShowEnsOffchainWarning, - enableEnsOffchainLookup, - searchingForDomain, - processAddressOrUrl, - sendAssetOptions - } -} -export default useSend diff --git a/components/brave_wallet_ui/common/hooks/token.test.ts b/components/brave_wallet_ui/common/hooks/token.test.ts index ea19640ed55e..cdd52362a910 100644 --- a/components/brave_wallet_ui/common/hooks/token.test.ts +++ b/components/brave_wallet_ui/common/hooks/token.test.ts @@ -3,13 +3,13 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // you can obtain one at https://mozilla.org/MPL/2.0/. import { renderHook, act } from '@testing-library/react-hooks' -import { - mockERC20Token, - mockNetwork -} from '../constants/mocks' import { GetBlockchainTokenInfoReturnInfo } from '../../constants/types' import useTokenInfo from './token' +// mocks +import { mockNetwork } from '../constants/mocks' +import { mockERC20Token } from '../../stories/mock-data/mock-asset-options' + const findBlockchainTokenInfo = async (address: string) => { return { token: null } as GetBlockchainTokenInfoReturnInfo } @@ -23,7 +23,7 @@ describe('useTokenInfo hook', () => { expect(result.current.foundTokenInfoByContractAddress?.name).toEqual('Dog Coin') }) - it('Should not find info and return undifined', () => { + it('Should not find info and return undefined', () => { const { result } = renderHook(() => useTokenInfo( findBlockchainTokenInfo, [mockERC20Token], mockNetwork )) diff --git a/components/brave_wallet_ui/common/hooks/useOnClickOutside.ts b/components/brave_wallet_ui/common/hooks/useOnClickOutside.ts index d27d3951dc65..1f20dc7dcb91 100644 --- a/components/brave_wallet_ui/common/hooks/useOnClickOutside.ts +++ b/components/brave_wallet_ui/common/hooks/useOnClickOutside.ts @@ -7,6 +7,34 @@ import * as React from 'react' type Event = MouseEvent | TouchEvent +export const useModal = () => { + const modalRef = React.useRef(null) + + const [isModalShown, setShowModal] = + React.useState(false) + + const openModal = React.useCallback(() => { + setShowModal(true) + }, []) + + const closeModal = React.useCallback(() => { + setShowModal(false) + }, []) + + useOnClickOutside( + modalRef, + closeModal, + isModalShown + ) + + return { + openModal, + closeModal, + ref: modalRef, + isModalShown + } +} + export const useOnClickOutside = ( ref: React.RefObject, handler: (event: Event) => void, diff --git a/components/brave_wallet_ui/common/slices/api-base.slice.ts b/components/brave_wallet_ui/common/slices/api-base.slice.ts index 328431de3d17..173b3828fcd0 100644 --- a/components/brave_wallet_ui/common/slices/api-base.slice.ts +++ b/components/brave_wallet_ui/common/slices/api-base.slice.ts @@ -55,7 +55,9 @@ export function createWalletApiBase () { 'SimpleHashSpamNFTs', 'LocalIPFSNodeStatus', 'EthTokenDecimals', - 'EthTokenSymbol' + 'EthTokenSymbol', + 'EnsOffchainLookupEnabled', + 'NameServiceAddress', ], endpoints: ({ mutation, query }) => ({}) }) diff --git a/components/brave_wallet_ui/common/slices/api-slice-extras-tests/use-combined-tokens-list.test.ts b/components/brave_wallet_ui/common/slices/api-slice-extras-tests/use-combined-tokens-list.test.ts index bdcc1df93125..0f4041d181c1 100644 --- a/components/brave_wallet_ui/common/slices/api-slice-extras-tests/use-combined-tokens-list.test.ts +++ b/components/brave_wallet_ui/common/slices/api-slice-extras-tests/use-combined-tokens-list.test.ts @@ -45,6 +45,6 @@ describe('useCombinedTokensList', () => { // done loading expect(result.current.isLoading).toBe(false) - expect(result.current.data).toHaveLength(16) + expect(result.current.data).toHaveLength(17) }) }) diff --git a/components/brave_wallet_ui/common/slices/api.slice.extra.ts b/components/brave_wallet_ui/common/slices/api.slice.extra.ts index 0cf624096bfb..5c581c71177c 100644 --- a/components/brave_wallet_ui/common/slices/api.slice.extra.ts +++ b/components/brave_wallet_ui/common/slices/api.slice.extra.ts @@ -35,7 +35,8 @@ import { selectCombinedTokensList } from '../slices/entities/blockchain-token.entity' import { - findAccountByAccountId + findAccountByAccountId, + findAccountByAddress } from '../../utils/account-utils' import { getCoinFromTxDataUnion } from '../../utils/network-utils' import { selectPendingTransactions } from './entities/transaction.entity' @@ -58,13 +59,28 @@ export const useAccountQuery = ( }) } +export const useAccountFromAddressQuery = ( + address: string | undefined | typeof skipToken +) => { + const skip = address === undefined || address === skipToken + return useGetAccountInfosRegistryQuery(skip ? skipToken : undefined, { + skip: skip, + selectFromResult: (res) => ({ + isLoading: res.isLoading, + error: res.error, + account: + res.data && !skip ? findAccountByAddress(address, res.data) : undefined + }) + }) +} + export const useSelectedAccountQuery = () => { const { data: accountInfosRegistry = accountInfoEntityAdaptorInitialState, - isLoading: isLoadingAccounts + isFetching: isLoadingAccounts } = useGetAccountInfosRegistryQuery(undefined) - const { data: selectedAccountId, isLoading: isLoadingSelectedAccountId } = + const { data: selectedAccountId, isFetching: isLoadingSelectedAccountId } = useGetSelectedAccountIdQuery(isLoadingAccounts ? skipToken : undefined) const selectedAccount = selectedAccountId diff --git a/components/brave_wallet_ui/common/slices/api.slice.ts b/components/brave_wallet_ui/common/slices/api.slice.ts index daa424d24fbb..ec3c43b0c29c 100644 --- a/components/brave_wallet_ui/common/slices/api.slice.ts +++ b/components/brave_wallet_ui/common/slices/api.slice.ts @@ -110,6 +110,7 @@ import { coingeckoEndpoints } from './endpoints/coingecko-endpoints' import { tokenSuggestionsEndpoints // } from './endpoints/token_suggestions.endpoints' +import { addressEndpoints } from './endpoints/address.endpoints' type GetAccountTokenCurrentBalanceArg = { accountId: BraveWallet.AccountId @@ -260,17 +261,25 @@ export function createWalletApi () { BraveWallet.AccountId, BraveWallet.AccountId >({ - queryFn: async (accountId, api, extraOptions, baseQuery) => { - const { - cache, - data: { keyringService } - } = baseQuery(undefined) + queryFn: async (accountId, { endpoint }, extraOptions, baseQuery) => { + try { + const { + cache, + data: { keyringService } + } = baseQuery(undefined) - await keyringService.setSelectedAccount(accountId) - cache.clearSelectedAccount() + await keyringService.setSelectedAccount(accountId) + cache.clearSelectedAccount() - return { - data: accountId + return { + data: accountId + } + } catch (error) { + return handleEndpointError( + endpoint, + `Failed to select account (${accountId})`, + error + ) } }, invalidatesTags: [ @@ -3094,6 +3103,8 @@ export function createWalletApi () { .injectEndpoints({ endpoints: tokenSuggestionsEndpoints }) // QR Code generator endpoints .injectEndpoints({ endpoints: qrCodeEndpoints }) + // ENS, SNS, UD Address endpoints + .injectEndpoints({ endpoints: addressEndpoints }) } export type WalletApi = ReturnType @@ -3111,17 +3122,21 @@ export const { useApproveTransactionMutation, useCancelTransactionMutation, useClosePanelUIMutation, + useEnableEnsOffchainLookupMutation, + useGenerateReceiveAddressMutation, useGetAccountInfosRegistryQuery, useGetAccountTokenCurrentBalanceQuery, useGetAddressByteCodeQuery, + useGetAddressFromNameServiceUrlQuery, useGetAutopinEnabledQuery, useGetBuyUrlQuery, useGetCoingeckoIdQuery, useGetCombinedTokenBalanceForAllAccountsQuery, useGetDefaultFiatCurrencyQuery, + useGetERC721MetadataQuery, + useGetEthAddressChecksumQuery, useGetEthTokenDecimalsQuery, useGetEthTokenSymbolQuery, - useGetERC721MetadataQuery, useGetEVMTransactionSimulationQuery, useGetExternalRewardsWalletQuery, useGetFVMAddressQuery, @@ -3129,6 +3144,7 @@ export const { useGetHardwareAccountDiscoveryBalanceQuery, useGetIpfsGatewayTranslatedNftUrlQuery, useGetIPFSUrlFromGatewayLikeUrlQuery, + useGetIsBase58EncodedSolPubkeyQuery, useGetIsTxSimulationOptInStatusQuery, useGetLocalIpfsNodeStatusQuery, useGetNetworksRegistryQuery, @@ -3197,8 +3213,10 @@ export const { useRemoveUserTokenMutation, useReportActiveWalletsToP3AMutation, useRetryTransactionMutation, + useSendBtcTransactionMutation, useSendERC20TransferMutation, useSendERC721TransferFromMutation, + useSendETHFilForwarderTransferMutation, useSendEthTransactionMutation, useSendFilTransactionMutation, useSendSolTransactionMutation, @@ -3220,7 +3238,6 @@ export const { useUpdateUnapprovedTransactionSpendAllowanceMutation, useUpdateUserAssetVisibleMutation, useUpdateUserTokenMutation, - useGenerateReceiveAddressMutation, } = walletApi // Derived Data Queries diff --git a/components/brave_wallet_ui/common/slices/endpoints/address.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/address.endpoints.ts new file mode 100644 index 000000000000..a69e39c933f1 --- /dev/null +++ b/components/brave_wallet_ui/common/slices/endpoints/address.endpoints.ts @@ -0,0 +1,181 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +// types +import { BraveWallet } from '../../../constants/types' +import { WalletApiEndpointBuilderParams } from '../api-base.slice' + +// utils +import { handleEndpointError } from '../../../utils/api-utils' +import { endsWithAny } from '../../../utils/string-utils' +import { + allSupportedExtensions, + supportedENSExtensions, + supportedSNSExtensions, + supportedUDExtensions +} from '../../constants/domain-extensions' + +export const addressEndpoints = ({ + mutation, + query +}: WalletApiEndpointBuilderParams) => { + return { + enableEnsOffchainLookup: mutation({ + queryFn: async (_arg, { endpoint }, extraOptions, baseQuery) => { + try { + const { data: api } = baseQuery(undefined) + api.jsonRpcService.setEnsOffchainLookupResolveMethod( + BraveWallet.ResolveMethod.kEnabled + ) + return { + data: true + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Failed to enable Ens Off-chain Lookup', + error + ) + } + }, + invalidatesTags: ['NameServiceAddress', 'EnsOffchainLookupEnabled'] + }), + + getIsBase58EncodedSolPubkey: query({ + queryFn: async (pubKeyArg, { endpoint }, _extra, baseQuery) => { + try { + const { data: api } = baseQuery(undefined) + const { result } = + await api.braveWalletService.isBase58EncodedSolanaPubkey(pubKeyArg) + + return { + data: result + } + } catch (error) { + return handleEndpointError( + endpoint, + `Failed to check Base58 encoding for pubkey: ${pubKeyArg}`, + error + ) + } + } + }), + + getEthAddressChecksum: query({ + queryFn: async (addressArg, { endpoint }, _extra, baseQuery) => { + try { + const { data: api } = baseQuery(undefined) + const { checksumAddress } = + await api.keyringService.getChecksumEthAddress(addressArg) + + return { + data: checksumAddress + } + } catch (error) { + return handleEndpointError( + endpoint, + `Failed to check Base58 encoding for pubkey: ${addressArg}`, + error + ) + } + } + }), + + getAddressFromNameServiceUrl: query< + { address: string; requireOffchainConsent: boolean }, + { url: string; tokenId: string | null } + >({ + queryFn: async (arg, { endpoint }, _extra, baseQuery) => { + try { + const { data: api, cache } = baseQuery(undefined) + + // Ens + if (endsWithAny(supportedENSExtensions, arg.url)) { + const { address, errorMessage, requireOffchainConsent } = + await api.jsonRpcService.ensGetEthAddr(arg.url) + + if (errorMessage) { + throw new Error(errorMessage) + } + + return { + data: { + address, + requireOffchainConsent + } + } + } + + // Sns + if (endsWithAny(supportedSNSExtensions, arg.url)) { + const { address, errorMessage } = + await api.jsonRpcService.snsGetSolAddr(arg.url) + + if (errorMessage) { + throw new Error(errorMessage) + } + + return { + data: { + address, + requireOffchainConsent: false + } + } + } + + // Unstoppable-Domains + if (endsWithAny(supportedUDExtensions, arg.url)) { + const token = arg.tokenId + ? (await cache.getUserTokensRegistry()).entities[arg.tokenId] || + null + : null + + const { address, errorMessage } = + await api.jsonRpcService.unstoppableDomainsGetWalletAddr( + arg.url, + token + ) + + if (errorMessage) { + throw new Error(errorMessage) + } + + return { + data: { + address, + requireOffchainConsent: false + } + } + } + + throw new Error( + `${ + arg.url + } does not end in a valid extension (${allSupportedExtensions.join( + ', ' + )})` + ) + } catch (error) { + return handleEndpointError( + endpoint, + `Failed to lookup Address for domain URL: url: ${ + arg.url // + }, + tokenId: ${arg.tokenId}`, + error + ) + } + }, + providesTags: (res, err, arg) => [ + err + ? 'UNKNOWN_ERROR' + : { + type: 'NameServiceAddress', + id: [arg.url, arg.tokenId].filter((arg) => arg !== null).join('-') + } + ] + }) + } +} diff --git a/components/brave_wallet_ui/components/extension/confirm-transaction-panel/common/evm_state_changes.tsx b/components/brave_wallet_ui/components/extension/confirm-transaction-panel/common/evm_state_changes.tsx index 81b312a65cc3..2c2ea6fe7668 100644 --- a/components/brave_wallet_ui/components/extension/confirm-transaction-panel/common/evm_state_changes.tsx +++ b/components/brave_wallet_ui/components/extension/confirm-transaction-panel/common/evm_state_changes.tsx @@ -81,7 +81,9 @@ export const EvmNativeAssetOrErc20TokenTransfer = ({ const normalizedAmount = new Amount(transfer.amount.after) .minus(transfer.amount.before) .divideByDecimals(transfer.asset.decimals) + const isReceive = normalizedAmount.isPositive() + const isNativeAsset = asset.contractAddress === NATIVE_EVM_ASSET_CONTRACT_ADDRESS || asset.contractAddress === '' diff --git a/components/brave_wallet_ui/page/container.tsx b/components/brave_wallet_ui/page/container.tsx index 3f64c3822916..db174fc4ac0b 100644 --- a/components/brave_wallet_ui/page/container.tsx +++ b/components/brave_wallet_ui/page/container.tsx @@ -58,7 +58,7 @@ import { OnboardingSuccess } from './screens/onboarding/onboarding-success/onboa import { DepositFundsScreen } from './screens/fund-wallet/deposit-funds' import { RestoreWallet } from './screens/restore-wallet/restore-wallet' import { Swap } from './screens/swap/swap' -import { SendScreen } from './screens/send/send-page/send-screen' +import { SendScreen } from './screens/send/send_screen/send_screen' import { DevBitcoin } from './screens/dev-bitcoin/dev-bitcoin' import { WalletPageWrapper diff --git a/components/brave_wallet_ui/page/screens/send/android/send.tsx b/components/brave_wallet_ui/page/screens/send/android/send.tsx index 1edb4b86ec26..0114209d6129 100644 --- a/components/brave_wallet_ui/page/screens/send/android/send.tsx +++ b/components/brave_wallet_ui/page/screens/send/android/send.tsx @@ -26,7 +26,7 @@ import * as WalletActions from '../../../../common/actions/wallet_actions' import { store } from '../../../store' import BraveCoreThemeProvider from '../../../../../common/BraveCoreThemeProvider' -import { SendScreen } from '../send-page/send-screen' +import { SendScreen } from '../send_screen/send_screen' import { LibContext } from '../../../../common/context/lib.context' export function AndroidSendApp() { diff --git a/components/brave_wallet_ui/page/screens/send/components/select-token-modal/select-token-modal.tsx b/components/brave_wallet_ui/page/screens/send/components/select-token-modal/select-token-modal.tsx index 75d582bfb727..63e4e932311c 100644 --- a/components/brave_wallet_ui/page/screens/send/components/select-token-modal/select-token-modal.tsx +++ b/components/brave_wallet_ui/page/screens/send/components/select-token-modal/select-token-modal.tsx @@ -7,8 +7,12 @@ import * as React from 'react' import { skipToken } from '@reduxjs/toolkit/query/react' // Selectors -import { WalletSelectors } from '../../../../../common/selectors' -import { useUnsafeWalletSelector } from '../../../../../common/hooks/use-safe-selector' +import { + selectAllVisibleUserAssetsFromQueryResult // +} from '../../../../../common/slices/entities/blockchain-token.entity' +import { + selectAllAccountInfosFromQuery // +} from '../../../../../common/slices/entities/account-info.entity' // Types import { @@ -29,6 +33,7 @@ import Amount from '../../../../../utils/amount' import { getBalance } from '../../../../../utils/balance-utils' +import { getAssetIdKey } from '../../../../../utils/asset-utils' // Queries import { @@ -37,6 +42,8 @@ import { useSetNetworkMutation, useGetTokenSpotPricesQuery, useSetSelectedAccountMutation, + useGetUserTokensRegistryQuery, + useGetAccountInfosRegistryQuery, } from '../../../../../common/slices/api.slice' import { querySubscriptionOptions60s @@ -77,10 +84,6 @@ export const SelectTokenModal = React.forwardRef( (props: Props, forwardedRef) => { const { onClose, selectedSendOption, selectSendAsset } = props - // Wallet Selectors - const accounts = useUnsafeWalletSelector(WalletSelectors.accounts) - const userVisibleTokensInfo = useUnsafeWalletSelector(WalletSelectors.userVisibleTokensInfo) - // State const [searchValue, setSearchValue] = React.useState('') const [showNetworkDropDown, setShowNetworkDropDown] = React.useState(false) @@ -91,6 +94,17 @@ export const SelectTokenModal = React.forwardRef( const [setNetwork] = useSetNetworkMutation() const [setSelectedAccount] = useSetSelectedAccountMutation() const { data: networks } = useGetVisibleNetworksQuery() + const { accounts } = useGetAccountInfosRegistryQuery(undefined, { + selectFromResult: (res) => ({ + accounts: selectAllAccountInfosFromQuery(res) + }) + }) + + const { userVisibleTokensInfo } = useGetUserTokensRegistryQuery(undefined, { + selectFromResult: result => ({ + userVisibleTokensInfo: selectAllVisibleUserAssetsFromQueryResult(result) + }) + }) // Methods const getTokenListByAccount = React.useCallback( @@ -278,13 +292,6 @@ export const SelectTokenModal = React.forwardRef( ).flat(1).length === 0 }, [accounts, getTokensBySearchValue, isLoadingBalances]) - const modalTitle = React.useMemo(() => { - if (selectedSendOption === SendPageTabHashes.nft) { - return getLocale('braveWalletSendTabSelectNFTTitle') - } - return getLocale('braveWalletSendTabSelectTokenTitle') - }, [selectedSendOption]) - const tokensByAccount = React.useMemo(() => { if (isLoadingBalances) { return ( @@ -311,49 +318,49 @@ export const SelectTokenModal = React.forwardRef( {getLocale('braveWalletNoAvailableTokens')} } + return accounts.map((account) => - getTokensBySearchValue(account).length > 0 && - - - 0 ? ( + + - {account.name} - - {selectedSendOption === SendPageTabHashes.token && - - {getAccountFiatValue(account)} + + {account.name} - } - - - - {getTokensBySearchValue(account).map((token) => - onSelectSendAsset(token, account)} - key={`${token.contractAddress}-${token.chainId}-${token.tokenId}`} - balance={getBalance(account.accountId, token, tokenBalancesRegistry)} - spotPrice={ - spotPriceRegistry - ? getTokenPriceAmountFromRegistry(spotPriceRegistry, token) - .format() - : '' - } - /> - )} + {selectedSendOption === SendPageTabHashes.token && ( + + {getAccountFiatValue(account)} + + )} + + + + {getTokensBySearchValue(account).map((token) => ( + onSelectSendAsset(token, account)} + key={getAssetIdKey(token)} + balance={getBalance( + account.accountId, + token, + tokenBalancesRegistry + )} + spotPrice={ + spotPriceRegistry + ? getTokenPriceAmountFromRegistry( + spotPriceRegistry, + token + ).format() + : '' + } + /> + ))} + - + ) : null ) }, [ accounts, @@ -366,6 +373,13 @@ export const SelectTokenModal = React.forwardRef( isLoadingBalances ]) + // computed + const modalTitle = getLocale( + selectedSendOption === SendPageTabHashes.nft + ? 'braveWalletSendTabSelectNFTTitle' + : 'braveWalletSendTabSelectTokenTitle' + ) + // render return ( diff --git a/components/brave_wallet_ui/page/screens/send/send-page/send-screen.tsx b/components/brave_wallet_ui/page/screens/send/send-page/send-screen.tsx deleted file mode 100644 index d253476bdcee..000000000000 --- a/components/brave_wallet_ui/page/screens/send/send-page/send-screen.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// you can obtain one at https://mozilla.org/MPL/2.0/. - -import * as React from 'react' - -// Hooks -import { useOnClickOutside } from '../../../../common/hooks/useOnClickOutside' - -// Components -import { Send } from '../send/send' - -interface Props { - isAndroid?: boolean -} - -export const SendScreen = ({ isAndroid }: Props) => { - // State - const [showSelectTokenModal, setShowSelectTokenModal] = React.useState(false) - - // Refs - const selectTokenModalRef = React.useRef(null) - - // Hooks - useOnClickOutside( - selectTokenModalRef, - () => setShowSelectTokenModal(false), - showSelectTokenModal - ) - - // render - return ( - <> - setShowSelectTokenModal(true)} - onHideSelectTokenModal={() => setShowSelectTokenModal(false)} - showSelectTokenModal={showSelectTokenModal} - selectTokenModalRef={selectTokenModalRef} - isAndroid={isAndroid} - /> - - ) -} - -export default SendScreen diff --git a/components/brave_wallet_ui/page/screens/send/send.stories.tsx b/components/brave_wallet_ui/page/screens/send/send.stories.tsx index ac211f4fa2fa..e3c5a1b47180 100644 --- a/components/brave_wallet_ui/page/screens/send/send.stories.tsx +++ b/components/brave_wallet_ui/page/screens/send/send.stories.tsx @@ -5,12 +5,30 @@ import * as React from 'react' -import WalletPageStory from '../../../stories/wrappers/wallet-page-story-wrapper' -import { SendScreen } from './send-page/send-screen' +import { + WalletPageStory // +} from '../../../stories/wrappers/wallet-page-story-wrapper' +import { SendScreen } from './send_screen/send_screen' + +// mocks +import { + mockAccount, + mockNativeBalanceRegistry, + mockTokenBalanceRegistry +} from '../../../common/constants/mocks' export const _SendScreen = () => { return ( - + ) diff --git a/components/brave_wallet_ui/page/screens/send/send/send.tsx b/components/brave_wallet_ui/page/screens/send/send/send.tsx deleted file mode 100644 index eca47995fa13..000000000000 --- a/components/brave_wallet_ui/page/screens/send/send/send.tsx +++ /dev/null @@ -1,664 +0,0 @@ -// Copyright (c) 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// you can obtain one at https://mozilla.org/MPL/2.0/. - -import * as React from 'react' -import { skipToken } from '@reduxjs/toolkit/query/react' -import { - useParams, - useHistory, - useLocation -} from 'react-router' - -// Messages -import { ENSOffchainLookupMessage, FEVMAddressConvertionMessage, FailedChecksumMessage } from '../send-ui-messages' - -// Types -import { - SendPageTabHashes, - AddressMessageInfo, - WalletRoutes -} from '../../../../constants/types' - -// Selectors -import { WalletSelectors } from '../../../../common/selectors' -import { useUnsafeWalletSelector } from '../../../../common/hooks/use-safe-selector' - -// Constants -import { allSupportedExtensions } from '../../../../common/constants/domain-extensions' - -// Utils -import { getLocale } from '../../../../../common/locale' -import Amount from '../../../../utils/amount' -import { getBalance, formatTokenBalanceWithSymbol, getPercentAmount } from '../../../../utils/balance-utils' -import { computeFiatAmount } from '../../../../utils/pricing-utils' -import { endsWithAny } from '../../../../utils/string-utils' -import { getPriceIdForToken } from '../../../../utils/api-utils' - -// Hooks -import { useSend } from '../../../../common/hooks/send' -import { - useScopedBalanceUpdater -} from '../../../../common/hooks/use-scoped-balance-updater' -import { useOnClickOutside } from '../../../../common/hooks/useOnClickOutside' -import { - useGetDefaultFiatCurrencyQuery, - useSetSelectedAccountMutation, - useSetNetworkMutation, - useGetTokenSpotPricesQuery, - useGetUserTokensRegistryQuery -} from '../../../../common/slices/api.slice' -import { useSelectedAccountQuery } from '../../../../common/slices/api.slice.extra' -import { - querySubscriptionOptions60s -} from '../../../../common/slices/constants' -import { - selectAllVisibleUserAssetsFromQueryResult -} from '../../../../common/slices/entities/blockchain-token.entity' - -// Styled Components -import { - SendContainer, - SectionBox, - AddressInput, - AmountInput, - DIVForWidth, - InputRow, - DomainLoadIcon, - sendContainerWidth -} from './send.style' -import { Column, Text, Row, HorizontalDivider } from '../shared.styles' - -// Components -import { SelectSendOptionButton } from '../components/select-send-option-button/select-send-option-button' -import { StandardButton } from '../components/standard-button/standard-button' -import { SelectTokenButton } from '../components/select-token-button/select-token-button' -import { PresetButton } from '../components/preset-button/preset-button' -import { AccountSelector } from '../components/account-selector/account-selector' -import { AddressMessage } from '../components/address-message/address-message' -import { SelectTokenModal } from '../components/select-token-modal/select-token-modal' -import { CopyAddress } from '../components/copy-address/copy-address' -import { ChecksumInfoModal } from '../components/checksum-info-modal/checksum-info-modal' -import WalletPageWrapper from '../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper' -import { PageTitleHeader } from '../../../../components/desktop/card-headers/page-title-header' - -interface Props { - onShowSelectTokenModal: () => void - onHideSelectTokenModal: () => void - selectTokenModalRef: React.RefObject - showSelectTokenModal: boolean - isAndroid?: boolean -} - -export const Send = (props: Props) => { - const { - onShowSelectTokenModal, - onHideSelectTokenModal, - selectTokenModalRef, - showSelectTokenModal, - isAndroid - } = props - - // Wallet Selectors - const accounts = useUnsafeWalletSelector(WalletSelectors.accounts) - - // routing - const { - chainId, - accountAddress, - contractAddressOrSymbol, - tokenId - } = useParams<{ - chainId?: string - accountAddress?: string - contractAddressOrSymbol?: string - tokenId?: string - }>() - const { hash } = useLocation() - const history = useHistory() - - const { - toAddressOrUrl, - toAddress, - enableEnsOffchainLookup, - showEnsOffchainWarning, - setShowEnsOffchainWarning, - showFilecoinFEVMWarning, - fevmTranslatedAddresses, - addressError, - addressWarning, - sendAmount, - selectedSendAsset, - sendAmountValidationError, - setSendAmount, - updateToAddressOrUrl, - submitSend, - selectSendAsset, - searchingForDomain, - processAddressOrUrl - } = useSend() - - // Queries & Mutations - const { data: defaultFiatCurrency } = useGetDefaultFiatCurrencyQuery() - const [setSelectedAccount] = useSetSelectedAccountMutation() - const [setNetwork] = useSetNetworkMutation() - const { data: selectedAccount } = useSelectedAccountQuery() - - // Refs - const checksumInfoModalRef = React.useRef(null) - - // State - const [domainPosition, setDomainPosition] = React.useState(0) - const [showChecksumInfoModal, setShowChecksumInfoModal] = React.useState(false) - - // Constants - const selectedSendOption = hash - ? hash as SendPageTabHashes - : '#token' as SendPageTabHashes - - useOnClickOutside( - checksumInfoModalRef, - () => setShowChecksumInfoModal(false), - showChecksumInfoModal - ) - - // Methods - const handleInputAmountChange = React.useCallback( - (event: React.ChangeEvent) => { - setSendAmount(event.target.value) - }, - [] - ) - - const handleInputAddressChange = React.useCallback( - (event: React.ChangeEvent) => { - updateToAddressOrUrl(event.target.value) - }, - [updateToAddressOrUrl] - ) - - const onSelectSendOption = React.useCallback( - (option: SendPageTabHashes) => { - selectSendAsset(undefined) - history.push(`${WalletRoutes.SendPageStart}${option}`) - }, [selectSendAsset]) - - const onClickReviewOrENSConsent = React.useCallback(() => { - if (showEnsOffchainWarning) { - enableEnsOffchainLookup() - setShowEnsOffchainWarning(false) - processAddressOrUrl(toAddressOrUrl) - return - } - submitSend() - }, [ - showEnsOffchainWarning, - setShowEnsOffchainWarning, - submitSend, - enableEnsOffchainLookup, - processAddressOrUrl, - toAddressOrUrl - ]) - - const updateLoadingIconPosition = React.useCallback((ref: HTMLDivElement | null) => { - const position = ref?.clientWidth - setDomainPosition(position ? position + 22 : 0) - }, []) - - const { - data: tokenBalancesRegistry, - isLoading: isLoadingBalances, - } = useScopedBalanceUpdater( - selectedAccount && selectedSendAsset - ? { - network: { - chainId: selectedSendAsset.chainId, - coin: selectedAccount.accountId.coin - }, - accounts: [selectedAccount], - tokens: [selectedSendAsset] - } - : skipToken - ) - - const setPresetAmountValue = React.useCallback((percent: number) => { - if (!selectedSendAsset || !selectedAccount) { - return - } - - setSendAmount( - getPercentAmount( - selectedSendAsset, - selectedAccount.accountId, - percent, - tokenBalancesRegistry - ) - ) - }, [setSendAmount, selectedSendAsset, selectedAccount, tokenBalancesRegistry]) - - const sendAssetBalance = React.useMemo(() => { - if (!selectedAccount || !selectedSendAsset || !tokenBalancesRegistry) { - return '' - } - - return getBalance( - selectedAccount.accountId, - selectedSendAsset, - tokenBalancesRegistry - ) - }, [selectedAccount, selectedSendAsset, tokenBalancesRegistry]) - - const accountNameAndBalance = React.useMemo(() => { - if (!selectedSendAsset || sendAssetBalance === '') { - return '' - } - if (selectedSendOption === SendPageTabHashes.nft) { - return selectedAccount?.name - } - return `${selectedAccount?.name}: ${formatTokenBalanceWithSymbol( - sendAssetBalance, - selectedSendAsset.decimals, - selectedSendAsset.symbol, - 4 - )}` - }, [ - selectedAccount?.name, - selectedSendAsset, - sendAssetBalance - ]) - - const insufficientFundsError = React.useMemo((): boolean => { - if (!selectedSendAsset) { - return false - } - - const amountWei = new Amount(sendAmount).multiplyByDecimals( - selectedSendAsset.decimals - ) - - if (amountWei.isZero()) { - return false - } - - return amountWei.gt(sendAssetBalance) - }, [sendAssetBalance, sendAmount, selectedSendAsset]) - - const tokenPriceIds = React.useMemo(() => - selectedSendAsset - ? [getPriceIdForToken(selectedSendAsset)] - : [], - [selectedSendAsset] - ) - - const { - data: spotPriceRegistry - } = useGetTokenSpotPricesQuery( - !isLoadingBalances && tokenPriceIds.length && defaultFiatCurrency - ? { ids: tokenPriceIds, toCurrency: defaultFiatCurrency } - : skipToken, - querySubscriptionOptions60s - ) - - const sendAmountFiatValue = React.useMemo(() => { - if ( - !selectedSendAsset || - sendAssetBalance === '' || - selectedSendOption === SendPageTabHashes.nft - ) { - return '' - } - - return computeFiatAmount({ - spotPriceRegistry, - value: new Amount(sendAmount !== '' ? sendAmount : '0') - .multiplyByDecimals(selectedSendAsset.decimals) // ETH → Wei conversion - .toHex(), - token: selectedSendAsset, - }).formatAsFiat(defaultFiatCurrency) - }, [ - spotPriceRegistry, - selectedSendAsset, - sendAmount, - defaultFiatCurrency, - sendAssetBalance, - selectedSendOption - ]) - - const reviewButtonText = React.useMemo(() => { - return showEnsOffchainWarning - ? getLocale('braveWalletEnsOffChainButton') - : searchingForDomain - ? getLocale('braveWalletSearchingForDomain') - : sendAmountValidationError - ? getLocale('braveWalletDecimalPlacesError') - : insufficientFundsError - ? getLocale('braveWalletNotEnoughFunds') - : ( - addressError !== undefined && - addressError !== '' && - addressError !== getLocale('braveWalletNotValidChecksumAddressError') - ) - ? addressError - : ( - addressWarning !== undefined && - addressWarning !== '' && - addressWarning !== getLocale('braveWalletAddressMissingChecksumInfoWarning') - ) - ? addressWarning - : getLocale('braveWalletReviewSend') - }, [insufficientFundsError, addressError, addressWarning, sendAmountValidationError, searchingForDomain, showEnsOffchainWarning]) - - const isReviewButtonDisabled = React.useMemo(() => { - // We only need to check if showEnsOffchainWarning is true here to return - // false early before any other checks are made. This is to allow the button - // to be pressed to enable offchain lookup. - return !showEnsOffchainWarning && - (searchingForDomain || - toAddressOrUrl === '' || - parseFloat(sendAmount) === 0 || - sendAmount === '' || - insufficientFundsError || - (addressError !== undefined && addressError !== '') || - sendAmountValidationError !== undefined) - }, - [ - toAddressOrUrl, - sendAmount, - insufficientFundsError, - addressError, - sendAmountValidationError, - searchingForDomain, - showEnsOffchainWarning - ] - ) - - const reviewButtonHasError = React.useMemo(() => { - return searchingForDomain - ? false - : insufficientFundsError || - (addressError !== undefined && - addressError !== '' && - addressError !== getLocale('braveWalletNotValidChecksumAddressError')) - }, [searchingForDomain, insufficientFundsError, addressError]) - - const hasAddressError = React.useMemo(() => { - return searchingForDomain - ? false - : !!addressError - }, [searchingForDomain, addressError]) - - const addressMessageInformation: AddressMessageInfo | undefined = React.useMemo(() => { - if (showFilecoinFEVMWarning) { - return { - ...FEVMAddressConvertionMessage, - placeholder: fevmTranslatedAddresses?.[toAddressOrUrl] - } - } - if (showEnsOffchainWarning) { - return ENSOffchainLookupMessage - } - if (addressError === getLocale('braveWalletNotValidChecksumAddressError')) { - return { ...FailedChecksumMessage, type: 'error' } - } - if (addressWarning === getLocale('braveWalletAddressMissingChecksumInfoWarning')) { - return { ...FailedChecksumMessage, type: 'warning' } - } - return undefined - }, [toAddressOrUrl, showFilecoinFEVMWarning, fevmTranslatedAddresses, - showEnsOffchainWarning, addressError, addressWarning]) - - const showResolvedDomain = React.useMemo(() => { - return (addressError === undefined || - addressError === '' || - addressError === getLocale('braveWalletSameAddressError')) && - toAddress && - endsWithAny(allSupportedExtensions, toAddressOrUrl.toLowerCase()) - }, [addressError, toAddress, toAddressOrUrl]) - - const showSearchingForDomainIcon = React.useMemo(() => { - return (endsWithAny(allSupportedExtensions, toAddressOrUrl.toLowerCase()) && searchingForDomain) || - showEnsOffchainWarning - }, [toAddressOrUrl, searchingForDomain, showEnsOffchainWarning]) - - const { userVisibleTokensInfo } = useGetUserTokensRegistryQuery(undefined, { - selectFromResult: result => ({ - userVisibleTokensInfo: selectAllVisibleUserAssetsFromQueryResult(result) - }) - }) - - const selectedAssetFromParams = React.useMemo(() => { - if (!contractAddressOrSymbol || !chainId) return - - return userVisibleTokensInfo.find(token => - tokenId - ? token.chainId === chainId && - token.contractAddress.toLowerCase() === - contractAddressOrSymbol.toLowerCase() && - token.tokenId === tokenId - : ( - token.contractAddress.toLowerCase() === - contractAddressOrSymbol.toLowerCase() && - token.chainId === chainId) || - ( - token.symbol.toLowerCase() === - contractAddressOrSymbol.toLowerCase() && - token.chainId === chainId && - token.contractAddress === '' - ) - ) - }, [ - userVisibleTokensInfo, - chainId, - contractAddressOrSymbol, - tokenId - ]) - - const accountFromParams = React.useMemo(() => { - return accounts.find( - (account) => account.address === accountAddress - ) - }, [accountAddress, accounts]) - - // Effects - React.useEffect(() => { - // check if the user has selected an asset - if (!chainId || !selectedAssetFromParams || !accountFromParams || selectedSendAsset) return - - ;(async () => { - try { - await setSelectedAccount(accountFromParams.accountId) - await setNetwork({ - chainId: chainId, - coin: selectedAssetFromParams.coin - }) - } catch (e) { - console.error(e) - } - })() - - selectSendAsset(selectedAssetFromParams) - }, [ - selectSendAsset, - selectedSendAsset, - chainId, - selectedAssetFromParams, - accountFromParams - ]) - - // render - return ( - <> - - } - > - - - - - - {selectedSendOption === SendPageTabHashes.token && - - - - {accountNameAndBalance} - - - - - - { - selectedSendOption === SendPageTabHashes.token && - selectedSendAsset && - <> - - setPresetAmountValue(0.5)} /> - setPresetAmountValue(1)} /> - - } - - {selectedSendOption === SendPageTabHashes.token && - - } - - - - {sendAmountFiatValue} - - - - } - {selectedSendOption === SendPageTabHashes.nft && - - - - - - - {accountNameAndBalance} - - - - } - - - - {showSearchingForDomainIcon && - - } - updateLoadingIconPosition(ref)}>{toAddressOrUrl} - - - - {showResolvedDomain && - - } - {addressMessageInformation && - setShowChecksumInfoModal(true) - : undefined - } - /> - } - - - - - {showSelectTokenModal && - - } - {showChecksumInfoModal && - setShowChecksumInfoModal(false)} - ref={checksumInfoModalRef} - /> - } - - ) -} - -export default Send diff --git a/components/brave_wallet_ui/page/screens/send/send/send.style.ts b/components/brave_wallet_ui/page/screens/send/send_screen/send.style.ts similarity index 80% rename from components/brave_wallet_ui/page/screens/send/send/send.style.ts rename to components/brave_wallet_ui/page/screens/send/send_screen/send.style.ts index c451f21ebf4b..aa997aabb0f8 100644 --- a/components/brave_wallet_ui/page/screens/send/send/send.style.ts +++ b/components/brave_wallet_ui/page/screens/send/send_screen/send.style.ts @@ -1,9 +1,10 @@ // Copyright (c) 2022 The Brave Authors. All rights reserved. // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, -// you can obtain one at https://mozilla.org/MPL/2.0/. +// You can obtain one at https://mozilla.org/MPL/2.0/. import styled from 'styled-components' +import ProgressRing from '@brave/leo/react/progressRing' // Assets import { LoaderIcon } from 'brave-ui/components/icons' @@ -20,7 +21,7 @@ export const SendContainer = styled(StyledDiv)` position: relative; ` -export const SectionBox = styled(StyledDiv) <{ +export const SectionBox = styled(StyledDiv)<{ hasError?: boolean hasWarning?: boolean minHeight?: number @@ -28,12 +29,17 @@ export const SectionBox = styled(StyledDiv) <{ boxDirection?: 'row' | 'column' }>` background-color: ${(p) => p.theme.color.background02}; - flex-direction: ${(p) => p.boxDirection ? p.boxDirection : 'column'}; + flex-direction: ${(p) => (p.boxDirection ? p.boxDirection : 'column')}; box-sizing: border-box; border-radius: 16px; border: 1px solid - ${(p) => (p.hasError ? p.theme.color.errorBorder : p.hasWarning ? p.theme.color.warningBorder : p.theme.color.divider01)}; - padding: ${(p) => p.noPadding ? '0px' : '16px 16px 16px 8px'}; + ${(p) => + p.hasError + ? p.theme.color.errorBorder + : p.hasWarning + ? p.theme.color.warningBorder + : p.theme.color.divider01}; + padding: ${(p) => (p.noPadding ? '0px' : '16px 16px 16px 8px')}; width: 100%; position: relative; margin-bottom: 16px; @@ -99,3 +105,7 @@ export const DomainLoadIcon = styled(LoaderIcon) <{ position: number }>` z-index: 8; left: ${(p) => p.position}px; ` + +export const SmallLoadingRing = styled(ProgressRing)` + --leo-progressring-size: 14px; +` diff --git a/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx b/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx new file mode 100644 index 000000000000..12609ff311f1 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx @@ -0,0 +1,1213 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +/* eslint-disable @typescript-eslint/key-spacing */ +import * as React from 'react' +import { skipToken } from '@reduxjs/toolkit/query/react' +import { useParams, useHistory, useLocation } from 'react-router' + +// Messages +import { + ENSOffchainLookupMessage, + FEVMAddressConvertionMessage, + FailedChecksumMessage +} from '../send-ui-messages' + +// Types +import { + SendPageTabHashes, + AddressMessageInfo, + WalletRoutes, + CoinTypesMap, + BraveWallet, + BaseTransactionParams, + AmountValidationErrorType +} from '../../../../constants/types' + +// Utils +import { getLocale } from '../../../../../common/locale' +import Amount from '../../../../utils/amount' +import { + getBalance, + formatTokenBalanceWithSymbol, + getPercentAmount +} from '../../../../utils/balance-utils' +import { computeFiatAmount } from '../../../../utils/pricing-utils' +import { + findTokenByContractAddress, + getAssetIdKey +} from '../../../../utils/asset-utils' +import { endsWithAny } from '../../../../utils/string-utils' +import { + supportedENSExtensions, + supportedSNSExtensions, + supportedUDExtensions +} from '../../../../common/constants/domain-extensions' +import { getPriceIdForToken } from '../../../../utils/api-utils' +import { + isValidEVMAddress, + isValidFilAddress +} from '../../../../utils/address-utils' +import { + selectAllVisibleUserAssetsFromQueryResult // +} from '../../../../common/slices/entities/blockchain-token.entity' + +// Hooks +import { + useScopedBalanceUpdater +} from '../../../../common/hooks/use-scoped-balance-updater' +import { useModal } from '../../../../common/hooks/useOnClickOutside' +import { + useGetDefaultFiatCurrencyQuery, + useSetSelectedAccountMutation, + useSetNetworkMutation, + useGetTokenSpotPricesQuery, + useGetUserTokensRegistryQuery, + useGetSelectedChainQuery, + useEnableEnsOffchainLookupMutation, + useGetFVMAddressQuery, + useGetEthAddressChecksumQuery, + useGetIsBase58EncodedSolPubkeyQuery, + useSendSPLTransferMutation, + useSendTransactionMutation, + useSendERC20TransferMutation, + useSendERC721TransferFromMutation, + useSendETHFilForwarderTransferMutation, + useGetAddressFromNameServiceUrlQuery, +} from '../../../../common/slices/api.slice' +import { + useAccountFromAddressQuery, + useGetCombinedTokensListQuery, + useSelectedAccountQuery +} from '../../../../common/slices/api.slice.extra' +import { + querySubscriptionOptions60s // +} from '../../../../common/slices/constants' + +// Styled Components +import { + SendContainer, + SectionBox, + AddressInput, + AmountInput, + DIVForWidth, + InputRow, + sendContainerWidth, + SmallLoadingRing, + DomainLoadIcon +} from './send.style' +import { Column, Text, Row, HorizontalDivider } from '../shared.styles' + +// Components +import { + SelectSendOptionButton // +} from '../components/select-send-option-button/select-send-option-button' +import { StandardButton } from '../components/standard-button/standard-button' +import { + SelectTokenButton // +} from '../components/select-token-button/select-token-button' +import { PresetButton } from '../components/preset-button/preset-button' +import { + AccountSelector // +} from '../components/account-selector/account-selector' +import { AddressMessage } from '../components/address-message/address-message' +import { + SelectTokenModal // +} from '../components/select-token-modal/select-token-modal' +import { CopyAddress } from '../components/copy-address/copy-address' +import { + ChecksumInfoModal // +} from '../components/checksum-info-modal/checksum-info-modal' +import { + WalletPageWrapper // +} from '../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper' +import { + PageTitleHeader // +} from '../../../../components/desktop/card-headers/page-title-header' + +interface Props { + isAndroid?: boolean +} + +const ErrorFailedChecksumMessage: AddressMessageInfo = { + ...FailedChecksumMessage, + type: 'error' +} + +const WarningFailedChecksumMessage: AddressMessageInfo = { + ...FailedChecksumMessage, + type: 'warning' +} + +export const SendScreen = React.memo((props: Props) => { + const { isAndroid = false } = props + + // routing + const { chainId, accountAddress, contractAddressOrSymbol, tokenId } = + useParams<{ + chainId?: string + accountAddress?: string + contractAddressOrSymbol?: string + tokenId?: string + }>() + const history = useHistory() + const { hash } = useLocation() + const selectedSendOption = (hash as SendPageTabHashes) || '#token' + + const { account: accountFromParams } = + useAccountFromAddressQuery(accountAddress) + + // Refs + const addressWidthRef = React.useRef(null) + + // State + const [sendAmount, setSendAmount] = React.useState('') + const [selectedSendAsset, setSelectedSendAsset] = React.useState< + BraveWallet.BlockchainToken | undefined + >(undefined) + const [toAddressOrUrl, setToAddressOrUrl] = React.useState('') + const trimmedToAddressOrUrl = toAddressOrUrl.trim() + + const [isOffChainEnsWarningDismissed, dismissOffchainEnsWarning] = + React.useState(false) + + const [domainPosition, setDomainPosition] = React.useState(0) + + // Mutations + const [enableEnsOffchainLookup] = useEnableEnsOffchainLookupMutation() + const [setNetwork] = useSetNetworkMutation() + const [setSelectedAccount] = useSetSelectedAccountMutation() + const [sendSPLTransfer] = useSendSPLTransferMutation() + const [sendTransaction] = useSendTransactionMutation() + const [sendERC20Transfer] = useSendERC20TransferMutation() + const [sendERC721TransferFrom] = useSendERC721TransferFromMutation() + const [sendETHFilForwarderTransfer] = useSendETHFilForwarderTransferMutation() + + // Queries + const { data: selectedNetwork } = useGetSelectedChainQuery() + const { data: selectedAccount, isLoading: isLoadingSelectedAccount } = + useSelectedAccountQuery() + + const { data: fullTokenList } = useGetCombinedTokensListQuery() + + const { userVisibleTokensInfo } = useGetUserTokensRegistryQuery(undefined, { + selectFromResult: result => ({ + userVisibleTokensInfo: selectAllVisibleUserAssetsFromQueryResult(result) + }) + }) + + const selectedAssetFromParams = React.useMemo(() => { + if (!contractAddressOrSymbol || !chainId) return + + const contractOrSymbolLower = contractAddressOrSymbol.toLowerCase() + + return userVisibleTokensInfo.find((token) => + tokenId + ? token.chainId === chainId && + token.contractAddress.toLowerCase() === contractOrSymbolLower && + token.tokenId === tokenId + : (token.contractAddress.toLowerCase() === contractOrSymbolLower && + token.chainId === chainId) || + (token.symbol.toLowerCase() === contractOrSymbolLower && + token.chainId === chainId && + token.contractAddress === '') + ) + }, [userVisibleTokensInfo, chainId, contractAddressOrSymbol, tokenId]) + + const { data: defaultFiatCurrency } = useGetDefaultFiatCurrencyQuery() + + const { data: tokenBalancesRegistry, isFetching: isLoadingBalances } = + useScopedBalanceUpdater( + selectedAccount && selectedSendAsset + ? { + network: { + chainId: selectedSendAsset.chainId, + coin: selectedAccount.accountId.coin + }, + accounts: [selectedAccount], + tokens: [selectedSendAsset] + } + : skipToken + ) + + const { data: spotPriceRegistry, isFetching: isLoadingSpotPrices } = + useGetTokenSpotPricesQuery( + !isLoadingBalances && selectedSendAsset && defaultFiatCurrency + ? { + ids: [getPriceIdForToken(selectedSendAsset)], + toCurrency: defaultFiatCurrency + } + : skipToken, + querySubscriptionOptions60s + ) + + // Domain name lookup Queries + const selectedSendAssetId = selectedSendAsset + ? getAssetIdKey(selectedSendAsset) + : null + + const lowerCaseToAddress = toAddressOrUrl.toLowerCase() + + const toAddressHasValidExtension = toAddressOrUrl + ? endsWithAny(supportedUDExtensions, lowerCaseToAddress) || + (selectedSendAsset?.coin === BraveWallet.CoinType.SOL && + endsWithAny(supportedSNSExtensions, lowerCaseToAddress)) || + (selectedSendAsset?.coin === BraveWallet.CoinType.ETH && + endsWithAny(supportedENSExtensions, lowerCaseToAddress)) + : false + + const { + data: nameServiceInfo, + isFetching: isSearchingForDomain, + isError: hasNameServiceError = false + } = useGetAddressFromNameServiceUrlQuery( + toAddressHasValidExtension + ? { + tokenId: selectedSendAssetId, + url: toAddressOrUrl + } + : skipToken + ) + + const resolvedDomainAddress = nameServiceInfo?.address || '' + const showEnsOffchainWarning = + nameServiceInfo?.requireOffchainConsent || false + + const { data: fevmTranslatedAddresses } = useGetFVMAddressQuery( + selectedSendAsset?.coin === BraveWallet.CoinType.FIL && + trimmedToAddressOrUrl + ? { + coin: selectedSendAsset.coin, + addresses: [trimmedToAddressOrUrl], + isMainNet: selectedSendAsset.chainId === BraveWallet.FILECOIN_MAINNET + } + : skipToken + ) + + const { data: isBase58 = false } = useGetIsBase58EncodedSolPubkeyQuery( + !toAddressHasValidExtension && + selectedAccount?.accountId.coin === BraveWallet.CoinType.SOL && + trimmedToAddressOrUrl + ? trimmedToAddressOrUrl + : skipToken + ) + + const isValidEvmAddress = isValidEVMAddress(trimmedToAddressOrUrl) + + const { data: ethAddressChecksum = '' } = useGetEthAddressChecksumQuery( + isValidEvmAddress ? trimmedToAddressOrUrl : skipToken + ) + + // memos & computed + const sendAmountValidationError: AmountValidationErrorType | undefined = + React.useMemo(() => { + if (!sendAmount || !selectedSendAsset) { + return + } + + // extract BigNumber object wrapped by Amount + const amountBN = ethToWeiAmount(sendAmount, selectedSendAsset).value + + const amountDP = amountBN && amountBN.decimalPlaces() + return amountDP && amountDP > 0 ? 'fromAmountDecimalsOverflow' : undefined + }, [sendAmount, selectedSendAsset]) + + const sendAssetBalance = + !selectedAccount || !selectedSendAsset || !tokenBalancesRegistry + ? '' + : getBalance( + selectedAccount.accountId, + selectedSendAsset, + tokenBalancesRegistry + ) + + const accountNameAndBalance = + !selectedSendAsset || sendAssetBalance === '' + ? '' + : selectedSendOption === SendPageTabHashes.nft + ? selectedAccount?.name + : `${selectedAccount?.name}: ${formatTokenBalanceWithSymbol( + sendAssetBalance, + selectedSendAsset.decimals, + selectedSendAsset.symbol, + 4 + )}` + + const insufficientFundsError = React.useMemo((): boolean => { + if (!selectedSendAsset) { + return false + } + + const amountWei = new Amount(sendAmount).multiplyByDecimals( + selectedSendAsset.decimals + ) + + if (amountWei.isZero()) { + return false + } + + return amountWei.gt(sendAssetBalance) + }, [sendAssetBalance, sendAmount, selectedSendAsset]) + + const sendAmountFiatValue = React.useMemo(() => { + if ( + !selectedSendAsset || + sendAssetBalance === '' || + selectedSendOption === SendPageTabHashes.nft + ) { + return '' + } + + return computeFiatAmount({ + spotPriceRegistry, + value: ethToWeiAmount( + sendAmount !== '' ? sendAmount : '0', + selectedSendAsset + ).toHex(), + token: selectedSendAsset + }).formatAsFiat(defaultFiatCurrency) + }, [ + spotPriceRegistry, + selectedSendAsset, + sendAmount, + defaultFiatCurrency, + sendAssetBalance, + selectedSendOption + ]) + + const doneSearchingForDomain = !isSearchingForDomain + const hasResolvedDomain = doneSearchingForDomain && resolvedDomainAddress + const hasValidResolvedDomain = toAddressHasValidExtension && hasResolvedDomain + + const domainErrorLocaleKey = + toAddressOrUrl && doneSearchingForDomain + ? processDomainLookupResponseWarning( + toAddressHasValidExtension, + resolvedDomainAddress, + hasNameServiceError, + showEnsOffchainWarning, + selectedAccount?.address + ) + : undefined + + const resolvedDomainOrToAddressOrUrl = hasValidResolvedDomain + ? resolvedDomainAddress + : trimmedToAddressOrUrl + + const toAddressIsTokenContract = resolvedDomainOrToAddressOrUrl + ? findTokenByContractAddress( + resolvedDomainOrToAddressOrUrl, + fullTokenList + ) !== undefined + : undefined + + const toAddressIsSelectedAccount = + selectedAccount && + resolvedDomainOrToAddressOrUrl.toLowerCase() === + selectedAccount.address.toLowerCase() + + const addressWarningLocaleKey = toAddressIsTokenContract + ? 'braveWalletContractAddressError' + : isValidEvmAddress && + ethAddressChecksum !== toAddressOrUrl && + [lowerCaseToAddress, toAddressOrUrl.toUpperCase()].includes( + toAddressOrUrl + ) + ? 'braveWalletAddressMissingChecksumInfoWarning' + : undefined + + const hasAddressWarning = Boolean(addressWarningLocaleKey) + + const addressErrorLocaleKey = toAddressIsSelectedAccount + ? 'braveWalletSameAddressError' + : trimmedToAddressOrUrl.includes('.') + ? domainErrorLocaleKey + : selectedAccount + ? addressWarningLocaleKey !== + 'braveWalletAddressMissingChecksumInfoWarning' ? + processAddressOrUrl({ + addressOrUrl: trimmedToAddressOrUrl, + ethAddressChecksum, + isBase58, + coinType: selectedAccount.accountId.coin ?? BraveWallet.CoinType.ETH, + selectedSendAsset + }) : undefined + : undefined + + const addressError = addressErrorLocaleKey + ? getLocale(addressErrorLocaleKey).replace( + '$1', + CoinTypesMap[selectedNetwork?.coin ?? 0] + ) + : undefined + + const hasAddressError = doneSearchingForDomain && Boolean(addressError) + + const showResolvedDomain = hasValidResolvedDomain && !hasAddressError + + // reused locales + const braveWalletAddressMissingChecksumInfoWarning = getLocale( + 'braveWalletAddressMissingChecksumInfoWarning' + ) + const braveWalletNotValidChecksumAddressError = getLocale( + 'braveWalletNotValidChecksumAddressError' + ) + + const reviewButtonHasError = + doneSearchingForDomain && + (insufficientFundsError || + (addressError !== undefined && + addressError !== '' && + addressError !== braveWalletNotValidChecksumAddressError)) + + const showFilecoinFEVMWarning = + selectedAccount?.accountId.coin === BraveWallet.CoinType.FIL + ? trimmedToAddressOrUrl.startsWith('0x') && + !validateETHAddress(trimmedToAddressOrUrl, ethAddressChecksum) + : false + + const addressMessageInformation: AddressMessageInfo | undefined = + React.useMemo( + getAddressMessageInfo({ + showFilecoinFEVMWarning, + fevmTranslatedAddresses, + toAddressOrUrl, + showEnsOffchainWarning, + addressErrorKey: addressErrorLocaleKey, + addressWarningKey: addressWarningLocaleKey, + }), + [ + showFilecoinFEVMWarning, + fevmTranslatedAddresses, + toAddressOrUrl, + showEnsOffchainWarning, + addressErrorLocaleKey, + addressWarningLocaleKey + ] + ) + + // Methods + const selectSendAsset = React.useCallback( + (asset: BraveWallet.BlockchainToken | undefined) => { + if (asset?.isErc721 || asset?.isNft) { + setSendAmount('1') + } else { + setSendAmount('') + } + setToAddressOrUrl('') + setSelectedSendAsset(asset) + }, + [] + ) + + const resetSendFields = React.useCallback(() => { + selectSendAsset(undefined) + setToAddressOrUrl('') + setSendAmount('') + }, [selectSendAsset]) + + const submitSend = React.useCallback(async () => { + if (!selectedSendAsset) { + console.log('Failed to submit Send transaction: no send asset selected') + return + } + + if (!selectedAccount) { + console.log('Failed to submit Send transaction: no account selected') + return + } + + if (!selectedNetwork) { + console.log('Failed to submit Send transaction: no network selected') + return + } + + const fromAccount: BaseTransactionParams['fromAccount'] = { + accountId: selectedAccount.accountId, + address: selectedAccount.address, + hardware: selectedAccount.hardware + } + + const toAddress = showResolvedDomain + ? resolvedDomainAddress + : toAddressOrUrl + + selectedSendAsset.isErc20 && + (await sendERC20Transfer({ + network: selectedNetwork, + fromAccount, + to: toAddress, + value: ethToWeiAmount(sendAmount, selectedSendAsset).toHex(), + contractAddress: selectedSendAsset.contractAddress + })) + + selectedSendAsset.isErc721 && + (await sendERC721TransferFrom({ + network: selectedNetwork, + fromAccount, + to: toAddress, + value: '', + contractAddress: selectedSendAsset.contractAddress, + tokenId: selectedSendAsset.tokenId ?? '' + })) + + if ( + selectedAccount.accountId.coin === BraveWallet.CoinType.SOL && + selectedSendAsset.contractAddress !== '' && + !selectedSendAsset.isErc20 && + !selectedSendAsset.isErc721 + ) { + await sendSPLTransfer({ + network: selectedNetwork, + fromAccount, + to: toAddress, + value: !selectedSendAsset.isNft + ? new Amount(sendAmount) + .multiplyByDecimals(selectedSendAsset.decimals) + .toHex() + : new Amount(sendAmount).toHex(), + splTokenMintAddress: selectedSendAsset.contractAddress + }) + resetSendFields() + return + } + + if (selectedAccount.accountId.coin === BraveWallet.CoinType.FIL) { + await sendTransaction({ + network: selectedNetwork, + fromAccount, + to: toAddress, + value: new Amount(sendAmount) + .multiplyByDecimals(selectedSendAsset.decimals) + .toNumber() + .toString() + }) + resetSendFields() + return + } + + if (selectedSendAsset.isErc721 || selectedSendAsset.isErc20) { + resetSendFields() + return + } + + if ( + selectedAccount.accountId.coin === BraveWallet.CoinType.ETH && + (selectedSendAsset.chainId === + BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID || + selectedSendAsset.chainId === + BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID) && + isValidFilAddress(toAddress) + ) { + await sendETHFilForwarderTransfer({ + network: selectedNetwork, + fromAccount, + to: toAddress, + value: ethToWeiAmount(sendAmount, selectedSendAsset).toHex(), + contractAddress: '0x2b3ef6906429b580b7b2080de5ca893bc282c225' + }) + resetSendFields() + return + } + + await sendTransaction({ + network: selectedNetwork, + fromAccount, + to: toAddress, + value: + selectedAccount.accountId.coin === BraveWallet.CoinType.FIL + ? new Amount(sendAmount) + .multiplyByDecimals(selectedSendAsset.decimals) + .toString() + : new Amount(sendAmount) + .multiplyByDecimals(selectedSendAsset.decimals) + .toHex() + }) + + resetSendFields() + }, [ + selectedSendAsset, + selectedAccount, + selectedNetwork, + sendAmount, + toAddressOrUrl, + showResolvedDomain, + resolvedDomainAddress, + resetSendFields + ]) + + const setSelectedAccountAndNetwork = React.useCallback(async () => { + if (!chainId || !selectedAssetFromParams || !accountFromParams) { + return + } + + try { + await setSelectedAccount(accountFromParams.accountId) + await setNetwork({ + chainId: chainId, + coin: selectedAssetFromParams.coin + }) + } catch (e) { + console.error(e) + } + }, [accountFromParams, chainId, selectedAssetFromParams]) + + const handleInputAmountChange = React.useCallback( + (event: React.ChangeEvent) => { + setSendAmount(event.target.value) + }, + [] + ) + + const handleInputAddressChange = React.useCallback( + (event: React.ChangeEvent) => { + setToAddressOrUrl(event.target.value) + }, + [setToAddressOrUrl, addressWidthRef] + ) + + const onSelectSendOption = React.useCallback( + (option: SendPageTabHashes) => { + selectSendAsset(undefined) + history.push(`${WalletRoutes.SendPageStart}${option}`) + }, + [selectSendAsset] + ) + + const onENSConsent = React.useCallback(() => { + enableEnsOffchainLookup() + dismissOffchainEnsWarning(true) + }, [enableEnsOffchainLookup]) + + const setPresetAmountValue = React.useCallback( + (percent: number) => { + if (!selectedSendAsset || !selectedAccount) { + return + } + + setSendAmount( + getPercentAmount( + selectedSendAsset, + selectedAccount.accountId, + percent, + tokenBalancesRegistry + ) + ) + }, + [selectedSendAsset, selectedAccount, tokenBalancesRegistry] + ) + + // Modals + const { + closeModal: closeChecksumModal, + openModal: openChecksumModal, + ref: checksumInfoModalRef, + isModalShown: showChecksumInfoModal + } = useModal() + const { + closeModal: closeSelectTokenModal, + openModal: openSelectTokenModal, + ref: selectTokenModalRef, + isModalShown: showSelectTokenModal + } = useModal() + + // Effects + React.useLayoutEffect(() => { + // Update loading icon position when to-address changes. + // Using an effect instead of within an on-change + // because we want to have the latest text width from the dom + const position = addressWidthRef.current?.clientWidth + setDomainPosition(position ? position + 28 : 0) + }, [toAddressOrUrl]) + + React.useEffect(() => { + // check if the user has selected an asset + if (!selectedAssetFromParams || selectedSendAsset) { + return + } + setSelectedAccountAndNetwork() + selectSendAsset(selectedAssetFromParams) + }, [ + selectSendAsset, + selectedSendAsset, + selectedAssetFromParams, + setSelectedAccountAndNetwork + ]) + + // render + return ( + <> + } + > + + + + + + {selectedSendOption === SendPageTabHashes.token && ( + + + {isLoadingSelectedAccount || isLoadingBalances ? ( + + ) : ( + + {accountNameAndBalance} + + )} + + + + + {selectedSendOption === SendPageTabHashes.token && + selectedSendAsset && ( + <> + + setPresetAmountValue(0.5)} + /> + setPresetAmountValue(1)} + /> + + )} + + {selectedSendOption === SendPageTabHashes.token && ( + + )} + + + {isLoadingSpotPrices || isLoadingBalances ? ( + + ) : ( + + {sendAmountFiatValue} + + )} + + + )} + {selectedSendOption === SendPageTabHashes.nft && ( + + + + + + + {accountNameAndBalance} + + + + )} + + + + {isSearchingForDomain && ( + + )} + {toAddressOrUrl} + + + + {showResolvedDomain && ( + + )} + {addressMessageInformation && ( + + )} + + {showEnsOffchainWarning && !isOffChainEnsWarningDismissed ? ( + + ) : ( + + )} + + + {showSelectTokenModal ? ( + + ) : null} + {showChecksumInfoModal ? ( + + ) : null} + + ) +}) + +export default SendScreen + +const SendPageHeader = React.memo(() => { + return +}) + +/** + * ETH → Wei conversion + */ +function ethToWeiAmount( + sendAmount: string, + selectedSendAsset: BraveWallet.BlockchainToken +): Amount { + return new Amount(sendAmount).multiplyByDecimals(selectedSendAsset.decimals) +} + +function getAddressMessageInfo({ + addressErrorKey, + addressWarningKey, + fevmTranslatedAddresses, + showEnsOffchainWarning, + showFilecoinFEVMWarning, + toAddressOrUrl +}: { + showFilecoinFEVMWarning: boolean + fevmTranslatedAddresses: + | Map + | undefined + toAddressOrUrl: string + showEnsOffchainWarning: boolean + addressErrorKey: string | undefined + addressWarningKey: string | undefined +}): () => + | AddressMessageInfo + | { + placeholder: any + title: string + description?: string | undefined + url?: string | undefined + type?: 'error' | 'warning' | undefined + } + | undefined { + return () => { + if (showFilecoinFEVMWarning) { + return { + ...FEVMAddressConvertionMessage, + placeholder: fevmTranslatedAddresses?.[toAddressOrUrl] + } + } + if (showEnsOffchainWarning) { + return ENSOffchainLookupMessage + } + if (addressErrorKey === 'braveWalletNotValidChecksumAddressError') { + return ErrorFailedChecksumMessage + } + + if (addressWarningKey === 'braveWalletAddressMissingChecksumInfoWarning') { + return WarningFailedChecksumMessage + } + return undefined + } +} + +function getReviewButtonText( + searchingForDomain: boolean, + sendAmountValidationError: string | undefined, + insufficientFundsError: boolean, + addressError: string | undefined, + addressWarningKey: string | undefined +) { + if (searchingForDomain) { + return 'braveWalletSearchingForDomain' + } + if (sendAmountValidationError) { + return 'braveWalletDecimalPlacesError' + } + if (insufficientFundsError) { + return 'braveWalletNotEnoughFunds' + } + if ( + addressError && + addressError !== 'braveWalletNotValidChecksumAddressError' + ) { + return addressError + } + + if ( + addressWarningKey && + addressWarningKey !== 'braveWalletAddressMissingChecksumInfoWarning' + ) { + return addressWarningKey + } + + return 'braveWalletReviewSend' +} + +const processDomainLookupResponseWarning = ( + urlHasValidExtension: boolean, + resolvedAddress: string | undefined, + hasDomainLookupError: boolean, + requireOffchainConsent: boolean, + selectedAccountAddress?: string, +) => { + if (requireOffchainConsent) { + // handled separately + return undefined + } + + if (!urlHasValidExtension) { + return 'braveWalletInvalidRecipientAddress' + } + + if ( + hasDomainLookupError || + !resolvedAddress + ) { + return 'braveWalletNotDomain' + } + + // If found address is the same as the selectedAccounts Wallet Address + if ( + selectedAccountAddress && + resolvedAddress.toLowerCase() === selectedAccountAddress.toLowerCase() + ) { + return 'braveWalletSameAddressError' + } + + return undefined +} + +const validateETHAddress = (address: string, checksumAddress: string) => { + if (!isValidEVMAddress(address)) { + return 'braveWalletInvalidRecipientAddress' + } + + return checksumAddress && + checksumAddress !== address && + [address.toLowerCase(), address.toUpperCase()].includes(address) + ? 'braveWalletNotValidChecksumAddressError' + : undefined +} + +const processEthereumAddress = ( + addressOrUrl: string, + selectedSendAsset: BraveWallet.BlockchainToken | undefined, + checksumAddress: string +) => { + const valueToLowerCase = addressOrUrl.toLowerCase() + + if ( + selectedSendAsset && + (selectedSendAsset.chainId === + BraveWallet.FILECOIN_ETHEREUM_MAINNET_CHAIN_ID || + selectedSendAsset.chainId === + BraveWallet.FILECOIN_ETHEREUM_TESTNET_CHAIN_ID) && + isValidFilAddress(addressOrUrl) + ) { + return undefined + } + + // If value starts with 0x, will check if it's a valid address + if (valueToLowerCase.startsWith('0x')) { + return validateETHAddress(addressOrUrl, checksumAddress) + } + + // Fallback error state + return valueToLowerCase === '' + ? undefined + : 'braveWalletInvalidRecipientAddress' +} + +const processFilecoinAddress = ( + addressOrUrl: string, + checksum: string +) => { + const valueToLowerCase = addressOrUrl.toLowerCase() + + // If value starts with 0x, will check if it's a valid address + if (valueToLowerCase.startsWith('0x')) { + return validateETHAddress(addressOrUrl, checksum) + } + + if (!isValidFilAddress(valueToLowerCase)) { + return 'braveWalletInvalidRecipientAddress' + } + + // Default + return undefined +} + +const processSolanaAddress = ( + addressOrUrl: string, + isBase58Encoded: boolean | undefined +) => { + // Check if value is a Base58 Encoded Solana Pubkey + if (!isBase58Encoded) { + return 'braveWalletInvalidRecipientAddress' + } + + return undefined +} + +const processBitcoinAddress = (addressOrUrl: string) => { + // Check if value is the same as the sending address + // TODO(apaymyshev): should prohibit self transfers? + + // TODO(apaymyshev): validate address format. + return undefined +} + +function processAddressOrUrl({ + addressOrUrl, + ethAddressChecksum, + isBase58, + coinType, + selectedSendAsset, +}: { + addressOrUrl: string + coinType: BraveWallet.CoinType | undefined + selectedSendAsset: BraveWallet.BlockchainToken | undefined + ethAddressChecksum: string + isBase58: boolean +}) { + // Do nothing if value is an empty string + if (addressOrUrl === '') { + return undefined + } + + switch (coinType) { + case undefined: return undefined + case BraveWallet.CoinType.ETH: { + return processEthereumAddress( + addressOrUrl, + selectedSendAsset, + ethAddressChecksum + ) + } + case BraveWallet.CoinType.FIL: { + return processFilecoinAddress(addressOrUrl, ethAddressChecksum) + } + case BraveWallet.CoinType.SOL: { + return processSolanaAddress(addressOrUrl, isBase58) + } + case BraveWallet.CoinType.BTC: { + return processBitcoinAddress(addressOrUrl) + } + default: { + console.log(`Unknown coin ${coinType}`) + return undefined + } + } +} diff --git a/components/brave_wallet_ui/stories/mock-data/mock-asset-options.ts b/components/brave_wallet_ui/stories/mock-data/mock-asset-options.ts index 32b3a9bfd86a..e49f16f20e01 100644 --- a/components/brave_wallet_ui/stories/mock-data/mock-asset-options.ts +++ b/components/brave_wallet_ui/stories/mock-data/mock-asset-options.ts @@ -9,10 +9,13 @@ import { BNBIconUrl, BTCIconUrl, ETHIconUrl, + FILECOINIconUrl, + SOLIconUrl, USDCIconUrl, ZRXIconUrl } from './asset-icons' import MoonCatIcon from '../../assets/png-icons/mooncat.png' +import { getAssetIdKey } from '../../utils/asset-utils' export const mockEthToken = { contractAddress: '', @@ -26,11 +29,45 @@ export const mockEthToken = { decimals: 18, visible: true, tokenId: '', - coingeckoId: '', + coingeckoId: 'ethereum', chainId: '0x1', coin: BraveWallet.CoinType.ETH } as BraveWallet.BlockchainToken +export const mockSolToken = { + contractAddress: '', + name: 'Solana', + symbol: 'SOL', + logo: SOLIconUrl, + isErc20: false, + isErc721: false, + isNft: false, + isSpam: false, + decimals: 9, + visible: true, + tokenId: '', + coingeckoId: 'solana', + chainId: BraveWallet.SOLANA_MAINNET, + coin: BraveWallet.CoinType.SOL +} as BraveWallet.BlockchainToken + +export const mockFilToken = { + contractAddress: '', + name: 'Filecoin', + symbol: 'FIL', + logo: FILECOINIconUrl, + isErc20: false, + isErc721: false, + isNft: false, + isSpam: false, + decimals: 9, + visible: true, + tokenId: '', + coingeckoId: 'filecoin', + chainId: BraveWallet.FILECOIN_MAINNET, + coin: BraveWallet.CoinType.FIL +} as BraveWallet.BlockchainToken + export const mockBasicAttentionToken = { contractAddress: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', name: 'Basic Attention Token', @@ -175,13 +212,86 @@ export const mockMoonCatNFT = { chainId: '0x1' } +export const mockERC20Token: BraveWallet.BlockchainToken = { + contractAddress: 'mockContractAddress', + name: 'Dog Coin', + symbol: 'DOG', + logo: '', + isErc20: true, + isErc721: false, + isErc1155: false, + isNft: false, + isSpam: false, + decimals: 18, + visible: true, + tokenId: '', + coingeckoId: '', + coin: BraveWallet.CoinType.ETH, + chainId: BraveWallet.MAINNET_CHAIN_ID +} + +export const mockErc721Token: BraveWallet.BlockchainToken = { + contractAddress: '0x59468516a8259058bad1ca5f8f4bff190d30e066', + name: 'Invisible Friends', + symbol: 'INVSBLE', + logo: 'https://ipfs.io/ipfs/QmX4nfgA35MiW5APoc4P815hMcH8hAt7edi5H3wXkFm485/2D/2585.gif', + isErc20: false, + isErc721: true, + isErc1155: false, + isNft: true, + isSpam: false, + decimals: 18, + visible: true, + tokenId: '0x0a19', + coingeckoId: '', + coin: BraveWallet.CoinType.ETH, + chainId: BraveWallet.MAINNET_CHAIN_ID +} + +export const mockSplNft: BraveWallet.BlockchainToken = { + contractAddress: 'wt1PhURTzRSgmWKHBEJgSX8hN9TdkdNoKhPAnwCmnZE', + name: 'The Degen #2314', + symbol: 'BNFT', + logo: 'https://shdw-drive.genesysgo.net/FR3sEzyAmQMooUYhcPPnN4TmVLSZWi3cEwAWpB4nJvYJ/image-2.png', + isErc20: false, + isErc721: false, + isErc1155: false, + isNft: true, + isSpam: false, + decimals: 1, + visible: true, + tokenId: 'wt1PhURTzRSgmWKHBEJgSX8hN9TdkdNoKhPAnwCmnZE', + coingeckoId: '', + coin: BraveWallet.CoinType.SOL, + chainId: BraveWallet.SOLANA_MAINNET, +} + +const mockSplBat = { + ...mockBasicAttentionToken, + contractAddress: 'splBat498tu349u498j', + chainId: BraveWallet.SOLANA_MAINNET, +} + +const mockSplUSDC = { + ...mockUSDCoin, + contractAddress: 'splusd09856080378450y75', + chainId: BraveWallet.SOLANA_MAINNET, +} + export const mockAccountAssetOptions: BraveWallet.BlockchainToken[] = [ mockEthToken, + mockSolToken, + mockFilToken, mockBasicAttentionToken, mockBinanceCoinErc20Token, mockBitcoinErc20Token, mockAlgorandErc20Token, - mockZrxErc20Token + mockZrxErc20Token, + mockDaiToken, + mockUSDCoin, + mockSplBat, + mockSplNft, + mockSplUSDC, ] export const mockErc20TokensList = [ @@ -285,3 +395,18 @@ export const mockNewAssetOptions: BraveWallet.BlockchainToken[] = [ { ...mockMoonCatNFT, tokenId: '0x52a5' }, { ...mockMoonCatNFT, tokenId: '0x62a5' } ] + +export const mockBinanceCoinErc20TokenId = getAssetIdKey( + mockBinanceCoinErc20Token +) +export const mockBitcoinErc20TokenId = getAssetIdKey(mockBitcoinErc20Token) +export const mockAlgorandErc20TokenId = getAssetIdKey(mockAlgorandErc20Token) +export const mockZrxErc20TokenId = getAssetIdKey(mockZrxErc20Token) +export const mockDaiTokenId = getAssetIdKey(mockDaiToken) +export const mockSplNftId = getAssetIdKey(mockSplNft) + +export const mockBasicAttentionTokenId = getAssetIdKey(mockBasicAttentionToken) +export const mockUSDCoinId = getAssetIdKey(mockUSDCoin) + +export const mockSplBasicAttentionTokenId = getAssetIdKey(mockSplBat) +export const mockSplUSDCoinId = getAssetIdKey(mockSplUSDC) diff --git a/components/brave_wallet_ui/stories/wallet-components.tsx b/components/brave_wallet_ui/stories/wallet-components.tsx index b509884be599..7024782925f9 100644 --- a/components/brave_wallet_ui/stories/wallet-components.tsx +++ b/components/brave_wallet_ui/stories/wallet-components.tsx @@ -18,7 +18,7 @@ import { NftIpfsBanner } from '../components/desktop/nft-ipfs-banner/nft-ipfs-ba import { LocalIpfsNodeScreen } from '../components/desktop/local-ipfs-node/local-ipfs-node' import { InspectNftsScreen } from '../components/desktop/inspect-nfts/inspect-nfts' import WalletPageStory from './wrappers/wallet-page-story-wrapper' -import { mockErc721Token, mockNetwork } from '../common/constants/mocks' +import { mockNetwork } from '../common/constants/mocks' import { mockNFTMetadata } from './mock-data/mock-nft-metadata' import { NftPinningStatus } from '../components/desktop/nft-pinning-status/nft-pinning-status' import { NftsEmptyState } from '../components/desktop/views/nfts/components/nfts-empty-state/nfts-empty-state' @@ -31,6 +31,7 @@ import { AutoDiscoveryEmptyState } from '../components/desktop/views/nfts/compon import { MarketGrid } from '../components/shared/market-grid/market-grid' import { marketGridHeaders } from '../options/market-data-headers' import { coinMarketMockData } from './mock-data/mock-coin-market-data' +import { mockErc721Token } from './mock-data/mock-asset-options' export default { title: 'Wallet/Desktop/Components', diff --git a/components/brave_wallet_ui/utils/balance-utils.test.ts b/components/brave_wallet_ui/utils/balance-utils.test.ts index 8d1bd3f6ebdf..c33aac5da2fa 100644 --- a/components/brave_wallet_ui/utils/balance-utils.test.ts +++ b/components/brave_wallet_ui/utils/balance-utils.test.ts @@ -6,8 +6,8 @@ import { getBalance, getPercentAmount } from './balance-utils' // mocks -import { mockAccount, mockERC20Token } from '../common/constants/mocks' -import { mockBasicAttentionToken, mockBinanceCoinErc20Token } from '../stories/mock-data/mock-asset-options' +import { mockAccount } from '../common/constants/mocks' +import { mockBasicAttentionToken, mockBinanceCoinErc20Token, mockERC20Token } from '../stories/mock-data/mock-asset-options' describe('getBalance', () => { diff --git a/components/brave_wallet_ui/utils/tx-parsing-utils.test.ts b/components/brave_wallet_ui/utils/tx-parsing-utils.test.ts index c547d45de7c7..9c2960c58005 100644 --- a/components/brave_wallet_ui/utils/tx-parsing-utils.test.ts +++ b/components/brave_wallet_ui/utils/tx-parsing-utils.test.ts @@ -16,7 +16,6 @@ import Amount from './amount' // mocks import { getMockedTransactionInfo, - mockERC20Token, mockEthAccountInfo, } from '../common/constants/mocks' import { mockWalletState } from '../stories/mock-data/mock-wallet-state' @@ -28,6 +27,7 @@ import { transactionHasSameAddressError, isSendingToKnownTokenContractAddress } from './tx-utils' +import { mockERC20Token } from '../stories/mock-data/mock-asset-options' const tokenList = [ ...mockWalletState.fullTokenList,