From 4cfd133fb8a02069a56da3142d167cfe2de659ea Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 20 Nov 2024 14:37:55 +0100 Subject: [PATCH 01/28] feat: cross chain aggregated balance (#28456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Branch was ininitially based on top of https://github.com/MetaMask/metamask-extension/pull/28276 This PR calculates balance for current account cross chains while not taking into account the test networks. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28456?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** Start the app with `PORTFOLIO_VIEW=1 ` 1. Go to main page and click on Ethereum network 2. Import your tokens 3. You should see the total balance in Fiat of your native + ERC20 tokens 4. Switch to another chain where you also have tokens; exp polygon 5. Notice that your total fiat balance is now total fiat balance on Ethereum + current native balance on polygon in fiat 6. Import any tokens you have on polygon 7. Notice that the total fiat balance now added up the sum of ERC20 tokens you had on Polygon 8. Click on the network filter and notice you can see aggregated balance on all networks and aggregated balance on the current network 9. Switch to "Current network" on the network filter and the main view should now show you the aggregated balance on this network. 10. Click on account item and notice that you now see the balance in fiat cross networks 11. Go to settings and turn on the setting "show native token as main balance"; and go back to home page. 12. You should now see your native balance in crypto 13. Click on network filter and you should still be able to see the all networks and current network aggregated balance in fiat. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/ba52796d-1b39-4de0-be9c-175c8608e042 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Brian Bergeron Co-authored-by: MetaMask Bot --- app/_locales/en/messages.json | 4 + privacy-snapshot.json | 3 +- test/data/mock-send-state.json | 4 +- test/data/mock-state.json | 1 + test/e2e/default-fixture.js | 1 + ...rs-after-init-opt-in-background-state.json | 1 + .../errors-after-init-opt-in-ui-state.json | 2 +- ...s-before-init-opt-in-background-state.json | 3 +- .../errors-before-init-opt-in-ui-state.json | 1 + .../tests/settings/account-token-list.spec.js | 44 ++ .../data/integration-init-state.json | 3 +- .../asset-list-control-bar.tsx | 7 +- .../network-filter/network-filter.tsx | 68 +- ...entage-overview-cross-chains.test.tsx.snap | 23 + ...-percentage-overview-cross-chains.test.tsx | 588 ++++++++++++++++++ ...gated-percentage-overview-cross-chains.tsx | 187 ++++++ .../app/wallet-overview/btc-overview.test.tsx | 1 + .../app/wallet-overview/coin-overview.tsx | 76 ++- .../app/wallet-overview/eth-overview.test.js | 1 + .../account-list-item/account-list-item.js | 42 +- .../multichain/pages/send/send.test.js | 2 + ui/ducks/metamask/metamask.js | 4 + ...eAccountTotalCrossChainFiatBalance.test.ts | 226 +++++++ .../useAccountTotalCrossChainFiatBalance.ts | 117 ++++ .../useGetFormattedTokensPerChain.test.ts | 152 +++++ ui/hooks/useGetFormattedTokensPerChain.ts | 77 +++ ui/hooks/useTokenBalances.ts | 6 +- ui/pages/routes/routes.component.test.js | 2 + ui/selectors/selectors.js | 45 +- 29 files changed, 1642 insertions(+), 49 deletions(-) create mode 100644 ui/components/app/wallet-overview/__snapshots__/aggregated-percentage-overview-cross-chains.test.tsx.snap create mode 100644 ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx create mode 100644 ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx create mode 100644 ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts create mode 100644 ui/hooks/useAccountTotalCrossChainFiatBalance.ts create mode 100644 ui/hooks/useGetFormattedTokensPerChain.test.ts create mode 100644 ui/hooks/useGetFormattedTokensPerChain.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 74e7d745cae7..56a2ba405aef 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1343,6 +1343,10 @@ "creatorAddress": { "message": "Creator address" }, + "crossChainAggregatedBalancePopover": { + "message": "This reflects the value of all tokens you own on all networks. If you prefer seeing this value in ETH or other currencies, go to $1.", + "description": "$1 represents the settings page" + }, "crossChainSwapsLink": { "message": "Swap across networks with MetaMask Portfolio" }, diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 817d1e102bff..f5cf068b8728 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -52,12 +52,13 @@ "security-alerts.api.cx.metamask.io", "security-alerts.dev-api.cx.metamask.io", "sentry.io", + "sepolia.infura.io", "snaps.metamask.io", "sourcify.dev", "start.metamask.io", "static.cx.metamask.io", - "support.metamask.io", "support.metamask-institutional.io", + "support.metamask.io", "swap.api.cx.metamask.io", "test.metamask-phishing.io", "token.api.cx.metamask.io", diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 96cd95cfbd84..73468aca6171 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -63,6 +63,7 @@ "currentLocale": "en" }, "metamask": { + "accountsByChainId": {}, "ipfsGateway": "", "dismissSeedBackUpReminder": false, "usePhishDetect": true, @@ -131,7 +132,8 @@ "preferences": { "hideZeroBalanceTokens": false, "showFiatInTestnets": false, - "showTestNetworks": true + "showTestNetworks": true, + "tokenNetworkFilter": {} }, "seedPhraseBackedUp": null, "ensResolutionsByAddress": {}, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 80e5499447d7..2932e5fc56d9 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -379,6 +379,7 @@ "showNativeTokenAsMainBalance": true, "showTestNetworks": true, "smartTransactionsOptInStatus": true, + "tokenNetworkFilter": {}, "tokenSortConfig": { "key": "tokenFiatAmount", "order": "dsc", diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index fd2d5be42891..c2fba9d63424 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -225,6 +225,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { sortCallback: 'stringNumeric', }, shouldShowAggregatedBalancePopover: true, + tokenNetworkFilter: {}, }, selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', theme: 'light', diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index e47cfcd806b9..9df8707d1f17 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -237,6 +237,7 @@ "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", "tokenSortConfig": "object", + "tokenNetworkFilter": "object", "shouldShowAggregatedBalancePopover": "boolean" }, "ipfsGateway": "string", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 7fd8501eb2b8..006277f89160 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -38,7 +38,7 @@ "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", "tokenSortConfig": "object", - "showMultiRpcModal": "boolean", + "tokenNetworkFilter": "object", "shouldShowAggregatedBalancePopover": "boolean" }, "firstTimeFlowType": "import", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index fa1a00cbe4ef..91c994e9ab66 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -119,7 +119,8 @@ "showConfirmationAdvancedDetails": false, "tokenSortConfig": "object", "showMultiRpcModal": "boolean", - "shouldShowAggregatedBalancePopover": "boolean" + "shouldShowAggregatedBalancePopover": "boolean", + "tokenNetworkFilter": "object" }, "selectedAddress": "string", "theme": "light", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index b3fa8d117beb..552f089c6604 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -135,6 +135,7 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "showConfirmationAdvancedDetails": false, "tokenSortConfig": "object", + "tokenNetworkFilter": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, diff --git a/test/e2e/tests/settings/account-token-list.spec.js b/test/e2e/tests/settings/account-token-list.spec.js index 9e4822d0dbbc..ddd905501f50 100644 --- a/test/e2e/tests/settings/account-token-list.spec.js +++ b/test/e2e/tests/settings/account-token-list.spec.js @@ -5,9 +5,38 @@ const { logInWithBalanceValidation, unlockWallet, } = require('../../helpers'); +const { + switchToNetworkFlow, +} = require('../../page-objects/flows/network.flow'); +const { mockServerJsonRpc } = require('../ppom/mocks/mock-server-json-rpc'); const FixtureBuilder = require('../../fixture-builder'); +const infuraSepoliaUrl = + 'https://sepolia.infura.io/v3/00000000000000000000000000000000'; + +async function mockInfura(mockServer) { + await mockServerJsonRpc(mockServer, [ + ['eth_blockNumber'], + ['eth_getBlockByNumber'], + ]); + await mockServer + .forPost(infuraSepoliaUrl) + .withJsonBodyIncluding({ method: 'eth_getBalance' }) + .thenCallback(() => ({ + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '6857940763865360', + result: '0x15af1d78b58c40000', + }, + })); +} + +async function mockInfuraResponses(mockServer) { + await mockInfura(mockServer); +} + describe('Settings', function () { it('Should match the value of token list item and account list item for eth conversion', async function () { await withFixtures( @@ -49,6 +78,7 @@ describe('Settings', function () { .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), + testSpecificMock: mockInfuraResponses, }, async ({ driver }) => { await unlockWallet(driver); @@ -63,6 +93,20 @@ describe('Settings', function () { ); await driver.delay(1000); assert.equal(await tokenListAmount.getText(), '$42,500.00\nUSD'); + + // switch to Sepolia + // the account list item used to always show account.balance as long as its EVM network. + // Now we are showing aggregated fiat balance on non testnetworks; but if it is a testnetwork we will show account.balance. + // The current test was running initially on localhost + // which is not a testnetwork resulting in the code trying to calculate the aggregated total fiat balance which shows 0.00$ + // If this test switches to mainnet then switches back to localhost; the test will pass because switching to mainnet + // will make the code calculate the aggregate fiat balance on mainnet+Linea mainnet and because this account in this test + // has 42,500.00 native Eth on mainnet then the aggregated total fiat would be 42,500.00. When the user switches back to localhost + // it will show the total that the test is expecting. + + // I think we can slightly modify this test to switch to Sepolia network before checking the account List item value + await switchToNetworkFlow(driver, 'Sepolia'); + await driver.clickElement('[data-testid="account-menu-icon"]'); const accountTokenValue = await driver.waitForSelector( '.multichain-account-list-item .multichain-account-list-item__asset', diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index a0ae3a8fb146..ed42111c00e1 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -784,7 +784,8 @@ "smartTransactionsOptInStatus": true, "petnamesEnabled": false, "showConfirmationAdvancedDetails": false, - "showMultiRpcModal": false + "showMultiRpcModal": false, + "tokenNetworkFilter": {} }, "preventPollingOnNetworkRestart": true, "previousAppVersion": "11.14.4", diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx index c09421279265..7722eff36870 100644 --- a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { getCurrentNetwork, getPreferences } from '../../../../../selectors'; import { @@ -28,6 +28,7 @@ import { ENVIRONMENT_TYPE_POPUP, } from '../../../../../../shared/constants/app'; import NetworkFilter from '../network-filter'; +import { TEST_CHAINS } from '../../../../../../shared/constants/network'; type AssetListControlBarProps = { showTokensLinks?: boolean; @@ -43,6 +44,9 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { useState(false); const allNetworksFilterShown = Object.keys(tokenNetworkFilter ?? {}).length; + const isTestNetwork = useMemo(() => { + return (TEST_CHAINS as string[]).includes(currentNetwork.chainId); + }, [currentNetwork.chainId, TEST_CHAINS]); const windowType = getEnvironmentType(); const isFullScreen = @@ -84,6 +88,7 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { className="asset-list-control-bar__button" onClick={toggleNetworkFilterPopover} size={ButtonBaseSize.Sm} + disabled={isTestNetwork} endIconName={IconName.ArrowDown} backgroundColor={ isNetworkFilterPopoverOpen diff --git a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx index cc2d0f38210e..8b9fc06b33e7 100644 --- a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx +++ b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx @@ -4,15 +4,14 @@ import { setTokenNetworkFilter } from '../../../../../store/actions'; import { getCurrentChainId, getCurrentNetwork, - getIsTestnet, getPreferences, getSelectedInternalAccount, getShouldHideZeroBalanceTokens, getNetworkConfigurationsByChainId, + getChainIdsToPoll, } from '../../../../../selectors'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { SelectableListItem } from '../sort-control/sort-control'; -import { useAccountTotalFiatBalance } from '../../../../../hooks/useAccountTotalFiatBalance'; import { Text } from '../../../../component-library/text/text'; import { Display, @@ -24,6 +23,8 @@ import { Box } from '../../../../component-library/box/box'; import { AvatarNetwork } from '../../../../component-library'; import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../shared/constants/network'; +import { useGetFormattedTokensPerChain } from '../../../../../hooks/useGetFormattedTokensPerChain'; +import { useAccountTotalCrossChainFiatBalance } from '../../../../../hooks/useAccountTotalCrossChainFiatBalance'; type SortControlProps = { handleClose: () => void; @@ -36,15 +37,35 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => { const selectedAccount = useSelector(getSelectedInternalAccount); const currentNetwork = useSelector(getCurrentNetwork); const allNetworks = useSelector(getNetworkConfigurationsByChainId); - const isTestnet = useSelector(getIsTestnet); - const { tokenNetworkFilter, showNativeTokenAsMainBalance } = - useSelector(getPreferences); + const { tokenNetworkFilter } = useSelector(getPreferences); const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, ); - + const allChainIDs = useSelector(getChainIdsToPoll); + const { formattedTokensWithBalancesPerChain } = useGetFormattedTokensPerChain( + selectedAccount, + shouldHideZeroBalanceTokens, + true, // true to get formattedTokensWithBalancesPerChain for the current chain + allChainIDs, + ); const { totalFiatBalance: selectedAccountBalance } = - useAccountTotalFiatBalance(selectedAccount, shouldHideZeroBalanceTokens); + useAccountTotalCrossChainFiatBalance( + selectedAccount, + formattedTokensWithBalancesPerChain, + ); + + const { formattedTokensWithBalancesPerChain: formattedTokensForAllNetworks } = + useGetFormattedTokensPerChain( + selectedAccount, + shouldHideZeroBalanceTokens, + false, // false to get the value for all networks + allChainIDs, + ); + const { totalFiatBalance: selectedAccountBalanceForAllNetworks } = + useAccountTotalCrossChainFiatBalance( + selectedAccount, + formattedTokensForAllNetworks, + ); // TODO: fetch balances across networks // const multiNetworkAccountBalance = useMultichainAccountBalance() @@ -78,7 +99,15 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => { color={TextColor.textDefault} > {/* TODO: Should query cross chain account balance */} - $1,000.00 + + @@ -120,16 +149,19 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => { > {t('currentNetwork')} - + + + +
+

+ +$0.22 +

+

+ (+0.08%) +

+
+ +`; diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx new file mode 100644 index 000000000000..1a335de9c14b --- /dev/null +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx @@ -0,0 +1,588 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { getIntlLocale } from '../../../ducks/locale/locale'; +import { + getCurrentCurrency, + getSelectedAccount, + getShouldHideZeroBalanceTokens, + getPreferences, + getMarketData, + getNetworkConfigurationsByChainId, + getAllTokens, + getChainIdsToPoll, +} from '../../../selectors'; +import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountTotalCrossChainFiatBalance'; +import { AggregatedPercentageOverviewCrossChains } from './aggregated-percentage-overview-cross-chains'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn((selector) => selector()), +})); + +const mockUseGetFormattedTokensPerChain = jest.fn().mockReturnValue({ + formattedTokensWithBalancesPerChain: {}, +}); +jest.mock('../../../hooks/useGetFormattedTokensPerChain', () => ({ + useGetFormattedTokensPerChain: () => mockUseGetFormattedTokensPerChain(), +})); + +jest.mock('../../../ducks/locale/locale', () => ({ + getIntlLocale: jest.fn(), +})); + +jest.mock('../../../selectors', () => ({ + getCurrentCurrency: jest.fn(), + getSelectedAccount: jest.fn(), + getPreferences: jest.fn(), + getShouldHideZeroBalanceTokens: jest.fn(), + getMarketData: jest.fn(), + getNetworkConfigurationsByChainId: jest.fn(), + getAllTokens: jest.fn(), + getChainIdsToPoll: jest.fn(), +})); + +jest.mock('../../../hooks/useAccountTotalCrossChainFiatBalance', () => ({ + useAccountTotalCrossChainFiatBalance: jest.fn(), +})); + +const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock; +const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock; +const mockGetPreferences = getPreferences as jest.Mock; +const mockGetSelectedAccount = getSelectedAccount as unknown as jest.Mock; +const mockGetShouldHideZeroBalanceTokens = + getShouldHideZeroBalanceTokens as jest.Mock; + +const mockGetMarketData = getMarketData as jest.Mock; +const mockGetChainIdsToPoll = getChainIdsToPoll as unknown as jest.Mock; +const mockGetNetworkConfigurationsByChainId = + getNetworkConfigurationsByChainId as unknown as jest.Mock; +const mockGetAllTokens = getAllTokens as jest.Mock; + +const allTokens = { + '0x1': { + '0x2990079bcdee240329a520d2444386fc119da21a': [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + aggregators: [ + 'Metamask', + 'Aave', + 'Bancor', + 'Crypto.com', + 'CoinGecko', + '1inch', + 'PMM', + 'Sushiswap', + 'Zerion', + 'Lifi', + 'Socket', + 'Squid', + 'Openswap', + 'UniswapLabs', + 'Coinmarketcap', + ], + decimals: 6, + symbol: 'USDC', + }, + ], + }, + '0xe708': { + '0x2990079bcdee240329a520d2444386fc119da21a': [ + { + address: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + aggregators: ['LineaTeam', 'CoinGecko', 'Lifi', 'Rubic', 'Xswap'], + decimals: 18, + symbol: 'DAI', + }, + { + address: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + aggregators: [ + 'LineaTeam', + 'CoinGecko', + 'Lifi', + 'Squid', + 'Rubic', + 'Xswap', + ], + decimals: 6, + symbol: 'USDT', + }, + ], + }, +}; +const networkConfigsByChainId = { + '0x1': { + blockExplorerUrls: ['https://etherscan.io'], + chainId: '0x1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, + '0xaa36a7': { + blockExplorerUrls: ['https://sepolia.etherscan.io'], + chainId: '0xaa36a7', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Sepolia', + nativeCurrency: 'SepoliaETH', + rpcEndpoints: [ + { + networkClientId: 'sepolia', + type: 'infura', + url: 'https://sepolia.infura.io/v3/{infuraProjectId}', + }, + ], + }, + '0xe705': { + blockExplorerUrls: ['https://sepolia.lineascan.build'], + chainId: '0xe705', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Linea Sepolia', + nativeCurrency: 'LineaETH', + rpcEndpoints: [ + { + networkClientId: 'linea-sepolia', + type: 'infura', + url: 'https://linea-sepolia.infura.io/v3/{infuraProjectId}', + }, + ], + }, + '0xe708': { + blockExplorerUrls: ['https://lineascan.build'], + chainId: '0xe708', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Linea Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'linea-mainnet', + type: 'infura', + url: 'https://linea-mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, +}; +const selectedAccountMock = { + id: 'd51c0116-de36-4e77-b35b-408d4ea82d01', + address: '0x2990079bcdee240329a520d2444386fc119da21a', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 2', + importTime: 1725467263902, + lastSelected: 1725467263905, + keyring: { + type: 'Simple Key Pair', + }, + }, + balance: '0x0f7e2a03e67666', +}; + +const crossChainMarketDataMock = { + '0x1': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999974728621198, + pricePercentChange1d: 0.8551361112650235, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00031298237681361845, + pricePercentChange1d: -0.19413664311573345, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.00031298237681361845, + pricePercentChange1d: -0.08092791615953396, + }, + '0xdAC17F958D2ee523a2206206994597C13D831ec7': { + tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + currency: 'ETH', + id: 'tether', + price: 0.00031329535919043206, + pricePercentChange1d: -0.09790827980452445, + }, + }, + '0xaa36a7': {}, + '0xe705': {}, + '0xe708': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999974728621198, + pricePercentChange1d: 0.8551361112650235, + }, + '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5': { + tokenAddress: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + currency: 'ETH', + id: 'bridged-dai-stablecoin-linea', + price: 0.00031298237681361845, + pricePercentChange1d: -0.22242916875537241, + }, + '0xA219439258ca9da29E9Cc4cE5596924745e12B93': { + tokenAddress: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + currency: 'ETH', + id: 'bridged-tether-linea', + price: 0.0003136083415672457, + pricePercentChange1d: -0.2013707959252836, + }, + }, +}; + +const negativeCrossChainMarketDataMock = { + '0x1': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999974728621198, + pricePercentChange1d: -0.8551361112650235, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00031298237681361845, + pricePercentChange1d: -0.19413664311573345, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.00031298237681361845, + pricePercentChange1d: -0.08092791615953396, + }, + '0xdAC17F958D2ee523a2206206994597C13D831ec7': { + tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + currency: 'ETH', + id: 'tether', + price: 0.00031329535919043206, + pricePercentChange1d: -0.09790827980452445, + }, + }, + '0xaa36a7': {}, + '0xe705': {}, + '0xe708': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999974728621198, + pricePercentChange1d: -0.8551361112650235, + }, + '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5': { + tokenAddress: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + currency: 'ETH', + id: 'bridged-dai-stablecoin-linea', + price: 0.00031298237681361845, + pricePercentChange1d: -0.22242916875537241, + }, + '0xA219439258ca9da29E9Cc4cE5596924745e12B93': { + tokenAddress: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + currency: 'ETH', + id: 'bridged-tether-linea', + price: 0.0003136083415672457, + pricePercentChange1d: -0.2013707959252836, + }, + }, +}; +const positiveCrossChainMarketDataMock = { + '0x1': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999974728621198, + pricePercentChange1d: 0.8551361112650235, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00031298237681361845, + pricePercentChange1d: 0.19413664311573345, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.00031298237681361845, + pricePercentChange1d: 0.08092791615953396, + }, + '0xdAC17F958D2ee523a2206206994597C13D831ec7': { + tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + currency: 'ETH', + id: 'tether', + price: 0.00031329535919043206, + pricePercentChange1d: 0.09790827980452445, + }, + }, + '0xaa36a7': {}, + '0xe705': {}, + '0xe708': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999974728621198, + pricePercentChange1d: 0.8551361112650235, + }, + '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5': { + tokenAddress: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + currency: 'ETH', + id: 'bridged-dai-stablecoin-linea', + price: 0.00031298237681361845, + pricePercentChange1d: 0.22242916875537241, + }, + '0xA219439258ca9da29E9Cc4cE5596924745e12B93': { + tokenAddress: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + currency: 'ETH', + id: 'bridged-tether-linea', + price: 0.0003136083415672457, + pricePercentChange1d: 0.2013707959252836, + }, + }, +}; +describe('AggregatedPercentageOverviewCrossChains', () => { + beforeEach(() => { + mockGetIntlLocale.mockReturnValue('en-US'); + mockGetCurrentCurrency.mockReturnValue('USD'); + mockGetPreferences.mockReturnValue({ privacyMode: false }); + mockGetSelectedAccount.mockReturnValue(selectedAccountMock); + mockGetShouldHideZeroBalanceTokens.mockReturnValue(false); + + mockGetMarketData.mockReturnValue(crossChainMarketDataMock); + mockGetChainIdsToPoll.mockReturnValue(['0x1']); + mockGetNetworkConfigurationsByChainId.mockReturnValue( + networkConfigsByChainId, + ); + mockGetAllTokens.mockReturnValue(allTokens); + + jest.clearAllMocks(); + }); + + describe('render', () => { + it('renders correctly', () => { + (useAccountTotalCrossChainFiatBalance as jest.Mock).mockReturnValue({ + tokenFiatBalancesCrossChains: [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + }, + ], + tokenFiatBalances: ['70'], + nativeFiatValue: '69.96', + }, + { + chainId: '0xe708', + tokensWithBalances: [ + { + address: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + symbol: 'DAI', + decimals: 18, + }, + { + address: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + symbol: 'USDT', + decimals: 6, + }, + ], + tokenFiatBalances: ['50', '100'], + nativeFiatValue: '0', + }, + ], + totalFiatBalance: 289.96, + }); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + it('should display zero percentage and amount if balance is zero across chains', () => { + (useAccountTotalCrossChainFiatBalance as jest.Mock).mockReturnValue({ + tokenFiatBalancesCrossChains: [ + { + chainId: '0x1', + tokensWithBalances: [], + tokenFiatBalances: [], + nativeFiatValue: '0', + }, + { + chainId: '0xe708', + tokensWithBalances: [], + tokenFiatBalances: [], + nativeFiatValue: '0', + }, + ], + totalFiatBalance: 0, + }); + + render(); + const percentageElement = screen.getByText('(+0.00%)'); + const numberElement = screen.getByText('+$0.00'); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + + it('should display negative aggregated amount and percentage change with all negative market data cross chains', () => { + (useAccountTotalCrossChainFiatBalance as jest.Mock).mockReturnValue({ + tokenFiatBalancesCrossChains: [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + }, + ], + tokenFiatBalances: ['70'], + nativeFiatValue: '69.96', + }, + { + chainId: '0xe708', + tokensWithBalances: [ + { + address: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + symbol: 'DAI', + decimals: 18, + }, + { + address: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + symbol: 'USDT', + decimals: 6, + }, + ], + tokenFiatBalances: ['50', '100'], + nativeFiatValue: '0', + }, + ], + totalFiatBalance: 289.96, + }); + mockGetMarketData.mockReturnValue(negativeCrossChainMarketDataMock); + const expectedAmountChange = '-$0.97'; + const expectedPercentageChange = '(-0.33%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + + it('should display positive aggregated amount and percentage change with all positive market data', () => { + (useAccountTotalCrossChainFiatBalance as jest.Mock).mockReturnValue({ + tokenFiatBalancesCrossChains: [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + }, + ], + tokenFiatBalances: ['70'], + nativeFiatValue: '69.96', + }, + { + chainId: '0xe708', + tokensWithBalances: [ + { + address: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + symbol: 'DAI', + decimals: 18, + }, + { + address: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + symbol: 'USDT', + decimals: 6, + }, + ], + tokenFiatBalances: ['50', '100'], + nativeFiatValue: '0', + }, + ], + totalFiatBalance: 289.96, + }); + mockGetMarketData.mockReturnValue(positiveCrossChainMarketDataMock); + const expectedAmountChange = '+$0.96'; + const expectedPercentageChange = '(+0.33%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + + it('should display correct aggregated amount and percentage change with positive and negative market data', () => { + (useAccountTotalCrossChainFiatBalance as jest.Mock).mockReturnValue({ + tokenFiatBalancesCrossChains: [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + }, + ], + tokenFiatBalances: ['70'], + nativeFiatValue: '69.96', + }, + { + chainId: '0xe708', + tokensWithBalances: [ + { + address: '0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5', + symbol: 'DAI', + decimals: 18, + }, + { + address: '0xA219439258ca9da29E9Cc4cE5596924745e12B93', + symbol: 'USDT', + decimals: 6, + }, + ], + tokenFiatBalances: ['50', '100'], + nativeFiatValue: '0', + }, + ], + totalFiatBalance: 289.96, + }); + const expectedAmountChange = '+$0.22'; + const expectedPercentageChange = '(+0.08%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx new file mode 100644 index 000000000000..fe3698e2fc2f --- /dev/null +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx @@ -0,0 +1,187 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; + +import { zeroAddress, toChecksumAddress } from 'ethereumjs-util'; +import { + getCurrentCurrency, + getSelectedAccount, + getShouldHideZeroBalanceTokens, + getPreferences, + getMarketData, + getChainIdsToPoll, +} from '../../../selectors'; + +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { formatValue, isValidAmount } from '../../../../app/scripts/lib/util'; +import { getIntlLocale } from '../../../ducks/locale/locale'; +import { + Display, + TextColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { Box, SensitiveText } from '../../component-library'; +import { getCalculatedTokenAmount1dAgo } from '../../../helpers/utils/util'; +import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountTotalCrossChainFiatBalance'; +import { useGetFormattedTokensPerChain } from '../../../hooks/useGetFormattedTokensPerChain'; +import { TokenWithBalance } from '../assets/asset-list/asset-list'; + +export const AggregatedPercentageOverviewCrossChains = () => { + const locale = useSelector(getIntlLocale); + const fiatCurrency = useSelector(getCurrentCurrency); + const { privacyMode } = useSelector(getPreferences); + const selectedAccount = useSelector(getSelectedAccount); + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const crossChainMarketData = useSelector(getMarketData); + const allChainIDs = useSelector(getChainIdsToPoll); + const { formattedTokensWithBalancesPerChain } = useGetFormattedTokensPerChain( + selectedAccount, + shouldHideZeroBalanceTokens, + false, + allChainIDs, + ); + const { + totalFiatBalance: totalFiatCrossChains, + tokenFiatBalancesCrossChains, + } = useAccountTotalCrossChainFiatBalance( + selectedAccount, + formattedTokensWithBalancesPerChain, + ); + + const getPerChainTotalFiat1dAgo = ( + chainId: string, + tokenFiatBalances: (string | undefined)[], + tokensWithBalances: TokenWithBalance[], + ) => { + const totalPerChain1dAgoERC20 = tokensWithBalances.reduce( + (total1dAgo: number, item: { address: string }, idx: number) => { + const found = + crossChainMarketData?.[chainId]?.[toChecksumAddress(item.address)]; + + const tokenFiat1dAgo = getCalculatedTokenAmount1dAgo( + tokenFiatBalances[idx], + found?.pricePercentChange1d, + ); + return total1dAgo + Number(tokenFiat1dAgo); + }, + 0, + ); + + return totalPerChain1dAgoERC20; + }; + + const totalFiat1dAgoCrossChains = useMemo(() => { + return tokenFiatBalancesCrossChains.reduce( + ( + total1dAgoCrossChains: number, + item: { + chainId: string; + nativeFiatValue: string; + tokenFiatBalances: (string | undefined)[]; + tokensWithBalances: TokenWithBalance[]; + }, + ) => { + const perChainERC20Total = getPerChainTotalFiat1dAgo( + item.chainId, + item.tokenFiatBalances, + item.tokensWithBalances, + ); + const nativePricePercentChange1d = + crossChainMarketData?.[item.chainId]?.[zeroAddress()] + ?.pricePercentChange1d; + + const nativeFiat1dAgo = getCalculatedTokenAmount1dAgo( + item.nativeFiatValue, + nativePricePercentChange1d, + ); + return ( + total1dAgoCrossChains + perChainERC20Total + Number(nativeFiat1dAgo) + ); + }, + 0, + ); // Initial total1dAgo is 0 + }, [tokenFiatBalancesCrossChains, crossChainMarketData]); + + const totalCrossChainBalance: number = Number(totalFiatCrossChains); + const crossChainTotalBalance1dAgo = totalFiat1dAgoCrossChains; + + const amountChangeCrossChains = + totalCrossChainBalance - crossChainTotalBalance1dAgo; + const percentageChangeCrossChains = + (amountChangeCrossChains / crossChainTotalBalance1dAgo) * 100 || 0; + + const formattedPercentChangeCrossChains = formatValue( + amountChangeCrossChains === 0 ? 0 : percentageChangeCrossChains, + true, + ); + + let formattedAmountChangeCrossChains = ''; + if (isValidAmount(amountChangeCrossChains)) { + formattedAmountChangeCrossChains = + (amountChangeCrossChains as number) >= 0 ? '+' : ''; + + const options = { + notation: 'compact', + compactDisplay: 'short', + maximumFractionDigits: 2, + } as const; + + try { + // For currencies compliant with ISO 4217 Standard + formattedAmountChangeCrossChains += `${Intl.NumberFormat(locale, { + ...options, + style: 'currency', + currency: fiatCurrency, + }).format(amountChangeCrossChains as number)} `; + } catch { + // Non-standard Currency Codes + formattedAmountChangeCrossChains += `${Intl.NumberFormat(locale, { + ...options, + minimumFractionDigits: 2, + style: 'decimal', + }).format(amountChangeCrossChains as number)} `; + } + } + + let color = TextColor.textDefault; + + if (!privacyMode && isValidAmount(amountChangeCrossChains)) { + if ((amountChangeCrossChains as number) === 0) { + color = TextColor.textDefault; + } else if ((amountChangeCrossChains as number) > 0) { + color = TextColor.successDefault; + } else { + color = TextColor.errorDefault; + } + } else { + color = TextColor.textAlternative; + } + + return ( + + + {formattedAmountChangeCrossChains} + + + {formattedPercentChangeCrossChains} + + + ); +}; diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index 671e03a87ea8..93c9e09ff0fd 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -150,6 +150,7 @@ describe('BtcOverview', () => { // The balances won't be available preferences: { showNativeTokenAsMainBalance: false, + tokenNetworkFilter: {}, }, }, }), diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 2244f3b33e82..b06f0c06b374 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -50,6 +50,8 @@ import { getTokensMarketData, getIsTestnet, getShouldShowAggregatedBalancePopover, + getIsTokenNetworkFilterEqualCurrentNetwork, + getChainIdsToPoll, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getDataCollectionForMarketing, getMetaMetricsId, @@ -61,7 +63,6 @@ import Spinner from '../../ui/spinner'; import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change'; import { getMultichainIsEvm } from '../../../selectors/multichain'; -import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; import { setAggregatedBalancePopoverShown, setPrivacyMode, @@ -69,9 +70,13 @@ import { import { useTheme } from '../../../hooks/useTheme'; import { getSpecificSettingsRoute } from '../../../helpers/utils/settings-search'; import { useI18nContext } from '../../../hooks/useI18nContext'; +import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountTotalCrossChainFiatBalance'; + +import { useGetFormattedTokensPerChain } from '../../../hooks/useGetFormattedTokensPerChain'; import WalletOverview from './wallet-overview'; import CoinButtons from './coin-buttons'; import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; +import { AggregatedPercentageOverviewCrossChains } from './aggregated-percentage-overview-cross-chains'; export type CoinOverviewProps = { account: InternalAccount; @@ -136,12 +141,23 @@ export const CoinOverview = ({ const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } = useSelector(getPreferences); + const isTokenNetworkFilterEqualCurrentNetwork = useSelector( + getIsTokenNetworkFilterEqualCurrentNetwork, + ); + const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, ); - const { totalFiatBalance, loading } = useAccountTotalFiatBalance( + const allChainIDs = useSelector(getChainIdsToPoll); + const { formattedTokensWithBalancesPerChain } = useGetFormattedTokensPerChain( account, shouldHideZeroBalanceTokens, + isTokenNetworkFilterEqualCurrentNetwork, + allChainIDs, + ); + const { totalFiatBalance } = useAccountTotalCrossChainFiatBalance( + account, + formattedTokensWithBalancesPerChain, ); const isEvm = useSelector(getMultichainIsEvm); @@ -150,7 +166,7 @@ export const CoinOverview = ({ let balanceToDisplay; if (isNotAggregatedFiatBalance) { balanceToDisplay = balance; - } else if (!loading) { + } else { balanceToDisplay = totalFiatBalance; } @@ -225,7 +241,13 @@ export const CoinOverview = ({ } return ( - + {isTokenNetworkFilterEqualCurrentNetwork || + !process.env.PORTFOLIO_VIEW ? ( + + ) : ( + + )} + { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - {t('aggregatedBalancePopover', [ - - {t('settings')} - , - ])} + {process.env.PORTFOLIO_VIEW + ? t('crossChainAggregatedBalancePopover', [ + + {t('settings')} + , + ]) + : t('aggregatedBalancePopover', [ + + {t('settings')} + , + ])} diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index a8c490b923c6..40c0b818649c 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -94,6 +94,7 @@ describe('EthOverview', () => { }, preferences: { showNativeTokenAsMainBalance: true, + tokenNetworkFilter: {}, }, useExternalServices: true, useCurrencyRateCheck: true, diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index 143c4d142a16..e63266080be5 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -47,8 +47,11 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; import { isAccountConnectedToCurrentTab, - getShowFiatInTestnets, getUseBlockie, + getShouldHideZeroBalanceTokens, + getIsTokenNetworkFilterEqualCurrentNetwork, + getShowFiatInTestnets, + getChainIdsToPoll, } from '../../../selectors'; import { getMultichainIsTestnet, @@ -67,6 +70,8 @@ import { useTheme } from '../../../hooks/useTheme'; // eslint-disable-next-line import/no-restricted-paths import { normalizeSafeAddress } from '../../../../app/scripts/lib/multichain/address'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; +import { useGetFormattedTokensPerChain } from '../../../hooks/useGetFormattedTokensPerChain'; +import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountTotalCrossChainFiatBalance'; import { AccountListItemMenuTypes } from './account-list-item.types'; const MAXIMUM_CURRENCY_DECIMALS = 3; @@ -99,6 +104,7 @@ const AccountListItem = ({ const setAccountListItemMenuRef = (ref) => { setAccountListItemMenuElement(ref); }; + const isTestnet = useMultichainSelector(getMultichainIsTestnet, account); const isMainnet = !isTestnet; const shouldShowFiat = useMultichainSelector( @@ -110,14 +116,39 @@ const AccountListItem = ({ shouldShowFiat && (isMainnet || (isTestnet && showFiatInTestnets)); const accountTotalFiatBalances = useMultichainAccountTotalFiatBalance(account); + // cross chain agg balance + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const isTokenNetworkFilterEqualCurrentNetwork = useSelector( + getIsTokenNetworkFilterEqualCurrentNetwork, + ); + const allChainIDs = useSelector(getChainIdsToPoll); + const { formattedTokensWithBalancesPerChain } = useGetFormattedTokensPerChain( + account, + shouldHideZeroBalanceTokens, + isTokenNetworkFilterEqualCurrentNetwork, + allChainIDs, + ); + const { totalFiatBalance } = useAccountTotalCrossChainFiatBalance( + account, + formattedTokensWithBalancesPerChain, + ); + // cross chain agg balance const mappedOrderedTokenList = accountTotalFiatBalances.orderedTokenList.map( (item) => ({ avatarValue: item.iconUrl, }), ); - const balanceToTranslate = isEvmNetwork - ? account.balance - : accountTotalFiatBalances.totalBalance; + let balanceToTranslate; + if (isEvmNetwork) { + balanceToTranslate = + isTestnet || !process.env.PORTFOLIO_VIEW + ? account.balance + : totalFiatBalance; + } else { + balanceToTranslate = accountTotalFiatBalances.totalBalance; + } ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) const custodianIcon = useSelector((state) => @@ -313,6 +344,9 @@ const AccountListItem = ({ value={balanceToTranslate} type={PRIMARY} showFiat={showFiat} + isAggregatedFiatOverviewBalance={ + !isTestnet && process.env.PORTFOLIO_VIEW + } data-testid="first-currency-display" privacyMode={privacyMode} /> diff --git a/ui/components/multichain/pages/send/send.test.js b/ui/components/multichain/pages/send/send.test.js index 5195ee15de5b..bc8db6adf3b6 100644 --- a/ui/components/multichain/pages/send/send.test.js +++ b/ui/components/multichain/pages/send/send.test.js @@ -89,6 +89,7 @@ const baseStore = { }, }, metamask: { + accountsByChainId: {}, permissionHistory: {}, transactions: [ { @@ -168,6 +169,7 @@ const baseStore = { tokens: [], preferences: { showFiatInTestnets: true, + tokenNetworkFilter: {}, }, currentCurrency: 'USD', nativeCurrency: 'ETH', diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 2b5561739acf..af456e29acbc 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -369,6 +369,10 @@ export function getConversionRate(state) { ?.conversionRate; } +export function getCurrencyRates(state) { + return state.metamask.currencyRates; +} + export function getSendHexDataFeatureFlagState(state) { return state.metamask.featureFlags.sendHexData; } diff --git a/ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts b/ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts new file mode 100644 index 000000000000..9fe819d92171 --- /dev/null +++ b/ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts @@ -0,0 +1,226 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-dom/test-utils'; +import { + getCurrentCurrency, + getNetworkConfigurationsByChainId, + getCrossChainTokenExchangeRates, + getCrossChainMetaMaskCachedBalances, +} from '../selectors'; +import { getCurrencyRates } from '../ducks/metamask/metamask'; +import { + FormattedTokensWithBalances, + useAccountTotalCrossChainFiatBalance, +} from './useAccountTotalCrossChainFiatBalance'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn((selector) => selector()), +})); + +jest.mock('../selectors', () => ({ + getCurrentCurrency: jest.fn(), + getNetworkConfigurationsByChainId: jest.fn(), + getCrossChainTokenExchangeRates: jest.fn(), + getCrossChainMetaMaskCachedBalances: jest.fn(), +})); +jest.mock('../ducks/metamask/metamask', () => ({ + getCurrencyRates: jest.fn(), +})); + +const mockGetCurrencyRates = getCurrencyRates as jest.Mock; +const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock; +const mockGetNetworkConfigurationsByChainId = + getNetworkConfigurationsByChainId as unknown as jest.Mock; +const mockGetCrossChainTokenExchangeRates = + getCrossChainTokenExchangeRates as jest.Mock; +const mockGetCrossChainMetaMaskCachedBalances = + getCrossChainMetaMaskCachedBalances as jest.Mock; + +const mockUseTokenBalances = jest.fn().mockReturnValue({ + tokenBalances: { + '0xac7985f2e57609bdd7ad3003e4be868d83e4b6d5': { + '0x1': { + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0x2f18e6', + '0x6B175474E89094C44Da98b954EedeAC495271d0F': '0x378afc9a77b47a30', + }, + }, + }, +}); +jest.mock('./useTokenBalances', () => ({ + useTokenBalances: () => mockUseTokenBalances(), + stringifyBalance: jest.fn(), +})); + +const mockCurrencyRates = { + ETH: { + conversionDate: 1732040829.246, + conversionRate: 3124.56, + usdConversionRate: 3124.56, + }, + LineaETH: { + conversionDate: 1732040829.246, + conversionRate: 3124.56, + usdConversionRate: 3124.56, + }, +}; + +const mockNetworkConfigs = { + '0x1': { + blockExplorerUrls: ['https://etherscan.io'], + chainId: '0x1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, + '0xe708': { + blockExplorerUrls: ['https://lineascan.build'], + chainId: '0xe708', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Linea', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'linea-mainnet', + type: 'infura', + url: 'https://linea-mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, +}; + +const mockCrossChainTokenExchangeRates = { + '0x1': { + '0x0000000000000000000000000000000000000000': 1.0000131552270237, + '0x4d224452801ACEd8B2F0aebE155379bb5D594381': 0.0003643652288147761, + '0x6982508145454Ce325dDbE47a25d4ec3d2311933': 6.62249784302e-9, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': 0.00031961862176734744, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 0.00031993824038911484, + '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84': 0.9994154684043188, + }, + '0xe708': { + '0x0000000000000000000000000000000000000000': 0.9999084951480334, + }, +}; + +const mockCachedBalances = { + '0x1': { + '0xac7985f2e57609bdd7ad3003e4be868d83e4b6d5': '0x4e2adedda15fd6', + }, + '0xe708': { + '0xac7985f2e57609bdd7ad3003e4be868d83e4b6d5': '0x4e2adedda15fd6', + }, +}; + +describe('useAccountTotalCrossChainFiatBalance', () => { + beforeEach(() => { + mockGetCurrencyRates.mockReturnValue(mockCurrencyRates); + mockGetCurrentCurrency.mockReturnValue('usd'); + mockGetNetworkConfigurationsByChainId.mockReturnValue(mockNetworkConfigs); + mockGetCrossChainTokenExchangeRates.mockReturnValue( + mockCrossChainTokenExchangeRates, + ); + mockGetCrossChainMetaMaskCachedBalances.mockReturnValue(mockCachedBalances); + + jest.clearAllMocks(); + }); + it('should return totalFiatBalance successfully for eth and linea', async () => { + const testAccount = { + id: '7d3a1213-c465-4995-b42a-85e2ccfd2f22', + address: '0xac7985f2e57609bdd7ad3003e4be868d83e4b6d5', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + }; + const testFormattedTokensWithBalances = [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + balance: '3086566', + string: '3.08656', + image: '', + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + decimals: 18, + balance: '4002288959235586608', + string: '4.00228', + image: '', + }, + ], + }, + { + chainId: '0xe708', + tokensWithBalances: [], + }, + ]; + + const expectedResult = { + tokenFiatBalancesCrossChains: [ + { + chainId: '0x1', + nativeFiatValue: '68.75', + tokenFiatBalances: ['3.09', '4'], + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + balance: '3086566', + decimals: 6, + image: '', + string: '3.08656', + symbol: 'USDC', + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + balance: '4002288959235586608', + decimals: 18, + image: '', + string: '4.00228', + symbol: 'DAI', + }, + ], + }, + { + chainId: '0xe708', + nativeFiatValue: '68.75', + tokenFiatBalances: [], + tokensWithBalances: [], + }, + ], + totalFiatBalance: '144.59', + }; + + let result; + await act(async () => { + result = renderHook(() => + useAccountTotalCrossChainFiatBalance( + testAccount, + testFormattedTokensWithBalances as FormattedTokensWithBalances[], + ), + ); + }); + + expect((result as unknown as Record).result.current).toEqual( + expectedResult, + ); + }); +}); diff --git a/ui/hooks/useAccountTotalCrossChainFiatBalance.ts b/ui/hooks/useAccountTotalCrossChainFiatBalance.ts new file mode 100644 index 000000000000..d63328e4fbcf --- /dev/null +++ b/ui/hooks/useAccountTotalCrossChainFiatBalance.ts @@ -0,0 +1,117 @@ +import { shallowEqual, useSelector } from 'react-redux'; +import { toChecksumAddress } from 'ethereumjs-util'; +import { + getCurrentCurrency, + getNetworkConfigurationsByChainId, + getCrossChainTokenExchangeRates, + getCrossChainMetaMaskCachedBalances, +} from '../selectors'; +import { + getValueFromWeiHex, + sumDecimals, +} from '../../shared/modules/conversion.utils'; +import { getCurrencyRates } from '../ducks/metamask/metamask'; +import { getTokenFiatAmount } from '../helpers/utils/token-util'; +import { TokenWithBalance } from '../components/app/assets/asset-list/asset-list'; + +type AddressBalances = { + [address: string]: number; +}; + +export type Balances = { + [id: string]: AddressBalances; +}; + +export type FormattedTokensWithBalances = { + chainId: string; + tokensWithBalances: TokenWithBalance[]; +}; + +export const useAccountTotalCrossChainFiatBalance = ( + account: { address: string }, + formattedTokensWithBalancesPerChain: FormattedTokensWithBalances[], +) => { + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const currencyRates = useSelector(getCurrencyRates); + const currentCurrency = useSelector(getCurrentCurrency); + + const crossChainContractRates = useSelector( + getCrossChainTokenExchangeRates, + shallowEqual, + ); + const crossChainCachedBalances: Balances = useSelector( + getCrossChainMetaMaskCachedBalances, + ); + const mergedCrossChainRates: Balances = { + ...crossChainContractRates, // todo add confirmation exchange rates? + }; + + const tokenFiatBalancesCrossChains = formattedTokensWithBalancesPerChain.map( + (singleChainTokenBalances) => { + const { tokensWithBalances } = singleChainTokenBalances; + const matchedChainSymbol = + allNetworks[singleChainTokenBalances.chainId as `0x${string}`] + .nativeCurrency; + const conversionRate = + currencyRates?.[matchedChainSymbol]?.conversionRate; + const tokenFiatBalances = tokensWithBalances.map((token) => { + const tokenExchangeRate = + mergedCrossChainRates?.[singleChainTokenBalances.chainId]?.[ + toChecksumAddress(token.address) + ]; + const totalFiatValue = getTokenFiatAmount( + tokenExchangeRate, + conversionRate, + currentCurrency, + token.string, + token.symbol, + false, + false, + ); + + return totalFiatValue; + }); + + const balanceCached = + crossChainCachedBalances?.[singleChainTokenBalances.chainId]?.[ + account?.address + ] ?? 0; + const nativeFiatValue = getValueFromWeiHex({ + value: balanceCached, + toCurrency: currentCurrency, + conversionRate, + numberOfDecimals: 2, + }); + return { + ...singleChainTokenBalances, + tokenFiatBalances, + nativeFiatValue, + }; + }, + ); + + const finalTotal = tokenFiatBalancesCrossChains.reduce( + (accumulator, currentValue) => { + const tmpCurrentValueFiatBalances: string[] = + currentValue.tokenFiatBalances.filter( + (value): value is string => value !== undefined, + ); + const totalFiatBalance = sumDecimals( + currentValue.nativeFiatValue, + ...tmpCurrentValueFiatBalances, + ); + + const totalAsNumber = totalFiatBalance.toNumber + ? totalFiatBalance.toNumber() + : Number(totalFiatBalance); + + return accumulator + totalAsNumber; + }, + 0, + ); + + return { + totalFiatBalance: finalTotal.toString(10), + tokenFiatBalancesCrossChains, + }; +}; diff --git a/ui/hooks/useGetFormattedTokensPerChain.test.ts b/ui/hooks/useGetFormattedTokensPerChain.test.ts new file mode 100644 index 000000000000..973a16b0e648 --- /dev/null +++ b/ui/hooks/useGetFormattedTokensPerChain.test.ts @@ -0,0 +1,152 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-dom/test-utils'; +import { getAllTokens, getCurrentChainId } from '../selectors'; +import { useGetFormattedTokensPerChain } from './useGetFormattedTokensPerChain'; +import { stringifyBalance } from './useTokenBalances'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn((selector) => selector()), +})); + +jest.mock('../selectors', () => ({ + getCurrentChainId: jest.fn(), + getAllTokens: jest.fn(), +})); + +const mockGetAllTokens = getAllTokens as jest.Mock; +const mockGetCurrentChainId = getCurrentChainId as jest.Mock; + +const mockUseTokenBalances = jest.fn().mockReturnValue({ + tokenBalances: { + '0xac7985f2e57609bdd7ad3003e4be868d83e4b6d5': { + '0x1': { + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0x2f18e6', + '0x6B175474E89094C44Da98b954EedeAC495271d0F': '0x378afc9a77b47a30', + }, + }, + }, +}); +jest.mock('./useTokenBalances', () => ({ + useTokenBalances: () => mockUseTokenBalances(), + stringifyBalance: jest.fn(), +})); + +const allTokens = { + '0x1': { + '0xac7985f2e57609bdd7ad3003e4be868d83e4b6d5': [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + aggregators: [ + 'Metamask', + 'Aave', + 'Bancor', + 'Crypto.com', + '1inch', + 'PMM', + 'Sushiswap', + 'Zerion', + 'Lifi', + 'Socket', + 'Squid', + 'Openswap', + 'UniswapLabs', + 'Coinmarketcap', + ], + decimals: 6, + symbol: 'USDC', + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + aggregators: [ + 'Metamask', + 'Aave', + 'Bancor', + 'CMC', + 'Crypto.com', + '1inch', + 'PMM', + 'Sushiswap', + 'Zerion', + 'Lifi', + 'Socket', + 'Squid', + 'Openswap', + 'UniswapLabs', + 'Coinmarketcap', + ], + decimals: 18, + symbol: 'DAI', + }, + ], + }, +}; + +describe('useGetFormattedTokensPerChain', () => { + beforeEach(() => { + mockGetAllTokens.mockReturnValue(allTokens); + mockGetCurrentChainId.mockReturnValue('0x1'); + + jest.clearAllMocks(); + }); + it('should tokensWithBalances for an array of chainIds', async () => { + (stringifyBalance as jest.Mock).mockReturnValueOnce(10.5); + (stringifyBalance as jest.Mock).mockReturnValueOnce(13); + const allChainIDs = ['0x1']; + const isTokenNetworkFilterEqualCurrentNetwork = true; + const shouldHideZeroBalanceTokens = true; + const testAccount = { + id: '7d3a1213-c465-4995-b42a-85e2ccfd2f22', + address: '0xac7985f2e57609bdd7ad3003e4be868d83e4b6d5', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + }; + + const expectedResult = { + formattedTokensWithBalancesPerChain: [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + balance: '3086566', + decimals: 6, + string: 10.5, + symbol: 'USDC', + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + balance: '4002288959235586608', + decimals: 18, + string: 13, + symbol: 'DAI', + }, + ], + }, + ], + }; + + let result; + await act(async () => { + result = renderHook(() => + useGetFormattedTokensPerChain( + testAccount, + shouldHideZeroBalanceTokens, + isTokenNetworkFilterEqualCurrentNetwork, + allChainIDs, + ), + ); + }); + + expect((result as unknown as Record).result.current).toEqual( + expectedResult, + ); + }); +}); diff --git a/ui/hooks/useGetFormattedTokensPerChain.ts b/ui/hooks/useGetFormattedTokensPerChain.ts new file mode 100644 index 000000000000..a69f5be9e1e0 --- /dev/null +++ b/ui/hooks/useGetFormattedTokensPerChain.ts @@ -0,0 +1,77 @@ +import { useSelector } from 'react-redux'; +import { BN } from 'bn.js'; +import { Token } from '@metamask/assets-controllers'; +import { getAllTokens, getCurrentChainId } from '../selectors'; +import { hexToDecimal } from '../../shared/modules/conversion.utils'; + +import { TokenWithBalance } from '../components/multichain/asset-picker-amount/asset-picker-modal/types'; +import { stringifyBalance, useTokenBalances } from './useTokenBalances'; + +type AddressMapping = { + [chainId: string]: { + [tokenAddress: string]: string; + }; +}; + +type TokenBalancesMapping = { + [address: string]: AddressMapping; +}; + +export const useGetFormattedTokensPerChain = ( + account: { address: string }, + shouldHideZeroBalanceTokens: boolean, + shouldGetTokensPerCurrentChain: boolean, + allChainIDs: string[], +) => { + const currentChainId = useSelector(getCurrentChainId); + + const importedTokens = useSelector(getAllTokens); // returns the tokens only when they are imported + const currentTokenBalances: { tokenBalances: TokenBalancesMapping } = + useTokenBalances({ + chainIds: allChainIDs as `0x${string}`[], + }); + + // We will calculate aggregated balance only after the user imports the tokens to the wallet + // we need to format the balances we get from useTokenBalances and match them with symbol and decimals we get from getAllTokens + const networksToFormat = shouldGetTokensPerCurrentChain + ? [currentChainId] + : allChainIDs; + const formattedTokensWithBalancesPerChain = networksToFormat.map( + (singleChain) => { + const tokens = importedTokens?.[singleChain]?.[account?.address] ?? []; + + const tokensWithBalances = tokens.reduce( + (acc: TokenWithBalance[], token: Token) => { + const hexBalance = + currentTokenBalances.tokenBalances[account.address]?.[ + singleChain + ]?.[token.address] ?? '0x0'; + if (hexBalance !== '0x0' || !shouldHideZeroBalanceTokens) { + const decimalBalance = hexToDecimal(hexBalance); + acc.push({ + address: token.address, + symbol: token.symbol, + decimals: token.decimals, + balance: decimalBalance, + string: stringifyBalance( + new BN(decimalBalance), + new BN(token.decimals), + ), + }); + } + return acc; + }, + [], + ); + + return { + chainId: singleChain, + tokensWithBalances, + }; + }, + ); + + return { + formattedTokensWithBalancesPerChain, + }; +}; diff --git a/ui/hooks/useTokenBalances.ts b/ui/hooks/useTokenBalances.ts index 9ff92d488814..8d3a078f8d07 100644 --- a/ui/hooks/useTokenBalances.ts +++ b/ui/hooks/useTokenBalances.ts @@ -68,7 +68,11 @@ export const useTokenTracker = ({ // From https://github.com/MetaMask/eth-token-tracker/blob/main/lib/util.js // Ensures backwards compatibility with display formatting. -function stringifyBalance(balance: BN, bnDecimals: BN, balanceDecimals = 5) { +export function stringifyBalance( + balance: BN, + bnDecimals: BN, + balanceDecimals = 5, +) { if (balance.eq(new BN(0))) { return '0'; } diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index 8a516fd76d6a..6c08c9130761 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -112,6 +112,7 @@ describe('Routes Component', () => { ...mockSendState.metamask.swapsState, swapsFeatureIsLive: true, }, + accountsByChainId: {}, pendingApprovals: {}, approvalFlows: [], announcements: {}, @@ -123,6 +124,7 @@ describe('Routes Component', () => { order: 'dsc', sortCallback: 'stringNumeric', }, + tokenNetworkFilter: {}, }, }, send: { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 67d272b7642b..7e4d04eeb3de 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -449,6 +449,21 @@ export function getMetaMaskCachedBalances(state) { return {}; } +export function getCrossChainMetaMaskCachedBalances(state) { + const allAccountsByChainId = state.metamask.accountsByChainId; + return Object.keys(allAccountsByChainId).reduce((acc, topLevelKey) => { + acc[topLevelKey] = Object.keys(allAccountsByChainId[topLevelKey]).reduce( + (innerAcc, innerKey) => { + innerAcc[innerKey] = + allAccountsByChainId[topLevelKey][innerKey].balance; + return innerAcc; + }, + {}, + ); + + return acc; + }, {}); +} /** * @typedef {import('./selectors.types').InternalAccountWithBalance} InternalAccountWithBalance */ @@ -568,7 +583,6 @@ export function getTargetAccount(state, targetAddress) { export const getTokenExchangeRates = (state) => { const chainId = getCurrentChainId(state); const contractMarketData = state.metamask.marketData?.[chainId] ?? {}; - return Object.entries(contractMarketData).reduce( (acc, [address, marketData]) => { acc[address] = marketData?.price ?? null; @@ -578,6 +592,22 @@ export const getTokenExchangeRates = (state) => { ); }; +export const getCrossChainTokenExchangeRates = (state) => { + const contractMarketData = state.metamask.marketData ?? {}; + + return Object.keys(contractMarketData).reduce((acc, topLevelKey) => { + acc[topLevelKey] = Object.keys(contractMarketData[topLevelKey]).reduce( + (innerAcc, innerKey) => { + innerAcc[innerKey] = contractMarketData[topLevelKey][innerKey]?.price; + return innerAcc; + }, + {}, + ); + + return acc; + }, {}); +}; + /** * Get market data for tokens on the current chain * @@ -954,6 +984,19 @@ export function getPetnamesEnabled(state) { return petnamesEnabled; } +export function getIsTokenNetworkFilterEqualCurrentNetwork(state) { + const chainId = getCurrentChainId(state); + const { tokenNetworkFilter: tokenNetworkFilterValue } = getPreferences(state); + const tokenNetworkFilter = tokenNetworkFilterValue || {}; + if ( + Object.keys(tokenNetworkFilter).length === 1 && + Object.keys(tokenNetworkFilter)[0] === chainId + ) { + return true; + } + return false; +} + export function getUseTransactionSimulations(state) { return Boolean(state.metamask.useTransactionSimulations); } From 220e932002ebcf94b7e366c03ae1eb078ec8e7aa Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 20 Nov 2024 17:13:12 +0100 Subject: [PATCH 02/28] chore: Bump Snaps packages (#28557) ## **Description** Bump Snaps packages and handle any required changes. Summary of Snaps changes: - Add support for `Address` in `Card` title - Make `fetch` responses an instance of `Response` - Add `isSecureContext` global - Use `arrayBuffer` for fetching local Snaps - Add interface persistence (unused for now) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28557?quickstart=1) --------- Co-authored-by: Guillaume Roux --- app/scripts/metamask-controller.js | 3 + builds.yml | 8 +-- package.json | 10 ++-- .../app/snaps/snap-ui-card/snap-ui-card.tsx | 4 +- .../snaps/snap-ui-renderer/components/card.ts | 41 +++++++++---- yarn.lock | 58 +++++++++---------- 6 files changed, 74 insertions(+), 50 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b59b21ae8111..bfd1d88708d8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1526,6 +1526,9 @@ export default class MetamaskController extends EventEmitter { `${this.approvalController.name}:acceptRequest`, `${this.snapController.name}:get`, ], + allowedEvents: [ + 'NotificationServicesController:notificationsListUpdated', + ], }); this.snapInterfaceController = new SnapInterfaceController({ diff --git a/builds.yml b/builds.yml index 316e8f943eb1..fe33507c1e4a 100644 --- a/builds.yml +++ b/builds.yml @@ -27,7 +27,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.10.0/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -48,7 +48,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.10.0/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -70,7 +70,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - REJECT_INVALID_SNAPS_PLATFORM_VERSION: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.10.0/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -94,7 +94,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.10.0/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://support.metamask-institutional.io - SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io diff --git a/package.json b/package.json index 6d97d41c658f..e042fa713ea2 100644 --- a/package.json +++ b/package.json @@ -233,7 +233,7 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.10.0", + "@metamask/snaps-sdk": "^6.11.0", "@swc/types@0.1.5": "^0.1.6", "@babel/core": "patch:@babel/core@npm%3A7.25.9#~/.yarn/patches/@babel-core-npm-7.25.9-4ae3bff7f3.patch", "@babel/runtime": "patch:@babel/runtime@npm%3A7.25.9#~/.yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.patch", @@ -348,11 +348,11 @@ "@metamask/selected-network-controller": "^18.0.2", "@metamask/signature-controller": "^21.1.0", "@metamask/smart-transactions-controller": "^13.0.0", - "@metamask/snaps-controllers": "^9.12.0", - "@metamask/snaps-execution-environments": "^6.9.2", + "@metamask/snaps-controllers": "^9.13.0", + "@metamask/snaps-execution-environments": "^6.10.0", "@metamask/snaps-rpc-methods": "^11.5.1", - "@metamask/snaps-sdk": "^6.10.0", - "@metamask/snaps-utils": "^8.5.2", + "@metamask/snaps-sdk": "^6.11.0", + "@metamask/snaps-utils": "^8.6.0", "@metamask/solana-wallet-snap": "^0.1.9", "@metamask/transaction-controller": "^38.3.0", "@metamask/user-operation-controller": "^13.0.0", diff --git a/ui/components/app/snaps/snap-ui-card/snap-ui-card.tsx b/ui/components/app/snaps/snap-ui-card/snap-ui-card.tsx index b7a0468b5315..15e33ee201c8 100644 --- a/ui/components/app/snaps/snap-ui-card/snap-ui-card.tsx +++ b/ui/components/app/snaps/snap-ui-card/snap-ui-card.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, ReactNode } from 'react'; import { Display, FlexDirection, @@ -13,7 +13,7 @@ import { SnapUIImage } from '../snap-ui-image'; export type SnapUICardProps = { image?: string | undefined; - title: string; + title: string | ReactNode; description?: string | undefined; value: string; extra?: string | undefined; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/card.ts b/ui/components/app/snaps/snap-ui-renderer/components/card.ts index 64c2b1c12a57..932d1e7a532e 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/card.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/card.ts @@ -1,13 +1,34 @@ import { CardElement } from '@metamask/snaps-sdk/jsx'; +import { mapToTemplate } from '../utils'; import { UIComponentFactory } from './types'; -export const card: UIComponentFactory = ({ element }) => ({ - element: 'SnapUICard', - props: { - image: element.props.image, - title: element.props.title, - description: element.props.description, - value: element.props.value, - extra: element.props.extra, - }, -}); +export const card: UIComponentFactory = ({ + element, + ...params +}) => { + if (typeof element.props.title !== 'string') { + return { + element: 'SnapUICard', + props: { + image: element.props.image, + description: element.props.description, + value: element.props.value, + extra: element.props.extra, + }, + propComponents: { + title: mapToTemplate({ element: element.props.title, ...params }), + }, + }; + } + + return { + element: 'SnapUICard', + props: { + image: element.props.image, + title: element.props.title, + description: element.props.description, + value: element.props.value, + extra: element.props.extra, + }, + }; +}; diff --git a/yarn.lock b/yarn.lock index ea2e201ab268..d14fee7c6d97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6381,9 +6381,9 @@ __metadata: linkType: hard "@metamask/slip44@npm:^4.0.0": - version: 4.0.0 - resolution: "@metamask/slip44@npm:4.0.0" - checksum: 10/3e47e8834b0fbdabe1f126fd78665767847ddc1f9ccc8defb23007dd71fcd2e4899c8ca04857491be3630668a3765bad1e40fdfca9a61ef33236d8d08e51535e + version: 4.1.0 + resolution: "@metamask/slip44@npm:4.1.0" + checksum: 10/4265254a1800a24915bd1de15f86f196737132f9af2a084c2efc885decfc5dd87ad8f0687269d90b35e2ec64d3ea4fbff0caa793bcea6e585b1f3a290952b750 languageName: node linkType: hard @@ -6410,9 +6410,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.12.0": - version: 9.12.0 - resolution: "@metamask/snaps-controllers@npm:9.12.0" +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.13.0": + version: 9.13.0 + resolution: "@metamask/snaps-controllers@npm:9.13.0" dependencies: "@metamask/approval-controller": "npm:^7.1.1" "@metamask/base-controller": "npm:^7.0.2" @@ -6425,8 +6425,8 @@ __metadata: "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/snaps-registry": "npm:^3.2.2" "@metamask/snaps-rpc-methods": "npm:^11.5.1" - "@metamask/snaps-sdk": "npm:^6.10.0" - "@metamask/snaps-utils": "npm:^8.5.0" + "@metamask/snaps-sdk": "npm:^6.11.0" + "@metamask/snaps-utils": "npm:^8.6.0" "@metamask/utils": "npm:^10.0.0" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6440,30 +6440,30 @@ __metadata: semver: "npm:^7.5.4" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.9.2 + "@metamask/snaps-execution-environments": ^6.10.0 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/8d411ff2cfd43e62fe780092e935a1d977379488407b56cca1390edfa9408871cbaf3599f6e6ee999340d46fd3650f225a3270ceec9492c6f2dc4d93538c25ae + checksum: 10/bcf60b61de067f89439cb15acbdf6f808b4bcda8e1cbc9debd693ca2c545c9d38c4e6f380191c4703bd9d28d7dd41e4ce5111664d7b474d5e86e460bcefc3637 languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.9.2": - version: 6.9.2 - resolution: "@metamask/snaps-execution-environments@npm:6.9.2" +"@metamask/snaps-execution-environments@npm:^6.10.0": + version: 6.10.0 + resolution: "@metamask/snaps-execution-environments@npm:6.10.0" dependencies: "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/snaps-sdk": "npm:^6.10.0" - "@metamask/snaps-utils": "npm:^8.5.0" + "@metamask/snaps-sdk": "npm:^6.11.0" + "@metamask/snaps-utils": "npm:^8.6.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/f81dd3728417dc63ed16b102504cdf6c815bffef7b1dad9e7b0e064618b008e1f0fe6d05c225bcafeee09fb4bc473599ee710e1a26a6f3604e965f656fce8e36 + checksum: 10/a881696ec942f268d7485869fcb8c6bc0c278319bbfaf7e5c6099e86278c7f59049595f00ecfc27511d0106b5ad2f7621f734c7b17f088b835e38e638d80db01 languageName: node linkType: hard @@ -6495,16 +6495,16 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.10.0": - version: 6.10.0 - resolution: "@metamask/snaps-sdk@npm:6.10.0" +"@metamask/snaps-sdk@npm:^6.11.0": + version: 6.11.0 + resolution: "@metamask/snaps-sdk@npm:6.11.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" - checksum: 10/02f04536328a64ff1e9e48fb6b109698d6d83f42af5666a9758ccb1e7a1e67c0c2e296ef2fef419dd3d1c8f26bbf30b9f31911a1baa66f044f21cd0ecb7a11a7 + checksum: 10/0f9b507139d1544b1b3d85ff8de81b800d543012d3ee9414c607c23abe9562e0dca48de089ed94be69f5ad981730a0f443371edfe6bc2d5ffb140b28e437bfd2 languageName: node linkType: hard @@ -6539,9 +6539,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.5.0, @metamask/snaps-utils@npm:^8.5.2": - version: 8.5.2 - resolution: "@metamask/snaps-utils@npm:8.5.2" +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.5.0, @metamask/snaps-utils@npm:^8.6.0": + version: 8.6.0 + resolution: "@metamask/snaps-utils@npm:8.6.0" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6551,7 +6551,7 @@ __metadata: "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.2" - "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-sdk": "npm:^6.11.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" @@ -6566,7 +6566,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/e5d1344f948473e82d71007d2570272073cf070f40aa7746692a6d5e6f02cfce66a747cf50f439d32b29a3f6588486182453b26973f0d0c1d9f47914591d5790 + checksum: 10/c0f538f3f95e1875f6557b6ecc32f981bc4688d581af8cdc62c6c3ab8951c138286cd0b2d1cd82f769df24fcec10f71dcda67ae9a47edcff9ff73d52672df191 languageName: node linkType: hard @@ -26806,11 +26806,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^18.0.2" "@metamask/signature-controller": "npm:^21.1.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" - "@metamask/snaps-controllers": "npm:^9.12.0" - "@metamask/snaps-execution-environments": "npm:^6.9.2" + "@metamask/snaps-controllers": "npm:^9.13.0" + "@metamask/snaps-execution-environments": "npm:^6.10.0" "@metamask/snaps-rpc-methods": "npm:^11.5.1" - "@metamask/snaps-sdk": "npm:^6.10.0" - "@metamask/snaps-utils": "npm:^8.5.2" + "@metamask/snaps-sdk": "npm:^6.11.0" + "@metamask/snaps-utils": "npm:^8.6.0" "@metamask/solana-wallet-snap": "npm:^0.1.9" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" From fac35b88a07e73fe9f3230b23534b0ed2408ba36 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 20 Nov 2024 17:17:47 +0100 Subject: [PATCH 03/28] feat: account syncing various updates (#28541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds various account syncing house keeping improvements in order to be ready for re-enablement in a subsequent PR. The number of files changed by this PR is large, but none affects a user facing feature, since account syncing is disabled in production. - Bump `@metamask/profile-sync-controller` to version `1.0.2` - Add two new state keys linked to `UserStorageController`, `isAccountSyncingReadyToBeDispatched` and `hasAccountSyncingSyncedAtLeastOnce` - Wait for `_addAccountsWithBalance` to finish adding accounts after onboarding, then set `isAccountSyncingReadyToBeDispatched` to `true` - Use `USER_STORAGE_FEATURE_NAMES` exported constant from `@metamask/profile-sync-controller` to define user storage paths everywhere (no more magic strings) - Add batch delete delete support for E2E util `UserStorageMockttpController` - Update all account sync E2E tests in order to wait for account sync to have successfully been completed once before going on with the rest of the instructions. ⚠️ Please note that this PR does **NOT** re-enable account syncing in production. This will be done in a subsequent PR. ## **Related issues** ## **Manual testing steps** 1. Import your SRP 2. Add accounts, rename accounts 3. Uninstall & reinstall 4. Import the same SRP 5. Verify that previous updates made in step 2 are there ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- app/scripts/constants/sentry-state.ts | 2 + app/scripts/metamask-controller.js | 87 ++++--- lavamoat/browserify/beta/policy.json | 32 +-- lavamoat/browserify/flask/policy.json | 32 +-- lavamoat/browserify/main/policy.json | 32 +-- lavamoat/browserify/mmi/policy.json | 32 +-- package.json | 2 +- shared/constants/metametrics.ts | 1 + .../userStorageMockttpController.test.ts | 221 ++++++++++++------ .../userStorageMockttpController.ts | 120 ++++++---- test/e2e/page-objects/pages/homepage.ts | 12 + ...rs-after-init-opt-in-background-state.json | 4 +- .../errors-after-init-opt-in-ui-state.json | 2 + .../importing-private-key-account.spec.ts | 18 +- .../account-syncing/new-user-sync.spec.ts | 13 +- .../onboarding-with-opt-out.spec.ts | 21 +- .../sync-after-adding-account.spec.ts | 44 +++- .../sync-after-modifying-account-name.spec.ts | 18 +- .../sync-after-onboarding.spec.ts | 12 +- test/e2e/tests/notifications/mocks.ts | 34 ++- .../data/notification-state.ts | 2 + .../useProfileSyncing/profileSyncing.test.tsx | 4 + .../useProfileSyncing/profileSyncing.ts | 11 +- .../profile-syncing.test.ts | 2 + .../metamask-notifications/profile-syncing.ts | 21 +- ui/store/actions.test.js | 5 +- ui/store/actions.ts | 3 +- yarn.lock | 121 ++++++++-- 28 files changed, 582 insertions(+), 326 deletions(-) diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 5146e38e8a41..bd953e72d49c 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -389,6 +389,8 @@ export const SENTRY_BACKGROUND_STATE = { UserStorageController: { isProfileSyncingEnabled: true, isProfileSyncingUpdateLoading: false, + hasAccountSyncingSyncedAtLeastOnce: false, + isAccountSyncingReadyToBeDispatched: false, }, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) ...MMI_SENTRY_BACKGROUND_STATE, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index bfd1d88708d8..3b75349091ad 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1598,6 +1598,16 @@ export default class MetamaskController extends EventEmitter { }, }); }, + onAccountSyncErroneousSituation: (profileId, situationMessage) => { + this.metaMetricsController.trackEvent({ + category: MetaMetricsEventCategory.ProfileSyncing, + event: MetaMetricsEventName.AccountsSyncErroneousSituation, + properties: { + profile_id: profileId, + situation_message: situationMessage, + }, + }); + }, }, }, env: { @@ -1768,10 +1778,11 @@ export default class MetamaskController extends EventEmitter { if (!prevCompletedOnboarding && currCompletedOnboarding) { const { address } = this.accountsController.getSelectedAccount(); - this._addAccountsWithBalance(); + await this._addAccountsWithBalance(); this.postOnboardingInitialization(); this.triggerNetworkrequests(); + // execute once the token detection on the post-onboarding await this.tokenDetectionController.detectTokens({ selectedAddress: address, @@ -4441,43 +4452,51 @@ export default class MetamaskController extends EventEmitter { } async _addAccountsWithBalance() { - // Scan accounts until we find an empty one - const chainId = getCurrentChainId({ - metamask: this.networkController.state, - }); - const ethQuery = new EthQuery(this.provider); - const accounts = await this.keyringController.getAccounts(); - let address = accounts[accounts.length - 1]; - - for (let count = accounts.length; ; count++) { - const balance = await this.getBalance(address, ethQuery); - - if (balance === '0x0') { - // This account has no balance, so check for tokens - await this.tokenDetectionController.detectTokens({ - chainIds: [chainId], - selectedAddress: address, - }); + try { + // Scan accounts until we find an empty one + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); + const ethQuery = new EthQuery(this.provider); + const accounts = await this.keyringController.getAccounts(); + let address = accounts[accounts.length - 1]; + + for (let count = accounts.length; ; count++) { + const balance = await this.getBalance(address, ethQuery); - const tokens = - this.tokensController.state.allTokens?.[chainId]?.[address]; - const detectedTokens = - this.tokensController.state.allDetectedTokens?.[chainId]?.[address]; - - if ( - (tokens?.length ?? 0) === 0 && - (detectedTokens?.length ?? 0) === 0 - ) { - // This account has no balance or tokens - if (count !== 1) { - await this.removeAccount(address); + if (balance === '0x0') { + // This account has no balance, so check for tokens + await this.tokenDetectionController.detectTokens({ + chainIds: [chainId], + selectedAddress: address, + }); + + const tokens = + this.tokensController.state.allTokens?.[chainId]?.[address]; + const detectedTokens = + this.tokensController.state.allDetectedTokens?.[chainId]?.[address]; + + if ( + (tokens?.length ?? 0) === 0 && + (detectedTokens?.length ?? 0) === 0 + ) { + // This account has no balance or tokens + if (count !== 1) { + await this.removeAccount(address); + } + break; } - break; } - } - // This account has assets, so check the next one - address = await this.keyringController.addNewAccount(count); + // This account has assets, so check the next one + address = await this.keyringController.addNewAccount(count); + } + } catch (e) { + log.warn(`Failed to add accounts with balance. Error: ${e}`); + } finally { + await this.userStorageController.setIsAccountSyncingReadyToBeDispatched( + true, + ); } } diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 54659cd66695..f87946a09c0f 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1919,40 +1919,12 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "uuid": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 54659cd66695..f87946a09c0f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1919,40 +1919,12 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "uuid": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 54659cd66695..f87946a09c0f 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1919,40 +1919,12 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "uuid": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 38442fe5eb85..1bad9f3288a2 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2011,40 +2011,12 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "uuid": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { - "packages": { - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/package.json b/package.json index e042fa713ea2..929cc375f683 100644 --- a/package.json +++ b/package.json @@ -338,7 +338,7 @@ "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.35.1", "@metamask/preinstalled-example-snap": "^0.2.0", - "@metamask/profile-sync-controller": "^0.9.7", + "@metamask/profile-sync-controller": "^1.0.2", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^7.0.1", "@metamask/rate-limit-controller": "^6.0.0", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 3f1941a5c00e..8688b8cfa8ae 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -631,6 +631,7 @@ export enum MetaMetricsEventName { AccountRenamed = 'Account Renamed', AccountsSyncAdded = 'Accounts Sync Added', AccountsSyncNameUpdated = 'Accounts Sync Name Updated', + AccountsSyncErroneousSituation = 'Accounts Sync Erroneous Situation', ActivityDetailsOpened = 'Activity Details Opened', ActivityDetailsClosed = 'Activity Details Closed', AnalyticsPreferenceSelected = 'Analytics Preference Selected', diff --git a/test/e2e/helpers/user-storage/userStorageMockttpController.test.ts b/test/e2e/helpers/user-storage/userStorageMockttpController.test.ts index 1b6591899c0e..f4d8fdeb4e3d 100644 --- a/test/e2e/helpers/user-storage/userStorageMockttpController.test.ts +++ b/test/e2e/helpers/user-storage/userStorageMockttpController.test.ts @@ -1,4 +1,5 @@ import * as mockttp from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { UserStorageMockttpController } from './userStorageMockttpController'; describe('UserStorageMockttpController', () => { @@ -12,11 +13,14 @@ describe('UserStorageMockttpController', () => { it('handles GET requests that have empty response', async () => { const controller = new UserStorageMockttpController(); - controller.setupPath('accounts', mockServer); + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer); - const request = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, - }); + const request = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); expect(request.json).toEqual(null); }); @@ -36,13 +40,16 @@ describe('UserStorageMockttpController', () => { }, ]; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); - const request = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, - }); + const request = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); expect(request.json).toEqual(mockedData); }); @@ -62,13 +69,16 @@ describe('UserStorageMockttpController', () => { }, ]; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); - const request = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, - }); + const request = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); expect(request.json).toEqual(mockedData); }); @@ -88,13 +98,16 @@ describe('UserStorageMockttpController', () => { }, ]; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); - const request = await controller.onGet('accounts', { - path: `${baseUrl}/accounts/7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b`, - }); + const request = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}/7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b`, + }, + ); expect(request.json).toEqual(mockedData[0]); }); @@ -119,24 +132,30 @@ describe('UserStorageMockttpController', () => { Data: 'data3', }; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); - const putRequest = await controller.onPut('accounts', { - path: `${baseUrl}/accounts/6afbe024087495b4e0d56c4bdfc981c84eba44a7c284d4f455b5db4fcabc2173`, - body: { - getJson: async () => ({ - data: mockedAddedData.Data, - }), - } as unknown as mockttp.CompletedBody, - }); + const putRequest = await controller.onPut( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}/6afbe024087495b4e0d56c4bdfc981c84eba44a7c284d4f455b5db4fcabc2173`, + body: { + getJson: async () => ({ + data: mockedAddedData.Data, + }), + } as unknown as mockttp.CompletedBody, + }, + ); expect(putRequest.statusCode).toEqual(204); - const getRequest = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, - }); + const getRequest = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); expect(getRequest.json).toEqual([...mockedData, mockedAddedData]); }); @@ -161,24 +180,30 @@ describe('UserStorageMockttpController', () => { Data: 'data3', }; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); - const putRequest = await controller.onPut('accounts', { - path: `${baseUrl}/accounts/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, - body: { - getJson: async () => ({ - data: mockedUpdatedData.Data, - }), - } as unknown as mockttp.CompletedBody, - }); + const putRequest = await controller.onPut( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, + body: { + getJson: async () => ({ + data: mockedUpdatedData.Data, + }), + } as unknown as mockttp.CompletedBody, + }, + ); expect(putRequest.statusCode).toEqual(204); - const getRequest = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, - }); + const getRequest = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); expect(getRequest.json).toEqual([mockedData[0], mockedUpdatedData]); }); @@ -210,7 +235,7 @@ describe('UserStorageMockttpController', () => { }, ]; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); @@ -219,20 +244,26 @@ describe('UserStorageMockttpController', () => { putData[entry.HashedKey] = entry.Data; }); - const putRequest = await controller.onPut('accounts', { - path: `${baseUrl}/accounts`, - body: { - getJson: async () => ({ - data: putData, - }), - } as unknown as mockttp.CompletedBody, - }); + const putRequest = await controller.onPut( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + body: { + getJson: async () => ({ + data: putData, + }), + } as unknown as mockttp.CompletedBody, + }, + ); expect(putRequest.statusCode).toEqual(204); - const getRequest = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, - }); + const getRequest = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); expect(getRequest.json).toEqual(mockedUpdatedData); }); @@ -252,19 +283,25 @@ describe('UserStorageMockttpController', () => { }, ]; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); - const deleteRequest = await controller.onDelete('accounts', { - path: `${baseUrl}/accounts/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, - }); + const deleteRequest = await controller.onDelete( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, + }, + ); expect(deleteRequest.statusCode).toEqual(204); - const getRequest = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, - }); + const getRequest = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); expect(getRequest.json).toEqual([mockedData[0]]); }); @@ -282,22 +319,76 @@ describe('UserStorageMockttpController', () => { 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', Data: 'data2', }, + { + HashedKey: + 'x236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data3', + }, ]; - controller.setupPath('accounts', mockServer, { + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { getResponse: mockedData, }); - const deleteRequest = await controller.onDelete('accounts', { - path: `${baseUrl}/accounts`, - }); + const deleteRequest = await controller.onPut( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + body: { + getJson: async () => ({ + batch_delete: [mockedData[1].HashedKey, mockedData[2].HashedKey], + }), + } as unknown as mockttp.CompletedBody, + }, + ); expect(deleteRequest.statusCode).toEqual(204); - const getRequest = await controller.onGet('accounts', { - path: `${baseUrl}/accounts`, + const getRequest = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); + + expect(getRequest.json).toEqual([mockedData[0]]); + }); + + it('handles entire feature DELETE requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + controller.setupPath(USER_STORAGE_FEATURE_NAMES.accounts, mockServer, { + getResponse: mockedData, }); + const deleteRequest = await controller.onDelete( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); + + expect(deleteRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet( + USER_STORAGE_FEATURE_NAMES.accounts, + { + path: `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + }, + ); + expect(getRequest.json).toEqual(null); }); }); diff --git a/test/e2e/helpers/user-storage/userStorageMockttpController.ts b/test/e2e/helpers/user-storage/userStorageMockttpController.ts index 970a10d11120..ce8583b9adcd 100644 --- a/test/e2e/helpers/user-storage/userStorageMockttpController.ts +++ b/test/e2e/helpers/user-storage/userStorageMockttpController.ts @@ -1,13 +1,22 @@ import { CompletedRequest, Mockttp } from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; + +const baseUrl = + 'https://user-storage\\.api\\.cx\\.metamask\\.io\\/api\\/v1\\/userstorage'; -// TODO: Export user storage schema from @metamask/profile-sync-controller export const pathRegexps = { - accounts: - /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/accounts/u, - networks: - /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/networks/u, - notifications: - /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/notifications/u, + [USER_STORAGE_FEATURE_NAMES.accounts]: new RegExp( + `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.accounts}`, + 'u', + ), + [USER_STORAGE_FEATURE_NAMES.networks]: new RegExp( + `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.networks}`, + 'u', + ), + [USER_STORAGE_FEATURE_NAMES.notifications]: new RegExp( + `${baseUrl}/${USER_STORAGE_FEATURE_NAMES.notifications}`, + 'u', + ), }; type UserStorageResponseData = { HashedKey: string; Data: string }; @@ -70,50 +79,75 @@ export class UserStorageMockttpController { const isFeatureEntry = determineIfFeatureEntryFromURL(request.path); const data = (await request.body.getJson()) as { - data: string | { [key: string]: string }; + data?: string | Record; + batch_delete?: string[]; }; - const newOrUpdatedSingleOrBatchEntries = - isFeatureEntry && typeof data?.data === 'string' - ? [ - { - HashedKey: request.path.split('/').pop() as string, - Data: data?.data, - }, - ] - : Object.entries(data?.data).map(([key, value]) => ({ - HashedKey: key, - Data: value, - })); - - newOrUpdatedSingleOrBatchEntries.forEach((entry) => { + // We're handling batch delete inside the PUT method due to API limitations + if (data?.batch_delete) { + const keysToDelete = data.batch_delete; + const internalPathData = this.paths.get(path); if (!internalPathData) { - return; + return { + statusCode, + }; } - const doesThisEntryExist = internalPathData.response?.find( - (existingEntry) => existingEntry.HashedKey === entry.HashedKey, - ); + this.paths.set(path, { + ...internalPathData, + response: internalPathData.response.filter( + (entry) => !keysToDelete.includes(entry.HashedKey), + ), + }); + } - if (doesThisEntryExist) { - this.paths.set(path, { - ...internalPathData, - response: internalPathData.response.map((existingEntry) => - existingEntry.HashedKey === entry.HashedKey ? entry : existingEntry, - ), - }); - } else { - this.paths.set(path, { - ...internalPathData, - response: [ - ...(internalPathData?.response || []), - entry as { HashedKey: string; Data: string }, - ], - }); - } - }); + if (data?.data) { + const newOrUpdatedSingleOrBatchEntries = + isFeatureEntry && typeof data?.data === 'string' + ? [ + { + HashedKey: request.path.split('/').pop() as string, + Data: data?.data, + }, + ] + : Object.entries(data?.data).map(([key, value]) => ({ + HashedKey: key, + Data: value, + })); + + newOrUpdatedSingleOrBatchEntries.forEach((entry) => { + const internalPathData = this.paths.get(path); + + if (!internalPathData) { + return; + } + + const doesThisEntryExist = internalPathData.response?.find( + (existingEntry) => existingEntry.HashedKey === entry.HashedKey, + ); + + if (doesThisEntryExist) { + this.paths.set(path, { + ...internalPathData, + response: internalPathData.response.map((existingEntry) => + existingEntry.HashedKey === entry.HashedKey + ? entry + : existingEntry, + ), + }); + } else { + this.paths.set(path, { + ...internalPathData, + response: [ + ...(internalPathData?.response || []), + entry as { HashedKey: string; Data: string }, + ], + }); + } + }); + } return { statusCode, diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 89a32a550cc9..6a8a916d4349 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'assert'; import { Driver } from '../../webdriver/driver'; import { Ganache } from '../../seeder/ganache'; +import { getCleanAppState } from '../../helpers'; import HeaderNavbar from './header-navbar'; class HomePage { @@ -348,6 +349,17 @@ class HomePage { `Amount for transaction ${expectedNumber} is displayed as ${expectedAmount}`, ); } + + /** + * This function checks if account syncing has been successfully completed at least once. + */ + async check_hasAccountSyncingSyncedAtLeastOnce(): Promise { + console.log('Check if account syncing has synced at least once'); + await this.driver.wait(async () => { + const uiState = await getCleanAppState(this.driver); + return uiState.metamask.hasAccountSyncingSyncedAtLeastOnce === true; + }, 10000); + } } export default HomePage; diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 9df8707d1f17..39ec3fb2530b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -348,6 +348,8 @@ "UserOperationController": { "userOperations": "object" }, "UserStorageController": { "isProfileSyncingEnabled": null, - "isProfileSyncingUpdateLoading": "boolean" + "isProfileSyncingUpdateLoading": "boolean", + "hasAccountSyncingSyncedAtLeastOnce": "boolean", + "isAccountSyncingReadyToBeDispatched": "boolean" } } diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 006277f89160..615689cb6d19 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -224,6 +224,8 @@ "isSignedIn": "boolean", "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", + "hasAccountSyncingSyncedAtLeastOnce": "boolean", + "isAccountSyncingReadyToBeDispatched": "boolean", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", diff --git a/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts b/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts index 7b9e2378b058..1940d4bf3fd6 100644 --- a/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts @@ -1,4 +1,5 @@ import { Mockttp } from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; @@ -28,9 +29,13 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server, { - getResponse: accountsSyncMockResponse, - }); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + { + getResponse: accountsSyncMockResponse, + }, + ); return mockNotificationServices( server, @@ -47,6 +52,7 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -75,7 +81,10 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -91,6 +100,7 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts b/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts index 0d7010f25f21..8e2908682542 100644 --- a/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts @@ -1,4 +1,5 @@ import { Mockttp } from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; @@ -28,7 +29,10 @@ describe('Account syncing - New User @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, @@ -45,6 +49,7 @@ describe('Account syncing - New User @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); // Open account menu and validate 1 account is shown const header = new HeaderNavbar(driver); @@ -77,7 +82,10 @@ describe('Account syncing - New User @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -94,6 +102,7 @@ describe('Account syncing - New User @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); // Open account menu and validate the 2 accounts have been retrieved const header = new HeaderNavbar(driver); diff --git a/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts b/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts index 36776b367730..209d3a51fdaf 100644 --- a/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts @@ -1,4 +1,5 @@ import { Mockttp } from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; @@ -35,9 +36,13 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { // Mocks are still set up to ensure that requests are not matched - userStorageMockttpController.setupPath('accounts', server, { - getResponse: accountsSyncMockResponse, - }); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + { + getResponse: accountsSyncMockResponse, + }, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -94,7 +99,10 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { // Mocks are still set up to ensure that requests are not matched - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -146,7 +154,10 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { // Mocks are still set up to ensure that requests are not matched - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, userStorageMockttpController, diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts index 31f92520f13e..23a5d1eaf47b 100644 --- a/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts @@ -1,4 +1,5 @@ import { Mockttp } from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; @@ -27,9 +28,13 @@ describe('Account syncing - Add Account @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server, { - getResponse: accountsSyncMockResponse, - }); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + { + getResponse: accountsSyncMockResponse, + }, + ); return mockNotificationServices( server, @@ -46,6 +51,7 @@ describe('Account syncing - Add Account @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -73,7 +79,10 @@ describe('Account syncing - Add Account @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -89,6 +98,7 @@ describe('Account syncing - Add Account @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -97,8 +107,9 @@ describe('Account syncing - Add Account @no-mmi', function () { const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - const accountSyncResponse = - userStorageMockttpController.paths.get('accounts')?.response; + const accountSyncResponse = userStorageMockttpController.paths.get( + USER_STORAGE_FEATURE_NAMES.accounts, + )?.response; await accountListPage.check_numberOfAvailableAccounts( accountSyncResponse?.length as number, @@ -124,9 +135,13 @@ describe('Account syncing - Add Account @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server, { - getResponse: accountsSyncMockResponse, - }); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + { + getResponse: accountsSyncMockResponse, + }, + ); return mockNotificationServices( server, @@ -143,6 +158,7 @@ describe('Account syncing - Add Account @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -168,7 +184,10 @@ describe('Account syncing - Add Account @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -192,8 +211,9 @@ describe('Account syncing - Add Account @no-mmi', function () { const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - const accountSyncResponse = - userStorageMockttpController.paths.get('accounts')?.response; + const accountSyncResponse = userStorageMockttpController.paths.get( + USER_STORAGE_FEATURE_NAMES.accounts, + )?.response; await accountListPage.check_numberOfAvailableAccounts( accountSyncResponse?.length as number, diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts index 45ee3ab23a85..22618d70f3c5 100644 --- a/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts @@ -1,4 +1,5 @@ import { Mockttp } from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; @@ -27,9 +28,13 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server, { - getResponse: accountsSyncMockResponse, - }); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + { + getResponse: accountsSyncMockResponse, + }, + ); return mockNotificationServices( server, @@ -46,6 +51,7 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -72,7 +78,10 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -88,6 +97,7 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts index 5bebe7220e49..be2b2604633c 100644 --- a/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts @@ -1,4 +1,5 @@ import { Mockttp } from 'mockttp'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; @@ -27,9 +28,13 @@ describe('Account syncing - Onboarding @no-mmi', function () { fixtures: new FixtureBuilder({ onboarding: true }).build(), title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { - userStorageMockttpController.setupPath('accounts', server, { - getResponse: accountsSyncMockResponse, - }); + userStorageMockttpController.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + { + getResponse: accountsSyncMockResponse, + }, + ); return mockNotificationServices( server, userStorageMockttpController, @@ -45,6 +50,7 @@ describe('Account syncing - Onboarding @no-mmi', function () { const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_hasAccountSyncingSyncedAtLeastOnce(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/notifications/mocks.ts b/test/e2e/tests/notifications/mocks.ts index ce2ced3df210..748084918272 100644 --- a/test/e2e/tests/notifications/mocks.ts +++ b/test/e2e/tests/notifications/mocks.ts @@ -4,6 +4,7 @@ import { NotificationServicesController, NotificationServicesPushController, } from '@metamask/notification-services-controller'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { UserStorageMockttpController } from '../../helpers/user-storage/userStorageMockttpController'; const AuthMocks = AuthenticationController.Mocks; @@ -32,14 +33,35 @@ export async function mockNotificationServices( mockAPICall(server, AuthMocks.getMockAuthAccessTokenResponse()); // Storage - if (!userStorageMockttpControllerInstance?.paths.get('accounts')) { - userStorageMockttpControllerInstance.setupPath('accounts', server); + if ( + !userStorageMockttpControllerInstance?.paths.get( + USER_STORAGE_FEATURE_NAMES.accounts, + ) + ) { + userStorageMockttpControllerInstance.setupPath( + USER_STORAGE_FEATURE_NAMES.accounts, + server, + ); } - if (!userStorageMockttpControllerInstance?.paths.get('networks')) { - userStorageMockttpControllerInstance.setupPath('networks', server); + if ( + !userStorageMockttpControllerInstance?.paths.get( + USER_STORAGE_FEATURE_NAMES.networks, + ) + ) { + userStorageMockttpControllerInstance.setupPath( + USER_STORAGE_FEATURE_NAMES.networks, + server, + ); } - if (!userStorageMockttpControllerInstance?.paths.get('notifications')) { - userStorageMockttpControllerInstance.setupPath('notifications', server); + if ( + !userStorageMockttpControllerInstance?.paths.get( + USER_STORAGE_FEATURE_NAMES.notifications, + ) + ) { + userStorageMockttpControllerInstance.setupPath( + USER_STORAGE_FEATURE_NAMES.notifications, + server, + ); } // Notifications diff --git a/test/integration/notifications&auth/data/notification-state.ts b/test/integration/notifications&auth/data/notification-state.ts index c58bf707f521..61d74d161671 100644 --- a/test/integration/notifications&auth/data/notification-state.ts +++ b/test/integration/notifications&auth/data/notification-state.ts @@ -38,6 +38,8 @@ export const getMockedNotificationsState = () => { ...mockMetaMaskState, isProfileSyncingEnabled: true, isProfileSyncingUpdateLoading: false, + hasAccountSyncingSyncedAtLeastOnce: false, + isAccountSyncingReadyToBeDispatched: false, isMetamaskNotificationsFeatureSeen: true, isNotificationServicesEnabled: true, isFeatureAnnouncementsEnabled: true, diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx index 99d3064085ea..42e902e29401 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx +++ b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx @@ -14,6 +14,7 @@ type ArrangeMocksMetamaskStateOverrides = { isUnlocked?: boolean; useExternalServices?: boolean; completedOnboarding?: boolean; + isAccountSyncingReadyToBeDispatched?: boolean; }; const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = { @@ -22,6 +23,7 @@ const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = { isUnlocked: true, useExternalServices: true, completedOnboarding: true, + isAccountSyncingReadyToBeDispatched: true, }; const arrangeMockState = ( @@ -89,6 +91,7 @@ describe('useShouldDispatchProfileSyncing()', () => { 'isUnlocked', 'useExternalServices', 'completedOnboarding', + 'isAccountSyncingReadyToBeDispatched', ] as const; const baseState = { isSignedIn: true, @@ -96,6 +99,7 @@ describe('useShouldDispatchProfileSyncing()', () => { isUnlocked: true, useExternalServices: true, completedOnboarding: true, + isAccountSyncingReadyToBeDispatched: true, }; const failureStateCases: { diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts index 5c073fdf6d94..57820d2e633b 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts +++ b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts @@ -10,7 +10,10 @@ import { } from '../../../store/actions'; import { selectIsSignedIn } from '../../../selectors/metamask-notifications/authentication'; -import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; +import { + selectIsAccountSyncingReadyToBeDispatched, + selectIsProfileSyncingEnabled, +} from '../../../selectors/metamask-notifications/profile-syncing'; import { getUseExternalServices } from '../../../selectors'; import { getIsUnlocked, @@ -120,6 +123,9 @@ export function useSetIsProfileSyncingEnabled(): { * @returns a boolean if internally we can perform syncing features or not. */ export const useShouldDispatchProfileSyncing = () => { + const isAccountSyncingReadyToBeDispatched = useSelector( + selectIsAccountSyncingReadyToBeDispatched, + ); const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); const basicFunctionality: boolean | undefined = useSelector( getUseExternalServices, @@ -135,7 +141,8 @@ export const useShouldDispatchProfileSyncing = () => { isProfileSyncingEnabled && isUnlocked && isSignedIn && - completedOnboarding, + completedOnboarding && + isAccountSyncingReadyToBeDispatched, ); return shouldDispatchProfileSyncing; diff --git a/ui/selectors/metamask-notifications/profile-syncing.test.ts b/ui/selectors/metamask-notifications/profile-syncing.test.ts index 946ffd3f0b5f..d05512e59523 100644 --- a/ui/selectors/metamask-notifications/profile-syncing.test.ts +++ b/ui/selectors/metamask-notifications/profile-syncing.test.ts @@ -5,6 +5,8 @@ describe('Profile Syncing Selectors', () => { metamask: { isProfileSyncingEnabled: true, isProfileSyncingUpdateLoading: false, + isAccountSyncingReadyToBeDispatched: false, + hasAccountSyncingSyncedAtLeastOnce: false, }, }; diff --git a/ui/selectors/metamask-notifications/profile-syncing.ts b/ui/selectors/metamask-notifications/profile-syncing.ts index 8b9b8f4997d6..ae219f47be68 100644 --- a/ui/selectors/metamask-notifications/profile-syncing.ts +++ b/ui/selectors/metamask-notifications/profile-syncing.ts @@ -2,7 +2,9 @@ import { createSelector } from 'reselect'; import type { UserStorageController } from '@metamask/profile-sync-controller'; type AppState = { - metamask: UserStorageController.UserStorageControllerState; + metamask: UserStorageController.UserStorageControllerState & { + hasFinishedAddingAccountsWithBalance?: boolean; + }; }; const getMetamask = (state: AppState) => state.metamask; @@ -36,3 +38,20 @@ export const selectIsProfileSyncingUpdateLoading = createSelector( return metamask.isProfileSyncingUpdateLoading; }, ); + +/** + * Selector to determine if account syncing is ready to be dispatched. This is set to true after all operations adding accounts are completed. + * This is needed for account syncing in order to prevent conflicts with accounts that are being added by the above method during onboarding. + * + * This selector uses the `createSelector` function from 'reselect' to compute whether the update process for profile syncing is currently in a loading state, + * based on the `hasFinishedAddingAccountsWithBalance` property of the `metamask` object in the Redux store. + * + * @param {AppState} state - The current state of the Redux store. + * @returns {boolean} Returns true if the profile syncing update is loading, false otherwise. + */ +export const selectIsAccountSyncingReadyToBeDispatched = createSelector( + [getMetamask], + (metamask) => { + return metamask.isAccountSyncingReadyToBeDispatched; + }, +); diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 22e8db2fa281..10391adad3df 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -4,6 +4,7 @@ import thunk from 'redux-thunk'; import { EthAccountType } from '@metamask/keyring-api'; import { TransactionStatus } from '@metamask/transaction-controller'; import { NotificationServicesController } from '@metamask/notification-services-controller'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import enLocale from '../../app/_locales/en/messages.json'; @@ -2605,7 +2606,9 @@ describe('Actions', () => { await store.dispatch(actions.deleteAccountSyncingDataFromUserStorage()); expect( - deleteAccountSyncingDataFromUserStorageStub.calledOnceWith('accounts'), + deleteAccountSyncingDataFromUserStorageStub.calledOnceWith( + USER_STORAGE_FEATURE_NAMES.accounts, + ), ).toBe(true); }); }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 64ba84ddaf57..6344f823aa02 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -42,6 +42,7 @@ import { import { InterfaceState } from '@metamask/snaps-sdk'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { NotificationServicesController } from '@metamask/notification-services-controller'; +import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { Patch } from 'immer'; import { HandlerType } from '@metamask/snaps-utils'; import switchDirection from '../../shared/lib/switch-direction'; @@ -5661,7 +5662,7 @@ export function deleteAccountSyncingDataFromUserStorage(): ThunkAction< try { const response = await submitRequestToBackground( 'deleteAccountSyncingDataFromUserStorage', - ['accounts'], + [USER_STORAGE_FEATURE_NAMES.accounts], ); return response; } catch (error) { diff --git a/yarn.lock b/yarn.lock index d14fee7c6d97..0147c3106fa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5258,16 +5258,16 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-block-tracker@npm:^11.0.1": - version: 11.0.1 - resolution: "@metamask/eth-block-tracker@npm:11.0.1" +"@metamask/eth-block-tracker@npm:^11.0.1, @metamask/eth-block-tracker@npm:^11.0.2": + version: 11.0.2 + resolution: "@metamask/eth-block-tracker@npm:11.0.2" dependencies: - "@metamask/eth-json-rpc-provider": "npm:^4.1.1" + "@metamask/eth-json-rpc-provider": "npm:^4.1.5" "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/utils": "npm:^9.1.0" json-rpc-random-id: "npm:^1.0.1" pify: "npm:^5.0.0" - checksum: 10/6a5143dcd20ea87cd674efb25870275d97d4ffe921e843391a5b85876ebe074e5a587a128c268d27520904c74c9feecf91218ea086bd65cc6096f8501bdf8f32 + checksum: 10/11d22bd86056401aa41eff5a32e862f3644eaf03040d8aa54a95cb0c1dfd3e3ce7e650c25efabbe0954cc6ba5f92172c338b518df84f73c4601c4bbc960b588a languageName: node linkType: hard @@ -5310,6 +5310,18 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-json-rpc-infura@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/eth-json-rpc-infura@npm:10.0.0" + dependencies: + "@metamask/eth-json-rpc-provider": "npm:^4.1.5" + "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/rpc-errors": "npm:^7.0.0" + "@metamask/utils": "npm:^9.1.0" + checksum: 10/17e0147ff86c48107983035e9bda4d16fba321ee0e29733347e9338a4c795c506a2ffd643c44c9d5334886696412cf288f852d06311fed0d76edc8847ee6b8de + languageName: node + linkType: hard + "@metamask/eth-json-rpc-infura@npm:^9.1.0": version: 9.1.0 resolution: "@metamask/eth-json-rpc-infura@npm:9.1.0" @@ -5361,6 +5373,25 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-json-rpc-middleware@npm:^15.0.0": + version: 15.0.0 + resolution: "@metamask/eth-json-rpc-middleware@npm:15.0.0" + dependencies: + "@metamask/eth-block-tracker": "npm:^11.0.1" + "@metamask/eth-json-rpc-provider": "npm:^4.1.5" + "@metamask/eth-sig-util": "npm:^7.0.3" + "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/rpc-errors": "npm:^7.0.0" + "@metamask/utils": "npm:^9.1.0" + "@types/bn.js": "npm:^5.1.5" + bn.js: "npm:^5.2.1" + klona: "npm:^2.0.6" + pify: "npm:^5.0.0" + safe-stable-stringify: "npm:^2.4.3" + checksum: 10/3c48d34264c695535f2b4e819fb602d835b6ed37309116a06d04d1b706a7335e0205cd4ccdbf1d3e9dc15ebf40d88954a9a2dc18a91f223dcd6d6392e026a5e9 + languageName: node + linkType: hard + "@metamask/eth-json-rpc-middleware@patch:@metamask/eth-json-rpc-middleware@npm%3A14.0.1#~/.yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch": version: 14.0.1 resolution: "@metamask/eth-json-rpc-middleware@patch:@metamask/eth-json-rpc-middleware@npm%3A14.0.1#~/.yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch::version=14.0.1&hash=96e7e0" @@ -5402,16 +5433,16 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-provider@npm:^4.0.0, @metamask/eth-json-rpc-provider@npm:^4.1.0, @metamask/eth-json-rpc-provider@npm:^4.1.1, @metamask/eth-json-rpc-provider@npm:^4.1.3": - version: 4.1.3 - resolution: "@metamask/eth-json-rpc-provider@npm:4.1.3" +"@metamask/eth-json-rpc-provider@npm:^4.0.0, @metamask/eth-json-rpc-provider@npm:^4.1.0, @metamask/eth-json-rpc-provider@npm:^4.1.1, @metamask/eth-json-rpc-provider@npm:^4.1.3, @metamask/eth-json-rpc-provider@npm:^4.1.5, @metamask/eth-json-rpc-provider@npm:^4.1.6": + version: 4.1.6 + resolution: "@metamask/eth-json-rpc-provider@npm:4.1.6" dependencies: - "@metamask/json-rpc-engine": "npm:^9.0.2" - "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^10.0.0" uuid: "npm:^8.3.2" - checksum: 10/d581cc0f6485783ed59ac9517aa7f0eb37ee6a0674409eeaba1bbda4b54fcc5f633cc8ace66207871e2c2fac33195982969f4e61c18b04faf4656cccf79d8d3d + checksum: 10/aeec2c362a5386357e9f8c707da9baa4326e83889633723656b6801b6461ea8ab8f020b0d9ed0bbc2d8fd6add4af4c99cc9c9a1cbedca267a033a9f19da41200 languageName: node linkType: hard @@ -5817,6 +5848,27 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-controller@npm:^18.0.0": + version: 18.0.0 + resolution: "@metamask/keyring-controller@npm:18.0.0" + dependencies: + "@ethereumjs/util": "npm:^8.1.0" + "@keystonehq/metamask-airgapped-keyring": "npm:^0.14.1" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/browser-passworder": "npm:^4.3.0" + "@metamask/eth-hd-keyring": "npm:^7.0.4" + "@metamask/eth-sig-util": "npm:^8.0.0" + "@metamask/eth-simple-keyring": "npm:^6.0.5" + "@metamask/keyring-api": "npm:^8.1.3" + "@metamask/message-manager": "npm:^11.0.1" + "@metamask/utils": "npm:^10.0.0" + async-mutex: "npm:^0.5.0" + ethereumjs-wallet: "npm:^1.0.1" + immer: "npm:^9.0.6" + checksum: 10/c301e4e8b9ac9da914bfaa371a43342aa37f5bb8ad107bbbd92f1d21a13c22351619f8bd6176493b808f4194aa9934bce5618ff0aed12325933f4330cdfd308e + languageName: node + linkType: hard + "@metamask/logging-controller@npm:^6.0.0": version: 6.0.0 resolution: "@metamask/logging-controller@npm:6.0.0" @@ -5912,6 +5964,31 @@ __metadata: languageName: node linkType: hard +"@metamask/network-controller@npm:^22.0.2": + version: 22.0.2 + resolution: "@metamask/network-controller@npm:22.0.2" + dependencies: + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.3" + "@metamask/eth-block-tracker": "npm:^11.0.2" + "@metamask/eth-json-rpc-infura": "npm:^10.0.0" + "@metamask/eth-json-rpc-middleware": "npm:^15.0.0" + "@metamask/eth-json-rpc-provider": "npm:^4.1.6" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/swappable-obj-proxy": "npm:^2.2.0" + "@metamask/utils": "npm:^10.0.0" + async-mutex: "npm:^0.5.0" + immer: "npm:^9.0.6" + loglevel: "npm:^1.8.1" + reselect: "npm:^5.1.1" + uri-js: "npm:^4.4.1" + uuid: "npm:^8.3.2" + checksum: 10/9da27189a4263ef7fa4596ada2000d7f944bc3f4dea63a77cf6f8b2ea89412d499068cf0542785088d19437263bd0b3b3bb3299533f87439729ccd8ecee2b625 + languageName: node + linkType: hard + "@metamask/network-controller@patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch": version: 21.0.0 resolution: "@metamask/network-controller@patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch::version=21.0.0&hash=1a5039" @@ -6210,13 +6287,14 @@ __metadata: languageName: node linkType: hard -"@metamask/profile-sync-controller@npm:^0.9.7": - version: 0.9.7 - resolution: "@metamask/profile-sync-controller@npm:0.9.7" +"@metamask/profile-sync-controller@npm:^1.0.2": + version: 1.0.2 + resolution: "@metamask/profile-sync-controller@npm:1.0.2" dependencies: - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/keyring-api": "npm:^8.1.3" - "@metamask/keyring-controller": "npm:^17.2.2" + "@metamask/keyring-controller": "npm:^18.0.0" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/snaps-utils": "npm:^8.1.1" "@noble/ciphers": "npm:^0.5.2" @@ -6225,10 +6303,11 @@ __metadata: loglevel: "npm:^1.8.1" siwe: "npm:^2.3.2" peerDependencies: - "@metamask/accounts-controller": ^18.1.1 - "@metamask/keyring-controller": ^17.2.0 + "@metamask/accounts-controller": ^19.0.0 + "@metamask/keyring-controller": ^18.0.0 + "@metamask/network-controller": ^22.0.0 "@metamask/snaps-controllers": ^9.7.0 - checksum: 10/e53888533b2aae937bbe4e385dca2617c324b34e3e60af218cd98c26d514fb725f4c67b649f126e055f6a50a554817b229d37488115b98d70e8aee7b3a910bde + checksum: 10/e8ce9cc5749746bea3f6fb9207bbd4e8e3956f92447f3a6b790e3ba7203747e38b9a819f7a4f1896022cf6e1a065e6136a3c82ee83a4ec0ee56b23de27e23f03 languageName: node linkType: hard @@ -26796,7 +26875,7 @@ __metadata: "@metamask/ppom-validator": "npm:0.35.1" "@metamask/preferences-controller": "npm:^13.0.2" "@metamask/preinstalled-example-snap": "npm:^0.2.0" - "@metamask/profile-sync-controller": "npm:^0.9.7" + "@metamask/profile-sync-controller": "npm:^1.0.2" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^7.0.1" "@metamask/rate-limit-controller": "npm:^6.0.0" From f3cec693921e695bb9f8d8a6e8dacdd8833cdb76 Mon Sep 17 00:00:00 2001 From: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:30:10 -0500 Subject: [PATCH 04/28] fix: contact names should not allow duplication (#28249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This fix prevents users from having duplicate names in their contact list. A warning banner is now displayed in the contact list when duplicates are found. All duplicate contacts will also have a warning icon next to them to help users better identify duplicates. #### What is considered a duplicate contact? A duplicate is a contact with a different address but same name as another contact or account. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28249?quickstart=1) ## **Related issues** Fixes: - https://github.com/MetaMask/metamask-extension/issues/26621 - https://github.com/MetaMask/metamask-extension/issues/26696 ## **Manual testing steps** #### Adding a duplicate contact 1. Go to Settings > Contacts page 2. Attempt to add a new contact using an existing contact or account name 3. An error message will appear underneath the `username` input and the submit button will be disabled. #### Viewing duplicate contact warning banner 1. In your local environment, go to file `ui/components/app/contact-list/contact-list.component.js` 2. On line `138` (`{hasDuplicateContacts ? this.renderDuplicateContactWarning() : null}` change `hasDuplicateContacts` to `true` to display the banner. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/a7813050-e906-4d42-ac10-db05401d06d3 ### **After** https://github.com/user-attachments/assets/70e16e99-60ea-4404-9ea7-6f776f15d00e Screenshot 2024-11-15 at 1 59 21 PM Screenshot 2024-11-15 at 1 59 28 PM ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 9 ++ test/data/mock-data.js | 27 ++++ .../contact-list/contact-list.component.js | 61 ++++++++- .../app/contact-list/contact-list.test.js | 46 +++++++ .../recipient-group.component.js | 3 +- ui/components/app/contact-list/utils.ts | 61 +++++++++ .../address-list-item.test.tsx.snap | 102 ++++++++++++++- .../address-list-item.test.tsx | 38 +++++- .../address-list-item/address-list-item.tsx | 17 ++- .../multichain/address-list-item/index.scss | 5 + .../pages/send/components/address-book.tsx | 3 + .../domain-input-resolution-cell.tsx | 1 + ui/pages/confirmations/send/send.scss | 4 + .../add-contact/add-contact.component.js | 42 ++++-- .../add-contact/add-contact.container.js | 3 + .../add-contact/add-contact.test.js | 122 +++++++++++++++++- .../contact-list-tab.component.js | 6 +- .../contact-list-tab.container.js | 3 +- .../contact-list-tab.stories.js | 5 + .../edit-contact/edit-contact.component.js | 44 ++++++- .../edit-contact/edit-contact.container.js | 4 + .../edit-contact/edit-contact.test.js | 65 +++++++++- 22 files changed, 630 insertions(+), 41 deletions(-) create mode 100644 ui/components/app/contact-list/utils.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 56a2ba405aef..a59a48a21afe 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1734,6 +1734,12 @@ "dropped": { "message": "Dropped" }, + "duplicateContactTooltip": { + "message": "This contact name collides with an existing account or contact" + }, + "duplicateContactWarning": { + "message": "You have duplicate contacts" + }, "edit": { "message": "Edit" }, @@ -3028,6 +3034,9 @@ "message": "Address", "description": "Label above address field in name component modal." }, + "nameAlreadyInUse": { + "message": "Name is already in use" + }, "nameInstructionsNew": { "message": "If you know this address, give it a nickname to recognize it in the future.", "description": "Instruction text in name component modal when value is not recognised." diff --git a/test/data/mock-data.js b/test/data/mock-data.js index 4775f1dbb25e..c68d70a1fc59 100644 --- a/test/data/mock-data.js +++ b/test/data/mock-data.js @@ -1049,6 +1049,31 @@ const NETWORKS_2_API_MOCK_RESULT = { }, }; +const MOCK_ADDRESS_BOOK = [ + { + address: '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + chainId: '0x1', + isEns: false, + memo: '', + name: 'Contact 1', + }, + { + address: '0x43c9159B6251f3E205B9113A023C8256cDD40D91', + chainId: '0x1', + isEns: true, + memo: '', + name: 'example.eth', + }, +]; + +const MOCK_DOMAIN_RESOLUTION = { + addressBookEntryName: 'example.eth', + domainName: 'example.eth', + protocol: 'Ethereum Name Service', + resolvedAddress: '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + resolvingSnap: 'Ethereum Name Service resolver', +}; + module.exports = { TOKENS_API_MOCK_RESULT, TOP_ASSETS_API_MOCK_RESULT, @@ -1059,4 +1084,6 @@ module.exports = { SWAP_TEST_ETH_DAI_TRADES_MOCK, SWAP_TEST_ETH_USDC_TRADES_MOCK, NETWORKS_2_API_MOCK_RESULT, + MOCK_ADDRESS_BOOK, + MOCK_DOMAIN_RESOLUTION, }; diff --git a/ui/components/app/contact-list/contact-list.component.js b/ui/components/app/contact-list/contact-list.component.js index 548bbf68a90c..b7438f9fe195 100644 --- a/ui/components/app/contact-list/contact-list.component.js +++ b/ui/components/app/contact-list/contact-list.component.js @@ -2,10 +2,14 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { sortBy } from 'lodash'; import Button from '../../ui/button'; +import { BannerAlert, BannerAlertSeverity } from '../../component-library'; import RecipientGroup from './recipient-group/recipient-group.component'; +import { hasDuplicateContacts, buildDuplicateContactMap } from './utils'; export default class ContactList extends PureComponent { static propTypes = { + addressBook: PropTypes.array, + internalAccounts: PropTypes.array, searchForContacts: PropTypes.func, searchForRecents: PropTypes.func, searchForMyAccounts: PropTypes.func, @@ -22,6 +26,19 @@ export default class ContactList extends PureComponent { isShowingAllRecent: false, }; + renderDuplicateContactWarning() { + const { t } = this.context; + + return ( +
+ +
+ ); + } + renderRecents() { const { t } = this.context; const { isShowingAllRecent } = this.state; @@ -45,15 +62,40 @@ export default class ContactList extends PureComponent { } renderAddressBook() { - const unsortedContactsByLetter = this.props - .searchForContacts() - .reduce((obj, contact) => { + const { + addressBook, + internalAccounts, + searchForContacts, + selectRecipient, + selectedAddress, + } = this.props; + + const duplicateContactMap = buildDuplicateContactMap( + addressBook, + internalAccounts, + ); + + const unsortedContactsByLetter = searchForContacts().reduce( + (obj, contact) => { const firstLetter = contact.name[0].toUpperCase(); + + const isDuplicate = + (duplicateContactMap.get(contact.name.trim().toLowerCase()) ?? []) + .length > 1; + return { ...obj, - [firstLetter]: [...(obj[firstLetter] || []), contact], + [firstLetter]: [ + ...(obj[firstLetter] || []), + { + ...contact, + isDuplicate, + }, + ], }; - }, {}); + }, + {}, + ); const letters = Object.keys(unsortedContactsByLetter).sort(); @@ -71,8 +113,8 @@ export default class ContactList extends PureComponent { key={`${letter}-contact-group`} label={letter} items={groupItems} - onSelect={this.props.selectRecipient} - selectedAddress={this.props.selectedAddress} + onSelect={selectRecipient} + selectedAddress={selectedAddress} /> )); } @@ -95,11 +137,16 @@ export default class ContactList extends PureComponent { searchForRecents, searchForContacts, searchForMyAccounts, + addressBook, + internalAccounts, } = this.props; return (
{children || null} + {hasDuplicateContacts(addressBook, internalAccounts) + ? this.renderDuplicateContactWarning() + : null} {searchForRecents ? this.renderRecents() : null} {searchForContacts ? this.renderAddressBook() : null} {searchForMyAccounts ? this.renderMyAccounts() : null} diff --git a/ui/components/app/contact-list/contact-list.test.js b/ui/components/app/contact-list/contact-list.test.js index 6d0a990cb08c..1beecc17e0fa 100644 --- a/ui/components/app/contact-list/contact-list.test.js +++ b/ui/components/app/contact-list/contact-list.test.js @@ -1,6 +1,8 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { renderWithProvider } from '../../../../test/jest/rendering'; +import { MOCK_ADDRESS_BOOK } from '../../../../test/data/mock-data'; +import { createMockInternalAccount } from '../../../../test/jest/mocks'; import ContactList from '.'; describe('Contact List', () => { @@ -8,6 +10,48 @@ describe('Contact List', () => { metamask: {}, }); + const mockInternalAccounts = [createMockInternalAccount()]; + + it('displays the warning banner when multiple contacts have the same name', () => { + const mockAddressBook = [...MOCK_ADDRESS_BOOK, MOCK_ADDRESS_BOOK[0]]; // Adding duplicate contact + + const { getByText } = renderWithProvider( + , + store, + ); + + const duplicateContactBanner = getByText('You have duplicate contacts'); + + expect(duplicateContactBanner).toBeVisible(); + }); + + it('displays the warning banner when contact has same name as an existing account', () => { + const mockContactWithAccountName = { + address: '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + chainId: '0x1', + isEns: false, + memo: '', + name: mockInternalAccounts[0].metadata.name, + }; + + const mockAddressBook = [...MOCK_ADDRESS_BOOK, mockContactWithAccountName]; + + const { getByText } = renderWithProvider( + , + store, + ); + + const duplicateContactBanner = getByText('You have duplicate contacts'); + + expect(duplicateContactBanner).toBeVisible(); + }); + describe('given searchForContacts', () => { const selectRecipient = () => null; const selectedAddress = null; @@ -37,6 +81,8 @@ describe('Contact List', () => { searchForContacts={() => contacts} selectRecipient={selectRecipient} selectedAddress={selectedAddress} + addressBook={MOCK_ADDRESS_BOOK} + internalAccounts={mockInternalAccounts} />, store, ); diff --git a/ui/components/app/contact-list/recipient-group/recipient-group.component.js b/ui/components/app/contact-list/recipient-group/recipient-group.component.js index 6bb0b4c30dd6..0788d29aaecd 100644 --- a/ui/components/app/contact-list/recipient-group/recipient-group.component.js +++ b/ui/components/app/contact-list/recipient-group/recipient-group.component.js @@ -7,12 +7,13 @@ export default function RecipientGroup({ items, onSelect }) { return null; } - return items.map(({ address, name }) => ( + return items.map(({ address, name, isDuplicate }) => ( onSelect(address, name)} key={address} + isDuplicate={isDuplicate} /> )); } diff --git a/ui/components/app/contact-list/utils.ts b/ui/components/app/contact-list/utils.ts new file mode 100644 index 000000000000..4254988e4af6 --- /dev/null +++ b/ui/components/app/contact-list/utils.ts @@ -0,0 +1,61 @@ +import { AddressBookEntry } from '@metamask/address-book-controller'; +import { InternalAccount } from '@metamask/keyring-api'; + +export const buildDuplicateContactMap = ( + addressBook: AddressBookEntry[], + internalAccounts: InternalAccount[], +) => { + const contactMap = new Map( + internalAccounts.map((account) => [ + account.metadata.name.trim().toLowerCase(), + [`account-id-${account.id}`], + ]), + ); + + addressBook.forEach((entry) => { + const { name, address } = entry; + + const sanitizedName = name.trim().toLowerCase(); + + const currentArray = contactMap.get(sanitizedName) ?? []; + currentArray.push(address); + + contactMap.set(sanitizedName, currentArray); + }); + + return contactMap; +}; + +export const hasDuplicateContacts = ( + addressBook: AddressBookEntry[], + internalAccounts: InternalAccount[], +) => { + const uniqueContactNames = Array.from( + new Set(addressBook.map(({ name }) => name.toLowerCase().trim())), + ); + + const hasAccountNameCollision = internalAccounts.some((account) => + uniqueContactNames.includes(account.metadata.name.toLowerCase().trim()), + ); + + return ( + uniqueContactNames.length !== addressBook.length || hasAccountNameCollision + ); +}; + +export const isDuplicateContact = ( + addressBook: AddressBookEntry[], + internalAccounts: InternalAccount[], + newName: string, +) => { + const nameExistsInAddressBook = addressBook.some( + ({ name }) => name.toLowerCase().trim() === newName.toLowerCase().trim(), + ); + + const nameExistsInAccountList = internalAccounts.some( + ({ metadata }) => + metadata.name.toLowerCase().trim() === newName.toLowerCase().trim(), + ); + + return nameExistsInAddressBook || nameExistsInAccountList; +}; diff --git a/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap b/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap index 8d840ba595ce..c3895c50d76a 100644 --- a/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap +++ b/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap @@ -1,6 +1,106 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddressListItem renders the address and label 1`] = ` +exports[`AddressListItem displays duplicate contact warning icon 1`] = ` +
+ +
+`; + +exports[`AddressListItem renders the address and label without duplicate contact warning icon 1`] = `
@@ -152,9 +173,9 @@ export default class AddContact extends PureComponent { address={resolvedAddress} domainName={addressBookEntryName ?? domainName} onClick={() => { + this.handleNameChange(domainName); this.setState({ input: resolvedAddress, - newName: this.state.newName || domainName, }); this.props.resetDomainResolution(); }} @@ -164,9 +185,9 @@ export default class AddContact extends PureComponent { ); })}
- {errorToRender && ( + {addressError && (
- {t(errorToRender)} + {t(addressError)}
)} @@ -174,7 +195,10 @@ export default class AddContact extends PureComponent { { await addToAddressBook(newAddress, this.state.newName); diff --git a/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js b/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js index 3b18ebdde8e0..db7b87c48ea1 100644 --- a/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js +++ b/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js @@ -12,10 +12,13 @@ import { getDomainResolutions, resetDomainResolution, } from '../../../../ducks/domains'; +import { getAddressBook, getInternalAccounts } from '../../../../selectors'; import AddContact from './add-contact.component'; const mapStateToProps = (state) => { return { + addressBook: getAddressBook(state), + internalAccounts: getInternalAccounts(state), qrCodeData: getQrCodeData(state), domainError: getDomainError(state), domainResolutions: getDomainResolutions(state), diff --git a/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js b/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js index af130617ae19..09a0e0d96692 100644 --- a/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js +++ b/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js @@ -7,6 +7,12 @@ import '@testing-library/jest-dom/extend-expect'; import { mockNetworkState } from '../../../../../test/stub/networks'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; import { domainInitialState } from '../../../../ducks/domains'; +import { createMockInternalAccount } from '../../../../../test/jest/mocks'; +import { + MOCK_ADDRESS_BOOK, + MOCK_DOMAIN_RESOLUTION, +} from '../../../../../test/data/mock-data'; +import * as domainDucks from '../../../../ducks/domains'; import AddContact from './add-contact.component'; describe('AddContact component', () => { @@ -17,16 +23,29 @@ describe('AddContact component', () => { }, }; const props = { + addressBook: MOCK_ADDRESS_BOOK, + internalAccounts: [createMockInternalAccount()], history: { push: jest.fn() }, addToAddressBook: jest.fn(), scanQrCode: jest.fn(), qrCodeData: { type: 'address', values: { address: '0x123456789abcdef' } }, qrCodeDetected: jest.fn(), - domainResolution: '', + domainResolutions: [MOCK_DOMAIN_RESOLUTION], domainError: '', resetDomainResolution: jest.fn(), }; + beforeEach(() => { + jest.resetAllMocks(); + jest + .spyOn(domainDucks, 'lookupDomainName') + .mockImplementation(() => jest.fn()); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + it('should render the component with correct properties', () => { const store = configureMockStore(middleware)(state); @@ -113,4 +132,105 @@ describe('AddContact component', () => { }); expect(getByText('Save')).toBeDisabled(); }); + + it('should disable the submit button when the name is an existing account name', () => { + const duplicateName = 'Account 1'; + + const store = configureMockStore(middleware)(state); + const { getByText, getByTestId } = renderWithProvider( + , + store, + ); + + const nameInput = document.getElementById('nickname'); + fireEvent.change(nameInput, { target: { value: duplicateName } }); + + const addressInput = getByTestId('ens-input'); + + fireEvent.change(addressInput, { + target: { value: '0x43c9159B6251f3E205B9113A023C8256cDD40D91' }, + }); + + const saveButton = getByText('Save'); + expect(saveButton).toBeDisabled(); + }); + + it('should disable the submit button when the name is an existing contact name', () => { + const duplicateName = MOCK_ADDRESS_BOOK[0].name; + + const store = configureMockStore(middleware)(state); + const { getByText, getByTestId } = renderWithProvider( + , + store, + ); + + const nameInput = document.getElementById('nickname'); + fireEvent.change(nameInput, { target: { value: duplicateName } }); + + const addressInput = getByTestId('ens-input'); + + fireEvent.change(addressInput, { + target: { value: '0x43c9159B6251f3E205B9113A023C8256cDD40D91' }, + }); + + const saveButton = getByText('Save'); + expect(saveButton).toBeDisabled(); + }); + + it('should display error message when name entered is an existing account name', () => { + const duplicateName = 'Account 1'; + + const store = configureMockStore(middleware)(state); + + const { getByText } = renderWithProvider(, store); + + const nameInput = document.getElementById('nickname'); + + fireEvent.change(nameInput, { target: { value: duplicateName } }); + + const saveButton = getByText('Save'); + + expect(getByText('Name is already in use')).toBeDefined(); + expect(saveButton).toBeDisabled(); + }); + + it('should display error message when name entered is an existing contact name', () => { + const duplicateName = MOCK_ADDRESS_BOOK[0].name; + + const store = configureMockStore(middleware)(state); + + const { getByText } = renderWithProvider(, store); + + const nameInput = document.getElementById('nickname'); + + fireEvent.change(nameInput, { target: { value: duplicateName } }); + + const saveButton = getByText('Save'); + + expect(getByText('Name is already in use')).toBeDefined(); + expect(saveButton).toBeDisabled(); + }); + + it('should display error when ENS inserts a name that is already in use', () => { + const store = configureMockStore(middleware)(state); + + const { getByTestId, getByText } = renderWithProvider( + , + store, + ); + + const ensInput = getByTestId('ens-input'); + fireEvent.change(ensInput, { target: { value: 'example.eth' } }); + + const domainResolutionCell = getByTestId( + 'multichain-send-page__recipient__item', + ); + + fireEvent.click(domainResolutionCell); + + const saveButton = getByText('Save'); + + expect(getByText('Name is already in use')).toBeDefined(); + expect(saveButton).toBeDisabled(); + }); }); diff --git a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js index 6da9bbf4d14f..83cbaccd99d8 100644 --- a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js +++ b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js @@ -29,6 +29,7 @@ export default class ContactListTab extends Component { static propTypes = { addressBook: PropTypes.array, + internalAccounts: PropTypes.array, history: PropTypes.object, selectedAddress: PropTypes.string, viewingContact: PropTypes.bool, @@ -57,7 +58,8 @@ export default class ContactListTab extends Component { } renderAddresses() { - const { addressBook, history, selectedAddress } = this.props; + const { addressBook, internalAccounts, history, selectedAddress } = + this.props; const contacts = addressBook.filter(({ name }) => Boolean(name)); const nonContacts = addressBook.filter(({ name }) => !name); const { t } = this.context; @@ -66,6 +68,8 @@ export default class ContactListTab extends Component { return (
contacts} searchForRecents={() => nonContacts} selectRecipient={(address) => { diff --git a/ui/pages/settings/contact-list-tab/contact-list-tab.container.js b/ui/pages/settings/contact-list-tab/contact-list-tab.container.js index 98991e7e744d..b1715715b4f7 100644 --- a/ui/pages/settings/contact-list-tab/contact-list-tab.container.js +++ b/ui/pages/settings/contact-list-tab/contact-list-tab.container.js @@ -1,7 +1,7 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; -import { getAddressBook } from '../../../selectors'; +import { getAddressBook, getInternalAccounts } from '../../../selectors'; import { CONTACT_ADD_ROUTE, @@ -28,6 +28,7 @@ const mapStateToProps = (state, ownProps) => { editingContact, addingContact, addressBook: getAddressBook(state), + internalAccounts: getInternalAccounts(state), selectedAddress: pathNameTailIsAddress ? pathNameTail : '', hideAddressBook, currentPath: pathname, diff --git a/ui/pages/settings/contact-list-tab/contact-list-tab.stories.js b/ui/pages/settings/contact-list-tab/contact-list-tab.stories.js index 06cb6a153c5f..0cfe7be2dd5b 100644 --- a/ui/pages/settings/contact-list-tab/contact-list-tab.stories.js +++ b/ui/pages/settings/contact-list-tab/contact-list-tab.stories.js @@ -3,6 +3,7 @@ import { Provider } from 'react-redux'; import configureStore from '../../../store/store'; import testData from '../../../../.storybook/test-data'; +import { getInternalAccounts } from '../../../selectors'; import ContactListTab from './contact-list-tab.component'; // Using Test Data For Redux @@ -14,6 +15,7 @@ export default { decorators: [(story) => {story()}], argsTypes: { addressBook: { control: 'object' }, + internalAccounts: { control: 'object' }, hideAddressBook: { control: 'boolean' }, selectedAddress: { control: 'select' }, history: { action: 'history' }, @@ -23,6 +25,8 @@ export default { const { metamask } = store.getState(); const { addresses } = metamask; +const internalAccounts = getInternalAccounts(store.getState()); + export const DefaultStory = (args) => { return (
@@ -34,6 +38,7 @@ export const DefaultStory = (args) => { DefaultStory.storyName = 'Default'; DefaultStory.args = { addressBook: addresses, + internalAccounts, hideAddressBook: false, selectedAddress: addresses.map(({ address }) => address), }; diff --git a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js index 335c5166da05..afb7efb1cc01 100644 --- a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js +++ b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js @@ -21,6 +21,7 @@ import { Display, TextVariant, } from '../../../../helpers/constants/design-system'; +import { isDuplicateContact } from '../../../../components/app/contact-list/utils'; export default class EditContact extends PureComponent { static contextTypes = { @@ -28,6 +29,8 @@ export default class EditContact extends PureComponent { }; static propTypes = { + addressBook: PropTypes.array, + internalAccounts: PropTypes.array, addToAddressBook: PropTypes.func, removeFromAddressBook: PropTypes.func, history: PropTypes.object, @@ -48,7 +51,30 @@ export default class EditContact extends PureComponent { newName: this.props.name, newAddress: this.props.address, newMemo: this.props.memo, - error: '', + nameError: '', + addressError: '', + }; + + validateName = (newName) => { + if (newName === this.props.name) { + return true; + } + + const { addressBook, internalAccounts } = this.props; + + return !isDuplicateContact(addressBook, internalAccounts, newName); + }; + + handleNameChange = (e) => { + const newName = e.target.value; + + const isValidName = this.validateName(newName); + + this.setState({ + nameError: isValidName ? null : this.context.t('nameAlreadyInUse'), + }); + + this.setState({ newName }); }; render() { @@ -118,9 +144,10 @@ export default class EditContact extends PureComponent { id="nickname" placeholder={this.context.t('addAlias')} value={this.state.newName} - onChange={(e) => this.setState({ newName: e.target.value })} + onChange={this.handleNameChange} fullWidth margin="dense" + error={this.state.nameError} />
@@ -132,7 +159,7 @@ export default class EditContact extends PureComponent { type="text" id="address" value={this.state.newAddress} - error={this.state.error} + error={this.state.addressError} onChange={(e) => this.setState({ newAddress: e.target.value })} fullWidth multiline @@ -189,7 +216,9 @@ export default class EditContact extends PureComponent { ); history.push(listRoute); } else { - this.setState({ error: this.context.t('invalidAddress') }); + this.setState({ + addressError: this.context.t('invalidAddress'), + }); } } else { // update name @@ -205,12 +234,13 @@ export default class EditContact extends PureComponent { history.push(`${viewRoute}/${address}`); }} submitText={this.context.t('save')} - disabled={ + disabled={Boolean( (this.state.newName === name && this.state.newAddress === address && this.state.newMemo === memo) || - !this.state.newName.trim() - } + !this.state.newName.trim() || + this.state.nameError, + )} />
); diff --git a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js index af248b04d330..ff10d850345e 100644 --- a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js +++ b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js @@ -2,8 +2,10 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { + getAddressBook, getAddressBookEntry, getInternalAccountByAddress, + getInternalAccounts, } from '../../../../selectors'; import { getProviderConfig } from '../../../../ducks/metamask/metamask'; import { @@ -34,6 +36,8 @@ const mapStateToProps = (state, ownProps) => { return { address: contact ? address : null, + addressBook: getAddressBook(state), + internalAccounts: getInternalAccounts(state), chainId, name, memo, diff --git a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.test.js b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.test.js index 779416140e10..958385d2c79a 100644 --- a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.test.js +++ b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.test.js @@ -4,6 +4,8 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import '@testing-library/jest-dom/extend-expect'; +import { MOCK_ADDRESS_BOOK } from '../../../../../test/data/mock-data'; +import { createMockInternalAccount } from '../../../../../test/jest/mocks'; import EditContact from './edit-contact.component'; describe('AddContact component', () => { @@ -11,11 +13,17 @@ describe('AddContact component', () => { const state = { metamask: {}, }; + + const mockAccount1 = createMockInternalAccount(); + const mockAccount2 = createMockInternalAccount({ name: 'Test Contact' }); + const props = { + addressBook: MOCK_ADDRESS_BOOK, + internalAccounts: [mockAccount1, mockAccount2], addToAddressBook: jest.fn(), removeFromAddressBook: jest.fn(), history: { push: jest.fn() }, - name: '', + name: mockAccount1.metadata.name, address: '0x0000000000000000001', chainId: '', memo: '', @@ -36,11 +44,14 @@ describe('AddContact component', () => { const store = configureMockStore(middleware)(state); const { getByText } = renderWithProvider(, store); - const input = document.getElementById('address'); - fireEvent.change(input, { target: { value: 'invalid address' } }); - setTimeout(() => { - expect(getByText('Invalid address')).toBeInTheDocument(); - }, 100); + const addressInput = document.getElementById('address'); + fireEvent.change(addressInput, { target: { value: 'invalid address' } }); + + const submitButton = getByText('Save'); + + fireEvent.click(submitButton); + + expect(getByText('Invalid address')).toBeInTheDocument(); }); it('should get disabled submit button when username field is empty', () => { @@ -53,4 +64,46 @@ describe('AddContact component', () => { const saveButton = getByText('Save'); expect(saveButton).toBeDisabled(); }); + + it('should display error when entering a name that is in use by an existing contact', () => { + const store = configureMockStore(middleware)(state); + const { getByText } = renderWithProvider(, store); + + const input = document.getElementById('nickname'); + fireEvent.change(input, { target: { value: MOCK_ADDRESS_BOOK[0].name } }); + + const saveButton = getByText('Save'); + + expect(saveButton).toBeDisabled(); + expect(getByText('Name is already in use')).toBeDefined(); + }); + + it('should display error when entering a name that is in use by an existing account', () => { + const store = configureMockStore(middleware)(state); + const { getByText } = renderWithProvider(, store); + + const input = document.getElementById('nickname'); + fireEvent.change(input, { target: { value: mockAccount2.metadata.name } }); + + const saveButton = getByText('Save'); + + expect(saveButton).toBeDisabled(); + expect(getByText('Name is already in use')).toBeDefined(); + }); + + it('should not display error when entering the current contact name', () => { + const store = configureMockStore(middleware)(state); + const { getByText, queryByText } = renderWithProvider( + , + store, + ); + + const input = document.getElementById('nickname'); + fireEvent.change(input, { target: { value: mockAccount1.metadata.name } }); + + const saveButton = getByText('Save'); + + expect(saveButton).toBeDisabled(); + expect(queryByText('Name is already in use')).toBeNull(); + }); }); From 62430fb657ead13e81969520a1f2a64ecd34fc0d Mon Sep 17 00:00:00 2001 From: Gauthier Petetin Date: Wed, 20 Nov 2024 16:44:38 -0300 Subject: [PATCH 05/28] fix(sentry sampling): divide by 2 our sentry trace sample rate to avoid exceeding our quota (#28573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Divide by 2 our sentry trace sample rate to avoid exceeding our quota ## **Related issues** Fixes: None ## **Manual testing steps** - None ## **Screenshots/Recordings** - None ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. Co-authored-by: Harika <153644847+hjetpoluru@users.noreply.github.com> --- app/scripts/lib/setupSentry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 354bb0bbb620..e268d25b2810 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -134,7 +134,7 @@ function getTracesSampleRate(sentryTarget) { return 1.0; } - return 0.02; + return 0.01; } /** From 94a7b20741d3bb0a6a6f45da7e74d1cf8bce16e7 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Wed, 20 Nov 2024 13:03:37 -0800 Subject: [PATCH 06/28] fix: use PORTFOLIO_VIEW flag to determine chain polling (#28504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates the token price and detection hooks to only poll across chains when `PORTFOLIO_VIEW` is set. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28504?quickstart=1) ## **Related issues** ## **Manual testing steps** 1. With `PORTFOLIO_VIEW=1`, requests should go to the price api across all chains. 2. Without `PORTFOLIO_VIEW=1`, requests should go to the price api on the current chain. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../onboarding-token-price-call-privacy.spec.ts | 7 ------- ui/hooks/useTokenDetectionPolling.ts | 8 +++++++- ui/hooks/useTokenRatesPolling.ts | 10 ++++++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/test/e2e/tests/privacy/onboarding-token-price-call-privacy.spec.ts b/test/e2e/tests/privacy/onboarding-token-price-call-privacy.spec.ts index a599d96fd5d4..f565c0bb354e 100644 --- a/test/e2e/tests/privacy/onboarding-token-price-call-privacy.spec.ts +++ b/test/e2e/tests/privacy/onboarding-token-price-call-privacy.spec.ts @@ -21,13 +21,6 @@ async function mockTokenPriceApi( statusCode: 200, json: {}, })), - // linea - await mockServer - .forGet('https://price.api.cx.metamask.io/v2/chains/59144/spot-prices') - .thenCallback(() => ({ - statusCode: 200, - json: {}, - })), ]; } diff --git a/ui/hooks/useTokenDetectionPolling.ts b/ui/hooks/useTokenDetectionPolling.ts index 790384e21cbf..d2e08d01892d 100644 --- a/ui/hooks/useTokenDetectionPolling.ts +++ b/ui/hooks/useTokenDetectionPolling.ts @@ -1,5 +1,6 @@ import { useSelector } from 'react-redux'; import { + getCurrentChainId, getNetworkConfigurationsByChainId, getUseTokenDetection, } from '../selectors'; @@ -17,14 +18,19 @@ const useTokenDetectionPolling = () => { const useTokenDetection = useSelector(getUseTokenDetection); const completedOnboarding = useSelector(getCompletedOnboarding); const isUnlocked = useSelector(getIsUnlocked); + const currentChainId = useSelector(getCurrentChainId); const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); const enabled = completedOnboarding && isUnlocked && useTokenDetection; + const chainIds = process.env.PORTFOLIO_VIEW + ? Object.keys(networkConfigurations) + : [currentChainId]; + useMultiPolling({ startPolling: tokenDetectionStartPolling, stopPollingByPollingToken: tokenDetectionStopPollingByPollingToken, - input: enabled ? [Object.keys(networkConfigurations)] : [], + input: enabled ? [chainIds] : [], }); return {}; diff --git a/ui/hooks/useTokenRatesPolling.ts b/ui/hooks/useTokenRatesPolling.ts index a740a426e36c..37864ec89b82 100644 --- a/ui/hooks/useTokenRatesPolling.ts +++ b/ui/hooks/useTokenRatesPolling.ts @@ -1,5 +1,6 @@ import { useSelector } from 'react-redux'; import { + getCurrentChainId, getMarketData, getNetworkConfigurationsByChainId, getTokenExchangeRates, @@ -16,10 +17,11 @@ import { } from '../ducks/metamask/metamask'; import useMultiPolling from './useMultiPolling'; -const useTokenRatesPolling = ({ chainIds }: { chainIds?: string[] } = {}) => { +const useTokenRatesPolling = () => { // Selectors to determine polling input const completedOnboarding = useSelector(getCompletedOnboarding); const isUnlocked = useSelector(getIsUnlocked); + const currentChainId = useSelector(getCurrentChainId); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); @@ -30,10 +32,14 @@ const useTokenRatesPolling = ({ chainIds }: { chainIds?: string[] } = {}) => { const enabled = completedOnboarding && isUnlocked && useCurrencyRateCheck; + const chainIds = process.env.PORTFOLIO_VIEW + ? Object.keys(networkConfigurations) + : [currentChainId]; + useMultiPolling({ startPolling: tokenRatesStartPolling, stopPollingByPollingToken: tokenRatesStopPollingByPollingToken, - input: enabled ? chainIds ?? Object.keys(networkConfigurations) : [], + input: enabled ? chainIds : [], }); return { From f2706214cc5d311a57e3074f3d08c060dde02076 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 20 Nov 2024 23:07:15 +0100 Subject: [PATCH 07/28] fix: fix coin-overview display when price setting is off (#28569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes display bug on coin overview and account list item when user toggles OFF the Show balance and token price checker setting [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28569?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28567 ## **Manual testing steps** 1. Go to settings turn off `Show balance and token price checker ` 2. Go back to home page you should see correct balance in crypto 3. Click on account item menu and you should see balance in crypto 4. Go to settings and turn it ON and you should see correct fiat balance. 5. Go to settings and turn on the "show native token as main balance" 6. You should see balance in crypto 7. Go to settings and turn OFF "show native token as main balance" also turn OFF "Show balance and token price checker" 8. You should see balance in crypto ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../app/wallet-overview/coin-overview.tsx | 18 +++++++++++++++--- .../account-list-item/account-list-item.js | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index b06f0c06b374..bf054d993e74 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -62,7 +62,10 @@ import { import Spinner from '../../ui/spinner'; import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change'; -import { getMultichainIsEvm } from '../../../selectors/multichain'; +import { + getMultichainIsEvm, + getMultichainShouldShowFiat, +} from '../../../selectors/multichain'; import { setAggregatedBalancePopoverShown, setPrivacyMode, @@ -73,6 +76,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountTotalCrossChainFiatBalance'; import { useGetFormattedTokensPerChain } from '../../../hooks/useGetFormattedTokensPerChain'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import WalletOverview from './wallet-overview'; import CoinButtons from './coin-buttons'; import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; @@ -160,9 +164,15 @@ export const CoinOverview = ({ formattedTokensWithBalancesPerChain, ); + const shouldShowFiat = useMultichainSelector( + getMultichainShouldShowFiat, + account, + ); + const isEvm = useSelector(getMultichainIsEvm); const isNotAggregatedFiatBalance = - showNativeTokenAsMainBalance || isTestnet || !isEvm; + !shouldShowFiat || showNativeTokenAsMainBalance || isTestnet || !isEvm; + let balanceToDisplay; if (isNotAggregatedFiatBalance) { balanceToDisplay = balance; @@ -300,7 +310,9 @@ export const CoinOverview = ({ hideTitle shouldCheckShowNativeToken isAggregatedFiatOverviewBalance={ - !showNativeTokenAsMainBalance && !isTestnet + !showNativeTokenAsMainBalance && + !isTestnet && + shouldShowFiat } privacyMode={privacyMode} /> diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index e63266080be5..6be8e3f67f0b 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -143,7 +143,7 @@ const AccountListItem = ({ let balanceToTranslate; if (isEvmNetwork) { balanceToTranslate = - isTestnet || !process.env.PORTFOLIO_VIEW + shouldShowFiat || isTestnet || !process.env.PORTFOLIO_VIEW ? account.balance : totalFiatBalance; } else { From d5c0aaa0a0f4a4897fe187eb9c39cca180973bc6 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:34:30 -0500 Subject: [PATCH 08/28] ci: limit playwright install to chromium browser only (#28580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Storybook CI jobs are failing due to the `playwright install` step timing out due to an AWS issue. We may be able to work around this issue by reducing the number of browsers we download. We only use chromium, so this PR limits it to just chromium [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28580?quickstart=1) --- .circleci/config.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a81017489c1..a8185c00ee2f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -964,7 +964,7 @@ jobs: at: . - run: name: Install Playwright browsers - command: yarn exec playwright install + command: yarn exec playwright install chromium - run: name: Test Storybook command: yarn test-storybook:ci diff --git a/package.json b/package.json index 929cc375f683..7117462c957f 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "ts-migration:dashboard:watch": "yarn ts-migration:dashboard:build --watch", "ts-migration:enumerate": "ts-node development/ts-migration-dashboard/scripts/write-list-of-files-to-convert.ts", "test-storybook": "test-storybook -c .storybook", - "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn storybook:build && npx http-server storybook-build --port 6006 \" \"wait-on tcp:6006 && echo 'Build done. Running storybook tests...' && npx playwright install && yarn test-storybook --maxWorkers=2\"", + "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn storybook:build && npx http-server storybook-build --port 6006 \" \"wait-on tcp:6006 && echo 'Build done. Running storybook tests...' && yarn test-storybook --maxWorkers=2\"", "githooks:install": "husky install", "fitness-functions": "ts-node development/fitness-functions/index.ts", "generate-beta-commit": "node ./development/generate-beta-commit.js", From d440363914be3ef7fd9092154b2e37b5b193f3f7 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Wed, 20 Nov 2024 15:18:02 -0800 Subject: [PATCH 09/28] fix: use PORTFOLIO_VIEW flag to determine token list polling (#28579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates the token list hook to only poll across chains when `PORTFOLIO_VIEW` is set. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28579?quickstart=1) ## **Related issues** ## **Manual testing steps** 1. With `PORTFOLIO_VIEW=1`, requests should go to the token api across all chains. 2. Without `PORTFOLIO_VIEW=1`, requests should go to the token api on the current chain. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ...rs-after-init-opt-in-background-state.json | 6 +----- .../errors-after-init-opt-in-ui-state.json | 6 +----- ui/hooks/useTokenListPolling.test.ts | 21 +++++++++++-------- ui/hooks/useTokenListPolling.ts | 8 ++++++- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 39ec3fb2530b..d74eb479fbc5 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -321,11 +321,7 @@ "TokenListController": { "tokenList": "object", "tokensChainsCache": { - "0x1": "object", - "0x539": "object", - "0xaa36a7": "object", - "0xe705": "object", - "0xe708": "object" + "0x539": "object" }, "preventPollingOnNetworkRestart": false }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 615689cb6d19..96a80b021f0b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -175,11 +175,7 @@ "nonRPCGasFeeApisDisabled": "boolean", "tokenList": "object", "tokensChainsCache": { - "0x1": "object", - "0x539": "object", - "0xaa36a7": "object", - "0xe705": "object", - "0xe708": "object" + "0x539": "object" }, "tokenBalances": "object", "preventPollingOnNetworkRestart": false, diff --git a/ui/hooks/useTokenListPolling.test.ts b/ui/hooks/useTokenListPolling.test.ts index 09a22fffea50..001dca71c80e 100644 --- a/ui/hooks/useTokenListPolling.test.ts +++ b/ui/hooks/useTokenListPolling.test.ts @@ -22,16 +22,23 @@ describe('useTokenListPolling', () => { jest.clearAllMocks(); }); - it('should poll for token lists on each chain when enabled, and stop on dismount', async () => { + it('should poll the selected network when enabled, and stop on dismount', async () => { const state = { metamask: { isUnlocked: true, completedOnboarding: true, useExternalServices: true, useTokenDetection: true, + selectedNetworkClientId: 'selectedNetworkClientId', networkConfigurationsByChainId: { - '0x1': {}, - '0x89': {}, + '0x1': { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'selectedNetworkClientId', + }, + ], + }, }, }, }; @@ -43,19 +50,15 @@ describe('useTokenListPolling', () => { // Should poll each chain await Promise.all(mockPromises); - expect(tokenListStartPolling).toHaveBeenCalledTimes(2); + expect(tokenListStartPolling).toHaveBeenCalledTimes(1); expect(tokenListStartPolling).toHaveBeenCalledWith('0x1'); - expect(tokenListStartPolling).toHaveBeenCalledWith('0x89'); // Stop polling on dismount unmount(); - expect(tokenListStopPollingByPollingToken).toHaveBeenCalledTimes(2); + expect(tokenListStopPollingByPollingToken).toHaveBeenCalledTimes(1); expect(tokenListStopPollingByPollingToken).toHaveBeenCalledWith( '0x1_token', ); - expect(tokenListStopPollingByPollingToken).toHaveBeenCalledWith( - '0x89_token', - ); }); it('should not poll before onboarding is completed', async () => { diff --git a/ui/hooks/useTokenListPolling.ts b/ui/hooks/useTokenListPolling.ts index 9b43c3c6959a..7f7de517c304 100644 --- a/ui/hooks/useTokenListPolling.ts +++ b/ui/hooks/useTokenListPolling.ts @@ -1,5 +1,6 @@ import { useSelector } from 'react-redux'; import { + getCurrentChainId, getNetworkConfigurationsByChainId, getPetnamesEnabled, getUseExternalServices, @@ -17,6 +18,7 @@ import { import useMultiPolling from './useMultiPolling'; const useTokenListPolling = () => { + const currentChainId = useSelector(getCurrentChainId); const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); const useTokenDetection = useSelector(getUseTokenDetection); const useTransactionSimulations = useSelector(getUseTransactionSimulations); @@ -31,10 +33,14 @@ const useTokenListPolling = () => { useExternalServices && (useTokenDetection || petnamesEnabled || useTransactionSimulations); + const chainIds = process.env.PORTFOLIO_VIEW + ? Object.keys(networkConfigurations) + : [currentChainId]; + useMultiPolling({ startPolling: tokenListStartPolling, stopPollingByPollingToken: tokenListStopPollingByPollingToken, - input: enabled ? Object.keys(networkConfigurations) : [], + input: enabled ? chainIds : [], }); return {}; From 1975c85f2b078e30d7bf12c7bcbb585d37021bb9 Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 20 Nov 2024 15:42:36 -0800 Subject: [PATCH 10/28] fix: Gracefully handle bad responses from `net_version` calls to RPC endpoint when getting Provider Network State (#27509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Currently some users are seeing `Error: Cannot parse as a valid network ID: 'undefined'` errors when connected to RPC endpoints that fail to provide a value from `net_version` calls that can be parsed into a decimal string. This PR makes our internally handling of this case more flexibly by using `null` as the network version value sent to the inpage provider when receiving an unexpected `net_version` call value. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27509?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27487 ## **Manual testing steps** 1. Open extension background 2. Intercept a request for net_version (you can use a proxy like Burpsuite) and make it return a bad value 3. There should be no error in extension background related to this 4. Open a dapp 5. Open the dapp developer console 6. Expect to see an error like `JsonRpcError: MetaMask: Disconnected from chain. Attempting to connect.` 7. Test that `window.ethereum.request` works as expected despite this ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- app/scripts/metamask-controller.js | 2 +- shared/modules/network.utils.test.ts | 49 ++++++++++++++++++++++++++++ shared/modules/network.utils.ts | 10 +++--- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3b75349091ad..0d0344d4e7e3 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3209,7 +3209,7 @@ export default class MetamaskController extends EventEmitter { const { completedOnboarding } = this.onboardingController.state; let networkVersion = this.deprecatedNetworkVersions[networkClientId]; - if (!networkVersion && completedOnboarding) { + if (networkVersion === undefined && completedOnboarding) { const ethQuery = new EthQuery(networkClient.provider); networkVersion = await new Promise((resolve) => { ethQuery.sendAsync({ method: 'net_version' }, (error, result) => { diff --git a/shared/modules/network.utils.test.ts b/shared/modules/network.utils.test.ts index ee4ef3f8399e..783d764f8825 100644 --- a/shared/modules/network.utils.test.ts +++ b/shared/modules/network.utils.test.ts @@ -3,6 +3,7 @@ import { isSafeChainId, isPrefixedFormattedHexString, isTokenDetectionEnabledForNetwork, + convertNetworkId, } from './network.utils'; describe('network utils', () => { @@ -83,4 +84,52 @@ describe('network utils', () => { expect(isTokenDetectionEnabledForNetwork(undefined)).toBe(false); }); }); + + describe('convertNetworkId', () => { + it('returns decimal strings for postive integer number values', () => { + expect(convertNetworkId(0)).toStrictEqual('0'); + expect(convertNetworkId(123)).toStrictEqual('123'); + expect(convertNetworkId(1337)).toStrictEqual('1337'); + }); + + it('returns null for negative numbers', () => { + expect(convertNetworkId(-1)).toStrictEqual(null); + }); + + it('returns null for non integer numbers', () => { + expect(convertNetworkId(0.1)).toStrictEqual(null); + expect(convertNetworkId(1.1)).toStrictEqual(null); + }); + + it('returns null for NaN', () => { + expect(convertNetworkId(Number.NaN)).toStrictEqual(null); + }); + + it('returns decimal strings for strict valid hex values', () => { + expect(convertNetworkId('0x0')).toStrictEqual('0'); + expect(convertNetworkId('0x1')).toStrictEqual('1'); + expect(convertNetworkId('0x539')).toStrictEqual('1337'); + }); + + it('returns null for invalid hex values', () => { + expect(convertNetworkId('0xG')).toStrictEqual(null); + expect(convertNetworkId('0x@')).toStrictEqual(null); + expect(convertNetworkId('0xx1')).toStrictEqual(null); + }); + + it('returns the value as is if already a postive decimal string', () => { + expect(convertNetworkId('0')).toStrictEqual('0'); + expect(convertNetworkId('1')).toStrictEqual('1'); + expect(convertNetworkId('1337')).toStrictEqual('1337'); + }); + + it('returns null for negative number strings', () => { + expect(convertNetworkId('-1')).toStrictEqual(null); + }); + + it('returns null for non integer number strings', () => { + expect(convertNetworkId('0.1')).toStrictEqual(null); + expect(convertNetworkId('1.1')).toStrictEqual(null); + }); + }); }); diff --git a/shared/modules/network.utils.ts b/shared/modules/network.utils.ts index 8985bdcf7f5a..764a34bb8520 100644 --- a/shared/modules/network.utils.ts +++ b/shared/modules/network.utils.ts @@ -78,16 +78,16 @@ function isSafeInteger(value: unknown): value is number { * as either a number, a decimal string, or a 0x-prefixed hex string. * * @param value - The network ID to convert, in an unknown format. - * @returns A valid network ID (as a decimal string) - * @throws If the given value cannot be safely parsed. + * @returns A valid network ID (as a decimal string) or null if + * the given value cannot be parsed. */ -export function convertNetworkId(value: unknown): string { - if (typeof value === 'number' && !Number.isNaN(value)) { +export function convertNetworkId(value: unknown): string | null { + if (typeof value === 'number' && Number.isInteger(value) && value >= 0) { return `${value}`; } else if (isStrictHexString(value)) { return `${convertHexToDecimal(value)}`; } else if (typeof value === 'string' && /^\d+$/u.test(value)) { return value; } - throw new Error(`Cannot parse as a valid network ID: '${value}'`); + return null; } From bd2248dbc42dcf943370c13567568141802b9ff1 Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:54:06 -0800 Subject: [PATCH 11/28] refactor: Cherry pick asset-list-control-bar updates (#28575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Cherry picks design updates for `AssetListControlBar` introduced from https://github.com/MetaMask/metamask-extension/pull/28386 separately in it's own PR to help minimize diff in main feature branch. Also includes unit test and e2e updates impacted from these changes. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28575?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** Run with feature flag and without feature flag: `yarn webpack --watch` `PORTFOLIO_VIEW=1 yarn webpack --watch` Validate that sort works, validate that import works, validate that refresh list works. ## **Screenshots/Recordings** Without feature flag: https://github.com/user-attachments/assets/445d4fd1-93d1-4cee-bd7b-bcc36518d7ca With feature flag (network filter not yet integrated) https://github.com/user-attachments/assets/d1aa8812-9787-49b5-9696-39e56d82ed56 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Jonathan Bursztyn --- .storybook/test-data.js | 27 +++ app/scripts/constants/sentry-state.ts | 1 + test/e2e/fixture-builder.js | 3 + .../erc20-approve-redesign.spec.ts | 1 + ...rs-after-init-opt-in-background-state.json | 8 +- .../errors-after-init-opt-in-ui-state.json | 8 +- ...s-before-init-opt-in-background-state.json | 2 +- .../errors-before-init-opt-in-ui-state.json | 2 +- .../tests/privacy/basic-functionality.spec.js | 6 +- test/e2e/tests/tokens/add-hide-token.spec.js | 1 + .../tokens/custom-token-add-approve.spec.js | 1 + test/e2e/tests/tokens/import-tokens.spec.js | 1 + test/e2e/tests/tokens/token-details.spec.ts | 1 + test/e2e/tests/tokens/token-list.spec.ts | 1 + test/e2e/tests/tokens/token-sort.spec.ts | 1 + .../asset-list-control-bar.tsx | 176 ++++++++++++++---- .../asset-list-control-bar/index.scss | 3 +- .../app/assets/asset-list/asset-list.test.tsx | 36 +++- .../app/assets/asset-list/asset-list.tsx | 20 +- .../import-control/import-control.tsx | 32 +--- .../asset-list/sort-control/sort-control.tsx | 4 +- .../account-overview-btc.test.tsx | 29 ++- .../account-overview-eth.test.tsx | 29 ++- .../import-token-link.test.js.snap | 47 ----- .../import-token-link.stories.tsx | 14 -- .../import-token-link.test.js | 93 --------- .../import-token-link/import-token-link.tsx | 40 ---- .../import-token-link.types.ts | 8 - .../multichain/import-token-link/index.ts | 1 - ui/components/multichain/index.js | 1 - ui/pages/routes/routes.component.test.js | 70 +++++++ 31 files changed, 373 insertions(+), 294 deletions(-) delete mode 100644 ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap delete mode 100644 ui/components/multichain/import-token-link/import-token-link.stories.tsx delete mode 100644 ui/components/multichain/import-token-link/import-token-link.test.js delete mode 100644 ui/components/multichain/import-token-link/import-token-link.tsx delete mode 100644 ui/components/multichain/import-token-link/import-token-link.types.ts delete mode 100644 ui/components/multichain/import-token-link/index.ts diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 13006e5d1ff7..a36cbf944981 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -487,6 +487,32 @@ const state = { }, }, }, + allTokens: { + '0x1': { + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + aggregators: [], + decimals: 6, + symbol: 'USDC', + }, + { + address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', + aggregators: [], + decimals: 18, + symbol: 'YFI', + }, + ], + }, + }, + tokenBalances: { + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { + '0x1': { + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0xbdbd', + '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': '0x501b4176a64d6', + }, + }, + }, tokens: [ { address: '0xaD6D458402F60fD3Bd25163575031ACDce07538A', @@ -682,6 +708,7 @@ const state = { order: 'dsc', sortCallback: 'stringNumeric', }, + tokenNetworkFilter: {}, }, incomingTransactionsPreferences: { [CHAIN_IDS.MAINNET]: true, diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index bd953e72d49c..d0fbe7bcb085 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -245,6 +245,7 @@ export const SENTRY_BACKGROUND_STATE = { showFiatInTestnets: true, showTestNetworks: true, smartTransactionsOptInStatus: true, + tokenNetworkFilter: {}, showNativeTokenAsMainBalance: true, petnamesEnabled: true, showConfirmationAdvancedDetails: true, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index bea9e9bad77f..844c4766db3e 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -89,6 +89,7 @@ function onboardingFixture() { order: 'dsc', sortCallback: 'stringNumeric', }, + tokenNetworkFilter: {}, shouldShowAggregatedBalancePopover: true, }, useExternalServices: true, @@ -126,6 +127,7 @@ function onboardingFixture() { }, showTestNetworks: false, smartTransactionsOptInStatus: true, + tokenNetworkFilter: {}, }, QueuedRequestController: { queuedRequestCount: 0, @@ -664,6 +666,7 @@ class FixtureBuilder { return this.withPreferencesController({ preferences: { smartTransactionsOptInStatus: true, + tokenNetworkFilter: {}, }, }); } diff --git a/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts index baa3638330b6..4e340f5ef3ac 100644 --- a/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts @@ -118,6 +118,7 @@ async function mocks(server: MockttpServer) { export async function importTST(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); await driver.clickElement('[data-testid="import-token-button"]'); + await driver.clickElement('[data-testid="importTokens"]'); await driver.waitForSelector({ css: '.import-tokens-modal__button-tab', diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index d74eb479fbc5..cea439180b5b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -237,7 +237,13 @@ "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", "tokenSortConfig": "object", - "tokenNetworkFilter": "object", + "tokenNetworkFilter": { + "0x1": "boolean", + "0xaa36a7": "boolean", + "0xe705": "boolean", + "0xe708": "boolean", + "0x539": "boolean" + }, "shouldShowAggregatedBalancePopover": "boolean" }, "ipfsGateway": "string", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 96a80b021f0b..3bc7057435c8 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -38,7 +38,13 @@ "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", "tokenSortConfig": "object", - "tokenNetworkFilter": "object", + "tokenNetworkFilter": { + "0x1": "boolean", + "0xaa36a7": "boolean", + "0xe705": "boolean", + "0xe708": "boolean", + "0x539": "boolean" + }, "shouldShowAggregatedBalancePopover": "boolean" }, "firstTimeFlowType": "import", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 91c994e9ab66..5f5f47f3e7ee 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -120,7 +120,7 @@ "tokenSortConfig": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean", - "tokenNetworkFilter": "object" + "tokenNetworkFilter": {} }, "selectedAddress": "string", "theme": "light", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 552f089c6604..f997b89bcd28 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -135,7 +135,7 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "showConfirmationAdvancedDetails": false, "tokenSortConfig": "object", - "tokenNetworkFilter": "object", + "tokenNetworkFilter": {}, "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index e6439c569339..77e9dad0b46f 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -102,7 +102,8 @@ describe('MetaMask onboarding @no-mmi', function () { // Wait until network is fully switched and refresh tokens before asserting to mitigate flakiness await driver.assertElementNotPresent('.loading-overlay'); - await driver.clickElement('[data-testid="refresh-list-button"]'); + await driver.clickElement(`[data-testid="import-token-button"]`); + await driver.clickElement('[data-testid="refreshList"]'); for (let i = 0; i < mockedEndpoints.length; i += 1) { const requests = await mockedEndpoints[i].getSeenRequests(); @@ -157,7 +158,8 @@ describe('MetaMask onboarding @no-mmi', function () { // Wait until network is fully switched and refresh tokens before asserting to mitigate flakiness await driver.assertElementNotPresent('.loading-overlay'); - await driver.clickElement('[data-testid="refresh-list-button"]'); + await driver.clickElement(`[data-testid="import-token-button"]`); + await driver.clickElement('[data-testid="refreshList"]'); // intended delay to allow for network requests to complete await driver.delay(1000); for (let i = 0; i < mockedEndpoints.length; i += 1) { diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js index 6bd0c8744fba..c9a1f26ad9eb 100644 --- a/test/e2e/tests/tokens/add-hide-token.spec.js +++ b/test/e2e/tests/tokens/add-hide-token.spec.js @@ -130,6 +130,7 @@ describe('Add existing token using search', function () { await unlockWallet(driver); await driver.clickElement(`[data-testid="import-token-button"]`); + await driver.clickElement(`[data-testid="importTokens"]`); await driver.fill('input[placeholder="Search tokens"]', 'BAT'); await driver.clickElement({ text: 'BAT', diff --git a/test/e2e/tests/tokens/custom-token-add-approve.spec.js b/test/e2e/tests/tokens/custom-token-add-approve.spec.js index 8fa765a36164..4e85aae76fd6 100644 --- a/test/e2e/tests/tokens/custom-token-add-approve.spec.js +++ b/test/e2e/tests/tokens/custom-token-add-approve.spec.js @@ -36,6 +36,7 @@ describe('Create token, approve token and approve token without gas', function ( await clickNestedButton(driver, 'Tokens'); await driver.clickElement(`[data-testid="import-token-button"]`); + await driver.clickElement(`[data-testid="importTokens"]`); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/import-tokens.spec.js b/test/e2e/tests/tokens/import-tokens.spec.js index 3055f7109551..7b1bf60964ab 100644 --- a/test/e2e/tests/tokens/import-tokens.spec.js +++ b/test/e2e/tests/tokens/import-tokens.spec.js @@ -69,6 +69,7 @@ describe('Import flow', function () { await driver.assertElementNotPresent('.loading-overlay'); await driver.clickElement('[data-testid="import-token-button"]'); + await driver.clickElement('[data-testid="importTokens"]'); await driver.fill('input[placeholder="Search tokens"]', 'cha'); diff --git a/test/e2e/tests/tokens/token-details.spec.ts b/test/e2e/tests/tokens/token-details.spec.ts index 56d9515e727d..2ee84d339ea8 100644 --- a/test/e2e/tests/tokens/token-details.spec.ts +++ b/test/e2e/tests/tokens/token-details.spec.ts @@ -28,6 +28,7 @@ describe('Token Details', function () { const importToken = async (driver: Driver) => { await driver.clickElement(`[data-testid="import-token-button"]`); + await driver.clickElement(`[data-testid="importTokens"]`); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/token-list.spec.ts b/test/e2e/tests/tokens/token-list.spec.ts index c20a9f13b0e3..f7b032c92a4c 100644 --- a/test/e2e/tests/tokens/token-list.spec.ts +++ b/test/e2e/tests/tokens/token-list.spec.ts @@ -28,6 +28,7 @@ describe('Token List', function () { const importToken = async (driver: Driver) => { await driver.clickElement(`[data-testid="import-token-button"]`); + await driver.clickElement(`[data-testid="importTokens"]`); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/token-sort.spec.ts b/test/e2e/tests/tokens/token-sort.spec.ts index 1fc1df7efd2c..ed6005a710ad 100644 --- a/test/e2e/tests/tokens/token-sort.spec.ts +++ b/test/e2e/tests/tokens/token-sort.spec.ts @@ -26,6 +26,7 @@ describe('Token List', function () { const importToken = async (driver: Driver) => { await driver.clickElement(`[data-testid="import-token-button"]`); + await driver.clickElement(`[data-testid="importTokens"]`); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx index 7722eff36870..2925277c14bd 100644 --- a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx @@ -1,6 +1,10 @@ -import React, { useMemo, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { getCurrentNetwork, getPreferences } from '../../../../../selectors'; +import React, { useEffect, useRef, useState, useContext, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + getCurrentNetwork, + getNetworkConfigurationsByChainId, + getPreferences, +} from '../../../../../selectors'; import { Box, ButtonBase, @@ -9,17 +13,22 @@ import { Popover, PopoverPosition, } from '../../../../component-library'; -import SortControl from '../sort-control'; +import SortControl, { SelectableListItem } from '../sort-control/sort-control'; import { BackgroundColor, - BorderColor, - BorderStyle, Display, JustifyContent, TextColor, + TextVariant, } from '../../../../../helpers/constants/design-system'; import ImportControl from '../import-control'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { TEST_CHAINS } from '../../../../../../shared/constants/network'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../../../shared/constants/metametrics'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../../../../app/scripts/lib/util'; @@ -28,7 +37,12 @@ import { ENVIRONMENT_TYPE_POPUP, } from '../../../../../../shared/constants/app'; import NetworkFilter from '../network-filter'; -import { TEST_CHAINS } from '../../../../../../shared/constants/network'; +import { + detectTokens, + setTokenNetworkFilter, + showImportTokensModal, +} from '../../../../../store/actions'; +import Tooltip from '../../../../ui/tooltip'; type AssetListControlBarProps = { showTokensLinks?: boolean; @@ -36,18 +50,55 @@ type AssetListControlBarProps = { const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { const t = useI18nContext(); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); const popoverRef = useRef(null); const currentNetwork = useSelector(getCurrentNetwork); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const { tokenNetworkFilter } = useSelector(getPreferences); const [isTokenSortPopoverOpen, setIsTokenSortPopoverOpen] = useState(false); + const [isImportTokensPopoverOpen, setIsImportTokensPopoverOpen] = + useState(false); const [isNetworkFilterPopoverOpen, setIsNetworkFilterPopoverOpen] = useState(false); - const allNetworksFilterShown = Object.keys(tokenNetworkFilter ?? {}).length; const isTestNetwork = useMemo(() => { return (TEST_CHAINS as string[]).includes(currentNetwork.chainId); }, [currentNetwork.chainId, TEST_CHAINS]); + const allOpts: Record = {}; + Object.keys(allNetworks).forEach((chainId) => { + allOpts[chainId] = true; + }); + + const allNetworksFilterShown = + Object.keys(tokenNetworkFilter).length !== Object.keys(allOpts).length; + + useEffect(() => { + if (isTestNetwork) { + const testnetFilter = { [currentNetwork.chainId]: true }; + dispatch(setTokenNetworkFilter(testnetFilter)); + } + }, [isTestNetwork, currentNetwork.chainId, dispatch]); + + // TODO: This useEffect should be a migration + // We need to set the default filter for all users to be all included networks, rather than defaulting to empty object + // This effect is to unblock and derisk in the short-term + useEffect(() => { + if (Object.keys(tokenNetworkFilter).length === 0) { + dispatch(setTokenNetworkFilter(allOpts)); + } + }, []); + + // When a network gets added/removed we want to make sure that we switch to the filtered list of the current network + // We only want to do this if the "Current Network" filter is selected + useEffect(() => { + if (Object.keys(tokenNetworkFilter).length === 1) { + dispatch(setTokenNetworkFilter({ [currentNetwork.chainId]: true })); + } + }, [Object.keys(allNetworks).length]); + const windowType = getEnvironmentType(); const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && @@ -55,37 +106,65 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { const toggleTokenSortPopover = () => { setIsNetworkFilterPopoverOpen(false); + setIsImportTokensPopoverOpen(false); setIsTokenSortPopoverOpen(!isTokenSortPopoverOpen); }; const toggleNetworkFilterPopover = () => { setIsTokenSortPopoverOpen(false); + setIsImportTokensPopoverOpen(false); setIsNetworkFilterPopoverOpen(!isNetworkFilterPopoverOpen); }; + const toggleImportTokensPopover = () => { + setIsTokenSortPopoverOpen(false); + setIsNetworkFilterPopoverOpen(false); + setIsImportTokensPopoverOpen(!isImportTokensPopoverOpen); + }; + const closePopover = () => { setIsTokenSortPopoverOpen(false); setIsNetworkFilterPopoverOpen(false); + setIsImportTokensPopoverOpen(false); + }; + + const handleImport = () => { + dispatch(showImportTokensModal()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.TokenImportButtonClicked, + properties: { + location: 'HOME', + }, + }); + closePopover(); + }; + + const handleRefresh = () => { + dispatch(detectTokens()); + closePopover(); }; return ( {process.env.PORTFOLIO_VIEW && ( { ? BackgroundColor.backgroundPressed : BackgroundColor.backgroundDefault } - borderColor={BorderColor.borderMuted} - borderStyle={BorderStyle.solid} color={TextColor.textDefault} marginRight={isFullScreen ? 2 : null} ellipsis @@ -107,26 +184,33 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { )} - - {t('sortBy')} - + + + - + + { { > + + + + {t('importTokensCamelCase')} + + + {t('refreshList')} + + ); }; diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss index da8376679356..1fee45c33a87 100644 --- a/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss @@ -8,7 +8,8 @@ &__network_control { justify-content: space-between; - min-width: 185px; + width: auto; + min-width: auto; border-radius: 8px; padding: 0 8px !important; gap: 5px; diff --git a/ui/components/app/assets/asset-list/asset-list.test.tsx b/ui/components/app/assets/asset-list/asset-list.test.tsx index fd65e740238d..00a47df1c633 100644 --- a/ui/components/app/assets/asset-list/asset-list.test.tsx +++ b/ui/components/app/assets/asset-list/asset-list.test.tsx @@ -1,10 +1,13 @@ import React from 'react'; import { screen, act, waitFor } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; import { renderWithProvider } from '../../../../../test/jest'; -import configureStore, { MetaMaskReduxState } from '../../../../store/store'; +import { MetaMaskReduxState } from '../../../../store/store'; import mockState from '../../../../../test/data/mock-state.json'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol'; +import useMultiPolling from '../../../../hooks/useMultiPolling'; import { getTokenSymbol } from '../../../../store/actions'; import { getSelectedInternalAccountFromMockState } from '../../../../../test/jest/mocks'; import { mockNetworkState } from '../../../../../test/stub/networks'; @@ -64,11 +67,19 @@ jest.mock('../../../../hooks/useIsOriginalNativeTokenSymbol', () => { jest.mock('../../../../store/actions', () => { return { getTokenSymbol: jest.fn(), + setTokenNetworkFilter: jest.fn(() => ({ + type: 'TOKEN_NETWORK_FILTER', + })), tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'), tokenBalancesStopPollingByPollingToken: jest.fn(), }; }); +jest.mock('../../../../hooks/useMultiPolling', () => ({ + __esModule: true, + default: jest.fn(), +})); + const mockSelectedInternalAccount = getSelectedInternalAccountFromMockState( mockState as unknown as MetaMaskReduxState, ); @@ -103,7 +114,7 @@ const render = (balance = ETH_BALANCE, chainId = CHAIN_IDS.MAINNET) => { }, }, }; - const store = configureStore(state); + const store = configureMockStore([thunk])(state); return renderWithProvider( undefined} showTokensLinks />, store, @@ -111,6 +122,22 @@ const render = (balance = ETH_BALANCE, chainId = CHAIN_IDS.MAINNET) => { }; describe('AssetList', () => { + (useMultiPolling as jest.Mock).mockClear(); + + // Mock implementation for useMultiPolling + (useMultiPolling as jest.Mock).mockImplementation(({ input }) => { + // Mock startPolling and stopPollingByPollingToken for each input + const startPolling = jest.fn().mockResolvedValue('mockPollingToken'); + const stopPollingByPollingToken = jest.fn(); + + input.forEach((inputItem: string) => { + const key = JSON.stringify(inputItem); + // Simulate returning a unique token for each input + startPolling.mockResolvedValueOnce(`mockToken-${key}`); + }); + + return { startPolling, stopPollingByPollingToken }; + }); (useIsOriginalNativeTokenSymbol as jest.Mock).mockReturnValue(true); (getTokenSymbol as jest.Mock).mockImplementation(async (address) => { @@ -126,13 +153,14 @@ describe('AssetList', () => { return null; }); - it('renders AssetList component and shows Refresh List text', async () => { + it('renders AssetList component and shows AssetList control bar', async () => { await act(async () => { render(); }); await waitFor(() => { - expect(screen.getByText('Refresh list')).toBeInTheDocument(); + expect(screen.getByTestId('sort-by-popover-toggle')).toBeInTheDocument(); + expect(screen.getByTestId('import-token-button')).toBeInTheDocument(); }); }); }); diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 1d265ffd4a4a..9ed6b718cbd8 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -13,8 +13,8 @@ import { getMultichainSelectedAccountCachedBalance, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getMultichainIsBitcoin, - ///: END:ONLY_INCLUDE_IF getMultichainSelectedAccountCachedBalanceIsZero, + ///: END:ONLY_INCLUDE_IF } from '../../../../selectors/multichain'; import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; @@ -23,11 +23,7 @@ import { MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; import DetectedToken from '../../detected-token/detected-token'; -import { - DetectedTokensBanner, - ImportTokenLink, - ReceiveModal, -} from '../../../multichain'; +import { DetectedTokensBanner, ReceiveModal } from '../../../multichain'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -88,11 +84,10 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { setShowReceiveModal(true); }; + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const balanceIsZero = useSelector( getMultichainSelectedAccountCachedBalanceIsZero, ); - - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBuyableChain = useSelector(getIsNativeTokenBuyable); const shouldShowBuy = isBuyableChain && balanceIsZero; const isBtc = useSelector(getMultichainIsBitcoin); @@ -113,7 +108,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { margin={4} /> )} - + } onTokenClick={(chainId: string, tokenAddress: string) => { @@ -144,13 +139,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { ) : null ///: END:ONLY_INCLUDE_IF } - {shouldShowTokensLinks && ( - 0 && !balanceIsZero ? 0 : 2} - /> - )} {showDetectedTokens && ( )} diff --git a/ui/components/app/assets/asset-list/import-control/import-control.tsx b/ui/components/app/assets/asset-list/import-control/import-control.tsx index ca5e3a09051a..d3a9bfd9ccb7 100644 --- a/ui/components/app/assets/asset-list/import-control/import-control.tsx +++ b/ui/components/app/assets/asset-list/import-control/import-control.tsx @@ -1,5 +1,5 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React from 'react'; +import { useSelector } from 'react-redux'; import { ButtonBase, ButtonBaseSize, @@ -9,21 +9,18 @@ import { BackgroundColor, TextColor, } from '../../../../../helpers/constants/design-system'; -import { showImportTokensModal } from '../../../../../store/actions'; -import { MetaMetricsContext } from '../../../../../contexts/metametrics'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../../../../shared/constants/metametrics'; + import { getMultichainIsEvm } from '../../../../../selectors/multichain'; type AssetListControlBarProps = { showTokensLinks?: boolean; + onClick?: () => void; }; -const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { - const dispatch = useDispatch(); - const trackEvent = useContext(MetaMetricsContext); +const AssetListControlBar = ({ + showTokensLinks, + onClick, +}: AssetListControlBarProps) => { const isEvm = useSelector(getMultichainIsEvm); // NOTE: Since we can parametrize it now, we keep the original behavior // for EVM assets @@ -35,19 +32,10 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { data-testid="import-token-button" disabled={!shouldShowTokensLinks} size={ButtonBaseSize.Sm} - startIconName={IconName.Add} + startIconName={IconName.MoreVertical} backgroundColor={BackgroundColor.backgroundDefault} color={TextColor.textDefault} - onClick={() => { - dispatch(showImportTokensModal()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.TokenImportButtonClicked, - properties: { - location: 'HOME', - }, - }); - }} + onClick={onClick} /> ); }; diff --git a/ui/components/app/assets/asset-list/sort-control/sort-control.tsx b/ui/components/app/assets/asset-list/sort-control/sort-control.tsx index 8e216b5ed6c2..7e2bd48e7c4b 100644 --- a/ui/components/app/assets/asset-list/sort-control/sort-control.tsx +++ b/ui/components/app/assets/asset-list/sort-control/sort-control.tsx @@ -22,7 +22,7 @@ import { getCurrencySymbol } from '../../../../../helpers/utils/common.util'; // inspired from ui/components/multichain/network-list-item // should probably be broken out into component library type SelectableListItemProps = { - isSelected: boolean; + isSelected?: boolean; onClick?: React.MouseEventHandler; testId?: string; children: ReactNode; @@ -39,7 +39,7 @@ export const SelectableListItem = ({ diff --git a/ui/components/multichain/account-overview/account-overview-btc.test.tsx b/ui/components/multichain/account-overview/account-overview-btc.test.tsx index fa32883ce773..b171840a540e 100644 --- a/ui/components/multichain/account-overview/account-overview-btc.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-btc.test.tsx @@ -3,6 +3,7 @@ import mockState from '../../../../test/data/mock-state.json'; import configureStore from '../../../store/store'; import { renderWithProvider } from '../../../../test/jest/rendering'; import { setBackgroundConnection } from '../../../store/background-connection'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; import { AccountOverviewBtc, AccountOverviewBtcProps, @@ -11,8 +12,20 @@ import { jest.mock('../../../store/actions', () => ({ tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'), tokenBalancesStopPollingByPollingToken: jest.fn(), + setTokenNetworkFilter: jest.fn(), })); +// Mock the dispatch function +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: () => mockDispatch, + }; +}); + const defaultProps: AccountOverviewBtcProps = { defaultHomeActiveTabName: null, onTabClick: jest.fn(), @@ -22,7 +35,16 @@ const defaultProps: AccountOverviewBtcProps = { const render = (props: AccountOverviewBtcProps = defaultProps) => { const store = configureStore({ - metamask: mockState.metamask, + metamask: { + ...mockState.metamask, + preferences: { + ...mockState.metamask.preferences, + tokenNetworkFilter: { + [CHAIN_IDS.MAINNET]: true, + [CHAIN_IDS.LINEA_MAINNET]: true, + }, + }, + }, }); return renderWithProvider(, store); @@ -30,7 +52,10 @@ const render = (props: AccountOverviewBtcProps = defaultProps) => { describe('AccountOverviewBtc', () => { beforeEach(() => { - setBackgroundConnection({ setBridgeFeatureFlags: jest.fn() } as never); + setBackgroundConnection({ + setBridgeFeatureFlags: jest.fn(), + tokenBalancesStartPolling: jest.fn(), + } as never); }); it('shows only Tokens and Activity tabs', () => { diff --git a/ui/components/multichain/account-overview/account-overview-eth.test.tsx b/ui/components/multichain/account-overview/account-overview-eth.test.tsx index f9b53665e753..a886608ec169 100644 --- a/ui/components/multichain/account-overview/account-overview-eth.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-eth.test.tsx @@ -3,6 +3,7 @@ import mockState from '../../../../test/data/mock-state.json'; import configureStore from '../../../store/store'; import { renderWithProvider } from '../../../../test/jest/rendering'; import { setBackgroundConnection } from '../../../store/background-connection'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; import { AccountOverviewEth, AccountOverviewEthProps, @@ -11,11 +12,32 @@ import { jest.mock('../../../store/actions', () => ({ tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'), tokenBalancesStopPollingByPollingToken: jest.fn(), + setTokenNetworkFilter: jest.fn(), })); +// Mock the dispatch function +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: () => mockDispatch, + }; +}); + const render = (props: AccountOverviewEthProps) => { const store = configureStore({ - metamask: mockState.metamask, + metamask: { + ...mockState.metamask, + preferences: { + ...mockState.metamask.preferences, + tokenNetworkFilter: { + [CHAIN_IDS.MAINNET]: true, + [CHAIN_IDS.LINEA_MAINNET]: true, + }, + }, + }, }); return renderWithProvider(, store); @@ -23,7 +45,10 @@ const render = (props: AccountOverviewEthProps) => { describe('AccountOverviewEth', () => { beforeEach(() => { - setBackgroundConnection({ setBridgeFeatureFlags: jest.fn() } as never); + setBackgroundConnection({ + setBridgeFeatureFlags: jest.fn(), + tokenBalancesStartPolling: jest.fn(), + } as never); }); it('shows all tabs', () => { const { queryByTestId } = render({ diff --git a/ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap b/ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap deleted file mode 100644 index e8fa1e945dba..000000000000 --- a/ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Import Token Link should match snapshot for goerli chainId 1`] = ` -
- -
-`; - -exports[`Import Token Link should match snapshot for mainnet chainId 1`] = ` -
- -
-`; diff --git a/ui/components/multichain/import-token-link/import-token-link.stories.tsx b/ui/components/multichain/import-token-link/import-token-link.stories.tsx deleted file mode 100644 index 85c09e2184dd..000000000000 --- a/ui/components/multichain/import-token-link/import-token-link.stories.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { StoryFn, Meta } from '@storybook/react'; -import { ImportTokenLink } from '.'; - -export default { - title: 'Components/Multichain/ImportTokenLink', - component: ImportTokenLink, -} as Meta; - -export const DefaultStory: StoryFn = () => ( - -); - -DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/import-token-link/import-token-link.test.js b/ui/components/multichain/import-token-link/import-token-link.test.js deleted file mode 100644 index 2539f98e6491..000000000000 --- a/ui/components/multichain/import-token-link/import-token-link.test.js +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { fireEvent, screen } from '@testing-library/react'; -import { detectTokens } from '../../../store/actions'; -import { renderWithProvider } from '../../../../test/lib/render-helpers'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { mockNetworkState } from '../../../../test/stub/networks'; -import ImportControl from '../../app/assets/asset-list/import-control'; -import { ImportTokenLink } from '.'; - -const mockPushHistory = jest.fn(); - -jest.mock('react-router-dom', () => { - const original = jest.requireActual('react-router-dom'); - return { - ...original, - useLocation: jest.fn(() => ({ search: '' })), - useHistory: () => ({ - push: mockPushHistory, - }), - }; -}); - -jest.mock('../../../store/actions.ts', () => ({ - detectTokens: jest.fn().mockImplementation(() => ({ type: 'DETECT_TOKENS' })), - showImportTokensModal: jest - .fn() - .mockImplementation(() => ({ type: 'UI_IMPORT_TOKENS_POPOVER_OPEN' })), -})); - -describe('Import Token Link', () => { - it('should match snapshot for goerli chainId', () => { - const mockState = { - metamask: { - ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), - }, - }; - - const store = configureMockStore()(mockState); - - const { container } = renderWithProvider(, store); - - expect(container).toMatchSnapshot(); - }); - - it('should match snapshot for mainnet chainId', () => { - const mockState = { - metamask: { - ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), - }, - }; - - const store = configureMockStore()(mockState); - - const { container } = renderWithProvider(, store); - - expect(container).toMatchSnapshot(); - }); - - it('should detectTokens when clicking refresh', () => { - const mockState = { - metamask: { - ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), - }, - }; - - const store = configureMockStore()(mockState); - - renderWithProvider(, store); // should this be RefreshTokenLink? - - const refreshList = screen.getByTestId('refresh-list-button'); - fireEvent.click(refreshList); - - expect(detectTokens).toHaveBeenCalled(); - }); - - it('should push import token route', () => { - const mockState = { - metamask: { - ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), - }, - }; - - const store = configureMockStore()(mockState); - - renderWithProvider(, store); - - const importToken = screen.getByTestId('import-token-button'); - fireEvent.click(importToken); - - expect(screen.getByTestId('import-token-button')).toBeInTheDocument(); - }); -}); diff --git a/ui/components/multichain/import-token-link/import-token-link.tsx b/ui/components/multichain/import-token-link/import-token-link.tsx deleted file mode 100644 index 022369dd5002..000000000000 --- a/ui/components/multichain/import-token-link/import-token-link.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { useDispatch } from 'react-redux'; -import classnames from 'classnames'; -import { - ButtonLink, - IconName, - Box, - ButtonLinkSize, -} from '../../component-library'; -import { AlignItems, Display } from '../../../helpers/constants/design-system'; -import { useI18nContext } from '../../../hooks/useI18nContext'; -import { detectTokens } from '../../../store/actions'; -import type { BoxProps } from '../../component-library/box'; -import type { ImportTokenLinkProps } from './import-token-link.types'; - -export const ImportTokenLink: React.FC = ({ - className = '', - ...props -}): JSX.Element => { - const t = useI18nContext(); - const dispatch = useDispatch(); - - return ( - )} - > - - dispatch(detectTokens())} - > - {t('refreshList')} - - - - ); -}; diff --git a/ui/components/multichain/import-token-link/import-token-link.types.ts b/ui/components/multichain/import-token-link/import-token-link.types.ts deleted file mode 100644 index 0ad9350ca998..000000000000 --- a/ui/components/multichain/import-token-link/import-token-link.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { StyleUtilityProps } from '../../component-library/box'; - -// TODO: Convert to a `type` in a future major version. -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export interface ImportTokenLinkProps extends StyleUtilityProps { - /** * Additional class name for the ImportTokenLink component. */ - className?: string; -} diff --git a/ui/components/multichain/import-token-link/index.ts b/ui/components/multichain/import-token-link/index.ts deleted file mode 100644 index db139df76890..000000000000 --- a/ui/components/multichain/import-token-link/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ImportTokenLink } from './import-token-link'; diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index 5ecc5a2a7d3a..10b0a61b3eef 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -7,7 +7,6 @@ export { ActivityListItem } from './activity-list-item'; export { AppHeader } from './app-header'; export { DetectedTokensBanner } from './detected-token-banner'; export { GlobalMenu } from './global-menu'; -export { ImportTokenLink } from './import-token-link'; export { TokenListItem } from './token-list-item'; export { AddressCopyButton } from './address-copy-button'; export { ConnectedSiteMenu } from './connected-site-menu'; diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index 6c08c9130761..1b1823728a2f 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -15,6 +15,7 @@ import { useIsOriginalNativeTokenSymbol } from '../../hooks/useIsOriginalNativeT import { createMockInternalAccount } from '../../../test/jest/mocks'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { mockNetworkState } from '../../../test/stub/networks'; +import useMultiPolling from '../../hooks/useMultiPolling'; import Routes from '.'; const middlewares = [thunk]; @@ -45,8 +46,20 @@ jest.mock('../../store/actions', () => ({ hideNetworkDropdown: () => mockHideNetworkDropdown, tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'), tokenBalancesStopPollingByPollingToken: jest.fn(), + setTokenNetworkFilter: jest.fn(), })); +// Mock the dispatch function +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: () => mockDispatch, + }; +}); + jest.mock('../../ducks/bridge/actions', () => ({ setBridgeFeatureFlags: () => jest.fn(), })); @@ -79,6 +92,11 @@ jest.mock( '../../components/app/metamask-template-renderer/safe-component-list', ); +jest.mock('../../hooks/useMultiPolling', () => ({ + __esModule: true, + default: jest.fn(), +})); + const render = async (route, state) => { const store = configureMockStore(middlewares)({ ...mockSendState, @@ -97,6 +115,26 @@ const render = async (route, state) => { describe('Routes Component', () => { useIsOriginalNativeTokenSymbol.mockImplementation(() => true); + beforeEach(() => { + // Clear previous mock implementations + useMultiPolling.mockClear(); + + // Mock implementation for useMultiPolling + useMultiPolling.mockImplementation(({ input }) => { + // Mock startPolling and stopPollingByPollingToken for each input + const startPolling = jest.fn().mockResolvedValue('mockPollingToken'); + const stopPollingByPollingToken = jest.fn(); + + input.forEach((inputItem) => { + const key = JSON.stringify(inputItem); + // Simulate returning a unique token for each input + startPolling.mockResolvedValueOnce(`mockToken-${key}`); + }); + + return { startPolling, stopPollingByPollingToken }; + }); + }); + afterEach(() => { mockShowNetworkDropdown.mockClear(); mockHideNetworkDropdown.mockClear(); @@ -126,6 +164,9 @@ describe('Routes Component', () => { }, tokenNetworkFilter: {}, }, + tokenBalances: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': '0x176270e2b862e4ed3', + }, }, send: { ...mockSendState.send, @@ -160,12 +201,27 @@ describe('toast display', () => { ...mockState, metamask: { ...mockState.metamask, + allTokens: {}, announcements: {}, approvalFlows: [], completedOnboarding: true, usedNetworks: [], pendingApprovals: {}, pendingApprovalCount: 0, + preferences: { + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + tokenNetworkFilter: { + [CHAIN_IDS.MAINNET]: true, + [CHAIN_IDS.LINEA_MAINNET]: true, + }, + }, + tokenBalances: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': '0x176270e2b862e4ed3', + }, swapsState: { swapsFeatureIsLive: true }, newPrivacyPolicyToastShownDate: date, }, @@ -184,6 +240,17 @@ describe('toast display', () => { swapsState: { swapsFeatureIsLive: true }, newPrivacyPolicyToastShownDate: new Date(0), newPrivacyPolicyToastClickedOrClosed: true, + preferences: { + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + tokenNetworkFilter: { + [CHAIN_IDS.MAINNET]: true, + [CHAIN_IDS.LINEA_MAINNET]: true, + }, + }, surveyLinkLastClickedOrClosed: true, showPrivacyPolicyToast: false, showSurveyToast: false, @@ -193,6 +260,9 @@ describe('toast display', () => { unconnectedAccount: true, }, termsOfUseLastAgreed: new Date(0).getTime(), + tokenBalances: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': '0x176270e2b862e4ed3', + }, internalAccounts: { accounts: { [mockAccount.id]: mockAccount, From 8bcd777d0b060267d19ae1c3d25a36ab99f6ea87 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 20 Nov 2024 18:46:57 -0600 Subject: [PATCH 12/28] fix: PortfolioView: Remove pausedChainIds from selector (#28552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** After speaking with Infura, we no longer need this `pausedChainIds` property from the remote API. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28552?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. No manual testing, simply removing property and its tests ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/selectors/selectors.js | 18 ++++-------------- ui/selectors/selectors.test.js | 26 -------------------------- 2 files changed, 4 insertions(+), 40 deletions(-) diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 7e4d04eeb3de..3c49befb6dd3 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2240,30 +2240,23 @@ export const getAllEnabledNetworks = createDeepEqualSelector( ); export const getChainIdsToPoll = createDeepEqualSelector( - getPreferences, getNetworkConfigurationsByChainId, getCurrentChainId, - (preferences, networkConfigurations, currentChainId) => { - const { pausedChainIds = [] } = preferences; - + (networkConfigurations, currentChainId) => { if (!process.env.PORTFOLIO_VIEW) { return [currentChainId]; } return Object.keys(networkConfigurations).filter( - (chainId) => - !TEST_CHAINS.includes(chainId) && !pausedChainIds.includes(chainId), + (chainId) => !TEST_CHAINS.includes(chainId), ); }, ); export const getNetworkClientIdsToPoll = createDeepEqualSelector( - getPreferences, getNetworkConfigurationsByChainId, getCurrentChainId, - (preferences, networkConfigurations, currentChainId) => { - const { pausedChainIds = [] } = preferences; - + (networkConfigurations, currentChainId) => { if (!process.env.PORTFOLIO_VIEW) { const networkConfiguration = networkConfigurations[currentChainId]; return [ @@ -2275,10 +2268,7 @@ export const getNetworkClientIdsToPoll = createDeepEqualSelector( return Object.entries(networkConfigurations).reduce( (acc, [chainId, network]) => { - if ( - !TEST_CHAINS.includes(chainId) && - !pausedChainIds.includes(chainId) - ) { + if (!TEST_CHAINS.includes(chainId)) { acc.push( network.rpcEndpoints[network.defaultRpcEndpointIndex] .networkClientId, diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 85180dec45f4..d3799885eaf6 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -873,7 +873,6 @@ describe('Selectors', () => { it('returns only non-test chain IDs', () => { const chainIds = selectors.getChainIdsToPoll({ metamask: { - preferences: { pausedChainIds: [] }, networkConfigurationsByChainId, selectedNetworkClientId: 'mainnet', }, @@ -884,18 +883,6 @@ describe('Selectors', () => { CHAIN_IDS.LINEA_MAINNET, ]); }); - - it('does not return paused chain IDs', () => { - const chainIds = selectors.getChainIdsToPoll({ - metamask: { - preferences: { pausedChainIds: [CHAIN_IDS.LINEA_MAINNET] }, - networkConfigurationsByChainId, - selectedNetworkClientId: 'mainnet', - }, - }); - expect(Object.values(chainIds)).toHaveLength(1); - expect(chainIds).toStrictEqual([CHAIN_IDS.MAINNET]); - }); }); describe('#getNetworkClientIdsToPoll', () => { @@ -933,7 +920,6 @@ describe('Selectors', () => { it('returns only non-test chain IDs', () => { const chainIds = selectors.getNetworkClientIdsToPoll({ metamask: { - preferences: { pausedChainIds: [] }, networkConfigurationsByChainId, selectedNetworkClientId: 'mainnet', }, @@ -941,18 +927,6 @@ describe('Selectors', () => { expect(Object.values(chainIds)).toHaveLength(2); expect(chainIds).toStrictEqual(['mainnet', 'linea-mainnet']); }); - - it('does not return paused chain IDs', () => { - const chainIds = selectors.getNetworkClientIdsToPoll({ - metamask: { - preferences: { pausedChainIds: [CHAIN_IDS.LINEA_MAINNET] }, - networkConfigurationsByChainId, - selectedNetworkClientId: 'mainnet', - }, - }); - expect(Object.values(chainIds)).toHaveLength(1); - expect(chainIds).toStrictEqual(['mainnet']); - }); }); describe('#isHardwareWallet', () => { From f455a6ebe1dc56e53865f94e89f74fc15fcb430a Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 21 Nov 2024 12:01:31 +0000 Subject: [PATCH 13/28] feat: Better handle very long names in the name component (#28560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Truncates long names (>15 characters). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28560?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3630 ## **Manual testing steps** 1. Trigger a new confirmation 2. Add a long petname, by clicking an address and writing it in the input field 3. The name should be truncated with an ellipsis. ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2024-11-20 at 11 24 21 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../app/name/__snapshots__/name.test.tsx.snap | 62 +++++++++++++++++++ ui/components/app/name/name.test.tsx | 18 ++++++ ui/components/app/name/name.tsx | 10 ++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/ui/components/app/name/__snapshots__/name.test.tsx.snap b/ui/components/app/name/__snapshots__/name.test.tsx.snap index 379c00faab11..7e3f98c8576e 100644 --- a/ui/components/app/name/__snapshots__/name.test.tsx.snap +++ b/ui/components/app/name/__snapshots__/name.test.tsx.snap @@ -24,6 +24,68 @@ exports[`Name renders address with image 1`] = ` `; +exports[`Name renders address with long saved name 1`] = ` +
+
+
+
+
+
+ + + + + +
+
+
+

+ Very long and l... +

+
+
+
+`; + exports[`Name renders address with no saved name 1`] = `
{ expect(container).toMatchSnapshot(); }); + it('renders address with long saved name', () => { + useDisplayNameMock.mockReturnValue({ + name: "Very long and length saved name that doesn't seem to end, really.", + hasPetname: true, + }); + + const { container } = renderWithProvider( + , + store, + ); + + expect(container).toMatchSnapshot(); + }); + it('renders address with image', () => { useDisplayNameMock.mockReturnValue({ name: SAVED_NAME_MOCK, diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx index 2097d21faf07..75f2a2f79c15 100644 --- a/ui/components/app/name/name.tsx +++ b/ui/components/app/name/name.tsx @@ -9,7 +9,7 @@ import { NameType } from '@metamask/name-controller'; import classnames from 'classnames'; import { toChecksumAddress } from 'ethereumjs-util'; import { Box, Icon, IconName, IconSize, Text } from '../../component-library'; -import { shortenAddress } from '../../../helpers/utils/util'; +import { shortenAddress, shortenString } from '../../../helpers/utils/util'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, @@ -103,6 +103,12 @@ const Name = memo( }, [setModalOpen]); const formattedValue = formatValue(value, type); + const formattedName = shortenString(name || undefined, { + truncatedCharLimit: 15, + truncatedStartChars: 15, + truncatedEndChars: 0, + skipCharacterInEnd: true, + }); const hasDisplayName = Boolean(name); return ( @@ -135,7 +141,7 @@ const Name = memo( )} {hasDisplayName ? ( - {name} + {formattedName} ) : ( From ba11881e3f1a7963db6d54534c647e60a834ea85 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 21 Nov 2024 13:23:10 +0100 Subject: [PATCH 14/28] fix: fix account list item for portfolio view (#28598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes account list item wit Portfolio view feature flag. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28598?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28592 ## **Manual testing steps** Make sure you run the steps with AND without the PORTFOLIO_VIEW flag 1. Go to settings => Security and privacy => and disable "Show balance and token price checker" 2. Open account picker make sure you see crypto values 3. Enable the setting again and you should see Fiat values ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../tests/settings/account-token-list.spec.js | 37 +++++++++++++++++++ .../account-list-item/account-list-item.js | 4 +- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/settings/account-token-list.spec.js b/test/e2e/tests/settings/account-token-list.spec.js index ddd905501f50..ec24119dd44b 100644 --- a/test/e2e/tests/settings/account-token-list.spec.js +++ b/test/e2e/tests/settings/account-token-list.spec.js @@ -116,4 +116,41 @@ describe('Settings', function () { }, ); }); + + it('Should show crypto value when price checker setting is off', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withShowFiatTestnetEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .withConversionRateDisabled() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockInfuraResponses, + }, + async ({ driver }) => { + await unlockWallet(driver); + + await driver.clickElement('[data-testid="popover-close"]'); + await driver.clickElement( + '[data-testid="account-overview__asset-tab"]', + ); + + const tokenListAmount = await driver.findElement( + '.eth-overview__primary-container', + ); + await driver.delay(1000); + assert.equal(await tokenListAmount.getText(), '25\nETH'); + + await driver.clickElement('[data-testid="account-menu-icon"]'); + const accountTokenValue = await driver.waitForSelector( + '.multichain-account-list-item .multichain-account-list-item__asset', + ); + + assert.equal(await accountTokenValue.getText(), '25ETH'); + }, + ); + }); }); diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index 6be8e3f67f0b..207b8fa0fe58 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -143,7 +143,7 @@ const AccountListItem = ({ let balanceToTranslate; if (isEvmNetwork) { balanceToTranslate = - shouldShowFiat || isTestnet || !process.env.PORTFOLIO_VIEW + !shouldShowFiat || isTestnet || !process.env.PORTFOLIO_VIEW ? account.balance : totalFiatBalance; } else { @@ -345,7 +345,7 @@ const AccountListItem = ({ type={PRIMARY} showFiat={showFiat} isAggregatedFiatOverviewBalance={ - !isTestnet && process.env.PORTFOLIO_VIEW + !isTestnet && process.env.PORTFOLIO_VIEW && shouldShowFiat } data-testid="first-currency-display" privacyMode={privacyMode} From c20ac9958b395559a42a00f3b05ca94806c0a21e Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 21 Nov 2024 13:25:27 +0000 Subject: [PATCH 15/28] chore: upgrade transaction controller to increase polling rate (#28452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Upgrade `@metamask/transaction-controller` to increase the pending transaction polling rate. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28452?quickstart=1) ## **Related issues** Fixes: [#3629](https://github.com/MetaMask/MetaMask-planning/issues/3629) ## **Manual testing steps** Regression of pending transaction polling including: - Alternate Chains - Queued Transactions - Sequential Transactions - Multiple Transactions ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 7117462c957f..fb6218ebb5c6 100644 --- a/package.json +++ b/package.json @@ -354,7 +354,7 @@ "@metamask/snaps-sdk": "^6.11.0", "@metamask/snaps-utils": "^8.6.0", "@metamask/solana-wallet-snap": "^0.1.9", - "@metamask/transaction-controller": "^38.3.0", + "@metamask/transaction-controller": "^39.1.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", diff --git a/yarn.lock b/yarn.lock index 0147c3106fa4..527af97c08f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6741,9 +6741,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^38.3.0": - version: 38.3.0 - resolution: "@metamask/transaction-controller@npm:38.3.0" +"@metamask/transaction-controller@npm:^39.1.0": + version: 39.1.0 + resolution: "@metamask/transaction-controller@npm:39.1.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6752,7 +6752,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/nonce-tracker": "npm:^6.0.0" @@ -6766,11 +6766,11 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@babel/runtime": ^7.23.9 - "@metamask/accounts-controller": ^18.0.0 + "@metamask/accounts-controller": ^19.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^22.0.0 "@metamask/network-controller": ^22.0.0 - checksum: 10/f4e8e3a1a31e3e62b0d1a59bbe15ebfa4dc3e4cf077fb95c1815c00661c60ef4676046c49f57eab9749cd31d3e55ac3fed7bc247e3f5a3d459f2dcb03998633d + checksum: 10/9c18f01167ca70556323190c3b3b8df29d5c1d45846e6d50208b49d27bd3d361ab89f103d5f4a784bbc70cee3e5ef595bab8cf568926c790236d32ace07a1283 languageName: node linkType: hard @@ -26893,7 +26893,7 @@ __metadata: "@metamask/solana-wallet-snap": "npm:^0.1.9" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" - "@metamask/transaction-controller": "npm:^38.3.0" + "@metamask/transaction-controller": "npm:^39.1.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^10.0.1" "@ngraveio/bc-ur": "npm:^1.1.12" From 6023787fc836cdc297f970608fda0073f66a07a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Tavares?= Date: Thu, 21 Nov 2024 15:10:11 +0000 Subject: [PATCH 16/28] chore: centralize redesigned confirmation decision logic (#28445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR centralizes the redesigned confirmation decision logic to improve code organization and reduce duplication across the codebase. Currently, redesign confirmation decision logic is scattered across multiple files, making it harder to maintain and more prone to inconsistencies. #### Motivation The existing implementation has several issues: 1. Duplicate logic for handling confirmation decisions across different transaction types 2. Prevent inconsistent handling of redesigned confirmation flows Key changes: - Move supported redesigned confirmation decision logic to the shared dir (to be used both in the `ui` and `app` bundles) - Updated signature metrics tracking to support developer mode enabled Types of changes: - Code style update (changes that do not affect the meaning of the code) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28445?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Ensure that when either the experimental confirmations redesign toggle, developer option confirmations redesign toggle or the env ENABLE_CONFIRMATIONS_REDESIGN is enabled the we are presenting the new redesign confirmations. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../lib/createRPCMethodTrackingMiddleware.js | 20 +- .../createRPCMethodTrackingMiddleware.test.js | 13 ++ app/scripts/lib/transaction/metrics.ts | 32 +-- app/scripts/metamask-controller.js | 43 ++-- shared/lib/confirmation.utils.test.ts | 208 ++++++++++++++++++ shared/lib/confirmation.utils.ts | 169 ++++++++++++++ .../components/confirm/footer/footer.tsx | 7 +- .../components/confirm/header/header-info.tsx | 11 +- .../components/confirm/nav/nav.tsx | 5 +- .../scroll-to-bottom/scroll-to-bottom.tsx | 7 +- .../usePendingTransactionAlerts.ts | 11 +- .../useSigningOrSubmittingAlerts.ts | 11 +- .../hooks/alerts/useBlockaidAlerts.ts | 14 +- .../hooks/useCurrentConfirmation.ts | 55 ++--- ui/pages/confirmations/utils/confirm.test.ts | 22 -- ui/pages/confirmations/utils/confirm.ts | 33 --- ui/pages/routes/routes.component.js | 19 +- 17 files changed, 485 insertions(+), 195 deletions(-) create mode 100644 shared/lib/confirmation.utils.test.ts create mode 100644 shared/lib/confirmation.utils.ts diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index 27358d18f543..5ca2374db05b 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -27,7 +27,7 @@ import { } from '../../../ui/helpers/utils/metrics'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { REDESIGN_APPROVAL_TYPES } from '../../../ui/pages/confirmations/utils/confirm'; +import { shouldUseRedesignForSignatures } from '../../../shared/lib/confirmation.utils'; import { getSnapAndHardwareInfoForMetrics } from './snap-keyring/metrics'; /** @@ -196,6 +196,7 @@ function finalizeSignatureFragment( * @param {Function} opts.getAccountType * @param {Function} opts.getDeviceModel * @param {Function} opts.isConfirmationRedesignEnabled + * @param {Function} opts.isRedesignedConfirmationsDeveloperEnabled * @param {RestrictedControllerMessenger} opts.snapAndHardwareMessenger * @param {number} [opts.globalRateLimitTimeout] - time, in milliseconds, of the sliding * time window that should limit the number of method calls tracked to globalRateLimitMaxAmount. @@ -214,6 +215,7 @@ export default function createRPCMethodTrackingMiddleware({ getAccountType, getDeviceModel, isConfirmationRedesignEnabled, + isRedesignedConfirmationsDeveloperEnabled, snapAndHardwareMessenger, appStateController, metaMetricsController, @@ -315,13 +317,15 @@ export default function createRPCMethodTrackingMiddleware({ req.securityAlertResponse.description; } - const isConfirmationRedesign = - isConfirmationRedesignEnabled() && - REDESIGN_APPROVAL_TYPES.find( - (type) => type === MESSAGE_TYPE_TO_APPROVAL_TYPE[method], - ); - - if (isConfirmationRedesign) { + if ( + shouldUseRedesignForSignatures({ + approvalType: MESSAGE_TYPE_TO_APPROVAL_TYPE[method], + isRedesignedSignaturesUserSettingEnabled: + isConfirmationRedesignEnabled(), + isRedesignedConfirmationsDeveloperEnabled: + isRedesignedConfirmationsDeveloperEnabled(), + }) + ) { eventProperties.ui_customizations = [ ...(eventProperties.ui_customizations || []), MetaMetricsEventUiCustomization.RedesignedConfirmation, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index 9ebb3f92130b..1949ca7f876e 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -118,6 +118,7 @@ const createHandler = (opts) => appStateController, metaMetricsController, isConfirmationRedesignEnabled: () => false, + isRedesignedConfirmationsDeveloperEnabled: () => false, ...opts, }); @@ -217,10 +218,22 @@ describe('createRPCMethodTrackingMiddleware', () => { }); describe('participateInMetaMetrics is set to true', () => { + const originalEnableConfirmationRedesign = + process.env.ENABLE_CONFIRMATION_REDESIGN; + beforeEach(() => { metaMetricsController.setParticipateInMetaMetrics(true); }); + beforeAll(() => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'false'; + }); + + afterAll(() => { + process.env.ENABLE_CONFIRMATION_REDESIGN = + originalEnableConfirmationRedesign; + }); + it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event`, async () => { const req = { id: MOCK_ID, diff --git a/app/scripts/lib/transaction/metrics.ts b/app/scripts/lib/transaction/metrics.ts index 3cdc14619b0d..375a8bd4a8ab 100644 --- a/app/scripts/lib/transaction/metrics.ts +++ b/app/scripts/lib/transaction/metrics.ts @@ -43,16 +43,12 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../../ui/helpers/utils/metrics'; -import { - REDESIGN_DEV_TRANSACTION_TYPES, - REDESIGN_USER_TRANSACTION_TYPES, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../../ui/pages/confirmations/utils'; + import { getSnapAndHardwareInfoForMetrics, type SnapAndHardwareMessenger, } from '../snap-keyring/metrics'; +import { shouldUseRedesignForTransactions } from '../../../../shared/lib/confirmation.utils'; export type TransactionMetricsRequest = { createEventFragment: ( @@ -996,23 +992,15 @@ async function buildEventFragmentProperties({ if (simulationFails) { uiCustomizations.push(MetaMetricsEventUiCustomization.GasEstimationFailed); } - const isRedesignedConfirmationsDeveloperSettingEnabled = - transactionMetricsRequest.getIsRedesignedConfirmationsDeveloperEnabled() || - process.env.ENABLE_CONFIRMATION_REDESIGN === 'true'; - const isRedesignedTransactionsUserSettingEnabled = - transactionMetricsRequest.getRedesignedTransactionsEnabled(); - - if ( - (isRedesignedConfirmationsDeveloperSettingEnabled && - REDESIGN_DEV_TRANSACTION_TYPES.includes( - transactionMeta.type as TransactionType, - )) || - (isRedesignedTransactionsUserSettingEnabled && - REDESIGN_USER_TRANSACTION_TYPES.includes( - transactionMeta.type as TransactionType, - )) - ) { + const isRedesignedForTransaction = shouldUseRedesignForTransactions({ + transactionMetadataType: transactionMeta.type as TransactionType, + isRedesignedTransactionsUserSettingEnabled: + transactionMetricsRequest.getRedesignedTransactionsEnabled(), + isRedesignedConfirmationsDeveloperEnabled: + transactionMetricsRequest.getIsRedesignedConfirmationsDeveloperEnabled(), + }); + if (isRedesignedForTransaction) { uiCustomizations.push( MetaMetricsEventUiCustomization.RedesignedConfirmation, ); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0d0344d4e7e3..e297a016fd76 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -5789,16 +5789,14 @@ export default class MetamaskController extends EventEmitter { ), ); - const isConfirmationRedesignEnabled = () => { - return this.preferencesController.state.preferences - .redesignedConfirmationsEnabled; - }; - engine.push( createRPCMethodTrackingMiddleware({ getAccountType: this.getAccountType.bind(this), getDeviceModel: this.getDeviceModel.bind(this), - isConfirmationRedesignEnabled, + isConfirmationRedesignEnabled: + this.isConfirmationRedesignEnabled.bind(this), + isRedesignedConfirmationsDeveloperEnabled: + this.isConfirmationRedesignDeveloperEnabled.bind(this), snapAndHardwareMessenger: this.controllerMessenger.getRestricted({ name: 'SnapAndHardwareMessenger', allowedActions: [ @@ -6436,6 +6434,21 @@ export default class MetamaskController extends EventEmitter { }); } + isConfirmationRedesignEnabled() { + return this.preferencesController.state.preferences + .redesignedConfirmationsEnabled; + } + + isTransactionsRedesignEnabled() { + return this.preferencesController.state.preferences + .redesignedTransactionsEnabled; + } + + isConfirmationRedesignDeveloperEnabled() { + return this.preferencesController.state.preferences + .isRedesignedConfirmationsDeveloperEnabled; + } + /** * The chain list is fetched live at runtime, falling back to a cache. * This preseeds the cache at startup with a static list provided at build. @@ -6620,14 +6633,10 @@ export default class MetamaskController extends EventEmitter { txHash, ); }, - getRedesignedConfirmationsEnabled: () => { - return this.preferencesController.state.preferences - .redesignedConfirmationsEnabled; - }, - getRedesignedTransactionsEnabled: () => { - return this.preferencesController.state.preferences - .redesignedTransactionsEnabled; - }, + getRedesignedConfirmationsEnabled: + this.isConfirmationRedesignEnabled.bind(this), + getRedesignedTransactionsEnabled: + this.isTransactionsRedesignEnabled.bind(this), getMethodData: (data) => { if (!data) { return null; @@ -6645,10 +6654,8 @@ export default class MetamaskController extends EventEmitter { this.provider, ); }, - getIsRedesignedConfirmationsDeveloperEnabled: () => { - return this.preferencesController.state.preferences - .isRedesignedConfirmationsDeveloperEnabled; - }, + getIsRedesignedConfirmationsDeveloperEnabled: + this.isConfirmationRedesignDeveloperEnabled.bind(this), getIsConfirmationAdvancedDetailsOpen: () => { return this.preferencesController.state.preferences .showConfirmationAdvancedDetails; diff --git a/shared/lib/confirmation.utils.test.ts b/shared/lib/confirmation.utils.test.ts new file mode 100644 index 000000000000..552d78827a2e --- /dev/null +++ b/shared/lib/confirmation.utils.test.ts @@ -0,0 +1,208 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { ApprovalType } from '@metamask/controller-utils'; +import { + shouldUseRedesignForTransactions, + shouldUseRedesignForSignatures, +} from './confirmation.utils'; + +describe('confirmation.utils', () => { + describe('shouldUseRedesignForTransactions', () => { + const supportedTransactionTypes = [ + TransactionType.contractInteraction, + TransactionType.deployContract, + TransactionType.tokenMethodApprove, + TransactionType.tokenMethodIncreaseAllowance, + TransactionType.tokenMethodSetApprovalForAll, + TransactionType.tokenMethodTransfer, + TransactionType.tokenMethodTransferFrom, + TransactionType.tokenMethodSafeTransferFrom, + TransactionType.simpleSend, + ]; + + const unsupportedTransactionType = TransactionType.swap; + + describe('when user setting is enabled', () => { + it('should return true for supported transaction types', () => { + supportedTransactionTypes.forEach((transactionType) => { + expect( + shouldUseRedesignForTransactions({ + transactionMetadataType: transactionType, + isRedesignedTransactionsUserSettingEnabled: true, // user setting enabled + isRedesignedConfirmationsDeveloperEnabled: false, // developer mode disabled + }), + ).toBe(true); + }); + }); + + it('should return false for unsupported transaction types', () => { + expect( + shouldUseRedesignForTransactions({ + transactionMetadataType: unsupportedTransactionType, + isRedesignedTransactionsUserSettingEnabled: true, // user setting enabled + isRedesignedConfirmationsDeveloperEnabled: false, // developer mode disabled + }), + ).toBe(false); + }); + }); + + describe('when developer mode is enabled', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should return true for supported transaction types when ENABLE_CONFIRMATION_REDESIGN is true', () => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'true'; + + supportedTransactionTypes.forEach((transactionType) => { + expect( + shouldUseRedesignForTransactions({ + transactionMetadataType: transactionType, + isRedesignedTransactionsUserSettingEnabled: false, // user setting disabled + isRedesignedConfirmationsDeveloperEnabled: false, // developer setting disabled + }), + ).toBe(true); + }); + }); + + it('should return true for supported transaction types when developer setting is enabled', () => { + supportedTransactionTypes.forEach((transactionType) => { + expect( + shouldUseRedesignForTransactions({ + transactionMetadataType: transactionType, + isRedesignedTransactionsUserSettingEnabled: false, // user setting disabled + isRedesignedConfirmationsDeveloperEnabled: true, // developer setting enabled + }), + ).toBe(true); + }); + }); + + it('should return false for unsupported transaction types even if developer mode is enabled', () => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'true'; + + expect( + shouldUseRedesignForTransactions({ + transactionMetadataType: unsupportedTransactionType, + isRedesignedTransactionsUserSettingEnabled: false, // user setting disabled + isRedesignedConfirmationsDeveloperEnabled: true, // developer setting enabled + }), + ).toBe(false); + }); + }); + + describe('when both user setting and developer mode are disabled', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + process.env.ENABLE_CONFIRMATION_REDESIGN = 'false'; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should return false for all transaction types', () => { + [...supportedTransactionTypes, unsupportedTransactionType].forEach( + (transactionType) => { + expect( + shouldUseRedesignForTransactions({ + transactionMetadataType: transactionType, + isRedesignedTransactionsUserSettingEnabled: false, // user setting disabled + isRedesignedConfirmationsDeveloperEnabled: false, // developer setting disabled + }), + ).toBe(false); + }, + ); + }); + }); + }); + + describe('shouldUseRedesignForSignatures', () => { + const originalEnv = process.env; + + const supportedSignatureApprovalTypes = [ + ApprovalType.EthSignTypedData, + ApprovalType.PersonalSign, + ]; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + process.env.ENABLE_CONFIRMATION_REDESIGN = 'false'; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should return true for supported approval types when user setting is enabled', () => { + supportedSignatureApprovalTypes.forEach((approvalType) => { + expect( + shouldUseRedesignForSignatures({ + approvalType, + isRedesignedSignaturesUserSettingEnabled: true, // user setting enabled + isRedesignedConfirmationsDeveloperEnabled: false, // developer setting disabled + }), + ).toBe(true); + }); + }); + + it('should return true for supported approval types when developer mode is enabled via env', () => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'true'; + + supportedSignatureApprovalTypes.forEach((approvalType) => { + expect( + shouldUseRedesignForSignatures({ + approvalType, + isRedesignedSignaturesUserSettingEnabled: false, // user setting disabled + isRedesignedConfirmationsDeveloperEnabled: false, // developer setting disabled + }), + ).toBe(true); + }); + }); + + it('should return true for supported approval types when developer setting is enabled', () => { + supportedSignatureApprovalTypes.forEach((approvalType) => { + expect( + shouldUseRedesignForSignatures({ + approvalType, + isRedesignedSignaturesUserSettingEnabled: false, // user setting disabled + isRedesignedConfirmationsDeveloperEnabled: true, // developer setting enabled + }), + ).toBe(true); + }); + }); + + it('should return false for unsupported approval types', () => { + const unsupportedApprovalType = ApprovalType.AddEthereumChain; + + expect( + shouldUseRedesignForSignatures({ + approvalType: unsupportedApprovalType, + isRedesignedSignaturesUserSettingEnabled: true, // user setting enabled + isRedesignedConfirmationsDeveloperEnabled: true, // developer setting enabled + }), + ).toBe(false); + }); + + it('should return false when both user setting and developer mode are disabled', () => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'false'; + + supportedSignatureApprovalTypes.forEach((approvalType) => { + expect( + shouldUseRedesignForSignatures({ + approvalType, + isRedesignedSignaturesUserSettingEnabled: false, // user setting disabled + isRedesignedConfirmationsDeveloperEnabled: false, // developer setting disabled + }), + ).toBe(false); + }); + }); + }); +}); diff --git a/shared/lib/confirmation.utils.ts b/shared/lib/confirmation.utils.ts new file mode 100644 index 000000000000..24c5f258a5d0 --- /dev/null +++ b/shared/lib/confirmation.utils.ts @@ -0,0 +1,169 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { ApprovalType } from '@metamask/controller-utils'; + +/* eslint-disable jsdoc/require-param, jsdoc/check-param-names */ + +/** List of signature approval types that support the redesigned confirmation flow */ +const REDESIGN_SIGNATURE_APPROVAL_TYPES = [ + ApprovalType.EthSignTypedData, + ApprovalType.PersonalSign, +]; + +/** List of transaction types that support the redesigned confirmation flow for users */ +const REDESIGN_USER_TRANSACTION_TYPES = [ + TransactionType.contractInteraction, + TransactionType.deployContract, + TransactionType.tokenMethodApprove, + TransactionType.tokenMethodIncreaseAllowance, + TransactionType.tokenMethodSetApprovalForAll, + TransactionType.tokenMethodTransfer, + TransactionType.tokenMethodTransferFrom, + TransactionType.tokenMethodSafeTransferFrom, + TransactionType.simpleSend, +]; + +/** List of transaction types that support the redesigned confirmation flow for developers */ +const REDESIGN_DEV_TRANSACTION_TYPES = [...REDESIGN_USER_TRANSACTION_TYPES]; + +/** + * Determines whether to use the redesigned confirmation flow for a given transaction + * based on user settings and developer mode + * + * @param opts.transactionMetadataType - The type of transaction to check + * @param opts.isRedesignedTransactionsUserSettingEnabled - Whether the user has enabled the redesigned flow + * @param opts.isRedesignedConfirmationsDeveloperEnabled - Whether developer mode is enabled + */ +export function shouldUseRedesignForTransactions({ + transactionMetadataType, + isRedesignedTransactionsUserSettingEnabled, + isRedesignedConfirmationsDeveloperEnabled, +}: { + transactionMetadataType?: TransactionType; + isRedesignedTransactionsUserSettingEnabled: boolean; + isRedesignedConfirmationsDeveloperEnabled: boolean; +}): boolean { + return ( + shouldUseRedesignForTransactionsUserMode( + isRedesignedTransactionsUserSettingEnabled, + transactionMetadataType, + ) || + shouldUseRedesignForTransactionsDeveloperMode( + isRedesignedConfirmationsDeveloperEnabled, + transactionMetadataType, + ) + ); +} + +/** + * Determines whether to use the redesigned confirmation flow for a given signature + * based on user settings and developer mode + * + * @param opts.approvalType - The type of signature approval to check + * @param opts.isRedesignedSignaturesUserSettingEnabled - Whether the user has enabled the redesigned flow + * @param opts.isRedesignedConfirmationsDeveloperEnabled - Whether developer mode is enabled + */ +export function shouldUseRedesignForSignatures({ + approvalType, + isRedesignedSignaturesUserSettingEnabled, + isRedesignedConfirmationsDeveloperEnabled, +}: { + approvalType?: ApprovalType; + isRedesignedSignaturesUserSettingEnabled: boolean; + isRedesignedConfirmationsDeveloperEnabled: boolean; +}): boolean { + const isRedesignedConfirmationsDeveloperSettingEnabled = + process.env.ENABLE_CONFIRMATION_REDESIGN === 'true' || + isRedesignedConfirmationsDeveloperEnabled; + + if (!isCorrectSignatureApprovalType(approvalType)) { + return false; + } + + return ( + isRedesignedSignaturesUserSettingEnabled || + isRedesignedConfirmationsDeveloperSettingEnabled + ); +} + +/** + * Checks if an redesign approval type is supported for signature redesign + * + * @param approvalType - The type of approval to check + */ +export function isCorrectSignatureApprovalType( + approvalType?: ApprovalType, +): boolean { + if (!approvalType) { + return false; + } + + return REDESIGN_SIGNATURE_APPROVAL_TYPES.includes(approvalType); +} + +/** + * Checks if a redesigned transaction type is supported in developer mode + * + * @param transactionMetadataType - The type of transaction to check + */ +export function isCorrectDeveloperTransactionType( + transactionMetadataType?: TransactionType, +): boolean { + if (!transactionMetadataType) { + return false; + } + + return REDESIGN_DEV_TRANSACTION_TYPES.includes(transactionMetadataType); +} + +/** + * Checks if a redesigned transaction type is supported in user mode + * + * @param transactionMetadataType - The type of transaction to check + */ +function isCorrectUserTransactionType( + transactionMetadataType?: TransactionType, +): boolean { + if (!transactionMetadataType) { + return false; + } + + return REDESIGN_USER_TRANSACTION_TYPES.includes(transactionMetadataType); +} + +/** + * Determines if the redesigned confirmation flow should be used for transactions + * when in developer mode + * + * @param isRedesignedConfirmationsDeveloperEnabled - Whether developer mode is enabled + * @param transactionMetadataType - The type of transaction to check + */ +function shouldUseRedesignForTransactionsDeveloperMode( + isRedesignedConfirmationsDeveloperEnabled: boolean, + transactionMetadataType?: TransactionType, +): boolean { + const isDeveloperModeEnabled = + process.env.ENABLE_CONFIRMATION_REDESIGN === 'true' || + isRedesignedConfirmationsDeveloperEnabled; + + return ( + isDeveloperModeEnabled && + isCorrectDeveloperTransactionType(transactionMetadataType) + ); +} + +/** + * Determines if the redesigned confirmation flow should be used for transactions + * when in user mode + * + * @param isRedesignedTransactionsUserSettingEnabled - Whether the user has enabled the redesigned flow + * @param transactionMetadataType - The type of transaction to check + */ +function shouldUseRedesignForTransactionsUserMode( + isRedesignedTransactionsUserSettingEnabled: boolean, + transactionMetadataType?: TransactionType, +): boolean { + return ( + isRedesignedTransactionsUserSettingEnabled && + isCorrectUserTransactionType(transactionMetadataType) + ); +} diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index a37812899ec9..a9aea54c03f7 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -34,13 +34,13 @@ import { selectUseTransactionSimulations } from '../../../selectors/preferences' import { isPermitSignatureRequest, isSIWESignatureRequest, - REDESIGN_DEV_TRANSACTION_TYPES, } from '../../../utils'; import { useConfirmContext } from '../../../context/confirm'; import { getConfirmationSender } from '../utils'; import { MetaMetricsEventLocation } from '../../../../../../shared/constants/metametrics'; import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; import { Severity } from '../../../../../helpers/constants/design-system'; +import { isCorrectDeveloperTransactionType } from '../../../../../../shared/lib/confirmation.utils'; export type OnCancelHandler = ({ location, @@ -218,9 +218,10 @@ const Footer = () => { return; } - const isTransactionConfirmation = REDESIGN_DEV_TRANSACTION_TYPES.find( - (type) => type === currentConfirmation?.type, + const isTransactionConfirmation = isCorrectDeveloperTransactionType( + currentConfirmation?.type, ); + if (isTransactionConfirmation) { const mergeTxDataWithNonce = (transactionData: TransactionMeta) => customNonceValue diff --git a/ui/pages/confirmations/components/confirm/header/header-info.tsx b/ui/pages/confirmations/components/confirm/header/header-info.tsx index 9cc50b0fe676..03eabacef42e 100644 --- a/ui/pages/confirmations/components/confirm/header/header-info.tsx +++ b/ui/pages/confirmations/components/confirm/header/header-info.tsx @@ -1,4 +1,3 @@ -import { TransactionType } from '@metamask/transaction-controller'; import React, { useContext } from 'react'; import { useSelector } from 'react-redux'; import { @@ -42,10 +41,8 @@ import { useConfirmContext } from '../../../context/confirm'; import { useBalance } from '../../../hooks/useBalance'; import useConfirmationRecipientInfo from '../../../hooks/useConfirmationRecipientInfo'; import { SignatureRequestType } from '../../../types/confirm'; -import { - isSignatureTransactionType, - REDESIGN_DEV_TRANSACTION_TYPES, -} from '../../../utils/confirm'; +import { isSignatureTransactionType } from '../../../utils/confirm'; +import { isCorrectDeveloperTransactionType } from '../../../../../../shared/lib/confirmation.utils'; import { AdvancedDetailsButton } from './advanced-details-button'; const HeaderInfo = () => { @@ -89,8 +86,8 @@ const HeaderInfo = () => { trackEvent(event); } - const isShowAdvancedDetailsToggle = REDESIGN_DEV_TRANSACTION_TYPES.includes( - currentConfirmation?.type as TransactionType, + const isShowAdvancedDetailsToggle = isCorrectDeveloperTransactionType( + currentConfirmation?.type, ); return ( diff --git a/ui/pages/confirmations/components/confirm/nav/nav.tsx b/ui/pages/confirmations/components/confirm/nav/nav.tsx index de0637a9f641..4a58e1c364dd 100644 --- a/ui/pages/confirmations/components/confirm/nav/nav.tsx +++ b/ui/pages/confirmations/components/confirm/nav/nav.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { ApprovalType } from '@metamask/controller-utils'; import { QueueType } from '../../../../../../shared/constants/metametrics'; import { Box, @@ -34,7 +35,7 @@ import { pendingConfirmationsSortedSelector } from '../../../../../selectors'; import { rejectPendingApproval } from '../../../../../store/actions'; import { useConfirmContext } from '../../../context/confirm'; import { useQueuedConfirmationsEvent } from '../../../hooks/useQueuedConfirmationEvents'; -import { isSignatureApprovalRequest } from '../../../utils'; +import { isCorrectSignatureApprovalType } from '../../../../../../shared/lib/confirmation.utils'; const Nav = () => { const history = useHistory(); @@ -64,7 +65,7 @@ const Nav = () => { // "/confirm-transaction/" history.replace( `${CONFIRM_TRANSACTION_ROUTE}/${nextConfirmation.id}${ - isSignatureApprovalRequest(nextConfirmation) + isCorrectSignatureApprovalType(nextConfirmation.type as ApprovalType) ? SIGNATURE_REQUEST_PATH : '' }`, diff --git a/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx b/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx index c61053818923..61cb8f65563c 100644 --- a/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx +++ b/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx @@ -1,6 +1,5 @@ import React, { useContext, useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { TransactionType } from '@metamask/transaction-controller'; import { Box, ButtonIcon, @@ -21,7 +20,7 @@ import { usePrevious } from '../../../../../hooks/usePrevious'; import { useScrollRequired } from '../../../../../hooks/useScrollRequired'; import { useConfirmContext } from '../../../context/confirm'; import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; -import { REDESIGN_DEV_TRANSACTION_TYPES } from '../../../utils'; +import { isCorrectDeveloperTransactionType } from '../../../../../../shared/lib/confirmation.utils'; type ContentProps = { /** @@ -51,8 +50,8 @@ const ScrollToBottom = ({ children }: ContentProps) => { offsetPxFromBottom: 0, }); - const isTransactionRedesign = REDESIGN_DEV_TRANSACTION_TYPES.includes( - currentConfirmation?.type as TransactionType, + const isTransactionRedesign = isCorrectDeveloperTransactionType( + currentConfirmation?.type, ); const showScrollToBottom = diff --git a/ui/pages/confirmations/hooks/alerts/transactions/usePendingTransactionAlerts.ts b/ui/pages/confirmations/hooks/alerts/transactions/usePendingTransactionAlerts.ts index 7430f3ff4fd0..5753b5329ea8 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/usePendingTransactionAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/usePendingTransactionAlerts.ts @@ -1,7 +1,4 @@ -import { - TransactionMeta, - TransactionType, -} from '@metamask/transaction-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; @@ -9,9 +6,9 @@ import { submittedPendingTransactionsSelector } from '../../../../../selectors'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; import { Severity } from '../../../../../helpers/constants/design-system'; -import { REDESIGN_DEV_TRANSACTION_TYPES } from '../../../utils'; import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants'; import { useConfirmContext } from '../../../context/confirm'; +import { isCorrectDeveloperTransactionType } from '../../../../../../shared/lib/confirmation.utils'; export function usePendingTransactionAlerts(): Alert[] { const t = useI18nContext(); @@ -19,9 +16,7 @@ export function usePendingTransactionAlerts(): Alert[] { const { type } = currentConfirmation ?? ({} as TransactionMeta); const pendingTransactions = useSelector(submittedPendingTransactionsSelector); - const isValidType = REDESIGN_DEV_TRANSACTION_TYPES.includes( - type as TransactionType, - ); + const isValidType = isCorrectDeveloperTransactionType(type); const hasPendingTransactions = isValidType && Boolean(pendingTransactions.length); diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useSigningOrSubmittingAlerts.ts b/ui/pages/confirmations/hooks/alerts/transactions/useSigningOrSubmittingAlerts.ts index 11d9f1697a6e..3cb6c45b31e1 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/useSigningOrSubmittingAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/useSigningOrSubmittingAlerts.ts @@ -1,7 +1,4 @@ -import { - TransactionMeta, - TransactionType, -} from '@metamask/transaction-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; @@ -9,8 +6,8 @@ import { getApprovedAndSignedTransactions } from '../../../../../selectors'; import { Severity } from '../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; -import { REDESIGN_DEV_TRANSACTION_TYPES } from '../../../utils'; import { useConfirmContext } from '../../../context/confirm'; +import { isCorrectDeveloperTransactionType } from '../../../../../../shared/lib/confirmation.utils'; export function useSigningOrSubmittingAlerts(): Alert[] { const t = useI18nContext(); @@ -21,9 +18,7 @@ export function useSigningOrSubmittingAlerts(): Alert[] { getApprovedAndSignedTransactions, ); - const isValidType = REDESIGN_DEV_TRANSACTION_TYPES.includes( - type as TransactionType, - ); + const isValidType = isCorrectDeveloperTransactionType(type); const isSigningOrSubmitting = isValidType && signingOrSubmittingTransactions.length > 0; diff --git a/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts b/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts index 2f26fcefbee9..30ab2e71947e 100644 --- a/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts @@ -15,10 +15,8 @@ import { import { Alert } from '../../../../ducks/confirm-alerts/confirm-alerts'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { - SIGNATURE_TRANSACTION_TYPES, - REDESIGN_DEV_TRANSACTION_TYPES, -} from '../../utils'; +import { SIGNATURE_TRANSACTION_TYPES } from '../../utils'; +import { isCorrectDeveloperTransactionType } from '../../../../../shared/lib/confirmation.utils'; import { SecurityAlertResponse, SignatureRequestType, @@ -30,11 +28,6 @@ import { normalizeProviderAlert } from './utils'; // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const zlib = require('zlib'); -const SUPPORTED_TRANSACTION_TYPES = [ - ...SIGNATURE_TRANSACTION_TYPES, - ...REDESIGN_DEV_TRANSACTION_TYPES, -]; - const IGNORED_RESULT_TYPES = [ BlockaidResultType.Benign, BlockaidResultType.Loading, @@ -74,7 +67,8 @@ const useBlockaidAlerts = (): Alert[] => { signatureSecurityAlertResponse || transactionSecurityAlertResponse; const isTransactionTypeSupported = - SUPPORTED_TRANSACTION_TYPES.includes(transactionType); + isCorrectDeveloperTransactionType(transactionType) || + SIGNATURE_TRANSACTION_TYPES.includes(transactionType); const isResultTypeIgnored = IGNORED_RESULT_TYPES.includes( securityAlertResponse?.result_type as BlockaidResultType, diff --git a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts index cf5e8a1383a2..1771f807de25 100644 --- a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts +++ b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts @@ -1,8 +1,5 @@ import { ApprovalType } from '@metamask/controller-utils'; -import { - TransactionMeta, - TransactionType, -} from '@metamask/transaction-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -17,10 +14,9 @@ import { } from '../../../selectors'; import { selectUnapprovedMessage } from '../../../selectors/signatures'; import { - REDESIGN_APPROVAL_TYPES, - REDESIGN_DEV_TRANSACTION_TYPES, - REDESIGN_USER_TRANSACTION_TYPES, -} from '../utils'; + shouldUseRedesignForSignatures, + shouldUseRedesignForTransactions, +} from '../../../../shared/lib/confirmation.utils'; /** * Determine the current confirmation based on the pending approvals and controller state. @@ -47,10 +43,6 @@ const useCurrentConfirmation = () => { getIsRedesignedConfirmationsDeveloperEnabled, ); - const isRedesignedConfirmationsDeveloperSettingEnabled = - process.env.ENABLE_CONFIRMATION_REDESIGN === 'true' || - isRedesignedConfirmationsDeveloperEnabled; - const pendingApproval = useSelector((state) => selectPendingApproval(state as ApprovalsMetaMaskState, confirmationId), ); @@ -64,37 +56,20 @@ const useCurrentConfirmation = () => { selectUnapprovedMessage(state, confirmationId), ); - const isCorrectUserTransactionType = REDESIGN_USER_TRANSACTION_TYPES.includes( - transactionMetadata?.type as TransactionType, - ); - - const isCorrectDeveloperTransactionType = - REDESIGN_DEV_TRANSACTION_TYPES.includes( - transactionMetadata?.type as TransactionType, - ); - - const isCorrectApprovalType = REDESIGN_APPROVAL_TYPES.includes( - pendingApproval?.type as ApprovalType, - ); - - const shouldUseRedesignForSignatures = - (isRedesignedSignaturesUserSettingEnabled && isCorrectApprovalType) || - (isRedesignedConfirmationsDeveloperSettingEnabled && isCorrectApprovalType); + const useRedesignedForSignatures = shouldUseRedesignForSignatures({ + approvalType: pendingApproval?.type as ApprovalType, + isRedesignedSignaturesUserSettingEnabled, + isRedesignedConfirmationsDeveloperEnabled, + }); - const shouldUseRedesignForTransactions = - (isRedesignedTransactionsUserSettingEnabled && - isCorrectUserTransactionType) || - (isRedesignedConfirmationsDeveloperSettingEnabled && - isCorrectDeveloperTransactionType); + const useRedesignedForTransaction = shouldUseRedesignForTransactions({ + transactionMetadataType: transactionMetadata?.type, + isRedesignedTransactionsUserSettingEnabled, + isRedesignedConfirmationsDeveloperEnabled, + }); - // If the developer toggle or the build time environment variable are enabled, - // all the signatures and transactions in development are shown. If the user - // facing feature toggles for signature or transactions are enabled, we show - // only confirmations that shipped (contained in `REDESIGN_APPROVAL_TYPES` and - // `REDESIGN_USER_TRANSACTION_TYPES` or `REDESIGN_DEV_TRANSACTION_TYPES` - // respectively). const shouldUseRedesign = - shouldUseRedesignForSignatures || shouldUseRedesignForTransactions; + useRedesignedForSignatures || useRedesignedForTransaction; return useMemo(() => { if (!shouldUseRedesign) { diff --git a/ui/pages/confirmations/utils/confirm.test.ts b/ui/pages/confirmations/utils/confirm.test.ts index b1f6494ca627..9a8b3d1a0f8a 100644 --- a/ui/pages/confirmations/utils/confirm.test.ts +++ b/ui/pages/confirmations/utils/confirm.test.ts @@ -1,5 +1,3 @@ -import { ApprovalRequest } from '@metamask/approval-controller'; -import { ApprovalType } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; import { @@ -11,7 +9,6 @@ import { SignatureRequestType } from '../types/confirm'; import { isOrderSignatureRequest, isPermitSignatureRequest, - isSignatureApprovalRequest, isSignatureTransactionType, parseSanitizeTypedDataMessage, isValidASCIIURL, @@ -22,25 +19,6 @@ const typedDataMsg = '{"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF","0x06195827297c7A80a443b6894d3BDB8824b43896"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}'; describe('confirm util', () => { - describe('isSignatureApprovalRequest', () => { - it('returns true for signature approval requests', () => { - const result = isSignatureApprovalRequest({ - type: ApprovalType.PersonalSign, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as ApprovalRequest); - expect(result).toStrictEqual(true); - }); - it('returns false for request not of type signature', () => { - const result = isSignatureApprovalRequest({ - type: ApprovalType.Transaction, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as ApprovalRequest); - expect(result).toStrictEqual(false); - }); - }); - describe('parseSanitizeTypedDataMessage', () => { it('parses and sanitizes data passed correctly', () => { const result = parseSanitizeTypedDataMessage(typedDataMsg); diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index a007ca0aa0b2..f464f51e8159 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -1,7 +1,4 @@ -import { ApprovalRequest } from '@metamask/approval-controller'; -import { ApprovalType } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; -import { Json } from '@metamask/utils'; import { PRIMARY_TYPES_ORDER, PRIMARY_TYPES_PERMIT, @@ -11,36 +8,6 @@ import { sanitizeMessage } from '../../../helpers/utils/util'; import { Confirmation, SignatureRequestType } from '../types/confirm'; import { TYPED_SIGNATURE_VERSIONS } from '../constants'; -export const REDESIGN_APPROVAL_TYPES = [ - ApprovalType.EthSignTypedData, - ApprovalType.PersonalSign, -]; - -export const REDESIGN_USER_TRANSACTION_TYPES = [ - TransactionType.contractInteraction, - TransactionType.deployContract, - TransactionType.tokenMethodApprove, - TransactionType.tokenMethodIncreaseAllowance, - TransactionType.tokenMethodSetApprovalForAll, - TransactionType.tokenMethodTransfer, - TransactionType.tokenMethodTransferFrom, - TransactionType.tokenMethodSafeTransferFrom, - TransactionType.simpleSend, -]; - -export const REDESIGN_DEV_TRANSACTION_TYPES = [ - ...REDESIGN_USER_TRANSACTION_TYPES, -]; - -const SIGNATURE_APPROVAL_TYPES = [ - ApprovalType.PersonalSign, - ApprovalType.EthSignTypedData, -]; - -export const isSignatureApprovalRequest = ( - request: ApprovalRequest>, -) => SIGNATURE_APPROVAL_TYPES.includes(request.type as ApprovalType); - export const SIGNATURE_TRANSACTION_TYPES = [ TransactionType.personalSign, TransactionType.signTypedData, diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 3a2bc2989182..ef0ebbfa9ee3 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -115,9 +115,9 @@ import NftFullImage from '../../components/app/assets/nfts/nft-details/nft-full- import CrossChainSwap from '../bridge'; import { ToastMaster } from '../../components/app/toast-master/toast-master'; import { - REDESIGN_APPROVAL_TYPES, - REDESIGN_DEV_TRANSACTION_TYPES, -} from '../confirmations/utils'; + isCorrectDeveloperTransactionType, + isCorrectSignatureApprovalType, +} from '../../../shared/lib/confirmation.utils'; import { getConnectingLabel, hideAppHeader, @@ -471,13 +471,12 @@ export default class Routes extends Component { const pendingApproval = pendingApprovals.find( (approval) => approval.id === confirmationId, ); - const isCorrectApprovalType = REDESIGN_APPROVAL_TYPES.includes( + const isCorrectApprovalType = isCorrectSignatureApprovalType( pendingApproval?.type, ); - const isCorrectDeveloperTransactionType = - REDESIGN_DEV_TRANSACTION_TYPES.includes( - transactionsMetadata[confirmationId]?.type, - ); + const isCorrectTransactionType = isCorrectDeveloperTransactionType( + transactionsMetadata[confirmationId]?.type, + ); let isLoadingShown = isLoading && @@ -485,7 +484,7 @@ export default class Routes extends Component { // In the redesigned screens, we hide the general loading spinner and the // loading states are on a component by component basis. !isCorrectApprovalType && - !isCorrectDeveloperTransactionType; + !isCorrectTransactionType; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isLoadingShown = @@ -499,7 +498,7 @@ export default class Routes extends Component { // In the redesigned screens, we hide the general loading spinner and the // loading states are on a component by component basis. !isCorrectApprovalType && - !isCorrectDeveloperTransactionType; + !isCorrectTransactionType; ///: END:ONLY_INCLUDE_IF return ( From a1b6ba7cce7b5fbbd9b50aace2e821384a0edcef Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:18:23 -0500 Subject: [PATCH 17/28] feat: cross chain swaps - tx submit (#27262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR implements the following: 1. Submit bridge transaction for normal transactions 3. Submit bridge transaction for native gas tokens that don't require approval 4. Submit bridge transaction for ERC20s that require approval Does not fully: 1. Submit bridge transaction for smart transactions - You can submit an STX, but the status screens don't make the most sense right now. - Improved STX support be handled by https://github.com/MetaMask/metamask-extension/pull/28460 and https://github.com/MetaMask/core/pull/4918/ [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27262?quickstart=1) ## **Related issues** - Targeting: #27522 ## **Manual testing steps** 1. Go to Bridge 2. Fill in source/dest token and amounts 3. Get a quote 4. Execute Bridge ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/b73f917d-e3e4-468b-b0fa-29f41f559488 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 4 + .../bridge/bridge-controller.test.ts | 49 +- .../controllers/bridge/bridge-controller.ts | 38 +- app/scripts/controllers/bridge/constants.ts | 7 + app/scripts/controllers/bridge/types.ts | 2 + app/scripts/metamask-controller.js | 5 + shared/constants/bridge.ts | 4 + shared/constants/metametrics.ts | 1 + shared/constants/security-provider.ts | 2 + test/data/bridge/dummy-quotes.ts | 4005 +++++++++++++++++ ui/ducks/bridge/actions.ts | 13 +- ui/ducks/bridge/selectors.ts | 2 +- ui/ducks/bridge/utils.ts | 47 + ui/hooks/useTransactionDisplayData.js | 9 + ui/pages/bridge/bridge.util.ts | 24 + ui/pages/bridge/hooks/useAddToken.ts | 84 + ui/pages/bridge/hooks/useHandleApprovalTx.ts | 94 + ui/pages/bridge/hooks/useHandleBridgeTx.ts | 48 + ui/pages/bridge/hooks/useHandleTx.ts | 79 + .../hooks/useSubmitBridgeTransaction.test.tsx | 485 ++ .../hooks/useSubmitBridgeTransaction.ts | 49 + ui/pages/bridge/index.tsx | 3 + ui/pages/bridge/prepare/bridge-cta-button.tsx | 11 +- ui/pages/bridge/types.ts | 5 +- ui/pages/swaps/hooks/useSwapsFeatureFlags.ts | 14 + ui/pages/swaps/swaps.util.ts | 2 +- ui/store/actions.ts | 7 +- 27 files changed, 5082 insertions(+), 11 deletions(-) create mode 100644 test/data/bridge/dummy-quotes.ts create mode 100644 ui/ducks/bridge/utils.ts create mode 100644 ui/pages/bridge/hooks/useAddToken.ts create mode 100644 ui/pages/bridge/hooks/useHandleApprovalTx.ts create mode 100644 ui/pages/bridge/hooks/useHandleBridgeTx.ts create mode 100644 ui/pages/bridge/hooks/useHandleTx.ts create mode 100644 ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx create mode 100644 ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts create mode 100644 ui/pages/swaps/hooks/useSwapsFeatureFlags.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index a59a48a21afe..bdba7b1214a3 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -847,6 +847,10 @@ "bridge": { "message": "Bridge" }, + "bridgeApproval": { + "message": "Approve $1 for bridge", + "description": "Used in the transaction display list to describe a transaction that is an approve call on a token that is to be bridged. $1 is the symbol of a token that has been approved." + }, "bridgeCalculatingAmount": { "message": "Calculating..." }, diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 07224ae43c2e..8369d910f78b 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -22,11 +22,27 @@ const messengerMock = { publish: jest.fn(), } as unknown as jest.Mocked; +jest.mock('@ethersproject/contracts', () => { + return { + Contract: jest.fn(() => ({ + allowance: jest.fn(() => '100000000000000000000'), + })), + }; +}); + +jest.mock('@ethersproject/providers', () => { + return { + Web3Provider: jest.fn(), + }; +}); + describe('BridgeController', function () { let bridgeController: BridgeController; beforeAll(function () { - bridgeController = new BridgeController({ messenger: messengerMock }); + bridgeController = new BridgeController({ + messenger: messengerMock, + }); }); beforeEach(() => { @@ -43,6 +59,18 @@ describe('BridgeController', function () { 'extension-support': true, 'src-network-allowlist': [10, 534352], 'dest-network-allowlist': [137, 42161], + 'approval-gas-multiplier': { + '137': 1.1, + '42161': 1.2, + '10': 1.3, + '534352': 1.4, + }, + 'bridge-gas-multiplier': { + '137': 2.1, + '42161': 2.2, + '10': 2.3, + '534352': 2.4, + }, }); nock(BRIDGE_API_BASE_URL) .get('/getTokens?chainId=10') @@ -507,7 +535,10 @@ describe('BridgeController', function () { bridgeController, 'startPollingByNetworkClientId', ); - messengerMock.call.mockReturnValueOnce({ address: '0x123' } as never); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1, @@ -536,4 +567,18 @@ describe('BridgeController', function () { }), ); }); + + describe('getBridgeERC20Allowance', () => { + it('should return the atomic allowance of the ERC20 token contract', async () => { + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + const allowance = await bridgeController.getBridgeERC20Allowance( + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + '0xa', + ); + expect(allowance).toBe('100000000000000000000'); + }); + }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 00d28b044457..2518e9caa9bd 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -1,7 +1,11 @@ -import { StateMetadata } from '@metamask/base-controller'; import { add0x, Hex } from '@metamask/utils'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { NetworkClientId } from '@metamask/network-controller'; +import { StateMetadata } from '@metamask/base-controller'; +import { Contract } from '@ethersproject/contracts'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import { Web3Provider } from '@ethersproject/providers'; +import { BigNumber } from '@ethersproject/bignumber'; import { fetchBridgeFeatureFlags, fetchBridgeQuotes, @@ -25,6 +29,7 @@ import { DEFAULT_BRIDGE_CONTROLLER_STATE, REFRESH_INTERVAL_MS, RequestStatus, + METABRIDGE_CHAIN_TO_ADDRESS_MAP, } from './constants'; import { BridgeControllerState, @@ -60,6 +65,8 @@ export default class BridgeController extends StaticIntervalPollingController< this.setIntervalLength(REFRESH_INTERVAL_MS); + this.#abortController = new AbortController(); + // Register action handlers this.messagingSystem.registerActionHandler( `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), @@ -80,6 +87,10 @@ export default class BridgeController extends StaticIntervalPollingController< `${BRIDGE_CONTROLLER_NAME}:resetState`, this.resetState.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:getBridgeERC20Allowance`, + this.getBridgeERC20Allowance.bind(this), + ); } _executePoll = async ( @@ -277,4 +288,29 @@ export default class BridgeController extends StaticIntervalPollingController< chainId, ); } + + /** + * + * @param contractAddress - The address of the ERC20 token contract + * @param chainId - The hex chain ID of the bridge network + * @returns The atomic allowance of the ERC20 token contract + */ + getBridgeERC20Allowance = async ( + contractAddress: string, + chainId: Hex, + ): Promise => { + const provider = this.#getSelectedNetworkClient()?.provider; + if (!provider) { + throw new Error('No provider found'); + } + + const web3Provider = new Web3Provider(provider); + const contract = new Contract(contractAddress, abiERC20, web3Provider); + const { address: walletAddress } = this.#getSelectedAccount(); + const allowance = await contract.allowance( + walletAddress, + METABRIDGE_CHAIN_TO_ADDRESS_MAP[chainId], + ); + return BigNumber.from(allowance).toString(); + }; } diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index aa2d74f0a08e..ec60f8e6a0a4 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -1,4 +1,7 @@ import { zeroAddress } from 'ethereumjs-util'; +import { Hex } from '@metamask/utils'; +import { METABRIDGE_ETHEREUM_ADDRESS } from '../../../../shared/constants/bridge'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; import { BridgeControllerState, BridgeFeatureFlagsKey } from './types'; export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; @@ -36,3 +39,7 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { quotesLoadingStatus: undefined, quotesRefreshCount: 0, }; + +export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { + [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, +}; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index 2ab9ef8e7a37..577a9fa99836 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -53,6 +53,7 @@ export enum BridgeUserAction { export enum BridgeBackgroundAction { SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', RESET_STATE = 'resetState', + GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', } type BridgeControllerAction = { @@ -64,6 +65,7 @@ type BridgeControllerAction = { type BridgeControllerActions = | BridgeControllerAction | BridgeControllerAction + | BridgeControllerAction | BridgeControllerAction | BridgeControllerAction | BridgeControllerAction; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index e297a016fd76..676595dfbff3 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3985,6 +3985,11 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.RESET_STATE}`, ), + [BridgeBackgroundAction.GET_BRIDGE_ERC20_ALLOWANCE]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.GET_BRIDGE_ERC20_ALLOWANCE}`, + ), [BridgeUserAction.SELECT_SRC_NETWORK]: this.controllerMessenger.call.bind( this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_SRC_NETWORK}`, diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index e87b0689777f..ed3b21c6a581 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -20,3 +20,7 @@ export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS : BRIDGE_PROD_API_BASE_URL; export const BRIDGE_CLIENT_ID = 'extension'; + +export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; +export const METABRIDGE_ETHEREUM_ADDRESS = + '0x0439e60F02a8900a951603950d8D4527f400C3f1'; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 8688b8cfa8ae..760e3c26a31f 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -952,6 +952,7 @@ export enum MetaMetricsNetworkEventSource { Dapp = 'dapp', DeprecatedNetworkModal = 'deprecated_network_modal', NewAddNetworkFlow = 'new_add_network_flow', + Bridge = 'bridge', } export enum MetaMetricsSwapsEventSource { diff --git a/shared/constants/security-provider.ts b/shared/constants/security-provider.ts index c7c4a8df3b2c..82f8814da9da 100644 --- a/shared/constants/security-provider.ts +++ b/shared/constants/security-provider.ts @@ -112,6 +112,8 @@ export const SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES = [ TransactionType.swap, TransactionType.swapApproval, TransactionType.swapAndSend, + TransactionType.bridgeApproval, + TransactionType.bridge, ]; export const LOADING_SECURITY_ALERT_RESPONSE: SecurityAlertResponse = { diff --git a/test/data/bridge/dummy-quotes.ts b/test/data/bridge/dummy-quotes.ts new file mode 100644 index 000000000000..3328960a9b73 --- /dev/null +++ b/test/data/bridge/dummy-quotes.ts @@ -0,0 +1,4005 @@ +export const DummyQuotesNoApproval = { + OP_0_005_ETH_TO_ARB: [ + { + quote: { + requestId: 'be448070-7849-4d14-bb35-8dcdaf7a4d69', + srcChainId: 10, + srcTokenAmount: '4956250000000000', + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destChainId: 42161, + destTokenAmount: '4927504629714929', + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + feeData: { + metabridge: { + amount: '43750000000000', + asset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['across'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'across', + displayName: 'Across', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png', + }, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + srcAmount: '4956250000000000', + destAmount: '4927504629714929', + }, + ], + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x11c37937e08000', + data: '0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011c37937e0800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000119baee0ab04000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000027ca57357c00000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a5187000000000000000000000000000000000000000000000000000000000000006c5a39b10ad191481350ef776ac5fe6ef47965741f6f7a4734bf784bf3ae3f24520000a4b100149ae8681b4efd66f30743ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b710000000000000000000000000000000000000000a98f352a08c48ebdbb94ced425b06cf909d74f41e3dfea3c72061a0d88423d867545801fa572404f63d531757a740fd1fd0f12a89217856e4d59f771328fd4bb1c', + gasLimit: 155983, + }, + estimatedProcessingTimeInSeconds: 15, + }, + { + quote: { + requestId: '7e33348d-726c-4f91-b8a0-152828c565ff', + srcChainId: 10, + srcTokenAmount: '4956250000000000', + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destChainId: 42161, + destTokenAmount: '4955000000000000', + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + feeData: { + metabridge: { + amount: '43750000000000', + asset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['stargate'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'stargate', + displayName: 'StargateV2 (Fast mode)', + icon: 'https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png', + }, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + srcAmount: '4956250000000000', + destAmount: '4955000000000000', + }, + ], + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x11e6cbb0321441', + data: '0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011e6cbb032144100000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000119baee0ab04000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000027ca57357c00000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000003e414d53077000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002005828fdd30895f9a246394c8876a30c0a6debe54a9aaed574de790d6e9fe2c1f60000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000119baee0ab0400000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000002352785194410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000119baee0ab0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080c2800c127d224651f5dd852ca8a4abd8a3804aff686dcb794b566b7ea694865f1edd9965ed1810678d845410548d5cae2acc3f8ea98c936e7e566923c1229d1c', + gasLimit: 515457, + }, + estimatedProcessingTimeInSeconds: 51, + }, + { + quote: { + requestId: 'f49a25e4-e396-40d8-a2bf-95ef5ec03d9f', + srcChainId: 10, + srcTokenAmount: '4956250000000000', + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destChainId: 42161, + destTokenAmount: '4852705984263432', + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + feeData: { + metabridge: { + amount: '43750000000000', + asset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['hop'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'hop', + displayName: 'Hop', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/hop.png', + }, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2641', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + srcAmount: '4956250000000000', + destAmount: '4852705984263432', + }, + ], + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x11c37937e08000', + data: '0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011c37937e0800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000006ef81a18e1e432c289dc0d1a670b78e8bbf9aa350000000000000000000000006ef81a18e1e432c289dc0d1a670b78e8bbf9aa35000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000119baee0ab04000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000027ca57357c00000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000044161be542b8c35a6e235f7b26c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000a4b1000000000000000000005eb7c7b0c1c6000000000000000000113dac153d909300000000000000000000000000000000000000000000000000000000d7e5c567b37ba289b97e1a2eb3cda9ebd9811a004255d1fe44b1ccb372b6b41c3aea5aa0d70ad19378485c6e31e10df0eb3a7f957c8441d6474853e81acd4a991b', + gasLimit: 338772, + }, + estimatedProcessingTimeInSeconds: 56, + }, + { + quote: { + requestId: '625e7eb4-065c-4661-90d1-d94f6eb4adcc', + srcChainId: 10, + srcAsset: { + chainId: 10, + address: '0x0000000000000000000000000000000000000000', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + icon: 'https://assets.polygon.technology/tokenAssets/eth.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/eth.svg', + chainAgnosticId: null, + }, + srcTokenAmount: '4956250000000000', + destChainId: 42161, + destAsset: { + chainId: 42161, + address: '0x0000000000000000000000000000000000000000', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + icon: 'https://assets.polygon.technology/tokenAssets/eth.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/eth.svg', + chainAgnosticId: null, + }, + destTokenAmount: '4852928026153929', + feeData: { + metabridge: { + amount: '43750000000000', + asset: { + chainId: 10, + address: '0x0000000000000000000000000000000000000000', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + icon: 'https://assets.polygon.technology/tokenAssets/eth.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/eth.svg', + chainAgnosticId: null, + }, + }, + }, + bridgeId: 'socket', + bridges: ['hop'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'hop', + displayName: 'Hop', + icon: 'https://bridgelogos.s3.ap-south-1.amazonaws.com/hop.png', + }, + srcAsset: { + chainId: 10, + address: '0x0000000000000000000000000000000000000000', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + icon: 'https://assets.polygon.technology/tokenAssets/eth.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/eth.svg', + chainAgnosticId: null, + }, + destAsset: { + chainId: 42161, + address: '0x0000000000000000000000000000000000000000', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + icon: 'https://assets.polygon.technology/tokenAssets/eth.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/eth.svg', + chainAgnosticId: null, + }, + srcAmount: '4956250000000000', + destAmount: '4852928026153929', + }, + ], + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x11c37937e08000', + data: '0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011c37937e0800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000119baee0ab04000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000027ca57357c00000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a5187000000000000000000000000000000000000000000000000000000000000014800000011c8a6285e000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000086ca30bef97fb651b8d866d45503684b90cb331200000000000000000000000000000000000000000000000000119baee0ab0400000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000000000000000000000000000000060dcd8031258000000000000000000000000000000000000000000000000001185250b108f80000000000000000000000000000000000000000000000000000001924963f1dd00000000000000000000000000000000000000000000000000112728d1e75e79000000000000000000000000000000000000000000000000000001924963f1dd00000000000000000000000000000000000000000000000000000000000000c40000000000000000000000000000000000000000000000007d6791da46aae617c18c7b5987f2f25e8bc35083c3c973e71bfa0f7bd70088b0558f63bef3e55bc881d14bd276f41690d864e3d0380bfd4ac557b8b9dde896c51c', + gasLimit: 414453, + }, + estimatedProcessingTimeInSeconds: 60, + }, + ], +}; + +export const DummyQuotesWithApproval = { + ETH_11_USDC_TO_ARB: [ + { + quote: { + requestId: '0cd5caf6-9844-465b-89ad-9c89b639f432', + srcChainId: 1, + srcTokenAmount: '10903750', + srcAsset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 42161, + destTokenAmount: '10876521', + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '96250', + asset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['across'], + steps: [ + { + action: 'bridge', + srcChainId: 1, + destChainId: 42161, + protocol: { + name: 'across', + displayName: 'Across', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png', + }, + srcAsset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '10903750', + destAmount: '10876521', + }, + ], + }, + approval: { + chainId: 1, + to: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 56349, + }, + trade: { + chainId: 1, + to: '0x0439e60F02a8900a951603950d8D4527f400C3f1', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000000902340ab8fc3119af1d016a0eec5fe6ef47965741f6f7a4734bf784bf3ae3f2452a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000a660c60000a4b10008df3abdeb853d66fefedfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b7100000000000000000000000000000000740cfc1bc02079862368cb4eea1332bd9f2dfa925fc757fd51e40919859b87ca031a2a12d67e4ca4ba67d52b59114b3e18c1e8c839ae015112af82e92251db701b', + gasLimit: 209923, + }, + estimatedProcessingTimeInSeconds: 15, + }, + { + quote: { + requestId: 'f197aa3f-a1ed-46fe-8d5f-80866a860a9b', + srcChainId: 1, + srcTokenAmount: '10903750', + srcAsset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 42161, + destTokenAmount: '10803750', + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '96250', + asset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['celercircle'], + steps: [ + { + action: 'bridge', + srcChainId: 1, + destChainId: 42161, + protocol: { + name: 'celercircle', + displayName: 'Circle CCTP', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/circle.png', + }, + srcAsset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '10903750', + destAmount: '10803750', + }, + ], + }, + approval: { + chainId: 1, + to: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 56349, + }, + trade: { + chainId: 1, + to: '0x0439e60F02a8900a951603950d8D4527f400C3f1', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000001e4bab657d800000000000000000000000000000000000000000000000000000000000000202210951480e39a2501daae3e15f254f5431a326e0e6ceb775feb685843012458000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b63656c6572636972636c65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5f6d5c0d29059e2e6f6c5f03c44aa894aa0b2d888b46f8844d855a89c83b372148e183ab8a90043501865ea48326990c0783e195c85330e0f2f12db7953df991b', + gasLimit: 430753, + }, + estimatedProcessingTimeInSeconds: 989.412, + }, + { + quote: { + requestId: '4a954e96-a11d-4879-a1c0-54b24ae14ebb', + srcChainId: 1, + srcTokenAmount: '10903750', + srcAsset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 42161, + destTokenAmount: '10903640', + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '96250', + asset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['stargate'], + steps: [ + { + action: 'bridge', + srcChainId: 1, + destChainId: 42161, + protocol: { + name: 'stargate', + displayName: 'StargateV2 (Fast mode)', + icon: 'https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png', + }, + srcAsset: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 1, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0002000400080016', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '10903750', + destAmount: '10903640', + }, + ], + }, + approval: { + chainId: 1, + to: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 56349, + }, + trade: { + chainId: 1, + to: '0x0439e60F02a8900a951603950d8D4527f400C3f1', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x1be3c54359bf', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000003e414d53077000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002002df432cfa7217ed9dc0aae2d324260c237970c8f9c97439d3faf2a000347d96e000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000001be3c54359bf0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f4e69f41f7ff5673a2df84fb3f246a59101ad7cc25219d596995a85d89c4a1684e9d9e9b2fb8775295686d52b5189794fc16a1dea348e5487eddeeaaebaec7441b', + gasLimit: 634343, + }, + estimatedProcessingTimeInSeconds: 197, + }, + { + quote: { + requestId: '32ad49ef-d710-4fb9-904a-a5d9cc95bd78', + srcChainId: 1, + srcAsset: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USDCoin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: 'USDC', + }, + srcTokenAmount: '10903750', + destChainId: 42161, + destAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + destTokenAmount: '10503750', + feeData: { + metabridge: { + amount: '96250', + asset: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USDCoin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: 'USDC', + }, + }, + }, + bridgeId: 'socket', + bridges: ['celercircle'], + steps: [ + { + action: 'bridge', + srcChainId: 1, + destChainId: 42161, + protocol: { + name: 'cctp', + displayName: 'Circle CCTP', + icon: 'https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg', + }, + srcAsset: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USDCoin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: 'USDC', + }, + destAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: null, + }, + srcAmount: '10903750', + destAmount: '10503750', + }, + ], + }, + approval: { + chainId: 1, + to: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 56349, + }, + trade: { + chainId: 1, + to: '0x0439e60F02a8900a951603950d8D4527f400C3f1', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002400000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000000e800000197b7dfe9d00000000000000000000000000000000000000000000000000000000000a660c600000000000000000000000000000000000000000000000000000000000000c4000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000002f0cd4583d78e68889d534cc878714e5c66fd0a2867f348f8e969d35f7fddfde3d108c064ffb227225c3e4faaf59aa5df6a967534e63143227b9a9a00c4dd6671c', + gasLimit: 285725, + }, + estimatedProcessingTimeInSeconds: 1020, + }, + ], + ARB_11_USDC_TO_ETH: [ + { + quote: { + requestId: 'edbef62a-d3e6-4b33-aad5-9cdb81f85f53', + srcChainId: 42161, + srcAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + srcTokenAmount: '10903750', + destChainId: 1, + destAsset: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USDCoin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: 'USDC', + }, + destTokenAmount: '7821920', + feeData: { + metabridge: { + amount: '96250', + asset: { + chainId: 42161, + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + }, + }, + bridgeId: 'socket', + bridges: ['celercircle'], + steps: [ + { + action: 'bridge', + srcChainId: 42161, + destChainId: 1, + protocol: { + name: 'cctp', + displayName: 'Circle CCTP', + icon: 'https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg', + }, + srcAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: null, + }, + destAsset: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USDCoin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: 'USDC', + }, + srcAmount: '10903750', + destAmount: '7821920', + }, + ], + }, + approval: { + chainId: 42161, + to: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b300000000000000000000000023981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 116676, + }, + trade: { + chainId: 42161, + to: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002400000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000000000000000000000000000000000000000000001000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa00000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf00000000000000000000000000000000000000000000000000000000000000e80000018cb7dfe9d00000000000000000000000000000000000000000000000000000000000a660c600000000000000000000000000000000000000000000000000000000000000c4000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002f06660000000000000000000000000000000000000000000000000459142ce9a5e109850e867866af8a8e49ef715600981974e0b7f4e48c2e17044dd8921fac00ee87aae63468a52727e978cdf9debbe4cef61ae994d103092e881c', + gasLimit: 409790, + }, + estimatedProcessingTimeInSeconds: 1140, + }, + ], + ARB_11_USDC_TO_OP: [ + { + quote: { + requestId: 'dc63e7e6-dc9b-4aa8-80bb-714192ecd801', + srcChainId: 42161, + srcTokenAmount: '10903750', + srcAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 10, + destTokenAmount: '10897534', + destAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '96250', + asset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['across'], + steps: [ + { + action: 'bridge', + srcChainId: 42161, + destChainId: 10, + protocol: { + name: 'across', + displayName: 'Across', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png', + }, + srcAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '10903750', + destAmount: '10897534', + }, + ], + }, + approval: { + chainId: 42161, + to: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b300000000000000000000000023981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 280491, + }, + trade: { + chainId: 42161, + to: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa00000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf00000000000000000000000000000000000000000000000000000000000000902340ab8fc7e68ebe53823219c5fe6ef47965741f6f7a4734bf784bf3ae3f2452af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000a660c60000000a0002067997b930636705a29bffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b7100000000000000000000000000000000ec7648fdf42336c0860c532fb076be8938251a3349d81d611f21e620d8e1ab9f5719d1f40aac527243e6883beb9d22af9a584b5fb419af9d0970804b97976cbb1c', + gasLimit: 734160, + }, + estimatedProcessingTimeInSeconds: 51, + }, + { + quote: { + requestId: 'dd718a05-ee10-4ec4-99f2-9bc3676640a1', + srcChainId: 42161, + srcTokenAmount: '10903750', + srcAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 10, + destTokenAmount: '10903640', + destAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '96250', + asset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['stargate'], + steps: [ + { + action: 'bridge', + srcChainId: 42161, + destChainId: 10, + protocol: { + name: 'stargate', + displayName: 'StargateV2 (Fast mode)', + icon: 'https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png', + }, + srcAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '10903750', + destAmount: '10903640', + }, + ], + }, + approval: { + chainId: 42161, + to: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b300000000000000000000000023981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 280491, + }, + trade: { + chainId: 42161, + to: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x132018b59ef1', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa00000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf00000000000000000000000000000000000000000000000000000000000003e414d5307700000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200c564a18849d43f86f9dde5b38ad12801f40e4f909559b6195db0903f7398ef75000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000132018b59ef10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759f000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d28b64c753bb8cd4ac8863f4da513edc5d10a619ea4af9623a5f8fdb86f4296a4735ddf5981acb464fc0f35741dd26fb730eb9b5323586499631edf7870587ed1b', + gasLimit: 1232971, + }, + estimatedProcessingTimeInSeconds: 33, + }, + { + quote: { + requestId: 'f07aefdc-2be7-4c41-a0ab-87ac2ec16e3a', + srcChainId: 42161, + srcTokenAmount: '10903750', + srcAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 10, + destTokenAmount: '10803750', + destAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '96250', + asset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['celercircle'], + steps: [ + { + action: 'bridge', + srcChainId: 42161, + destChainId: 10, + protocol: { + name: 'celercircle', + displayName: 'Circle CCTP', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/circle.png', + }, + srcAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '10903750', + destAmount: '10803750', + }, + ], + }, + approval: { + chainId: 42161, + to: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b300000000000000000000000023981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 280491, + }, + trade: { + chainId: 42161, + to: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa00000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf00000000000000000000000000000000000000000000000000000000000001e4bab657d80000000000000000000000000000000000000000000000000000000000000020cc4b6b89255288b4450ce670297679230238eca5fc5572f0b3fdca8fcd60081f000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b63656c6572636972636c65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000454801e91c4c31ee115b5b1dadfa6abefc87bb88079879d7a68d6ac78ac9eb917678a2589c527e4819b0d2fd2bd3bf071d761f64f85ff8d77b90cca67ccbb5f01b', + gasLimit: 967319, + }, + estimatedProcessingTimeInSeconds: 1073, + }, + { + quote: { + requestId: 'ef05128f-c693-4d4a-adec-2b103f931a43', + srcChainId: 42161, + srcAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + srcTokenAmount: '10903750', + destChainId: 10, + destAsset: { + chainId: 10, + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + destTokenAmount: '10703750', + feeData: { + metabridge: { + amount: '96250', + asset: { + chainId: 42161, + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + }, + }, + bridgeId: 'socket', + bridges: ['celercircle'], + steps: [ + { + action: 'bridge', + srcChainId: 42161, + destChainId: 10, + protocol: { + name: 'cctp', + displayName: 'Circle CCTP', + icon: 'https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg', + }, + srcAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: null, + }, + destAsset: { + chainId: 10, + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: null, + }, + srcAmount: '10903750', + destAmount: '10703750', + }, + ], + }, + approval: { + chainId: 42161, + to: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b300000000000000000000000023981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 280491, + }, + trade: { + chainId: 42161, + to: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002400000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa00000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf00000000000000000000000000000000000000000000000000000000000000e80000018cb7dfe9d00000000000000000000000000000000000000000000000000000000000a660c600000000000000000000000000000000000000000000000000000000000000c4000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000030d40000000000000000000000000000000000000000000000000a798e0436c3787ba6e14fd44badd42b591886ba135ebdb6a441494c203e848c61c89a6a55b51f66c46023f5afda568446168e121a4acdfa1c60d7b8c31f0507c1c', + gasLimit: 783830, + }, + estimatedProcessingTimeInSeconds: 1080, + }, + { + quote: { + requestId: '4a15ec74e270a7ffc07aaad0bd59853e', + srcChainId: 42161, + srcTokenAmount: '10903750', + srcAsset: { + _id: '66d776fb76523303f628495c', + id: '42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.0009320510897841, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + volatility: 0, + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: [ + 'uusdc', + 'cctp-uusdc-arbitrum-to-noble', + 'btc-usdc-arb', + ], + enabled: true, + createdAt: '2024-09-03T20:52:11.579Z', + updatedAt: '2024-10-08T21:23:55.197Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + destChainId: 10, + destTokenAmount: '10900626', + destAsset: { + _id: '66d776fd76523303f628520c', + id: '10_0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.0009026800874075, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + volatility: 0, + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: ['uusdc', 'cctp-uusdc-optimism-to-noble'], + enabled: true, + createdAt: '2024-09-03T20:52:13.858Z', + updatedAt: '2024-10-08T21:23:55.474Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + feeData: { + metabridge: { + amount: '96250', + asset: { + _id: '66d776fb76523303f628495c', + id: '42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: + '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.0009320510897841, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + volatility: 0, + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: [ + 'uusdc', + 'cctp-uusdc-arbitrum-to-noble', + 'btc-usdc-arb', + ], + enabled: true, + createdAt: '2024-09-03T20:52:11.579Z', + updatedAt: '2024-10-08T21:23:55.197Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + }, + }, + bridgeId: 'squid', + bridges: ['axelar'], + steps: [ + { + action: 'swap', + srcChainId: 42161, + destChainId: 42161, + protocol: { + name: 'Pancakeswap V3', + displayName: 'Pancakeswap V3', + }, + srcAsset: { + _id: '66d776fb76523303f628495c', + id: '42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: + '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.0009320510897841, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: [ + 'uusdc', + 'cctp-uusdc-arbitrum-to-noble', + 'btc-usdc-arb', + ], + enabled: true, + createdAt: '2024-09-03T20:52:11.579Z', + updatedAt: '2024-10-08T21:23:55.197Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fb76523303f628495e', + id: '42161_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: + '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.0009320510897841, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:11.583Z', + updatedAt: '2024-10-08T21:23:55.197Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + srcAmount: '10903750', + destAmount: '10901792', + }, + { + action: 'bridge', + srcChainId: 42161, + destChainId: 10, + protocol: { + name: 'axelar', + displayName: 'Axelar', + }, + srcAsset: { + _id: '66d776fb76523303f628495e', + id: '42161_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: + '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.0009320510897841, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:11.583Z', + updatedAt: '2024-10-08T21:23:55.197Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fd76523303f6285210', + id: '10_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.0009026800874075, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:13.860Z', + updatedAt: '2024-10-08T21:23:55.474Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + srcAmount: '10901792', + destAmount: '10901792', + }, + { + action: 'swap', + srcChainId: 10, + destChainId: 10, + protocol: { + name: 'Uniswap V3', + displayName: 'Uniswap V3', + }, + srcAsset: { + _id: '66d776fd76523303f6285210', + id: '10_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.0009026800874075, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:13.860Z', + updatedAt: '2024-10-08T21:23:55.474Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fd76523303f628520a', + id: '10_0x7f5c764cbc14f9669b88837ca1490cca17c31607', + symbol: 'USDC.e', + address: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC.e', + decimals: 6, + usdPrice: 1.0009026800874075, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC.e', + subGraphIds: [], + enabled: true, + createdAt: '2024-09-03T20:52:13.857Z', + updatedAt: '2024-10-08T21:23:55.474Z', + __v: 0, + subGraphOnly: false, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + srcAmount: '10901792', + destAmount: '10902122', + }, + { + action: 'swap', + srcChainId: 10, + destChainId: 10, + protocol: { + name: 'Uniswap V3', + displayName: 'Uniswap V3', + }, + srcAsset: { + _id: '66d776fd76523303f628520a', + id: '10_0x7f5c764cbc14f9669b88837ca1490cca17c31607', + symbol: 'USDC.e', + address: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC.e', + decimals: 6, + usdPrice: 1.0009026800874075, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC.e', + subGraphIds: [], + enabled: true, + createdAt: '2024-09-03T20:52:13.857Z', + updatedAt: '2024-10-08T21:23:55.474Z', + __v: 0, + subGraphOnly: false, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fd76523303f628520c', + id: '10_0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.0009026800874075, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: ['uusdc', 'cctp-uusdc-optimism-to-noble'], + enabled: true, + createdAt: '2024-09-03T20:52:13.858Z', + updatedAt: '2024-10-08T21:23:55.474Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + srcAmount: '10902122', + destAmount: '10900626', + }, + ], + }, + approval: { + chainId: 42161, + to: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b300000000000000000000000023981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 280491, + }, + trade: { + chainId: 42161, + to: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x3bcba906c4fc', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000e737175696441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010e0000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa00000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf0000000000000000000000000000000000000000000000000000000000000f94846a1bc6000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c600000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000520000000000000000000000000000000000000000000000000000000000000056000000000000000000000000000000000000000000000000000000000000005a00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d6660000000000000000000000000000000000000000000000000000000000a660c60000000000000000000000000000000000000000000000000000000000a614fd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000761786c555344430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086f7074696d69736d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a3078636531364636393337353532306162303133373763653742383866354241384334384638443636360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000054000000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c0000000000000000000000000000000000000000000000000000000000a659200000000000000000000000000000000000000000000000000000000000a616460000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a65a6a0000000000000000000000000000000000000000000000000000000000a6107000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c3160700000000000000000000000000000000000000000000000000000000000000044a15ec74e270a7ffc07aaad0bd59853e000000000000000000000000000000004a15ec74e270a7ffc07aaad0bd59853e00000000000000000000000051039353c0cc77f171c42153ba91fbce7169a04b7d7aca7d0c6eeb24afe83301731e1c6247d68398068d4e73a5ab2d6e9abc76ed5a0fc5ddf1e6e340ee59bf3c1c', + gasLimit: 1491274, + }, + estimatedProcessingTimeInSeconds: 20, + }, + ], + OP_11_USDC_TO_ARB: [ + { + quote: { + requestId: '01fa78fd-ed49-42b3-ab0e-94c7108feea9', + srcChainId: 10, + srcTokenAmount: '11000000', + srcAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 42161, + destTokenAmount: '10950676', + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '0', + asset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['across'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'across', + displayName: 'Across', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png', + }, + srcAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '11000000', + destAmount: '10950676', + }, + ], + }, + approval: { + chainId: 10, + to: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 61865, + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000000902340ab8f15c1311a1d882e68c5fe6ef47965741f6f7a4734bf784bf3ae3f24520b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000a7d8c00000a4b1000fee2f88fb6d2f6705a29bffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b7100000000000000000000000000000000e901efe0f8781a535ac71317aa666e079e2b867f1a0f1aae7db8afdf38c6f5a663f8638a8fa1f578ba4f5613853bb4ff7b831b0cfeccdcf47bb3e46feff039371c', + gasLimit: 196468, + }, + estimatedProcessingTimeInSeconds: 15, + }, + { + quote: { + requestId: '04064397-73e1-44c0-a2ed-f938e5fe62f0', + srcChainId: 10, + srcTokenAmount: '11000000', + srcAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 42161, + destTokenAmount: '10999889', + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '0', + asset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['stargate'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'stargate', + displayName: 'StargateV2 (Fast mode)', + icon: 'https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png', + }, + srcAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '11000000', + destAmount: '10999889', + }, + ], + }, + approval: { + chainId: 10, + to: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 61865, + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x1f7968e0913f', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000003e414d5307700000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200f02847b1891a30122096ac33055ab7e4286cae991a862dbd35a181006f45b44d0000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a7d8c0000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000001f7968e0913f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a7d8c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ca2568629f30c9780c694cf80af2799e1836b70fd6a221915056dacf1584d63a531f3049719aaeb572635cf719df4410100859da7b9033ba52805691cace86ef1b', + gasLimit: 619670, + }, + estimatedProcessingTimeInSeconds: 50, + }, + { + quote: { + requestId: '26d1486d-1979-4a24-b066-aa87ea6a9cbf', + srcChainId: 10, + srcTokenAmount: '11000000', + srcAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destChainId: 42161, + destTokenAmount: '10900000', + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + feeData: { + metabridge: { + amount: '0', + asset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['celercircle'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'celercircle', + displayName: 'Circle CCTP', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/circle.png', + }, + srcAsset: { + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + chainId: 10, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '1.0007004903432404', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '11000000', + destAmount: '10900000', + }, + ], + }, + approval: { + chainId: 10, + to: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 61865, + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000001e4bab657d80000000000000000000000000000000000000000000000000000000000000020af6b3cbb61978d928e0a59f45df6e973d36326c48aaa054412683aba82adbed60000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a7d8c0000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b63656c6572636972636c65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000e2d38d411f459d41f584802754a11be4cff908688ddd18e4d274400fd0de6d9ff9a6eeb426c654afae9fdb3f99e511bb288a7246018e54432afb60be63691b', + gasLimit: 415725, + }, + estimatedProcessingTimeInSeconds: 1134, + }, + { + quote: { + requestId: '544ebf94-e5d4-4553-8c64-af881b55c6ff', + srcChainId: 10, + srcAsset: { + chainId: 10, + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + srcTokenAmount: '11000000', + destChainId: 42161, + destAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + destTokenAmount: '10600000', + feeData: { + metabridge: { + amount: '0', + asset: { + chainId: 10, + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://media.socket.tech/tokens/all/USDC', + logoURI: 'https://media.socket.tech/tokens/all/USDC', + chainAgnosticId: null, + }, + }, + }, + bridgeId: 'socket', + bridges: ['celercircle'], + steps: [ + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'cctp', + displayName: 'Circle CCTP', + icon: 'https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg', + }, + srcAsset: { + chainId: 10, + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: null, + }, + destAsset: { + chainId: 42161, + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + logoURI: 'https://assets.polygon.technology/tokenAssets/usdc.svg', + chainAgnosticId: null, + }, + srcAmount: '11000000', + destAmount: '10600000', + }, + ], + }, + approval: { + chainId: 10, + to: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 61865, + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002400000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000000e80000018cb7dfe9d00000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c4000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000aaa45c155ccfb09c996a750c7d53dc975065d8841b58669213d7f345318d2395a5796ece3d8c0729e128bd0a33dface658f5846a3496dcfbeeca77394fe9b5a1b', + gasLimit: 290954, + }, + estimatedProcessingTimeInSeconds: 1500, + }, + { + quote: { + requestId: '389140aaaebab60eca1d15b4134c27fa', + srcChainId: 10, + srcTokenAmount: '11000000', + srcAsset: { + _id: '66d776fd76523303f628520c', + id: '10_0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.001053579729135, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + volatility: 0, + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: ['uusdc', 'cctp-uusdc-optimism-to-noble'], + enabled: true, + createdAt: '2024-09-03T20:52:13.858Z', + updatedAt: '2024-10-08T21:24:40.381Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + destChainId: 42161, + destTokenAmount: '10996548', + destAsset: { + _id: '66d776fb76523303f628495c', + id: '42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.001053579729135, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + volatility: 0, + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: [ + 'uusdc', + 'cctp-uusdc-arbitrum-to-noble', + 'btc-usdc-arb', + ], + enabled: true, + createdAt: '2024-09-03T20:52:11.579Z', + updatedAt: '2024-10-08T21:24:40.127Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + feeData: { + metabridge: { + amount: '0', + asset: { + _id: '66d776fd76523303f628520c', + id: '10_0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.001053579729135, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + volatility: 0, + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: ['uusdc', 'cctp-uusdc-optimism-to-noble'], + enabled: true, + createdAt: '2024-09-03T20:52:13.858Z', + updatedAt: '2024-10-08T21:24:40.381Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + }, + }, + bridgeId: 'squid', + bridges: ['axelar'], + steps: [ + { + action: 'swap', + srcChainId: 10, + destChainId: 10, + protocol: { + name: 'Uniswap V3', + displayName: 'Uniswap V3', + }, + srcAsset: { + _id: '66d776fd76523303f628520c', + id: '10_0x0b2c639c533813f4aa9d7837caf62653d097ff85', + symbol: 'USDC', + address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.001053579729135, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: ['uusdc', 'cctp-uusdc-optimism-to-noble'], + enabled: true, + createdAt: '2024-09-03T20:52:13.858Z', + updatedAt: '2024-10-08T21:24:40.381Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fd76523303f628520a', + id: '10_0x7f5c764cbc14f9669b88837ca1490cca17c31607', + symbol: 'USDC.e', + address: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC.e', + decimals: 6, + usdPrice: 1.001053579729135, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC.e', + subGraphIds: [], + enabled: true, + createdAt: '2024-09-03T20:52:13.857Z', + updatedAt: '2024-10-08T21:24:40.381Z', + __v: 0, + subGraphOnly: false, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + srcAmount: '11000000', + destAmount: '10999308', + }, + { + action: 'swap', + srcChainId: 10, + destChainId: 10, + protocol: { + name: 'Uniswap V3', + displayName: 'Uniswap V3', + }, + srcAsset: { + _id: '66d776fd76523303f628520a', + id: '10_0x7f5c764cbc14f9669b88837ca1490cca17c31607', + symbol: 'USDC.e', + address: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: 'USDC.e', + decimals: 6, + usdPrice: 1.001053579729135, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC.e', + subGraphIds: [], + enabled: true, + createdAt: '2024-09-03T20:52:13.857Z', + updatedAt: '2024-10-08T21:24:40.381Z', + __v: 0, + subGraphOnly: false, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fd76523303f6285210', + id: '10_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.001053579729135, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:13.860Z', + updatedAt: '2024-10-08T21:24:40.381Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + srcAmount: '10999308', + destAmount: '10996775', + }, + { + action: 'bridge', + srcChainId: 10, + destChainId: 42161, + protocol: { + name: 'axelar', + displayName: 'Axelar', + }, + srcAsset: { + _id: '66d776fd76523303f6285210', + id: '10_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 10, + chain: { + _id: '66d776eb0befbcf39c0a01d5', + id: '10', + chainId: '10', + networkIdentifier: 'optimism', + chainName: 'Chain 10', + axelarChainName: 'optimism', + type: 'evm', + networkName: 'Optimism', + nativeCurrency: { + name: 'Optimism', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/optimism.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/optimism.webp', + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'optimism', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '2', + tokenMessenger: + '0x2B4069517957735bE00ceE0fadAE88a26365528f', + messageTransmitter: + '0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8', + }, + }, + rpcList: ['https://mainnet.optimism.io'], + internalRpc: [ + 'https://opt-mainnet.g.alchemy.com/v2/YLCHNNGouPGR8L-KViSmQ-4dCKaE6o6H', + 'https://cool-green-tree.optimism.quiknode.pro/c8f7de54a6b1d0e6a15924c5bf3aae2d6c24f0c0', + 'https://optimism-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://opt-mainnet.g.alchemy.com/v2/wDgIhJ3Yz4PvLRkQlA1k0y67dZRb146a', + 'https://opt-mainnet.g.alchemy.com/v2/E8BiF2_ABVQ5fy394vfMaM1JGkpPkIeY', + 'https://nd-739-933-380.p2pify.com/2c8029def5e92e4da7bec7f7c1c153a4', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x4200000000000000000000000000000000000006', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.745Z', + updatedAt: '2024-09-13T09:51:30.869Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.001053579729135, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:13.860Z', + updatedAt: '2024-10-08T21:24:40.381Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fb76523303f628495e', + id: '42161_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: + '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.001053579729135, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:11.583Z', + updatedAt: '2024-10-08T21:24:40.127Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + srcAmount: '10996775', + destAmount: '10996775', + }, + { + action: 'swap', + srcChainId: 42161, + destChainId: 42161, + protocol: { + name: 'Pancakeswap V3', + displayName: 'Pancakeswap V3', + }, + srcAsset: { + _id: '66d776fb76523303f628495e', + id: '42161_0xeb466342c4d449bc9f53a865d5cb90586f405215', + symbol: 'USDC.axl', + address: '0xeb466342c4d449bc9f53a865d5cb90586f405215', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: + '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: ' USDC (Axelar)', + decimals: 6, + usdPrice: 1.001053579729135, + interchainTokenId: null, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'axlUSDC', + subGraphOnly: false, + subGraphIds: ['uusdc'], + enabled: true, + createdAt: '2024-09-03T20:52:11.583Z', + updatedAt: '2024-10-08T21:24:40.127Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg', + }, + destAsset: { + _id: '66d776fb76523303f628495c', + id: '42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831', + symbol: 'USDC', + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + chainId: 42161, + chain: { + _id: '66d776eb0befbcf39c0a01d1', + id: '42161', + chainId: '42161', + networkIdentifier: 'arbitrum', + chainName: 'Chain 42161', + axelarChainName: 'Arbitrum', + type: 'evm', + networkName: 'Arbitrum', + nativeCurrency: { + name: 'Arbitrum', + symbol: 'ETH', + decimals: 18, + icon: 'https://raw.githubusercontent.com/axelarnetwork/axelar-docs/main/public/images/chains/arbitrum.svg', + }, + chainIconURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/arbitrum.webp', + blockExplorerUrls: ['https://arbiscan.io/'], + swapAmountForGas: '2000000', + sameChainSwapsSupported: true, + squidContracts: { + squidRouter: '0xce16F69375520ab01377ce7B88f5BA8C48F8D666', + defaultCrosschainToken: + '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + squidMulticall: '0xEa749Fd6bA492dbc14c24FE8A3d08769229b896c', + squidFeeCollector: + '0xd3F8F338FdAD6DEb491F0F225d09422A7a70cc45', + }, + compliance: { + trmIdentifier: 'arbitrum', + }, + boostSupported: true, + enableBoostByDefault: true, + bridges: { + axelar: { + gateway: '0xe432150cce91c13a887f7D836923d5597adD8E31', + itsService: '0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C', + }, + cctp: { + cctpDomain: '3', + tokenMessenger: + '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + messageTransmitter: + '0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca', + }, + chainflip: { + vault: '0x79001a5e762f3befc8e5871b42f6734e00498920', + }, + }, + rpcList: ['https://arb1.arbitrum.io/rpc'], + internalRpc: [ + 'https://arb-mainnet.g.alchemy.com/v2/3IBUO-R6MAFGVxOYRHTV-Gt4Pce5jmsz', + 'https://special-wispy-theorem.arbitrum-mainnet.quiknode.pro/befd7d6b704d6477ef747f7ed39299d252994e18', + 'https://arbitrum-mainnet.infura.io/v3/273aad656cd94f9aa022e4899b87dd6c', + 'https://arb-mainnet.g.alchemy.com/v2/2YR6kg9ueaoGnMxBsvLNXqJyCrmAKC11', + 'https://arb-mainnet.g.alchemy.com/v2/3q9qfCdKJcOA2WbdqVic8jtwnQTZGwlm', + 'https://arbitrum-mainnet.core.chainstack.com/10fd48bf1e4a75d901b11b2ff2c76ada', + ], + chainNativeContracts: { + wrappedNativeToken: + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + ensRegistry: '', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + usdcToken: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + }, + feeCurrencies: [], + currencies: [], + features: [], + enabled: true, + createdAt: '2024-09-03T20:51:55.733Z', + updatedAt: '2024-09-13T09:57:11.584Z', + __v: 1, + }, + name: 'USDC', + decimals: 6, + usdPrice: 1.001053579729135, + coingeckoId: 'usd-coin', + type: 'evm', + logoURI: + 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + axelarNetworkSymbol: 'USDC', + subGraphOnly: false, + subGraphIds: [ + 'uusdc', + 'cctp-uusdc-arbitrum-to-noble', + 'btc-usdc-arb', + ], + enabled: true, + createdAt: '2024-09-03T20:52:11.579Z', + updatedAt: '2024-10-08T21:24:40.127Z', + __v: 0, + active: true, + icon: 'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg', + }, + srcAmount: '10996775', + destAmount: '10996548', + }, + ], + }, + approval: { + chainId: 10, + to: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x00', + data: '0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000a7d8c0', + gasLimit: 61865, + }, + trade: { + chainId: 10, + to: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + value: '0x46366a86b7c6', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000e737175696441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010e0000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000f94846a1bc60000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000a7d8c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000000000000000000000000098000000000000000000000000000000000000000000000000000000000000009e0000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c0000000000000000000000000000000000000000000000000000000000a7d8c00000000000000000000000000000000000000000000000000000000000a7914d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d6660000000000000000000000000000000000000000000000000000000000a7d60c0000000000000000000000000000000000000000000000000000000000a7876c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000761786c55534443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008417262697472756d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a3078636531364636393337353532306162303133373763653742383866354241384334384638443636360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005700000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000a7cc270000000000000000000000000000000000000000000000000000000000a786890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000004389140aaaebab60eca1d15b4134c27fa00000000000000000000000000000000389140aaaebab60eca1d15b4134c27fa0000000000000000000000004f83a94a67ba9e24e0fee7799e54ddb2575d8454082cf56a7c0292457ce280df23aa57920d0bef49660cda25b12442e1fc410a745097e2ef21491f05082aa8661b', + gasLimit: 565594, + }, + estimatedProcessingTimeInSeconds: 20, + }, + ], +}; diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index f045c0dfbc12..a61d2fdcd8fd 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -7,11 +7,10 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../app/scripts/controllers/bridge/types'; - import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; -import { MetaMaskReduxDispatch } from '../../store/store'; import { QuoteRequest } from '../../pages/bridge/types'; +import { MetaMaskReduxDispatch } from '../../store/store'; import { bridgeSlice } from './bridge'; const { @@ -86,3 +85,13 @@ export const updateQuoteRequestParams = (params: Partial) => { ); }; }; + +export const getBridgeERC20Allowance = async ( + contractAddress: string, + chainId: Hex, +): Promise => { + return await submitRequestToBackground( + BridgeBackgroundAction.GET_BRIDGE_ERC20_ALLOWANCE, + [contractAddress, chainId], + ); +}; diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 60704aa6f094..5624a0ec5569 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -26,7 +26,7 @@ import { calcTokenAmount } from '../../../shared/lib/transactions-controller-uti import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { BridgeState } from './bridge'; -type BridgeAppState = { +export type BridgeAppState = { metamask: NetworkState & { bridgeState: BridgeControllerState } & { useExternalServices: boolean; }; diff --git a/ui/ducks/bridge/utils.ts b/ui/ducks/bridge/utils.ts new file mode 100644 index 000000000000..853c344310fe --- /dev/null +++ b/ui/ducks/bridge/utils.ts @@ -0,0 +1,47 @@ +import { Hex } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; +import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils'; +import { Numeric } from '../../../shared/modules/Numeric'; +import { TxData } from '../../pages/bridge/types'; +import { getTransaction1559GasFeeEstimates } from '../../pages/swaps/swaps.util'; + +// We don't need to use gas multipliers here because the gasLimit from Bridge API already included it +export const getHexMaxGasLimit = (gasLimit: number) => { + return new Numeric( + new BigNumber(gasLimit).toString(), + 10, + ).toPrefixedHexString() as Hex; +}; +export const getTxGasEstimates = async ({ + networkAndAccountSupports1559, + networkGasFeeEstimates, + txParams, + hexChainId, +}: { + networkAndAccountSupports1559: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + networkGasFeeEstimates: any; + txParams: TxData; + hexChainId: Hex; +}) => { + if (networkAndAccountSupports1559) { + const { estimatedBaseFeeGwei = '0' } = networkGasFeeEstimates; + const hexEstimatedBaseFee = decGWEIToHexWEI(estimatedBaseFeeGwei) as Hex; + const txGasFeeEstimates = await getTransaction1559GasFeeEstimates( + { + ...txParams, + chainId: hexChainId, + gasLimit: txParams.gasLimit?.toString(), + }, + hexEstimatedBaseFee, + hexChainId, + ); + return txGasFeeEstimates; + } + + return { + baseAndPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + }; +}; diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index 4b551b318b62..b008f2fdaf7d 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -357,6 +357,15 @@ export function useTransactionDisplayData(transactionGroup) { category = TransactionGroupCategory.send; title = t('send'); subtitle = t('toAddress', [shortenAddress(recipientAddress)]); + } else if (type === TransactionType.bridgeApproval) { + title = t('bridgeApproval'); + category = TransactionGroupCategory.approval; + title = t('bridgeApproval', [primaryTransaction.sourceTokenSymbol]); + subtitle = origin; + subtitleContainsOrigin = true; + primarySuffix = primaryTransaction.sourceTokenSymbol; // TODO this will be undefined right now + } else if (type === TransactionType.bridge) { + title = t('bridge'); } else { dispatch( captureSingleException( diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts index bbdddab53658..577b1827b160 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/ui/pages/bridge/bridge.util.ts @@ -1,4 +1,6 @@ +import { Contract } from '@ethersproject/contracts'; import { Hex, add0x } from '@metamask/utils'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; import { BridgeFeatureFlagsKey, BridgeFeatureFlags, @@ -8,6 +10,8 @@ import { import { BRIDGE_API_BASE_URL, BRIDGE_CLIENT_ID, + ETH_USDT_ADDRESS, + METABRIDGE_ETHEREUM_ADDRESS, } from '../../../shared/constants/bridge'; import { MINUTE } from '../../../shared/constants/time'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; @@ -26,6 +30,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import { BridgeAsset, BridgeFlag, @@ -185,3 +190,22 @@ export async function fetchBridgeQuotes( }); return filteredQuotes; } +/** + * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum + * + * @returns The txParam data that will reset allowance to 0, combine it with the approval tx params received from Bridge API + */ +export const getEthUsdtResetData = () => { + const UsdtContractInterface = new Contract(ETH_USDT_ADDRESS, abiERC20) + .interface; + const data = UsdtContractInterface.encodeFunctionData('approve', [ + METABRIDGE_ETHEREUM_ADDRESS, + '0', + ]); + + return data; +}; + +export const isEthUsdt = (chainId: Hex, address: string) => + chainId === CHAIN_IDS.MAINNET && + address.toLowerCase() === ETH_USDT_ADDRESS.toLowerCase(); diff --git a/ui/pages/bridge/hooks/useAddToken.ts b/ui/pages/bridge/hooks/useAddToken.ts new file mode 100644 index 000000000000..597149b16e49 --- /dev/null +++ b/ui/pages/bridge/hooks/useAddToken.ts @@ -0,0 +1,84 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { Numeric } from '../../../../shared/modules/Numeric'; +import { QuoteResponse } from '../types'; +import { + getNetworkConfigurationsByChainId, + getSelectedNetworkClientId, +} from '../../../selectors'; +import { FEATURED_RPCS } from '../../../../shared/constants/network'; +import { addToken, addNetwork } from '../../../store/actions'; + +export default function useAddToken() { + const dispatch = useDispatch(); + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const sourceNetworkClientId = useSelector(getSelectedNetworkClientId); + + const addSourceToken = (quoteResponse: QuoteResponse) => { + const { + address, + decimals, + symbol, + icon: image, + } = quoteResponse.quote.srcAsset; + dispatch( + addToken({ + address, + decimals, + symbol, + image, + networkClientId: sourceNetworkClientId, + }), + ); + }; + + const addDestToken = async (quoteResponse: QuoteResponse) => { + // Look up the destination chain + const hexDestChainId = new Numeric(quoteResponse.quote.destChainId, 10) + .toPrefixedHexString() + .toLowerCase() as `0x${string}`; + const foundDestNetworkConfig: NetworkConfiguration | undefined = + networkConfigurations[hexDestChainId]; + let addedDestNetworkConfig: NetworkConfiguration | undefined; + + // If user has not added the network in MetaMask, add it for them silently + if (!foundDestNetworkConfig) { + const featuredRpc = FEATURED_RPCS.find( + (rpc) => rpc.chainId === hexDestChainId, + ); + if (!featuredRpc) { + throw new Error('No featured RPC found'); + } + addedDestNetworkConfig = (await dispatch( + addNetwork(featuredRpc), + )) as unknown as NetworkConfiguration; + } + + const destNetworkConfig = foundDestNetworkConfig || addedDestNetworkConfig; + if (!destNetworkConfig) { + throw new Error('No destination network configuration found'); + } + + // Add the token after network is guaranteed to exist + const rpcEndpointIndex = destNetworkConfig.defaultRpcEndpointIndex; + const destNetworkClientId = + destNetworkConfig.rpcEndpoints[rpcEndpointIndex].networkClientId; + const { + address, + decimals, + symbol, + icon: image, + } = quoteResponse.quote.destAsset; + await dispatch( + addToken({ + address, + decimals, + symbol, + image, + networkClientId: destNetworkClientId, + }), + ); + }; + + return { addSourceToken, addDestToken }; +} diff --git a/ui/pages/bridge/hooks/useHandleApprovalTx.ts b/ui/pages/bridge/hooks/useHandleApprovalTx.ts new file mode 100644 index 000000000000..67f2abf67e7e --- /dev/null +++ b/ui/pages/bridge/hooks/useHandleApprovalTx.ts @@ -0,0 +1,94 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; +import { TxData, QuoteResponse, FeeType } from '../types'; +import { isEthUsdt, getEthUsdtResetData } from '../bridge.util'; +import { Numeric } from '../../../../shared/modules/Numeric'; +import { ETH_USDT_ADDRESS } from '../../../../shared/constants/bridge'; +import { getBridgeERC20Allowance } from '../../../ducks/bridge/actions'; +import useHandleTx from './useHandleTx'; + +export default function useHandleApprovalTx() { + const { handleTx } = useHandleTx(); + + const handleEthUsdtAllowanceReset = async ({ + approval, + quoteResponse, + hexChainId, + }: { + approval: TxData; + quoteResponse: QuoteResponse; + hexChainId: Hex; + }) => { + const allowance = new BigNumber( + await getBridgeERC20Allowance(ETH_USDT_ADDRESS, hexChainId), + ); + + // quote.srcTokenAmount is actually after the fees + // so we need to add fees back in for total allowance to give + const sentAmount = new BigNumber(quoteResponse.quote.srcTokenAmount) + .plus(quoteResponse.quote.feeData[FeeType.METABRIDGE].amount) + .toString(); + + const shouldResetApproval = allowance.lt(sentAmount) && allowance.gt(0); + + if (shouldResetApproval) { + const resetData = getEthUsdtResetData(); + const txParams = { + ...approval, + data: resetData, + }; + + await handleTx({ + txType: TransactionType.bridgeApproval, + txParams, + swapsOptions: { + hasApproveTx: true, + meta: { + type: TransactionType.bridgeApproval, + }, + }, + }); + } + }; + + const handleApprovalTx = async ({ + approval, + quoteResponse, + }: { + approval: TxData; + quoteResponse: QuoteResponse; + }) => { + const hexChainId = new Numeric( + approval.chainId, + 10, + ).toPrefixedHexString() as `0x${string}`; + + // On Ethereum, we need to reset the allowance to 0 for USDT first if we need to set a new allowance + // https://www.google.com/url?q=https://docs.unizen.io/trade-api/before-you-get-started/token-allowance-management-for-non-updatable-allowance-tokens&sa=D&source=docs&ust=1727386175513609&usg=AOvVaw3Opm6BSJeu7qO0Ve5iLTOh + if (isEthUsdt(hexChainId, quoteResponse.quote.srcAsset.address)) { + await handleEthUsdtAllowanceReset({ + approval, + quoteResponse, + hexChainId, + }); + } + + const txMeta = await handleTx({ + txType: TransactionType.bridgeApproval, + txParams: approval, + swapsOptions: { + hasApproveTx: true, + meta: { + type: TransactionType.bridgeApproval, + sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, + }, + }, + }); + + return txMeta.id; + }; + return { + handleApprovalTx, + }; +} diff --git a/ui/pages/bridge/hooks/useHandleBridgeTx.ts b/ui/pages/bridge/hooks/useHandleBridgeTx.ts new file mode 100644 index 000000000000..22b2a74fa077 --- /dev/null +++ b/ui/pages/bridge/hooks/useHandleBridgeTx.ts @@ -0,0 +1,48 @@ +import { BigNumber } from 'bignumber.js'; +import { TransactionType } from '@metamask/transaction-controller'; +import { Numeric } from '../../../../shared/modules/Numeric'; +import { FeeType, QuoteResponse } from '../types'; +import useHandleTx from './useHandleTx'; + +export default function useHandleBridgeTx() { + const { handleTx } = useHandleTx(); + + const handleBridgeTx = async ({ + quoteResponse, + approvalTxId, + }: { + quoteResponse: QuoteResponse; + approvalTxId: string | undefined; + }) => { + const sentAmount = new BigNumber(quoteResponse.quote.srcTokenAmount).plus( + quoteResponse.quote.feeData[FeeType.METABRIDGE].amount, + ); + const sentAmountDec = new Numeric(sentAmount, 10) + .shiftedBy(quoteResponse.quote.srcAsset.decimals) + .toString(); + + const txMeta = await handleTx({ + txType: TransactionType.bridge, + txParams: quoteResponse.trade, + swapsOptions: { + hasApproveTx: Boolean(quoteResponse?.approval), + meta: { + // estimatedBaseFee: decEstimatedBaseFee, + // swapMetaData, + type: TransactionType.bridge, + sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, + destinationTokenSymbol: quoteResponse.quote.destAsset.symbol, + destinationTokenDecimals: quoteResponse.quote.destAsset.decimals, + destinationTokenAddress: quoteResponse.quote.destAsset.address, + approvalTxId, + // this is the decimal (non atomic) amount (not USD value) of source token to swap + swapTokenValue: sentAmountDec, + }, + }, + }); + + return txMeta.id; + }; + + return { handleBridgeTx }; +} diff --git a/ui/pages/bridge/hooks/useHandleTx.ts b/ui/pages/bridge/hooks/useHandleTx.ts new file mode 100644 index 000000000000..a4cbf631c338 --- /dev/null +++ b/ui/pages/bridge/hooks/useHandleTx.ts @@ -0,0 +1,79 @@ +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; +import { useDispatch, useSelector } from 'react-redux'; +import { + forceUpdateMetamaskState, + addTransactionAndWaitForPublish, +} from '../../../store/actions'; +import { + getHexMaxGasLimit, + getTxGasEstimates, +} from '../../../ducks/bridge/utils'; +import { getGasFeeEstimates } from '../../../ducks/metamask/metamask'; +import { checkNetworkAndAccountSupports1559 } from '../../../selectors'; +import { ChainId } from '../types'; +import { Numeric } from '../../../../shared/modules/Numeric'; + +export default function useHandleTx() { + const dispatch = useDispatch(); + const networkAndAccountSupports1559 = useSelector( + checkNetworkAndAccountSupports1559, + ); + const networkGasFeeEstimates = useSelector(getGasFeeEstimates); + + const handleTx = async ({ + txType, + txParams, + swapsOptions, + }: { + txType: TransactionType.bridgeApproval | TransactionType.bridge; + txParams: { + chainId: ChainId; + to: string; + from: string; + value: string; + data: string; + gasLimit: number | null; + }; + swapsOptions: { + hasApproveTx: boolean; + meta: Partial; + }; + }) => { + const hexChainId = new Numeric( + txParams.chainId, + 10, + ).toPrefixedHexString() as `0x${string}`; + + const { maxFeePerGas, maxPriorityFeePerGas } = await getTxGasEstimates({ + networkAndAccountSupports1559, + networkGasFeeEstimates, + txParams, + hexChainId, + }); + const maxGasLimit = getHexMaxGasLimit(txParams.gasLimit ?? 0); + + const finalTxParams = { + ...txParams, + chainId: hexChainId, + gasLimit: maxGasLimit, + gas: maxGasLimit, + maxFeePerGas, + maxPriorityFeePerGas, + }; + + const txMeta = await addTransactionAndWaitForPublish(finalTxParams, { + requireApproval: false, + type: txType, + swaps: swapsOptions, + }); + + await forceUpdateMetamaskState(dispatch); + + return txMeta; + }; + + return { handleTx }; +} diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx new file mode 100644 index 000000000000..20f471b1065b --- /dev/null +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx @@ -0,0 +1,485 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { renderHook } from '@testing-library/react-hooks'; +import { Provider } from 'react-redux'; +import { MemoryRouter, useHistory } from 'react-router-dom'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import * as actions from '../../../store/actions'; +import * as selectors from '../../../selectors'; +import { + DummyQuotesNoApproval, + DummyQuotesWithApproval, +} from '../../../../test/data/bridge/dummy-quotes'; +import useSubmitBridgeTransaction from './useSubmitBridgeTransaction'; + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + return { + ...original, + useHistory: jest.fn().mockImplementation(original.useHistory), + }; +}); + +jest.mock('../../../ducks/bridge/utils', () => ({ + ...jest.requireActual('../../../ducks/bridge/utils'), + getTxGasEstimates: jest.fn(() => ({ + baseAndPriorityFeePerGas: '0', + maxFeePerGas: '0x1036640', + maxPriorityFeePerGas: '0x0', + })), +})); + +jest.mock('../../../store/actions', () => { + const original = jest.requireActual('../../../store/actions'); + return { + ...original, + addTransactionAndWaitForPublish: jest.fn(), + addToken: jest.fn().mockImplementation(original.addToken), + addNetwork: jest.fn().mockImplementation(original.addNetwork), + }; +}); + +jest.mock('../../../selectors', () => { + const original = jest.requireActual('../../../selectors'); + return { + ...original, + getIsBridgeEnabled: () => true, + getIsBridgeChain: () => true, + checkNetworkAndAccountSupports1559: () => true, + getSelectedNetworkClientId: () => 'mainnet', + getNetworkConfigurationsByChainId: jest.fn(() => ({ + '0x1': { + blockExplorerUrls: ['https://etherscan.io'], + chainId: '0x1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'https://mainnet.infura.io/v3/infuraProjectId', + }, + ], + }, + '0xa4b1': { + blockExplorerUrls: ['https://explorer.arbitrum.io'], + chainId: '0xa4b1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Arbitrum One', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: '3725601d-f497-43aa-9afa-97c26e9033a3', + type: 'custom', + url: 'https://arbitrum-mainnet.infura.io/v3/infuraProjectId', + }, + ], + }, + })), + }; +}); + +const middleware = [thunk]; + +const makeMockStore = () => { + const store = configureMockStore(middleware)( + createBridgeMockStore( + {}, + {}, + {}, + { + gasFeeEstimates: { + high: { + maxWaitTimeEstimate: 30000, + minWaitTimeEstimate: 15000, + suggestedMaxFeePerGas: '14.226414113', + suggestedMaxPriorityFeePerGas: '2', + }, + }, + useExternalServices: true, + }, + ), + ); + return store; +}; + +const makeWrapper = + (store: ReturnType) => + ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); + }; + +describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { + describe('submitBridgeTransaction', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('executes bridge transaction', async () => { + // Setup + const mockAddTransactionAndWaitForPublish = jest.fn(() => { + return { + id: 'txMetaId-01', + }; + }); + + // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead + (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( + mockAddTransactionAndWaitForPublish, + ); + const store = makeMockStore(); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + ); + + // Assert + expect(mockAddTransactionAndWaitForPublish).toHaveBeenLastCalledWith( + { + chainId: '0x1', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000000902340ab8fc3119af1d016a0eec5fe6ef47965741f6f7a4734bf784bf3ae3f2452a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000a660c60000a4b10008df3abdeb853d66fefedfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b7100000000000000000000000000000000740cfc1bc02079862368cb4eea1332bd9f2dfa925fc757fd51e40919859b87ca031a2a12d67e4ca4ba67d52b59114b3e18c1e8c839ae015112af82e92251db701b', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + gas: '0x33403', + gasLimit: '0x33403', + maxFeePerGas: '0x1036640', + maxPriorityFeePerGas: '0x0', + to: '0x0439e60F02a8900a951603950d8D4527f400C3f1', + value: '0x00', + }, + { + requireApproval: false, + swaps: { + hasApproveTx: true, + meta: { + approvalTxId: 'txMetaId-01', + destinationTokenAddress: + '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + destinationTokenDecimals: 6, + destinationTokenSymbol: 'USDC', + sourceTokenSymbol: 'USDC', + swapTokenValue: '11', + type: 'bridge', + }, + }, + type: 'bridge', + }, + ); + }); + it('executes approval transaction if it exists', async () => { + // Setup + const mockAddTransactionAndWaitForPublish = jest.fn(() => { + return { + id: 'txMetaId-01', + }; + }); + + // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead + (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( + mockAddTransactionAndWaitForPublish, + ); + const store = makeMockStore(); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + ); + + // Assert + expect(mockAddTransactionAndWaitForPublish).toHaveBeenNthCalledWith( + 1, + { + chainId: '0x1', + data: '0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000a7d8c0', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + gas: '0xdc1d', + gasLimit: '0xdc1d', + maxFeePerGas: '0x1036640', + maxPriorityFeePerGas: '0x0', + to: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + value: '0x00', + }, + { + requireApproval: false, + swaps: { + hasApproveTx: true, + meta: { sourceTokenSymbol: 'USDC', type: 'bridgeApproval' }, + }, + type: 'bridgeApproval', + }, + ); + expect(mockAddTransactionAndWaitForPublish).toHaveBeenNthCalledWith( + 2, + { + chainId: '0x1', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000000902340ab8fc3119af1d016a0eec5fe6ef47965741f6f7a4734bf784bf3ae3f2452a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000a660c60000a4b10008df3abdeb853d66fefedfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b7100000000000000000000000000000000740cfc1bc02079862368cb4eea1332bd9f2dfa925fc757fd51e40919859b87ca031a2a12d67e4ca4ba67d52b59114b3e18c1e8c839ae015112af82e92251db701b', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + gas: '0x33403', + gasLimit: '0x33403', + maxFeePerGas: '0x1036640', + maxPriorityFeePerGas: '0x0', + to: '0x0439e60F02a8900a951603950d8D4527f400C3f1', + value: '0x00', + }, + { + requireApproval: false, + swaps: { + hasApproveTx: true, + meta: { + approvalTxId: 'txMetaId-01', + destinationTokenAddress: + '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + destinationTokenDecimals: 6, + destinationTokenSymbol: 'USDC', + sourceTokenSymbol: 'USDC', + swapTokenValue: '11', + type: 'bridge', + }, + }, + type: 'bridge', + }, + ); + }); + it('adds source token if it not the native gas token', async () => { + // Setup + const store = makeMockStore(); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + (actions.addToken as jest.Mock).mockImplementation( + () => async () => ({}), + ); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + ); + + // Assert + expect(actions.addToken).toHaveBeenCalledWith({ + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + decimals: 6, + image: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + networkClientId: 'mainnet', + symbol: 'USDC', + }); + + // Reset + const originalAddToken = jest.requireActual( + '../../../store/actions', + ).addToken; + (actions.addToken as jest.Mock).mockImplementation(originalAddToken); + }); + it('does not add source token if source token is native gas token', async () => { + // Setup + const store = makeMockStore(); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + const mockAddTransactionAndWaitForPublish = jest.fn(() => { + return { + id: 'txMetaId-01', + }; + }); + // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead + (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( + mockAddTransactionAndWaitForPublish, + ); + (actions.addToken as jest.Mock).mockImplementation( + () => async () => ({}), + ); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesNoApproval.OP_0_005_ETH_TO_ARB[0] as any, + ); + + // Assert + expect(actions.addToken).not.toHaveBeenCalled(); + + // Reset + const originalAddToken = jest.requireActual( + '../../../store/actions', + ).addToken; + (actions.addToken as jest.Mock).mockImplementation(originalAddToken); + }); + it('adds dest token if it not the native gas token', async () => { + // Setup + const store = makeMockStore(); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + (actions.addToken as jest.Mock).mockImplementation( + () => async () => ({}), + ); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + ); + + // Assert + expect(actions.addToken).toHaveBeenCalledWith({ + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + decimals: 6, + image: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + networkClientId: '3725601d-f497-43aa-9afa-97c26e9033a3', + symbol: 'USDC', + }); + + // Reset + const originalAddToken = jest.requireActual( + '../../../store/actions', + ).addToken; + (actions.addToken as jest.Mock).mockImplementation(originalAddToken); + }); + it('does not add dest token if dest token is native gas token', async () => { + // Setup + const store = makeMockStore(); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + const mockAddTransactionAndWaitForPublish = jest.fn(() => { + return { + id: 'txMetaId-01', + }; + }); + // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead + (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( + mockAddTransactionAndWaitForPublish, + ); + (actions.addToken as jest.Mock).mockImplementation( + () => async () => ({}), + ); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesNoApproval.OP_0_005_ETH_TO_ARB[0] as any, + ); + + // Assert + expect(actions.addToken).not.toHaveBeenCalled(); + + // Reset + const originalAddToken = jest.requireActual( + '../../../store/actions', + ).addToken; + (actions.addToken as jest.Mock).mockImplementation(originalAddToken); + }); + it('adds dest network if it does not exist', async () => { + // Setup + const store = makeMockStore(); + + const mockAddTransactionAndWaitForPublish = jest.fn(() => { + return { + id: 'txMetaId-01', + }; + }); + // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead + (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( + mockAddTransactionAndWaitForPublish, + ); + const mockedGetNetworkConfigurationsByChainId = + // @ts-expect-error this is a jest mock + selectors.getNetworkConfigurationsByChainId as jest.Mock; + mockedGetNetworkConfigurationsByChainId.mockImplementationOnce(() => ({ + '0x1': { + blockExplorerUrls: ['https://etherscan.io'], + chainId: '0x1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'https://mainnet.infura.io/v3/infuraProjectId', + }, + ], + }, + })); + (actions.addNetwork as jest.Mock).mockImplementationOnce( + () => async () => ({ + blockExplorerUrls: ['https://explorer.arbitrum.io'], + chainId: '0xa4b1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Arbitrum One', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: '3725601d-f497-43aa-9afa-97c26e9033a3', + type: 'custom', + url: 'https://arbitrum-mainnet.infura.io/v3/infuraProjectId', + }, + ], + }), + ); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + ); + + // Assert + expect(actions.addNetwork).toHaveBeenCalledWith({ + blockExplorerUrls: ['https://explorer.arbitrum.io'], + chainId: '0xa4b1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Arbitrum One', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + type: 'custom', + url: 'https://arbitrum-mainnet.infura.io/v3/undefined', + }, + ], + }); + }); + it('routes to activity tab', async () => { + const store = makeMockStore(); + + const mockHistory = { + push: jest.fn(), + }; + (useHistory as jest.Mock).mockImplementationOnce(() => mockHistory); + const { result } = renderHook(() => useSubmitBridgeTransaction(), { + wrapper: makeWrapper(store), + }); + + // Execute + await result.current.submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + ); + + // Assert + expect(mockHistory.push).toHaveBeenCalledWith('/'); + }); + }); +}); diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts new file mode 100644 index 000000000000..db3b1c86ca06 --- /dev/null +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -0,0 +1,49 @@ +import { useDispatch } from 'react-redux'; +import { zeroAddress } from 'ethereumjs-util'; +import { useHistory } from 'react-router-dom'; +import { QuoteResponse } from '../types'; +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; +import { setDefaultHomeActiveTabName } from '../../../store/actions'; +import useAddToken from './useAddToken'; +import useHandleApprovalTx from './useHandleApprovalTx'; +import useHandleBridgeTx from './useHandleBridgeTx'; + +export default function useSubmitBridgeTransaction() { + const history = useHistory(); + const dispatch = useDispatch(); + const { addSourceToken, addDestToken } = useAddToken(); + const { handleApprovalTx } = useHandleApprovalTx(); + const { handleBridgeTx } = useHandleBridgeTx(); + + const submitBridgeTransaction = async (quoteResponse: QuoteResponse) => { + // Execute transaction(s) + let approvalTxId: string | undefined; + if (quoteResponse?.approval) { + approvalTxId = await handleApprovalTx({ + approval: quoteResponse.approval, + quoteResponse, + }); + } + + await handleBridgeTx({ + quoteResponse, + approvalTxId, + }); + + // Add tokens if not the native gas token + if (quoteResponse.quote.srcAsset.address !== zeroAddress()) { + addSourceToken(quoteResponse); + } + if (quoteResponse.quote.destAsset.address !== zeroAddress()) { + await addDestToken(quoteResponse); + } + + // Route user to activity tab on Home page + await dispatch(setDefaultHomeActiveTabName('activity')); + history.push(DEFAULT_ROUTE); + }; + + return { + submitBridgeTransaction, + }; +} diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index 62244e5793e5..2c9f082519e9 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -25,12 +25,15 @@ import { } from '../../components/multichain/pages/page'; import { getProviderConfig } from '../../ducks/metamask/metamask'; import { resetBridgeState, setFromChain } from '../../ducks/bridge/actions'; +import { useSwapsFeatureFlags } from '../swaps/hooks/useSwapsFeatureFlags'; import PrepareBridgePage from './prepare/prepare-bridge-page'; import { BridgeCTAButton } from './prepare/bridge-cta-button'; const CrossChainSwap = () => { const t = useContext(I18nContext); + // Load swaps feature flags so that we can use smart transactions + useSwapsFeatureFlags(); useBridging(); const history = useHistory(); diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index 28a1a2c1fbd6..06d784f2e0ea 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -1,19 +1,23 @@ import React, { useMemo } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Button } from '../../../components/component-library'; import { getBridgeQuotes, getFromAmount, getFromChain, getFromToken, + getRecommendedQuote, getToAmount, getToChain, getToToken, } from '../../../ducks/bridge/selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; +import useSubmitBridgeTransaction from '../hooks/useSubmitBridgeTransaction'; export const BridgeCTAButton = () => { + const dispatch = useDispatch(); const t = useI18nContext(); + const fromToken = useSelector(getFromToken); const toToken = useSelector(getToToken); @@ -24,6 +28,9 @@ export const BridgeCTAButton = () => { const toAmount = useSelector(getToAmount); const { isLoading } = useSelector(getBridgeQuotes); + const quoteResponse = useSelector(getRecommendedQuote); + + const { submitBridgeTransaction } = useSubmitBridgeTransaction(); const isTxSubmittable = fromToken && toToken && fromChain && toChain && fromAmount && toAmount; @@ -52,7 +59,7 @@ export const BridgeCTAButton = () => { data-testid="bridge-cta-button" onClick={() => { if (isTxSubmittable) { - // dispatch tx submission + dispatch(submitBridgeTransaction(quoteResponse)); } }} disabled={!isTxSubmittable} diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts index 5d001e7ef7fc..a1ee163eca48 100644 --- a/ui/pages/bridge/types.ts +++ b/ui/pages/bridge/types.ts @@ -6,6 +6,9 @@ export enum BridgeFlag { NETWORK_DEST_ALLOWLIST = 'dest-network-allowlist', } +type DecimalChainId = string; +export type GasMultiplierByChainId = Record; + export type FeatureFlagResponse = { [BridgeFlag.EXTENSION_CONFIG]: { refreshRate: number; @@ -89,7 +92,7 @@ export type QuoteResponse = { estimatedProcessingTimeInSeconds: number; }; -enum ChainId { +export enum ChainId { ETH = 1, OPTIMISM = 10, BSC = 56, diff --git a/ui/pages/swaps/hooks/useSwapsFeatureFlags.ts b/ui/pages/swaps/hooks/useSwapsFeatureFlags.ts new file mode 100644 index 000000000000..93460c892d7f --- /dev/null +++ b/ui/pages/swaps/hooks/useSwapsFeatureFlags.ts @@ -0,0 +1,14 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { fetchSwapsLivenessAndFeatureFlags } from '../../../ducks/swaps/swaps'; + +export function useSwapsFeatureFlags() { + const dispatch = useDispatch(); + + useEffect(() => { + const fetchSwapsLivenessAndFeatureFlagsWrapper = async () => { + await dispatch(fetchSwapsLivenessAndFeatureFlags()); + }; + fetchSwapsLivenessAndFeatureFlagsWrapper(); + }, [dispatch]); +} diff --git a/ui/pages/swaps/swaps.util.ts b/ui/pages/swaps/swaps.util.ts index 9cbf0b67a867..7065c7ae90dc 100644 --- a/ui/pages/swaps/swaps.util.ts +++ b/ui/pages/swaps/swaps.util.ts @@ -823,7 +823,7 @@ export const getSwap1559GasFeeEstimates = async ( }; }; -async function getTransaction1559GasFeeEstimates( +export async function getTransaction1559GasFeeEstimates( transactionParams: TransactionParams, estimatedBaseFee: Hex, chainId: Hex, diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 6344f823aa02..01c34dc2fe3d 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -2448,7 +2448,12 @@ export function createRetryTransaction( export function addNetwork( networkConfiguration: AddNetworkFields | UpdateNetworkFields, -): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { +): ThunkAction< + Promise, + MetaMaskReduxState, + unknown, + AnyAction +> { return async (dispatch: MetaMaskReduxDispatch) => { log.debug(`background.addNetwork`, networkConfiguration); try { From 02d52b49c06a6b1d8b65e4ee4450aa7db2357220 Mon Sep 17 00:00:00 2001 From: julesat22 <142838415+julesat22@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:38:43 -0800 Subject: [PATCH 18/28] feat: Hook in Portfolio Entry Points (#27607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** 1. What is the reason for the change? Portfolio has requested to add in some entry points into the extension, so users can easily navigate to the Portfolio to view/ manage their spending caps. 2. What is the improvement/solution? This adds value for the users who would like to view/ manage their spending caps as well as their portfolio. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27607?quickstart=1) ## **Manual testing steps** 1. Connect an account 2. Go to this the assets page in the extension 3. Click on an asset 4. Under "Token details", there should be a category for all native and non-native token types for "Spending caps" 5. Next to Spending caps, check the there is a link that routes the user to the portfolio with the "spendingCaps" tab and the user's account address passed as query params 6. Check that the link redirects to Portfolio ## **Screenshots/Recordings** ### **Before** Screenshot 2024-10-03 at 10 45 22 AM ### **After** Screenshot 2024-10-03 at 9 37 54 AM Screenshot 2024-10-03 at 9 33 21 AM Screenshot 2024-10-03 at 9 32 20 AM ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Julia Collins Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: Ziad Saab Co-authored-by: georgewrmarshall --- app/_locales/en/messages.json | 6 + ui/pages/asset/asset.scss | 20 +- .../__snapshots__/asset-page.test.tsx.snap | 224 ++++++++++++------ ui/pages/asset/components/asset-page.tsx | 77 ++++-- 4 files changed, 237 insertions(+), 90 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index bdba7b1214a3..624c7e2b163b 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1830,6 +1830,9 @@ "editGasTooLow": { "message": "Unknown processing time" }, + "editInPortfolio": { + "message": "Edit in Portfolio" + }, "editNetworkLink": { "message": "edit the original network" }, @@ -5382,6 +5385,9 @@ "spendingCapTooltipDesc": { "message": "This is the amount of tokens the spender will be able to access on your behalf." }, + "spendingCaps": { + "message": "Spending caps" + }, "srpInputNumberOfWords": { "message": "I have a $1-word phrase", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." diff --git a/ui/pages/asset/asset.scss b/ui/pages/asset/asset.scss index 8d682beae58b..d6f6a3d39d9b 100644 --- a/ui/pages/asset/asset.scss +++ b/ui/pages/asset/asset.scss @@ -1,4 +1,4 @@ -@use "design-system"; +@use 'design-system'; .asset { &__container { @@ -42,5 +42,19 @@ } } -.chart-up { stroke: var(--color-success-default); } -.chart-down { stroke: var(--color-error-default); } +.chart-up { + stroke: var(--color-success-default); +} + +.chart-down { + stroke: var(--color-error-default); +} + +.asset-page__spending-caps { + text-decoration: none; + + &:hover { + color: var(--color-primary-alternative); + text-decoration: underline; + } +} diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index b5ebc0a83eb6..8a5c60e340cb 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -254,6 +254,36 @@ exports[`AssetPage should render a native asset 1`] = `
+
+

+ Token details +

+
+
+

+ Spending caps +

+ + Edit in Portfolio + +
+
+
@@ -555,59 +585,84 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` Token details
-

- Contract address -

-
- + +
+ 0x30937...C4936 +
+
+ + +
+
+
+
+
+

+ Token decimal +

+

+ 18 +

+
-
-

- Token decimal + Spending caps

-

- 18 -

+ Edit in Portfolio +
@@ -1038,59 +1093,84 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` Token details
-

- Contract address -

-
- + +
+ 0xe4246...85f55 +
+
+ + +
+
+
+
+
+

+ Token decimal +

+

+ 18 +

+
- -

- Token decimal + Spending caps

-

- 18 -

+ Edit in Portfolio +
diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index 29078e3a248c..818ceb792ec3 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; @@ -6,8 +6,11 @@ import { EthMethod } from '@metamask/keyring-api'; import { isEqual } from 'lodash'; import { getCurrentCurrency, + getDataCollectionForMarketing, getIsBridgeChain, getIsSwapsChain, + getMetaMetricsId, + getParticipateInMetaMetrics, getSelectedInternalAccount, getSwapsDefaultToken, getTokensMarketData, @@ -24,6 +27,7 @@ import { Box, ButtonIcon, ButtonIconSize, + ButtonLink, IconName, Text, } from '../../../components/component-library'; @@ -42,6 +46,7 @@ import { getConversionRate } from '../../../ducks/metamask/metamask'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import CoinButtons from '../../../components/app/wallet-overview/coin-buttons'; import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; @@ -110,6 +115,10 @@ const AssetPage = ({ account.methods.includes(EthMethod.SignTransaction) || account.methods.includes(EthMethod.SignUserOperation); + const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); + const isMarketingEnabled = useSelector(getDataCollectionForMarketing); + const metaMetricsId = useSelector(getMetaMetricsId); + const { chainId, type, symbol, name, image, balance } = asset; const address = @@ -124,6 +133,20 @@ const AssetPage = ({ ? conversionRate * marketData.price : undefined; + const portfolioSpendingCapsUrl = useMemo( + () => + getPortfolioUrl( + '', + 'asset_page', + metaMetricsId, + isMetaMetricsEnabled, + isMarketingEnabled, + account.address, + 'spending-caps', + ), + [account.address, isMarketingEnabled, isMetaMetricsEnabled, metaMetricsId], + ); + return ( - {type === AssetType.token && ( + {[AssetType.token, AssetType.native].includes(type) && ( {t('tokenDetails')} - {renderRow( - t('contractAddress'), - , - )} - {asset.decimals !== undefined && - renderRow(t('tokenDecimal'), {asset.decimals})} - {asset.aggregators && asset.aggregators?.length > 0 && ( + {type === AssetType.token && ( - , + )} + - {t('tokenList')} - - {asset.aggregators?.join(', ')} + {asset.decimals !== undefined && + renderRow( + t('tokenDecimal'), + {asset.decimals}, + )} + {asset.aggregators && asset.aggregators.length > 0 && ( + + + {t('tokenList')} + + {asset.aggregators.join(', ')} + + )} + )} + {renderRow( + t('spendingCaps'), + + {t('editInPortfolio')} + , + )} )} From 9f8d61bd61687d8133c2bc0ae8d1809aa0d98a7a Mon Sep 17 00:00:00 2001 From: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:59:11 -0500 Subject: [PATCH 19/28] fix: replace unreliable setTimeout usage with waitFor (#28620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR aims to improve a flaky test for the `add-contact` component. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28620?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Run the test locally ## **Screenshots/Recordings** ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../contact-list-tab/add-contact/add-contact.test.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js b/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js index 09a0e0d96692..e162f84ba40f 100644 --- a/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js +++ b/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; @@ -55,7 +55,7 @@ describe('AddContact component', () => { expect(getByText('Ethereum public address')).toBeInTheDocument(); }); - it('should validate the address correctly', () => { + it('should validate the address correctly', async () => { const store = configureMockStore(middleware)(state); const { getByText, getByTestId } = renderWithProvider( , @@ -64,9 +64,10 @@ describe('AddContact component', () => { const input = getByTestId('ens-input'); fireEvent.change(input, { target: { value: 'invalid address' } }); - setTimeout(() => { - expect(getByText('Recipient address is invalid')).toBeInTheDocument(); - }, 600); + + await waitFor(() => + expect(getByText('Recipient address is invalid')).toBeInTheDocument(), + ); }); it('should get disabled submit button when username field is empty', () => { From a04b34bc3e6c59892b1a817d4beca0098db306e8 Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:54:05 -0800 Subject: [PATCH 20/28] feat: `PortfolioView` (#28593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This consolidates the changes from a series of 3 Multichain Asset List PRs that built on each other: 1. Product code (feature branch): https://github.com/MetaMask/metamask-extension/pull/28386 2. Unit tests: https://github.com/MetaMask/metamask-extension/pull/28451 3. e2e tests: https://github.com/MetaMask/metamask-extension/pull/28524 We created separate branches for rapid iteration and isolated testing. The code is now cleaner and stable enough for review and merge into develop, gated by the `PORTFOLIO_VIEW` feature flag. We will introduce another PR to remove this feature flag when we are ready to ship it. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28593?quickstart=1) ## **Related issues** Fixes: https://github.com/orgs/MetaMask/projects/85/views/35?pane=issue&itemId=82217837 ## **Manual testing steps** `PORTFOLIO_VIEW=1 yarn webpack --watch` 1. View tokens across all networks in one unified list. 2. Filter tokens by selected network 3. Crosschain navigation: - Token detail pages update to display data from the appropriate network. - Send/Swap actions automatically adjust the selected network for user convenience. - Ensure that network switch is functional, and sends/swaps happen on correct chain. Known caveats: 1. POL native token market data not populating. Will be addressed here: https://github.com/MetaMask/metamask-extension/pull/28584 and https://github.com/MetaMask/core/pull/4952 2. Native token swapping on different network than selected network swaps incorrect token: https://github.com/MetaMask/metamask-extension/pull/28587 3. Multichain token detection experimental draft: https://github.com/MetaMask/metamask-extension/pull/28380 ## **Screenshots/Recordings** https://github.com/user-attachments/assets/79e7fd2d-9908-4c7a-8134-089cbe6593cc https://github.com/user-attachments/assets/dfb4a54f-a8ae-48a4-a9e7-50327f56054a ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Jonathan Bursztyn Co-authored-by: chloeYue <105063779+chloeYue@users.noreply.github.com> Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> Co-authored-by: Monte Lai Co-authored-by: Charly Chevalier Co-authored-by: Pedro Figueiredo Co-authored-by: MetaMask Bot Co-authored-by: NidhiKJha Co-authored-by: sahar-fehri --- app/_locales/en/messages.json | 8 +- shared/constants/network.ts | 3 + test/data/mock-state.json | 5 +- ...rs-after-init-opt-in-background-state.json | 1 - ...s-before-init-opt-in-background-state.json | 1 + .../tests/privacy-mode/privacy-mode.spec.js | 10 +- test/e2e/tests/tokens/token-sort.spec.ts | 14 +- test/jest/mock-store.js | 1 + .../asset-list-control-bar.tsx | 9 +- .../asset-list/asset-list.ramps-card.test.js | 10 + .../app/assets/asset-list/asset-list.tsx | 4 +- .../asset-list/native-token/native-token.tsx | 12 +- .../network-filter/network-filter.tsx | 67 +++-- .../app/assets/nfts/nfts-items/nfts-items.js | 4 +- .../assets/nfts/nfts-items/nfts-items.test.js | 4 +- .../__snapshots__/token-cell.test.tsx.snap | 8 +- .../app/assets/token-cell/token-cell.test.tsx | 13 +- .../app/assets/token-cell/token-cell.tsx | 115 ++++++-- .../app/assets/token-list/token-list.tsx | 252 ++++++++++++----- .../app/assets/util/calculateTokenBalance.ts | 51 ++++ .../assets/util/calculateTokenFiatAmount.ts | 36 +++ .../app/toast-master/toast-master.js | 43 ++- ...-preferenced-currency-display.component.js | 2 +- .../app/wallet-overview/btc-overview.test.tsx | 23 ++ .../app/wallet-overview/coin-buttons.tsx | 44 ++- .../app/wallet-overview/eth-overview.test.js | 37 ++- .../account-overview-tabs.tsx | 4 +- .../asset-picker-amount.tsx | 8 +- .../asset-picker-modal/AssetList.tsx | 1 - .../asset-picker-modal/asset-picker-modal.tsx | 6 + .../asset-picker/asset-picker.tsx | 3 + .../network-list-menu.test.js | 6 + .../network-list-menu/network-list-menu.tsx | 17 ++ .../send/components/recipient-content.tsx | 1 + ui/components/multichain/pages/send/send.js | 1 + .../token-list-item.test.tsx.snap | 4 +- .../token-list-item/token-list-item.test.tsx | 33 ++- .../token-list-item/token-list-item.tsx | 47 ++-- ui/hooks/useAccountTotalFiatBalance.test.js | 42 ++- ...MultichainAccountTotalFiatBalance.test.tsx | 41 ++- ui/pages/asset/asset.tsx | 46 ++-- .../__snapshots__/asset-page.test.tsx.snap | 65 +++-- ui/pages/asset/components/asset-page.test.tsx | 136 ++++++--- ui/pages/asset/components/asset-page.tsx | 260 ++++++++++-------- ui/pages/asset/components/native-asset.tsx | 44 +-- ui/pages/asset/components/token-asset.tsx | 44 ++- ui/pages/asset/components/token-buttons.tsx | 49 +++- ui/pages/asset/util.test.ts | 8 +- ui/pages/asset/util.ts | 3 +- ui/pages/routes/routes.component.js | 11 +- ui/selectors/selectors.js | 145 +++++++++- ui/store/actions.ts | 2 +- 52 files changed, 1306 insertions(+), 498 deletions(-) create mode 100644 ui/components/app/assets/util/calculateTokenBalance.ts create mode 100644 ui/components/app/assets/util/calculateTokenFiatAmount.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 624c7e2b163b..f51c9708fc20 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -501,7 +501,7 @@ "message": "No accounts available to connect" }, "allNetworks": { - "message": "All Networks", + "message": "All networks", "description": "Speicifies to token network filter to filter by all Networks" }, "allOfYour": { @@ -1383,7 +1383,7 @@ "message": "Current language" }, "currentNetwork": { - "message": "Current Network", + "message": "Current network", "description": "Speicifies to token network filter to filter by current Network. Will render when network nickname is not available" }, "currentRpcUrlDeprecated": { @@ -6023,6 +6023,10 @@ "message": "$1 is now active on $2", "description": "$1 represents the account name, $2 represents the network name" }, + "switchedNetworkToastMessageNoOrigin": { + "message": "You're now using $1", + "description": "$1 represents the network name" + }, "switchedTo": { "message": "You're now using" }, diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 4844e7c2e981..aef122ea67bc 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -816,6 +816,9 @@ export const CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP = { export const CHAIN_ID_TOKEN_IMAGE_MAP = { [CHAIN_IDS.MAINNET]: ETH_TOKEN_IMAGE_URL, [CHAIN_IDS.TEST_ETH]: TEST_ETH_TOKEN_IMAGE_URL, + [CHAIN_IDS.ARBITRUM]: ETH_TOKEN_IMAGE_URL, + [CHAIN_IDS.BASE]: ETH_TOKEN_IMAGE_URL, + [CHAIN_IDS.LINEA_MAINNET]: ETH_TOKEN_IMAGE_URL, [CHAIN_IDS.BSC]: BNB_TOKEN_IMAGE_URL, [CHAIN_IDS.POLYGON]: POL_TOKEN_IMAGE_URL, [CHAIN_IDS.AVALANCHE]: AVAX_TOKEN_IMAGE_URL, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 2932e5fc56d9..d2b66cee3108 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -55,6 +55,8 @@ } }, "metamask": { + "allTokens": {}, + "tokenBalances": {}, "use4ByteResolution": true, "ipfsGateway": "dweb.link", "dismissSeedBackUpReminder": false, @@ -384,7 +386,8 @@ "key": "tokenFiatAmount", "order": "dsc", "sortCallback": "stringNumeric" - } + }, + "tokenNetworkFilter": {} }, "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index cea439180b5b..6252c91e73a2 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -342,7 +342,6 @@ }, "TxController": { "methodData": "object", - "submitHistory": "object", "transactions": "object", "lastFetchedBlockNumbers": "object", "submitHistory": "object" diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 5f5f47f3e7ee..5d6be06b692e 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -118,6 +118,7 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "showConfirmationAdvancedDetails": false, "tokenSortConfig": "object", + "tokenNetworkFilter": {}, "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean", "tokenNetworkFilter": {} diff --git a/test/e2e/tests/privacy-mode/privacy-mode.spec.js b/test/e2e/tests/privacy-mode/privacy-mode.spec.js index a4d2c2245752..ede37f900e66 100644 --- a/test/e2e/tests/privacy-mode/privacy-mode.spec.js +++ b/test/e2e/tests/privacy-mode/privacy-mode.spec.js @@ -18,7 +18,7 @@ describe('Privacy Mode', function () { async ({ driver }) => { async function checkForHeaderValue(value) { const balanceElement = await driver.findElement( - '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + '[data-testid="account-value-and-suffix"]', ); const surveyText = await balanceElement.getText(); assert.equal( @@ -30,7 +30,7 @@ describe('Privacy Mode', function () { async function checkForTokenValue(value) { const balanceElement = await driver.findElement( - '[data-testid="multichain-token-list-item-secondary-value"]', + '[data-testid="multichain-token-list-item-value"]', ); const surveyText = await balanceElement.getText(); assert.equal(surveyText, value, `Token balance should be "${value}"`); @@ -38,7 +38,7 @@ describe('Privacy Mode', function () { async function checkForPrivacy() { await checkForHeaderValue('••••••'); - await checkForTokenValue('•••••••••'); + await checkForTokenValue('••••••'); } async function checkForNoPrivacy() { @@ -48,7 +48,7 @@ describe('Privacy Mode', function () { async function togglePrivacy() { const balanceElement = await driver.findElement( - '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + '[data-testid="account-value-and-suffix"]', ); const initialText = await balanceElement.getText(); @@ -81,7 +81,7 @@ describe('Privacy Mode', function () { async function togglePrivacy() { const balanceElement = await driver.findElement( - '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + '[data-testid="account-value-and-suffix"]', ); const initialText = await balanceElement.getText(); diff --git a/test/e2e/tests/tokens/token-sort.spec.ts b/test/e2e/tests/tokens/token-sort.spec.ts index ed6005a710ad..ff2d35a917dc 100644 --- a/test/e2e/tests/tokens/token-sort.spec.ts +++ b/test/e2e/tests/tokens/token-sort.spec.ts @@ -66,10 +66,8 @@ describe('Token List', function () { assert.ok(tokenSymbolsBeforeSorting[0].includes('Ethereum')); - await await driver.clickElement( - '[data-testid="sort-by-popover-toggle"]', - ); - await await driver.clickElement('[data-testid="sortByAlphabetically"]'); + await driver.clickElement('[data-testid="sort-by-popover-toggle"]'); + await driver.clickElement('[data-testid="sortByAlphabetically"]'); await driver.delay(regularDelayMs); const tokenListAfterSortingAlphabetically = await driver.findElements( @@ -85,12 +83,8 @@ describe('Token List', function () { tokenListSymbolsAfterSortingAlphabetically[0].includes('ABC'), ); - await await driver.clickElement( - '[data-testid="sort-by-popover-toggle"]', - ); - await await driver.clickElement( - '[data-testid="sortByDecliningBalance"]', - ); + await driver.clickElement('[data-testid="sort-by-popover-toggle"]'); + await driver.clickElement('[data-testid="sortByDecliningBalance"]'); await driver.delay(regularDelayMs); const tokenListBeforeSortingByDecliningBalance = diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 3fe524634075..4720bf427372 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -139,6 +139,7 @@ export const createSwapsMockStore = () => { preferences: { showFiatInTestnets: true, smartTransactionsOptInStatus: true, + tokenNetworkFilter: {}, showMultiRpcModal: false, }, transactions: [ diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx index 2925277c14bd..e5c43aad281d 100644 --- a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx @@ -68,12 +68,13 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { }, [currentNetwork.chainId, TEST_CHAINS]); const allOpts: Record = {}; - Object.keys(allNetworks).forEach((chainId) => { + Object.keys(allNetworks || {}).forEach((chainId) => { allOpts[chainId] = true; }); const allNetworksFilterShown = - Object.keys(tokenNetworkFilter).length !== Object.keys(allOpts).length; + Object.keys(tokenNetworkFilter || {}).length !== + Object.keys(allOpts || {}).length; useEffect(() => { if (isTestNetwork) { @@ -86,7 +87,7 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { // We need to set the default filter for all users to be all included networks, rather than defaulting to empty object // This effect is to unblock and derisk in the short-term useEffect(() => { - if (Object.keys(tokenNetworkFilter).length === 0) { + if (Object.keys(tokenNetworkFilter || {}).length === 0) { dispatch(setTokenNetworkFilter(allOpts)); } }, []); @@ -162,7 +163,7 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { > {process.env.PORTFOLIO_VIEW && ( { )} } + // nativeToken is still needed to avoid breaking flask build's support for bitcoin + // TODO: refactor this to no longer be needed for non-evm chains + nativeToken={!isEvm && } onTokenClick={(chainId: string, tokenAddress: string) => { onClickAsset(chainId, tokenAddress); trackEvent({ diff --git a/ui/components/app/assets/asset-list/native-token/native-token.tsx b/ui/components/app/assets/asset-list/native-token/native-token.tsx index b1e86479bb79..29bc1ea8c1bd 100644 --- a/ui/components/app/assets/asset-list/native-token/native-token.tsx +++ b/ui/components/app/assets/asset-list/native-token/native-token.tsx @@ -10,23 +10,14 @@ import { } from '../../../../../selectors/multichain'; import { getPreferences } from '../../../../../selectors'; import { TokenListItem } from '../../../../multichain'; -import { useIsOriginalNativeTokenSymbol } from '../../../../../hooks/useIsOriginalNativeTokenSymbol'; import { AssetListProps } from '../asset-list'; import { useNativeTokenBalance } from './use-native-token-balance'; const NativeToken = ({ onClickAsset }: AssetListProps) => { const nativeCurrency = useSelector(getMultichainNativeCurrency); const isMainnet = useSelector(getMultichainIsMainnet); - const { chainId, ticker, type, rpcUrl } = useSelector( - getMultichainCurrentNetwork, - ); + const { chainId } = useSelector(getMultichainCurrentNetwork); const { privacyMode } = useSelector(getPreferences); - const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( - chainId, - ticker, - type, - rpcUrl, - ); const balance = useSelector(getMultichainSelectedAccountCachedBalance); const balanceIsLoading = !balance; @@ -50,7 +41,6 @@ const NativeToken = ({ onClickAsset }: AssetListProps) => { tokenSymbol={symbol} secondary={secondary} tokenImage={balanceIsLoading ? null : primaryTokenImage} - isOriginalTokenSymbol={isOriginalNativeSymbol} isNativeCurrency isStakeable={isStakeable} showPercentage diff --git a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx index 8b9fc06b33e7..29e4c97e2c82 100644 --- a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx +++ b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx @@ -1,28 +1,35 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { setTokenNetworkFilter } from '../../../../../store/actions'; import { getCurrentChainId, getCurrentNetwork, getPreferences, - getSelectedInternalAccount, - getShouldHideZeroBalanceTokens, getNetworkConfigurationsByChainId, getChainIdsToPoll, + getShouldHideZeroBalanceTokens, + getSelectedAccount, } from '../../../../../selectors'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { SelectableListItem } from '../sort-control/sort-control'; import { Text } from '../../../../component-library/text/text'; import { + AlignItems, Display, JustifyContent, TextColor, TextVariant, } from '../../../../../helpers/constants/design-system'; import { Box } from '../../../../component-library/box/box'; -import { AvatarNetwork } from '../../../../component-library'; +import { + AvatarNetwork, + AvatarNetworkSize, +} from '../../../../component-library'; import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'; -import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../shared/constants/network'; +import { + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, + TEST_CHAINS, +} from '../../../../../../shared/constants/network'; import { useGetFormattedTokensPerChain } from '../../../../../hooks/useGetFormattedTokensPerChain'; import { useAccountTotalCrossChainFiatBalance } from '../../../../../hooks/useAccountTotalCrossChainFiatBalance'; @@ -34,9 +41,10 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => { const t = useI18nContext(); const dispatch = useDispatch(); const chainId = useSelector(getCurrentChainId); - const selectedAccount = useSelector(getSelectedInternalAccount); const currentNetwork = useSelector(getCurrentNetwork); + const selectedAccount = useSelector(getSelectedAccount); const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const [chainsToShow, setChainsToShow] = useState([]); const { tokenNetworkFilter } = useSelector(getPreferences); const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, @@ -67,9 +75,6 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => { formattedTokensForAllNetworks, ); - // TODO: fetch balances across networks - // const multiNetworkAccountBalance = useMultichainAccountBalance() - const handleFilter = (chainFilters: Record) => { dispatch(setTokenNetworkFilter(chainFilters)); @@ -77,11 +82,28 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => { handleClose(); }; + useEffect(() => { + const testnetChains: string[] = TEST_CHAINS; + const mainnetChainIds = Object.keys(allNetworks || {}).filter( + (chain) => !testnetChains.includes(chain), + ); + setChainsToShow(mainnetChainIds); + }, []); + + const allOpts: Record = {}; + Object.keys(allNetworks || {}).forEach((chain) => { + allOpts[chain] = true; + }); + return ( <> handleFilter({})} + isSelected={ + Object.keys(tokenNetworkFilter || {}).length === + Object.keys(allNetworks || {}).length + } + onClick={() => handleFilter(allOpts)} + testId="network-filter-all" > { {t('allNetworks')} {/* TODO: Should query cross chain account balance */} @@ -110,19 +133,20 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => { /> - - {Object.values(allNetworks) + + {chainsToShow .slice(0, 5) // only show a max of 5 icons overlapping - .map((network, index) => { + .map((chain, index) => { const networkImageUrl = CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ - network.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + chain as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP ]; return ( { handleFilter({ [chainId]: true })} + testId="network-filter-current" > { } const firstNft = nfts[0]; - const nftRoute = `/asset/${firstNft.address}/${firstNft.tokenId}`; + const nftRoute = `/asset/${toHex(5)}/${firstNft.address}/${ + firstNft.tokenId + }`; expect(mockHistoryPush).toHaveBeenCalledWith(nftRoute); }); diff --git a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap index dfed6aeffa98..30efc922ec7e 100644 --- a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap +++ b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap @@ -29,7 +29,11 @@ exports[`Token Cell should match snapshot 1`] = `
- ? + network logo
@@ -64,7 +68,7 @@ exports[`Token Cell should match snapshot 1`] = ` class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--text-align-end mm-box--width-2/3 mm-box--color-text-default" data-testid="multichain-token-list-item-secondary-value" > - 5.00 + $5.00

{ if (selector === getIntlLocale) { return 'en-US'; } + if (selector === getCurrentCurrency) { + return 'usd'; + } + if (selector === getCurrencyRates) { + return { POL: '' }; + } return undefined; }); (useTokenFiatAmount as jest.Mock).mockReturnValue('5.00'); diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx index 81c237b441d9..56d6555258bd 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -1,62 +1,139 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { getTokenList } from '../../../../selectors'; -import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; +import { BigNumber } from 'bignumber.js'; +import { + getCurrentCurrency, + getTokenList, + selectERC20TokensByChain, + getNativeCurrencyForChain, +} from '../../../../selectors'; +import { + isChainIdMainnet, + getImageForChainId, + getMultichainIsEvm, +} from '../../../../selectors/multichain'; import { TokenListItem } from '../../../multichain'; import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; -import { useIsOriginalTokenSymbol } from '../../../../hooks/useIsOriginalTokenSymbol'; import { getIntlLocale } from '../../../../ducks/locale/locale'; +import { formatAmount } from '../../../../pages/confirmations/components/simulation-details/formatAmount'; type TokenCellProps = { address: string; symbol: string; string?: string; chainId: string; + tokenFiatAmount: number | null; image: string; + isNative?: boolean; privacyMode?: boolean; onClick?: (chainId: string, address: string) => void; }; +export const formatWithThreshold = ( + amount: number | null, + threshold: number, + locale: string, + options: Intl.NumberFormatOptions, +): string => { + if (amount === null) { + return ''; + } + if (amount === 0) { + return new Intl.NumberFormat(locale, options).format(0); + } + return amount < threshold + ? `<${new Intl.NumberFormat(locale, options).format(threshold)}` + : new Intl.NumberFormat(locale, options).format(amount); +}; + export default function TokenCell({ address, image, symbol, chainId, string, + tokenFiatAmount, + isNative, privacyMode = false, onClick, }: TokenCellProps) { + const locale = useSelector(getIntlLocale); + const currentCurrency = useSelector(getCurrentCurrency); const tokenList = useSelector(getTokenList); + const isEvm = useSelector(getMultichainIsEvm); + const erc20TokensByChain = useSelector(selectERC20TokensByChain); + const isMainnet = chainId ? isChainIdMainnet(chainId) : false; const tokenData = Object.values(tokenList).find( (token) => isEqualCaseInsensitive(token.symbol, symbol) && isEqualCaseInsensitive(token.address, address), ); - const title = tokenData?.name || symbol; - const tokenImage = tokenData?.iconUrl || image; - const formattedFiat = useTokenFiatAmount(address, string, symbol, {}, false); - const locale = useSelector(getIntlLocale); - const primary = new Intl.NumberFormat(locale, { - minimumSignificantDigits: 1, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - }).format(string.toString()); - const isOriginalTokenSymbol = useIsOriginalTokenSymbol(address, symbol); + const title = + tokenData?.name || + (chainId === '0x1' && symbol === 'ETH' + ? 'Ethereum' + : chainId && + erc20TokensByChain?.[chainId]?.data?.[address.toLowerCase()]?.name) || + symbol; + + const tokenImage = + tokenData?.iconUrl || + (chainId && + erc20TokensByChain?.[chainId]?.data?.[address.toLowerCase()]?.iconUrl) || + image; + + const secondaryThreshold = 0.01; + + // Format for fiat balance with currency style + const secondary = formatWithThreshold( + tokenFiatAmount, + secondaryThreshold, + locale, + { + style: 'currency', + currency: currentCurrency.toUpperCase(), + }, + ); + + const primary = formatAmount( + locale, + new BigNumber(Number(string) || '0', 10), + ); + + let isStakeable = isMainnet && isEvm && isNative; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + isStakeable = false; + ///: END:ONLY_INCLUDE_IF + + function handleOnClick() { + if (!onClick || !chainId) { + return; + } + onClick(chainId, address); + } + + if (!chainId) { + return null; + } + + const tokenChainImage = getImageForChainId(chainId); return ( onClick(chainId, address) : undefined} + onClick={handleOnClick} tokenSymbol={symbol} - tokenImage={tokenImage} - primary={`${primary || 0}`} - secondary={isOriginalTokenSymbol ? formattedFiat : null} + tokenImage={isNative ? getNativeCurrencyForChain(chainId) : tokenImage} + tokenChainImage={tokenChainImage || undefined} + primary={primary} + secondary={secondary} title={title} - isOriginalTokenSymbol={isOriginalTokenSymbol} address={address} + isStakeable={isStakeable} showPercentage privacyMode={privacyMode} + isNativeCurrency={isNative} + chainId={chainId} /> ); } diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 638c66610a80..08b5675dfe94 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -1,30 +1,77 @@ import React, { ReactNode, useEffect, useMemo } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; +import { shallowEqual, useSelector, useDispatch } from 'react-redux'; +import { Hex } from '@metamask/utils'; import TokenCell from '../token-cell'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { Box } from '../../../component-library'; -import { - AlignItems, - Display, - JustifyContent, -} from '../../../../helpers/constants/design-system'; -import { TokenWithBalance } from '../asset-list/asset-list'; +import { TEST_CHAINS } from '../../../../../shared/constants/network'; import { sortAssets } from '../util/sort'; import { - getCurrentChainId, + getCurrencyRates, + getCurrentNetwork, + getMarketData, + getNetworkConfigurationIdByChainId, + getNewTokensImported, getPreferences, getSelectedAccount, - getShouldHideZeroBalanceTokens, + getSelectedAccountNativeTokenCachedBalanceByChainId, + getSelectedAccountTokensAcrossChains, getTokenExchangeRates, } from '../../../../selectors'; -import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; -import { useNativeTokenBalance } from '../asset-list/native-token/use-native-token-balance'; +import { filterAssets } from '../util/filter'; +import { calculateTokenBalance } from '../util/calculateTokenBalance'; +import { calculateTokenFiatAmount } from '../util/calculateTokenFiatAmount'; import { endTrace, TraceName } from '../../../../../shared/lib/trace'; +import { useTokenBalances } from '../../../../hooks/useTokenBalances'; +import { setTokenNetworkFilter } from '../../../../store/actions'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; type TokenListProps = { onTokenClick: (chainId: string, address: string) => void; - nativeToken: ReactNode; + nativeToken?: ReactNode; +}; + +export type Token = { + address: Hex; + aggregators: string[]; + chainId: Hex; + decimals: number; + isNative: boolean; + symbol: string; + image: string; +}; + +export type TokenWithFiatAmount = Token & { + tokenFiatAmount: number | null; + balance?: string; + string: string; // needed for backwards compatability TODO: fix this +}; + +export type AddressBalanceMapping = Record>; +export type ChainAddressMarketData = Record< + Hex, + Record> +>; + +const useFilteredAccountTokens = (currentNetwork: { chainId: string }) => { + const isTestNetwork = useMemo(() => { + return (TEST_CHAINS as string[]).includes(currentNetwork.chainId); + }, [currentNetwork.chainId, TEST_CHAINS]); + + const selectedAccountTokensChains: Record = useSelector( + getSelectedAccountTokensAcrossChains, + ) as Record; + + const filteredAccountTokensChains = useMemo(() => { + return Object.fromEntries( + Object.entries(selectedAccountTokensChains).filter(([chainId]) => + isTestNetwork + ? (TEST_CHAINS as string[]).includes(chainId) + : !(TEST_CHAINS as string[]).includes(chainId), + ), + ); + }, [selectedAccountTokensChains, isTestNetwork, TEST_CHAINS]); + + return filteredAccountTokensChains; }; export default function TokenList({ @@ -32,78 +79,155 @@ export default function TokenList({ nativeToken, }: TokenListProps) { const t = useI18nContext(); - const currentChainId = useSelector(getCurrentChainId); + const dispatch = useDispatch(); + const currentNetwork = useSelector(getCurrentNetwork); + const allNetworks = useSelector(getNetworkConfigurationIdByChainId); const { tokenSortConfig, tokenNetworkFilter, privacyMode } = useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); - const nativeTokenWithBalance = useNativeTokenBalance(); - const shouldHideZeroBalanceTokens = useSelector( - getShouldHideZeroBalanceTokens, - ); const contractExchangeRates = useSelector( getTokenExchangeRates, shallowEqual, ); - const { tokensWithBalances, loading } = useAccountTotalFiatBalance( - selectedAccount, - shouldHideZeroBalanceTokens, - ) as { - tokensWithBalances: TokenWithBalance[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mergedRates: any; - loading: boolean; + const newTokensImported = useSelector(getNewTokensImported); + const selectedAccountTokensChains = useFilteredAccountTokens(currentNetwork); + + const { tokenBalances } = useTokenBalances(); + const selectedAccountTokenBalancesAcrossChains = + tokenBalances[selectedAccount.address]; + + const marketData: ChainAddressMarketData = useSelector( + getMarketData, + ) as ChainAddressMarketData; + + const currencyRates = useSelector(getCurrencyRates); + const nativeBalances: Record = useSelector( + getSelectedAccountNativeTokenCachedBalanceByChainId, + ) as Record; + + // Ensure newly added networks are included in the tokenNetworkFilter + useEffect(() => { + const allNetworkFilters = Object.fromEntries( + Object.keys(allNetworks).map((chainId) => [chainId, true]), + ); + + if (Object.keys(tokenNetworkFilter).length > 1) { + dispatch(setTokenNetworkFilter(allNetworkFilters)); + } + }, [Object.keys(allNetworks).length]); + + const consolidatedBalances = () => { + const tokensWithBalance: TokenWithFiatAmount[] = []; + + Object.entries(selectedAccountTokensChains).forEach( + ([stringChainKey, tokens]) => { + const chainId = stringChainKey as Hex; + tokens.forEach((token: Token) => { + const { isNative, address, decimals } = token; + const balance = + calculateTokenBalance({ + isNative, + chainId, + address, + decimals, + nativeBalances, + selectedAccountTokenBalancesAcrossChains, + }) || '0'; + + const tokenFiatAmount = calculateTokenFiatAmount({ + token, + chainId, + balance, + marketData, + currencyRates, + }); + + // Append processed token with balance and fiat amount + tokensWithBalance.push({ + ...token, + balance, + tokenFiatAmount, + chainId, + string: String(balance), + }); + }); + }, + ); + + return tokensWithBalance; }; - const sortedTokens = useMemo(() => { - // TODO filter assets by networkTokenFilter before sorting - return sortAssets( - [nativeTokenWithBalance, ...tokensWithBalances], - tokenSortConfig, + const sortedFilteredTokens = useMemo(() => { + const consolidatedTokensWithBalances = consolidatedBalances(); + const filteredAssets = filterAssets(consolidatedTokensWithBalances, [ + { + key: 'chainId', + opts: tokenNetworkFilter, + filterCallback: 'inclusive', + }, + ]); + + const { nativeTokens, nonNativeTokens } = filteredAssets.reduce<{ + nativeTokens: TokenWithFiatAmount[]; + nonNativeTokens: TokenWithFiatAmount[]; + }>( + (acc, token) => { + if (token.isNative) { + acc.nativeTokens.push(token); + } else { + acc.nonNativeTokens.push(token); + } + return acc; + }, + { nativeTokens: [], nonNativeTokens: [] }, ); + const assets = [...nativeTokens, ...nonNativeTokens]; + return sortAssets(assets, tokenSortConfig); }, [ - tokensWithBalances, tokenSortConfig, tokenNetworkFilter, conversionRate, contractExchangeRates, + currentNetwork, + selectedAccount, + selectedAccountTokensChains, + newTokensImported, ]); useEffect(() => { - if (!loading) { + if (sortedFilteredTokens) { endTrace({ name: TraceName.AccountOverviewAssetListTab }); } - }, [loading]); - - return loading ? ( - - {t('loadingTokens')} - - ) : ( + }, [sortedFilteredTokens]); + + // Displays nativeToken if provided + if (nativeToken) { + return React.cloneElement(nativeToken as React.ReactElement); + } + + // TODO: We can remove this string. However it will result in a huge file 50+ file diff + // Lets remove it in a separate PR + if (sortedFilteredTokens === undefined) { + console.log(t('loadingTokens')); + } + + return (

- {sortedTokens.map((tokenData) => { - if (tokenData?.isNative) { - // we need cloneElement so that we can pass the unique key - return React.cloneElement(nativeToken as React.ReactElement, { - key: `${tokenData.symbol}-${tokenData.address}`, - }); - } - return ( - - ); - })} + {sortedFilteredTokens.map((tokenData) => ( + + ))}
); } diff --git a/ui/components/app/assets/util/calculateTokenBalance.ts b/ui/components/app/assets/util/calculateTokenBalance.ts new file mode 100644 index 000000000000..3eb1fc7373e7 --- /dev/null +++ b/ui/components/app/assets/util/calculateTokenBalance.ts @@ -0,0 +1,51 @@ +import BN from 'bn.js'; +import { Hex } from '@metamask/utils'; +import { stringifyBalance } from '../../../../hooks/useTokenBalances'; +import { hexToDecimal } from '../../../../../shared/modules/conversion.utils'; +import { AddressBalanceMapping } from '../token-list/token-list'; + +type CalculateTokenBalanceParams = { + isNative: boolean; + chainId: Hex; + address: Hex; + decimals: number; + nativeBalances: Record; + selectedAccountTokenBalancesAcrossChains: AddressBalanceMapping; +}; + +export function calculateTokenBalance({ + isNative, + chainId, + address, + decimals, + nativeBalances, + selectedAccountTokenBalancesAcrossChains, +}: CalculateTokenBalanceParams): string | undefined { + let balance; + + if (isNative) { + const nativeTokenBalanceHex = nativeBalances?.[chainId]; + if (nativeTokenBalanceHex && nativeTokenBalanceHex !== '0x0') { + balance = stringifyBalance( + new BN(hexToDecimal(nativeTokenBalanceHex)), + new BN(decimals), + 5, // precision for native token balance + ); + } else { + balance = '0'; + } + } else { + const hexBalance = + selectedAccountTokenBalancesAcrossChains?.[chainId]?.[address]; + if (hexBalance && hexBalance !== '0x0') { + balance = stringifyBalance( + new BN(hexToDecimal(hexBalance)), + new BN(decimals), + ); + } else { + balance = '0'; + } + } + + return balance; +} diff --git a/ui/components/app/assets/util/calculateTokenFiatAmount.ts b/ui/components/app/assets/util/calculateTokenFiatAmount.ts new file mode 100644 index 000000000000..279fae37f582 --- /dev/null +++ b/ui/components/app/assets/util/calculateTokenFiatAmount.ts @@ -0,0 +1,36 @@ +import { Hex } from '@metamask/utils'; +import { ChainAddressMarketData, Token } from '../token-list/token-list'; + +type SymbolCurrencyRateMapping = Record>; + +type CalculateTokenFiatAmountParams = { + token: Token; + chainId: Hex; + balance: string | undefined; + marketData: ChainAddressMarketData; + currencyRates: SymbolCurrencyRateMapping; +}; + +export function calculateTokenFiatAmount({ + token, + chainId, + balance, + marketData, + currencyRates, +}: CalculateTokenFiatAmountParams): number | null { + const { address, isNative, symbol } = token; + + // Market and conversion rate data + const baseCurrency = marketData[chainId]?.[address]?.currency; + const tokenMarketPrice = Number(marketData[chainId]?.[address]?.price) || 0; + const tokenExchangeRate = currencyRates[baseCurrency]?.conversionRate || 0; + const parsedBalance = parseFloat(String(balance)); + + if (isNative && currencyRates) { + return (currencyRates[symbol]?.conversionRate || 0) * parsedBalance; + } + if (!tokenMarketPrice) { + return null; // when no market price is available, we don't want to render the fiat amount + } + return tokenMarketPrice * tokenExchangeRate * parsedBalance; +} diff --git a/ui/components/app/toast-master/toast-master.js b/ui/components/app/toast-master/toast-master.js index 584f1cc25983..18182f37b477 100644 --- a/ui/components/app/toast-master/toast-master.js +++ b/ui/components/app/toast-master/toast-master.js @@ -17,6 +17,9 @@ import { import { DEFAULT_ROUTE, REVIEW_PERMISSIONS, + SEND_ROUTE, + SWAPS_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import { getURLHost } from '../../../helpers/utils/util'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -61,9 +64,13 @@ export function ToastMaster() { const location = useLocation(); const onHomeScreen = location.pathname === DEFAULT_ROUTE; + const onSendScreen = location.pathname === SEND_ROUTE; + const onSwapsScreen = + location.pathname === SWAPS_ROUTE || + location.pathname === PREPARE_SWAP_ROUTE; - return ( - onHomeScreen && ( + if (onHomeScreen) { + return ( @@ -73,8 +80,18 @@ export function ToastMaster() { - ) - ); + ); + } + + if (onSendScreen || onSwapsScreen) { + return ( + + + + ); + } + + return null; } function ConnectAccountToast() { @@ -204,6 +221,19 @@ function SwitchedNetworkToast() { ); const isShown = switchedNetworkDetails && !switchedNetworkNeverShowMessage; + const hasOrigin = Boolean(switchedNetworkDetails?.origin); + + function getMessage() { + if (hasOrigin) { + return t('switchedNetworkToastMessage', [ + switchedNetworkDetails.nickname, + getURLHost(switchedNetworkDetails.origin), + ]); + } + return t('switchedNetworkToastMessageNoOrigin', [ + switchedNetworkDetails.nickname, + ]); + } return ( isShown && ( @@ -217,10 +247,7 @@ function SwitchedNetworkToast() { name={switchedNetworkDetails?.nickname} /> } - text={t('switchedNetworkToastMessage', [ - switchedNetworkDetails.nickname, - getURLHost(switchedNetworkDetails.origin), - ])} + text={getMessage()} actionText={t('switchedNetworkToastDecline')} onActionClick={setSwitchedNetworkNeverShowMessage} onClose={() => dispatch(clearSwitchedNetworkDetails())} diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index a466f7813672..2ec894ab702a 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -26,8 +26,8 @@ export default function UserPreferencedCurrencyDisplay({ type, showFiat, showNative, - showCurrencySuffix, shouldCheckShowNativeToken, + showCurrencySuffix, privacyMode = false, ...restProps }) { diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index 93c9e09ff0fd..a1c069948074 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -16,6 +16,7 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import useMultiPolling from '../../../hooks/useMultiPolling'; import BtcOverview from './btc-overview'; // We need to mock `dispatch` since we use it for `setDefaultHomeActiveTabName`. @@ -33,6 +34,11 @@ jest.mock('../../../store/actions', () => ({ tokenBalancesStopPollingByPollingToken: jest.fn(), })); +jest.mock('../../../hooks/useMultiPolling', () => ({ + __esModule: true, + default: jest.fn(), +})); + const PORTOFOLIO_URL = 'https://portfolio.test'; const BTC_OVERVIEW_BUY = 'coin-overview-buy'; @@ -131,6 +137,23 @@ function makePortfolioUrl(path: string, getParams: Record) { describe('BtcOverview', () => { beforeEach(() => { setBackgroundConnection({ setBridgeFeatureFlags: jest.fn() } as never); + // Clear previous mock implementations + (useMultiPolling as jest.Mock).mockClear(); + + // Mock implementation for useMultiPolling + (useMultiPolling as jest.Mock).mockImplementation(({ input }) => { + // Mock startPolling and stopPollingByPollingToken for each input + const startPolling = jest.fn().mockResolvedValue('mockPollingToken'); + const stopPollingByPollingToken = jest.fn(); + + input.forEach((inputItem: string) => { + const key = JSON.stringify(inputItem); + // Simulate returning a unique token for each input + startPolling.mockResolvedValueOnce(`mockToken-${key}`); + }); + + return { startPolling, stopPollingByPollingToken }; + }); }); it('shows the primary balance as BTC when showNativeTokenAsMainBalance if true', async () => { diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 88696a14e83f..c3708eaef344 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -54,6 +54,8 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-flask) getMemoizedUnapprovedTemplatedConfirmations, ///: END:ONLY_INCLUDE_IF + getNetworkConfigurationIdByChainId, + getCurrentChainId, } from '../../../selectors'; import Tooltip from '../../ui/tooltip'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -82,11 +84,15 @@ import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import useBridging from '../../../hooks/bridge/useBridging'; ///: END:ONLY_INCLUDE_IF import { ReceiveModal } from '../../multichain/receive-modal'; -///: BEGIN:ONLY_INCLUDE_IF(build-flask) import { + setActiveNetwork, + setSwitchedNetworkDetails, + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) sendMultichainTransaction, setDefaultHomeActiveTabName, + ///: END:ONLY_INCLUDE_IF } from '../../../store/actions'; +///: BEGIN:ONLY_INCLUDE_IF(build-flask) import { BITCOIN_WALLET_SNAP_ID } from '../../../../shared/lib/accounts/bitcoin-wallet-snap'; ///: END:ONLY_INCLUDE_IF import { @@ -132,6 +138,11 @@ const CoinButtons = ({ const { address: selectedAddress } = account; const history = useHistory(); + const networks = useSelector(getNetworkConfigurationIdByChainId) as Record< + string, + string + >; + const currentChainId = useSelector(getCurrentChainId); ///: BEGIN:ONLY_INCLUDE_IF(build-flask) const currentActivityTabName = useSelector( // @ts-expect-error TODO: fix state type @@ -315,6 +326,18 @@ const CoinButtons = ({ }, [unapprovedTemplatedConfirmations, history]); ///: END:ONLY_INCLUDE_IF + const setCorrectChain = useCallback(async () => { + if (currentChainId !== chainId) { + const networkConfigurationId = networks[chainId]; + await dispatch(setActiveNetwork(networkConfigurationId)); + await dispatch( + setSwitchedNetworkDetails({ + networkClientId: networkConfigurationId, + }), + ); + } + }, [currentChainId, chainId, networks, dispatch]); + const handleSendOnClick = useCallback(async () => { trackEvent( { @@ -352,13 +375,29 @@ const CoinButtons = ({ } ///: END:ONLY_INCLUDE_IF default: { + trackEvent( + { + event: MetaMetricsEventName.NavSendButtonClicked, + category: MetaMetricsEventCategory.Navigation, + properties: { + token_symbol: 'ETH', + location: 'Home', + text: 'Send', + chain_id: chainId, + }, + }, + { excludeMetaMetricsId: false }, + ); + await setCorrectChain(); await dispatch(startNewDraftTransaction({ type: AssetType.native })); history.push(SEND_ROUTE); } } - }, [chainId, account]); + }, [chainId, account, setCorrectChain]); const handleSwapOnClick = useCallback(async () => { + await setCorrectChain(); + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) global.platform.openTab({ url: `${mmiPortfolioUrl}/swap`, @@ -388,6 +427,7 @@ const CoinButtons = ({ } ///: END:ONLY_INCLUDE_IF }, [ + setCorrectChain, isSwapsChain, chainId, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index 40c0b818649c..5af6990d4499 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -1,12 +1,13 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor, act } from '@testing-library/react'; import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { renderWithProvider } from '../../../../test/jest/rendering'; import { KeyringType } from '../../../../shared/constants/keyring'; import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; +import useMultiPolling from '../../../hooks/useMultiPolling'; import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import { getIntlLocale } from '../../../ducks/locale/locale'; @@ -42,6 +43,11 @@ jest.mock('../../../store/actions', () => ({ tokenBalancesStopPollingByPollingToken: jest.fn(), })); +jest.mock('../../../hooks/useMultiPolling', () => ({ + __esModule: true, + default: jest.fn(), +})); + const mockGetIntlLocale = getIntlLocale; let openTabSpy; @@ -160,6 +166,23 @@ describe('EthOverview', () => { beforeEach(() => { openTabSpy.mockClear(); + // Clear previous mock implementations + useMultiPolling.mockClear(); + + // Mock implementation for useMultiPolling + useMultiPolling.mockImplementation(({ input }) => { + // Mock startPolling and stopPollingByPollingToken for each input + const startPolling = jest.fn().mockResolvedValue('mockPollingToken'); + const stopPollingByPollingToken = jest.fn(); + + input.forEach((inputItem) => { + const key = JSON.stringify(inputItem); + // Simulate returning a unique token for each input + startPolling.mockResolvedValueOnce(`mockToken-${key}`); + }); + + return { startPolling, stopPollingByPollingToken }; + }); }); it('should show the primary balance', async () => { @@ -283,14 +306,16 @@ describe('EthOverview', () => { expect(swapButton).toBeInTheDocument(); expect(swapButton).not.toBeDisabled(); - fireEvent.click(swapButton); - expect(openTabSpy).toHaveBeenCalledTimes(1); + await act(async () => { + fireEvent.click(swapButton); + }); - await waitFor(() => + await waitFor(() => { + expect(openTabSpy).toHaveBeenCalledTimes(1); expect(openTabSpy).toHaveBeenCalledWith({ url: 'https://metamask-institutional.io/swap', - }), - ); + }); + }); }); it('should have the Bridge button disabled if chain id is not part of supported chains', () => { diff --git a/ui/components/multichain/account-overview/account-overview-tabs.tsx b/ui/components/multichain/account-overview/account-overview-tabs.tsx index 8c356f9f9141..994bb3d22692 100644 --- a/ui/components/multichain/account-overview/account-overview-tabs.tsx +++ b/ui/components/multichain/account-overview/account-overview-tabs.tsx @@ -149,10 +149,8 @@ export const AccountOverviewTabs = ({ - history.push(`${ASSET_ROUTE}/${asset}`) + history.push(`${ASSET_ROUTE}/${chainId}/${asset}`) } /> { diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-amount.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-amount.tsx index 24ba5067971d..07d564daca7f 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-amount.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-amount.tsx @@ -49,6 +49,7 @@ type AssetPickerAmountProps = OverridingUnion< asset: Asset; amount: Amount; isAmountLoading?: boolean; + action?: 'send' | 'receive'; error?: string; /** * Callback for when the amount changes; disables the input when undefined @@ -65,6 +66,7 @@ export const AssetPickerAmount = ({ asset, amount, onAmountChange, + action, isAmountLoading, error: passedError, ...assetPickerProps @@ -210,7 +212,11 @@ export const AssetPickerAmount = ({ paddingTop={asset.details?.standard === TokenStandard.ERC721 ? 4 : 1} paddingBottom={asset.details?.standard === TokenStandard.ERC721 ? 4 : 1} > - + ) : ( diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index d9a1a3a08588..729a580f269e 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -68,6 +68,7 @@ type AssetPickerModalProps = { header: JSX.Element | string | null; isOpen: boolean; onClose: () => void; + action?: 'send' | 'receive'; asset?: ERC20Asset | NativeAsset | Pick; onAssetChange: ( asset: AssetWithDisplayData | AssetWithDisplayData, @@ -102,6 +103,7 @@ export function AssetPickerModal({ onAssetChange, sendingAsset, network, + action, onNetworkPickerClick, customTokenListGenerator, ...tabProps @@ -262,6 +264,10 @@ export function AssetPickerModal({ for (const token of (customTokenListGenerator ?? tokenListGenerator)( shouldAddToken, )) { + if (action === 'send' && token.balance === undefined) { + continue; + } + filteredTokensAddresses.add(token.address?.toLowerCase()); filteredTokens.push( customTokenListGenerator diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx index e2965687fed5..ff665ce0f8f8 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx @@ -66,6 +66,7 @@ export type AssetPickerProps = { ) => void; onClick?: () => void; isDisabled?: boolean; + action?: 'send' | 'receive'; networkProps?: Pick< React.ComponentProps, 'network' | 'networks' | 'onNetworkChange' @@ -82,6 +83,7 @@ export function AssetPicker({ onAssetChange, networkProps, sendingAsset, + action, onClick, isDisabled = false, visibleTabs, @@ -143,6 +145,7 @@ export function AssetPicker({ setShowAssetPickerModal(false)} asset={asset} diff --git a/ui/components/multichain/network-list-menu/network-list-menu.test.js b/ui/components/multichain/network-list-menu/network-list-menu.test.js index c140189cbf81..93a55538a4ab 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.test.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.test.js @@ -21,6 +21,7 @@ const mockSetNetworkClientIdForDomain = jest.fn(); const mockSetActiveNetwork = jest.fn(); const mockUpdateCustomNonce = jest.fn(); const mockSetNextNonce = jest.fn(); +const mockSetTokenNetworkFilter = jest.fn(); jest.mock('../../../store/actions.ts', () => ({ setShowTestNetworks: () => mockSetShowTestNetworks, @@ -30,6 +31,7 @@ jest.mock('../../../store/actions.ts', () => ({ setNextNonce: () => mockSetNextNonce, setNetworkClientIdForDomain: (network, id) => mockSetNetworkClientIdForDomain(network, id), + setTokenNetworkFilter: () => mockSetTokenNetworkFilter, })); const MOCK_ORIGIN = 'https://portfolio.metamask.io'; @@ -134,6 +136,10 @@ const render = ({ selectedNetworkClientId: NETWORK_TYPES.MAINNET, preferences: { showTestNetworks, + tokenNetworkFilter: { + [CHAIN_IDS.MAINNET]: true, + [CHAIN_IDS.LINEA_MAINNET]: true, + }, }, useRequestQueue: true, domains: { diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 518f9f19387a..4dcecbf696e7 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -28,6 +28,7 @@ import { showPermittedNetworkToast, updateCustomNonce, setNextNonce, + setTokenNetworkFilter, } from '../../../store/actions'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, @@ -50,6 +51,7 @@ import { getAllDomains, getPermittedChainsForSelectedTab, getPermittedAccountsForSelectedTab, + getPreferences, } from '../../../selectors'; import ToggleButton from '../../ui/toggle-button'; import { @@ -111,6 +113,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); + const { tokenNetworkFilter } = useSelector(getPreferences); const showTestNetworks = useSelector(getShowTestNetworks); const currentChainId = useSelector(getCurrentChainId); const selectedTabOrigin = useSelector(getOriginOfCurrentTab); @@ -123,6 +126,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { const completedOnboarding = useSelector(getCompletedOnboarding); const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); const showNetworkBanner = useSelector(getShowNetworkBanner); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); const { chainId: editingChainId, editCompleted } = useSelector(getEditedNetwork) ?? {}; @@ -253,6 +257,11 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { const canDeleteNetwork = isUnlocked && !isCurrentNetwork && network.chainId !== CHAIN_IDS.MAINNET; + const allOpts: Record = {}; + Object.keys(allNetworks).forEach((chainId) => { + allOpts[chainId] = true; + }); + return ( void }) => { dispatch(updateCustomNonce('')); dispatch(setNextNonce('')); + // as a user, I don't want my network selection to force update my filter when I have "All Networks" toggled on + // however, if I am already filtered on "Current Network", we'll want to filter by the selected network when the network changes + if (Object.keys(tokenNetworkFilter).length <= 1) { + dispatch(setTokenNetworkFilter({ [network.chainId]: true })); + } else { + dispatch(setTokenNetworkFilter(allOpts)); + } + if (permittedAccountAddresses.length > 0) { grantPermittedChain(selectedTabOrigin, network.chainId); if (!permittedChainIds.includes(network.chainId)) { diff --git a/ui/components/multichain/pages/send/components/recipient-content.tsx b/ui/components/multichain/pages/send/components/recipient-content.tsx index 5c32bb5f3b66..6fb1b00134a4 100644 --- a/ui/components/multichain/pages/send/components/recipient-content.tsx +++ b/ui/components/multichain/pages/send/components/recipient-content.tsx @@ -165,6 +165,7 @@ export const SendPageRecipientContent = ({ { {isSendFormShown && ( Ethereum Mainnet logo diff --git a/ui/components/multichain/token-list-item/token-list-item.test.tsx b/ui/components/multichain/token-list-item/token-list-item.test.tsx index 7864c221e90e..6f08f276a302 100644 --- a/ui/components/multichain/token-list-item/token-list-item.test.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.test.tsx @@ -1,12 +1,17 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; - import { fireEvent, waitFor } from '@testing-library/react'; +import { useSelector } from 'react-redux'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { getIntlLocale } from '../../../ducks/locale/locale'; import { mockNetworkState } from '../../../../test/stub/networks'; import { useSafeChains } from '../../../pages/settings/networks-tab/networks-form/use-safe-chains'; +import { + getCurrencyRates, + getNetworkConfigurationIdByChainId, +} from '../../../selectors'; +import { getMultichainIsEvm } from '../../../selectors/multichain'; import { TokenListItem } from '.'; const state = { @@ -55,6 +60,13 @@ jest.mock( }), }), ); +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useSelector: jest.fn(), + }; +}); const mockGetIntlLocale = getIntlLocale; const mockGetSafeChains = useSafeChains; @@ -70,9 +82,19 @@ describe('TokenListItem', () => { tokenImage: '', title: '', chainId: '0x1', + tokenChainImage: './eth-logo.png', }; it('should render correctly', () => { const store = configureMockStore()(state); + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === getNetworkConfigurationIdByChainId) { + return '0x1'; + } + if (selector === getMultichainIsEvm) { + return true; + } + return undefined; + }); const { getByTestId, container } = renderWithProvider( , store, @@ -111,6 +133,15 @@ describe('TokenListItem', () => { it('should display warning scam modal', () => { const store = configureMockStore()(state); + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === getCurrencyRates) { + return { ETH: '' }; + } + if (selector === getMultichainIsEvm) { + return true; + } + return undefined; + }); const propsToUse = { primary: '11.9751 ETH', isNativeCurrency: true, diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index 1f9f0b18bed5..540d2d8be98a 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -41,14 +41,13 @@ import { import { getMetaMetricsId, getTestNetworkBackgroundColor, - getTokensMarketData, getParticipateInMetaMetrics, getDataCollectionForMarketing, + getMarketData, + getNetworkConfigurationIdByChainId, + getCurrencyRates, } from '../../../selectors'; -import { - getMultichainCurrentNetwork, - getMultichainIsEvm, -} from '../../../selectors/multichain'; +import { getMultichainIsEvm } from '../../../selectors/multichain'; import Tooltip from '../../ui/tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -77,10 +76,10 @@ type TokenListItemProps = { secondary?: string | null; title: string; tooltipText?: string; - chainId: string; - isOriginalTokenSymbol?: boolean | null; isNativeCurrency?: boolean; isStakeable?: boolean; + tokenChainImage?: string; + chainId: string; address?: string | null; showPercentage?: boolean; isPrimaryTokenSymbolHidden?: boolean; @@ -96,8 +95,8 @@ export const TokenListItem = ({ secondary, title, tooltipText, + tokenChainImage, chainId, - isOriginalTokenSymbol, isPrimaryTokenSymbolHidden = false, isNativeCurrency = false, isStakeable = false, @@ -112,6 +111,7 @@ export const TokenListItem = ({ const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); const { safeChains } = useSafeChains(); + const currencyRates = useSelector(getCurrencyRates); const decimalChainId = isEvm && parseInt(hexToDecimal(chainId), 10); @@ -126,6 +126,8 @@ export const TokenListItem = ({ // we only use this option for EVM here: const shouldShowPercentage = isEvm && showPercentage; + const isOriginalTokenSymbol = tokenSymbol && currencyRates[tokenSymbol]; + // Scam warning const showScamWarning = isNativeCurrency && !isOriginalTokenSymbol && shouldShowPercentage; @@ -135,10 +137,6 @@ export const TokenListItem = ({ const history = useHistory(); const getTokenTitle = () => { - if (!isOriginalTokenSymbol) { - return title; - } - // We only consider native token symbols! switch (title) { case CURRENCY_SYMBOLS.ETH: return t('networkNameEthereum'); @@ -149,10 +147,10 @@ export const TokenListItem = ({ } }; - const tokensMarketData = useSelector(getTokensMarketData); + const multiChainMarketData = useSelector(getMarketData); const tokenPercentageChange = address - ? tokensMarketData?.[address]?.pricePercentChange1d + ? multiChainMarketData?.[chainId]?.[address]?.pricePercentChange1d : null; const tokenTitle = getTokenTitle(); @@ -212,7 +210,9 @@ export const TokenListItem = ({ ); // Used for badge icon - const currentNetwork = useSelector(getMultichainCurrentNetwork); + const allNetworks: Record = useSelector( + getNetworkConfigurationIdByChainId, + ); const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); return ( @@ -264,8 +264,8 @@ export const TokenListItem = ({ badge={ @@ -336,7 +336,8 @@ export const TokenListItem = ({ - {primary}{' '} - {isNativeCurrency || isPrimaryTokenSymbolHidden - ? '' - : tokenSymbol} + {primary} {isPrimaryTokenSymbolHidden ? '' : tokenSymbol}
) : ( @@ -421,10 +419,7 @@ export const TokenListItem = ({ isHidden={privacyMode} length={SensitiveTextLength.Short} > - {primary}{' '} - {isNativeCurrency || isPrimaryTokenSymbolHidden - ? '' - : tokenSymbol} + {primary} {isPrimaryTokenSymbolHidden ? '' : tokenSymbol}
)} diff --git a/ui/hooks/useAccountTotalFiatBalance.test.js b/ui/hooks/useAccountTotalFiatBalance.test.js index 6ac93cd08e33..b7913ac662f6 100644 --- a/ui/hooks/useAccountTotalFiatBalance.test.js +++ b/ui/hooks/useAccountTotalFiatBalance.test.js @@ -25,6 +25,24 @@ const renderUseAccountTotalFiatBalance = (address) => { conversionRate: 1612.92, }, }, + allTokens: { + [CHAIN_IDS.MAINNET]: { + [mockAccount.address]: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + aggregators: [], + decimals: 6, + symbol: 'USDC', + }, + { + address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', + aggregators: [], + decimals: 18, + symbol: 'YFI', + }, + ], + }, + }, internalAccounts: { accounts: { [mockAccount.id]: mockAccount, @@ -47,10 +65,18 @@ const renderUseAccountTotalFiatBalance = (address) => { }, }, }, + tokenBalances: { + [mockAccount.address]: { + [CHAIN_IDS.MAINNET]: { + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0xbdbd', + '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': '0x501b4176a64d6', + }, + }, + }, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), - allTokens: { - '0x1': { + detectedTokens: { + [CHAIN_IDS.MAINNET]: { '0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da': [ { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', @@ -67,14 +93,6 @@ const renderUseAccountTotalFiatBalance = (address) => { ], }, }, - tokenBalances: { - [mockAccount.address]: { - [CHAIN_IDS.MAINNET]: { - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0xBDBD', - '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': '0x501B4176A64D6', - }, - }, - }, }, }; @@ -101,18 +119,18 @@ describe('useAccountTotalFiatBalance', () => { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', balance: '48573', + balanceError: null, decimals: 6, string: 0.04857, - balanceError: null, tokenFiatAmount: '0.05', }, { address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', symbol: 'YFI', balance: '1409247882142934', + balanceError: null, decimals: 18, string: 0.00141, - balanceError: null, tokenFiatAmount: '7.52', }, ], diff --git a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx index e46eff925e50..921baaebab08 100644 --- a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx +++ b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx @@ -23,9 +23,9 @@ const mockTokenBalances = [ address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', symbol: 'YFI', balance: '1409247882142934', + balanceError: null, decimals: 18, string: 0.00141, - balanceError: null, tokenFiatAmount: '7.52', }, ]; @@ -49,6 +49,24 @@ const renderUseMultichainAccountTotalFiatBalance = ( metamask: { ...mockState.metamask, completedOnboarding: true, + allTokens: { + [CHAIN_IDS.MAINNET]: { + [mockAccount.address]: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + aggregators: [], + decimals: 6, + symbol: 'USDC', + }, + { + address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', + aggregators: [], + decimals: 18, + symbol: 'YFI', + }, + ], + }, + }, internalAccounts: { accounts: { [mockAccount.id]: mockAccount, @@ -92,10 +110,17 @@ const renderUseMultichainAccountTotalFiatBalance = ( }, }, }, + tokenBalances: { + [mockAccount.address]: { + [CHAIN_IDS.MAINNET]: { + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0xbdbd', + '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': '0x501b4176a64d6', + }, + }, + }, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), - - allTokens: { - '0x1': { + detectedTokens: { + [CHAIN_IDS.MAINNET]: { '0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da': [ { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', @@ -112,14 +137,6 @@ const renderUseMultichainAccountTotalFiatBalance = ( ], }, }, - tokenBalances: { - [mockAccount.address]: { - [CHAIN_IDS.MAINNET]: { - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0xBDBD', - '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': '0x501B4176A64D6', - }, - }, - }, }, }; diff --git a/ui/pages/asset/asset.tsx b/ui/pages/asset/asset.tsx index 933c39872265..1d1acdaaa582 100644 --- a/ui/pages/asset/asset.tsx +++ b/ui/pages/asset/asset.tsx @@ -1,32 +1,36 @@ import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { Redirect, useParams } from 'react-router-dom'; +import { Hex } from '@metamask/utils'; import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import NftDetails from '../../components/app/assets/nfts/nft-details/nft-details'; -import { - getNativeCurrency, - getNfts, - getTokens, -} from '../../ducks/metamask/metamask'; +import { getSelectedAccountTokensAcrossChains } from '../../selectors'; +import { getNFTsByChainId } from '../../ducks/metamask/metamask'; import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; -import NativeAsset from './components/native-asset'; +import { Token } from '../../components/app/assets/token-list/token-list'; import TokenAsset from './components/token-asset'; +import { findAssetByAddress } from './util'; +import NativeAsset from './components/native-asset'; /** A page representing a native, token, or NFT asset */ const Asset = () => { - const nativeCurrency = useSelector(getNativeCurrency); - const tokens = useSelector(getTokens); - const nfts = useSelector(getNfts); - const { asset, id } = useParams<{ asset: string; id: string }>(); - - const token = tokens.find(({ address }: { address: string }) => - // @ts-expect-error TODO: Fix this type error by handling undefined parameters - isEqualCaseInsensitive(address, asset), - ); + const selectedAccountTokensChains: Record = useSelector( + getSelectedAccountTokensAcrossChains, + ) as Record; + const params = useParams<{ + chainId: Hex; + asset: string; + id: string; + }>(); + const { chainId, asset, id } = params; + + const nfts = useSelector((state) => getNFTsByChainId(state, chainId)); + + const token = findAssetByAddress(selectedAccountTokensChains, asset, chainId); const nft = nfts.find( - ({ address, tokenId }: { address: string; tokenId: string }) => + ({ address, tokenId }: { address: Hex; tokenId: string }) => // @ts-expect-error TODO: Fix this type error by handling undefined parameters isEqualCaseInsensitive(address, asset) && id === tokenId.toString(), ); @@ -39,10 +43,12 @@ const Asset = () => { let content; if (nft) { content = ; - } else if (token) { - content = ; - } else if (asset === nativeCurrency) { - content = ; + } else if (token && chainId) { + if (token?.address) { + content = ; + } else { + content = ; + } } else { content = ; } diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index 8a5c60e340cb..a5965d2fbe30 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -180,9 +180,10 @@ exports[`AssetPage should render a native asset 1`] = ` class="mm-box multichain-token-list-item mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-column" data-testid="multichain-token-list-item" > -
Ethereum Mainnet logo @@ -221,12 +222,14 @@ exports[`AssetPage should render a native asset 1`] = ` > TEST -

- TEST -

+

+

+ > + $0.00 +

- 0 TEST + 0 - + TEST

@@ -249,7 +254,7 @@ exports[`AssetPage should render a native asset 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--justify-content-space-between" /> - +
diff --git a/ui/pages/asset/components/asset-page.test.tsx b/ui/pages/asset/components/asset-page.test.tsx index 5df516184004..60e322f8825e 100644 --- a/ui/pages/asset/components/asset-page.test.tsx +++ b/ui/pages/asset/components/asset-page.test.tsx @@ -11,6 +11,7 @@ import { AssetType } from '../../../../shared/constants/transaction'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import { setBackgroundConnection } from '../../../store/background-connection'; import { mockNetworkState } from '../../../../test/stub/networks'; +import useMultiPolling from '../../../hooks/useMultiPolling'; import AssetPage from './asset-page'; jest.mock('../../../store/actions', () => ({ @@ -39,6 +40,11 @@ jest.mock('../../../../shared/constants/network', () => ({ }, })); +jest.mock('../../../hooks/useMultiPolling', () => ({ + __esModule: true, + default: jest.fn(), +})); + describe('AssetPage', () => { const mockStore = { localeMessages: { @@ -46,12 +52,28 @@ describe('AssetPage', () => { }, metamask: { tokenList: {}, + tokenBalances: {}, + marketData: {}, + allTokens: {}, + accountsByChainId: { + '0x1': { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + balance: '0x00', + }, + }, + }, currentCurrency: 'usd', accounts: {}, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), currencyRates: { + TEST: { + conversionRate: 123, + ticker: 'ETH', + }, ETH: { conversionRate: 123, + ticker: 'ETH', }, }, useCurrencyRateCheck: true, @@ -59,7 +81,7 @@ describe('AssetPage', () => { internalAccounts: { accounts: { 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x1', + address: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', metadata: { name: 'Test Account', @@ -125,6 +147,23 @@ describe('AssetPage', () => { formatRangeToParts: jest.fn(), }; }); + // Clear previous mock implementations + (useMultiPolling as jest.Mock).mockClear(); + + // Mock implementation for useMultiPolling + (useMultiPolling as jest.Mock).mockImplementation(({ input }) => { + // Mock startPolling and stopPollingByPollingToken for each input + const startPolling = jest.fn().mockResolvedValue('mockPollingToken'); + const stopPollingByPollingToken = jest.fn(); + + input.forEach((inputItem: string) => { + const key = JSON.stringify(inputItem); + // Simulate returning a unique token for each input + startPolling.mockResolvedValueOnce(`mockToken-${key}`); + }); + + return { startPolling, stopPollingByPollingToken }; + }); }); afterEach(() => { @@ -287,6 +326,10 @@ describe('AssetPage', () => { , store, ); + const dynamicImages = container.querySelectorAll('img[alt*="logo"]'); + dynamicImages.forEach((img) => { + img.setAttribute('alt', 'static-logo'); + }); expect(container).toMatchSnapshot(); }); @@ -322,54 +365,67 @@ describe('AssetPage', () => { expect(chart).toBeNull(); }); + const dynamicImages = container.querySelectorAll('img[alt*="logo"]'); + dynamicImages.forEach((img) => { + img.setAttribute('alt', 'static-logo'); + }); + const elementsWithAria = container.querySelectorAll('[aria-describedby]'); + elementsWithAria.forEach((el) => + el.setAttribute('aria-describedby', 'static-tooltip-id'), + ); + expect(container).toMatchSnapshot(); }); it('should render an ERC20 token with prices', async () => { - // jest.useFakeTimers(); - try { - const address = '0xe4246B1Ac0Ba6839d9efA41a8A30AE3007185f55'; - const marketCap = 456; - - // Mock price history - nock('https://price.api.cx.metamask.io') - .get(`/v1/chains/${CHAIN_IDS.MAINNET}/historical-prices/${address}`) - .query(true) - .reply(200, { prices: [[1, 1]] }); - - const { queryByTestId, container } = renderWithProvider( - , - configureMockStore([thunk])({ - ...mockStore, - metamask: { - ...mockStore.metamask, - marketData: { - [CHAIN_IDS.MAINNET]: { - [address]: { - price: 123, - marketCap, - }, + const address = '0xe4246B1Ac0Ba6839d9efA41a8A30AE3007185f55'; + const marketCap = 456; + + // Mock price history + nock('https://price.api.cx.metamask.io') + .get(`/v1/chains/${CHAIN_IDS.MAINNET}/historical-prices/${address}`) + .query(true) + .reply(200, { prices: [[1, 1]] }); + + const { queryByTestId, container } = renderWithProvider( + , + configureMockStore([thunk])({ + ...mockStore, + metamask: { + ...mockStore.metamask, + marketData: { + [CHAIN_IDS.MAINNET]: { + [address]: { + price: 123, + marketCap, + currency: 'ETH', }, }, }, - }), - ); + }, + }), + ); - // Verify chart is rendered - await waitFor(() => { - const chart = queryByTestId('asset-price-chart'); - expect(chart).toHaveClass('mm-box--background-color-transparent'); - }); + // Verify chart is rendered + await waitFor(() => { + const chart = queryByTestId('asset-price-chart'); + expect(chart).toHaveClass('mm-box--background-color-transparent'); + }); - // Verify market data is rendered - const marketCapElement = queryByTestId('asset-market-cap'); - expect(marketCapElement).toHaveTextContent( - `${marketCap * mockStore.metamask.currencyRates.ETH.conversionRate}`, - ); + // Verify market data is rendered + const marketCapElement = queryByTestId('asset-market-cap'); + expect(marketCapElement).toHaveTextContent( + `${marketCap * mockStore.metamask.currencyRates.ETH.conversionRate}`, + ); - expect(container).toMatchSnapshot(); - } finally { - // jest.useRealTimers(); - } + const dynamicImages = container.querySelectorAll('img[alt*="logo"]'); + dynamicImages.forEach((img) => { + img.setAttribute('alt', 'static-logo'); + }); + const elementsWithAria = container.querySelectorAll('[aria-describedby]'); + elementsWithAria.forEach((el) => + el.setAttribute('aria-describedby', 'static-tooltip-id'), + ); + expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index 818ceb792ec3..d7ad8e568f4e 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -1,9 +1,10 @@ import React, { ReactNode, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; - import { useSelector } from 'react-redux'; import { EthMethod } from '@metamask/keyring-api'; import { isEqual } from 'lodash'; +import { Hex } from '@metamask/utils'; +import { zeroAddress } from 'ethereumjs-util'; import { getCurrentCurrency, getDataCollectionForMarketing, @@ -13,7 +14,10 @@ import { getParticipateInMetaMetrics, getSelectedInternalAccount, getSwapsDefaultToken, - getTokensMarketData, + getMarketData, + getCurrencyRates, + getSelectedAccountNativeTokenCachedBalanceByChainId, + getSelectedAccount, } from '../../../selectors'; import { Display, @@ -33,10 +37,7 @@ import { } from '../../../components/component-library'; import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { - AddressCopyButton, - TokenListItem, -} from '../../../components/multichain'; +import { AddressCopyButton } from '../../../components/multichain'; import { AssetType } from '../../../../shared/constants/transaction'; import TokenCell from '../../../components/app/assets/token-cell'; import TransactionList from '../../../components/app/transaction-list'; @@ -46,6 +47,8 @@ import { getConversionRate } from '../../../ducks/metamask/metamask'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import CoinButtons from '../../../components/app/wallet-overview/coin-buttons'; import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +import { calculateTokenBalance } from '../../../components/app/assets/util/calculateTokenBalance'; +import { useTokenBalances } from '../../../hooks/useTokenBalances'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; @@ -56,6 +59,7 @@ export type Asset = ( type: AssetType.native; /** Whether the symbol has been verified to match the chain */ isOriginalNativeSymbol: boolean; + decimals: number; } | { type: AssetType.token; @@ -68,29 +72,16 @@ export type Asset = ( } ) & { /** The hexadecimal chain id */ - chainId: `0x${string}`; + chainId: Hex; /** The asset's symbol, e.g. 'ETH' */ symbol: string; /** The asset's name, e.g. 'Ethereum' */ name?: string; /** A URL to the asset's image */ image: string; - balance: { - /** - * A decimal representation of the balance before applying - * decimals e.g. '12300000000000000' for 0.0123 ETH - */ - value: string; - /** - * A displayable representation of the balance after applying - * decimals e.g. '0.0123' for 12300000000000000 WEI - */ - display: string; - /** The balance's localized value in fiat e.g. '$12.34' or '56,78 €' */ - fiat?: string; - }; /** True if the asset implements ERC721 */ isERC721?: boolean; + balance?: { value: string; display: string; fiat: string }; }; // A page representing a native or token asset @@ -103,9 +94,9 @@ const AssetPage = ({ }) => { const t = useI18nContext(); const history = useHistory(); + const selectedAccount = useSelector(getSelectedAccount); const currency = useSelector(getCurrentCurrency); const conversionRate = useSelector(getConversionRate); - const allMarketData = useSelector(getTokensMarketData); const isBridgeChain = useSelector(getIsBridgeChain); const isBuyableChain = useSelector(getIsNativeTokenBuyable); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); @@ -115,24 +106,70 @@ const AssetPage = ({ account.methods.includes(EthMethod.SignTransaction) || account.methods.includes(EthMethod.SignUserOperation); + const marketData = useSelector(getMarketData); + const currencyRates = useSelector(getCurrencyRates); + + const nativeBalances: Record = useSelector( + getSelectedAccountNativeTokenCachedBalanceByChainId, + ) as Record; + + const { tokenBalances } = useTokenBalances(); + const selectedAccountTokenBalancesAcrossChains = + tokenBalances[selectedAccount.address]; + + const { chainId, type, symbol, name, image, decimals } = asset; const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); const metaMetricsId = useSelector(getMetaMetricsId); - const { chainId, type, symbol, name, image, balance } = asset; - const address = type === AssetType.token ? toChecksumHexAddress(asset.address) - : '0x0000000000000000000000000000000000000000'; + : zeroAddress(); + + const balance = calculateTokenBalance({ + isNative: type === AssetType.native, + chainId, + address: address as Hex, + decimals, + nativeBalances, + selectedAccountTokenBalancesAcrossChains, + }); - const marketData = allMarketData?.[address]; + // Market and conversion rate data + const baseCurrency = marketData[chainId]?.[address]?.currency; + const tokenMarketPrice = marketData[chainId]?.[address]?.price || 0; + const tokenExchangeRate = + type === AssetType.native + ? currencyRates[symbol]?.conversionRate + : currencyRates[baseCurrency]?.conversionRate || 0; + + // Calculate fiat amount + const tokenFiatAmount = + tokenMarketPrice * tokenExchangeRate * parseFloat(String(balance)); const currentPrice = - conversionRate !== undefined && marketData?.price !== undefined - ? conversionRate * marketData.price + tokenExchangeRate !== undefined && tokenMarketPrice !== undefined + ? tokenExchangeRate * tokenMarketPrice : undefined; + const tokenMarketDetails = marketData[chainId]?.[address]; + const shouldDisplayMarketData = + conversionRate > 0 && + tokenMarketDetails && + (tokenMarketDetails.marketCap > 0 || + tokenMarketDetails.totalVolume > 0 || + tokenMarketDetails.circulatingSupply > 0 || + tokenMarketDetails.allTimeHigh > 0 || + tokenMarketDetails.allTimeLow > 0); + + // this is needed in order to assign the correct balances to TokenButtons before sending/swapping + // without this, the balances we be populated as zero until the user refreshes the screen: https://github.com/MetaMask/metamask-extension/issues/28509 + asset.balance = { + value: '', // decimal value not needed + display: String(balance), + fiat: String(tokenFiatAmount), + }; const portfolioSpendingCapsUrl = useMemo( () => getPortfolioUrl( @@ -211,26 +248,15 @@ const AssetPage = ({ {t('yourBalance')} - {type === AssetType.native ? ( - - ) : ( - - )} + )} - {conversionRate > 0 && - (marketData?.marketCap > 0 || - marketData?.totalVolume > 0 || - marketData?.circulatingSupply > 0 || - marketData?.allTimeHigh > 0 || - marketData?.allTimeLow > 0) && ( - - - {t('marketDetails')} - - - {marketData?.marketCap > 0 && - renderRow( - t('marketCap'), - - {localizeLargeNumber( - t, - conversionRate * marketData.marketCap, - )} - , - )} - {marketData?.totalVolume > 0 && - renderRow( - t('totalVolume'), - - {localizeLargeNumber( - t, - conversionRate * marketData.totalVolume, - )} - , - )} - {marketData?.circulatingSupply > 0 && - renderRow( - t('circulatingSupply'), - - {localizeLargeNumber(t, marketData.circulatingSupply)} - , - )} - {marketData?.allTimeHigh > 0 && - renderRow( - t('allTimeHigh'), - - {formatCurrency( - `${conversionRate * marketData.allTimeHigh}`, - currency, - getPricePrecision( - conversionRate * marketData.allTimeHigh, - ), - )} - , - )} - {marketData?.allTimeLow > 0 && - renderRow( - t('allTimeLow'), - - {formatCurrency( - `${conversionRate * marketData.allTimeLow}`, - currency, - getPricePrecision( - conversionRate * marketData.allTimeLow, - ), - )} - , - )} - + {shouldDisplayMarketData && ( + + + {t('marketDetails')} + + + {tokenMarketDetails.marketCap > 0 && + renderRow( + t('marketCap'), + + {localizeLargeNumber( + t, + tokenExchangeRate * tokenMarketDetails.marketCap, + )} + , + )} + {tokenMarketDetails.totalVolume > 0 && + renderRow( + t('totalVolume'), + + {localizeLargeNumber( + t, + tokenExchangeRate * tokenMarketDetails.totalVolume, + )} + , + )} + {tokenMarketDetails.circulatingSupply > 0 && + renderRow( + t('circulatingSupply'), + + {localizeLargeNumber( + t, + tokenMarketDetails.circulatingSupply, + )} + , + )} + {tokenMarketDetails.allTimeHigh > 0 && + renderRow( + t('allTimeHigh'), + + {formatCurrency( + `${tokenExchangeRate * tokenMarketDetails.allTimeHigh}`, + currency, + getPricePrecision( + tokenExchangeRate * tokenMarketDetails.allTimeHigh, + ), + )} + , + )} + {tokenMarketDetails.allTimeLow > 0 && + renderRow( + t('allTimeLow'), + + {formatCurrency( + `${tokenExchangeRate * tokenMarketDetails.allTimeLow}`, + currency, + getPricePrecision( + tokenExchangeRate * tokenMarketDetails.allTimeLow, + ), + )} + , + )} - )} + + )} { - const nativeCurrency = useSelector(getNativeCurrency); - const balance = useSelector(getSelectedAccountCachedBalance); - const image = useSelector(getNativeCurrencyImage); - const showFiat = useSelector(getShouldShowFiat); - const currentCurrency = useSelector(getCurrentCurrency); - const chainId = useSelector(getCurrentChainId); - const { ticker, type } = useSelector(getProviderConfig) ?? {}; +const NativeAsset = ({ token, chainId }: { token: Token; chainId: Hex }) => { + const { symbol } = token; + const image = getNativeCurrencyForChain(chainId); + const { type } = useSelector(getProviderConfig) ?? {}; const { address } = useSelector(getSelectedInternalAccount); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); @@ -39,29 +28,18 @@ const NativeAsset = () => { const trackEvent = useContext(MetaMetricsContext); const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( chainId, - ticker, + symbol, type, ); - const [, { value: balanceDisplay }] = useCurrencyDisplay(balance, { - currency: nativeCurrency, - }); - const [fiatDisplay] = useCurrencyDisplay(balance, { - currency: currentCurrency, - }); - return ( { +const TokenAsset = ({ token, chainId }: { token: Token; chainId: Hex }) => { const { address, symbol, isERC721 } = token; const tokenList = useSelector(getTokenList); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const allNetworks: { + [key: `0x${string}`]: NetworkConfiguration; + } = useSelector(getNetworkConfigurationsByChainId); + // get the correct rpc url for the current token + const defaultIdx = allNetworks[chainId]?.defaultBlockExplorerUrlIndex; + const currentTokenBlockExplorer = + defaultIdx === undefined + ? null + : allNetworks[chainId]?.blockExplorerUrls[defaultIdx]; + const { address: walletAddress } = useSelector(getSelectedInternalAccount); + const erc20TokensByChain = useSelector(selectERC20TokensByChain); const history = useHistory(); const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); - const { name, iconUrl, aggregators } = - Object.values(tokenList).find( - (t) => - isEqualCaseInsensitive(t.symbol, symbol) && - isEqualCaseInsensitive(t.address, address), - ) ?? {}; + // Fetch token data from tokenList + const tokenData = Object.values(tokenList).find( + (t) => + isEqualCaseInsensitive(t.symbol, symbol) && + isEqualCaseInsensitive(t.address, address), + ); + + // If not found in tokenList, try erc20TokensByChain + const tokenDataFromChain = + erc20TokensByChain?.[chainId]?.data?.[address.toLowerCase()]; + + const name = tokenData?.name || tokenDataFromChain?.name || symbol; + const iconUrl = tokenData?.iconUrl || tokenDataFromChain?.iconUrl || ''; + const aggregators = tokenData?.aggregators; const { tokensWithBalances, @@ -55,8 +74,9 @@ const TokenAsset = ({ token }: { token: Token }) => { chainId, '', walletAddress, - rpcPrefs, + { blockExplorerUrl: currentTokenBlockExplorer ?? '' }, ); + console.log(tokenTrackerLink); return ( ; const isSwapsChain = useSelector(getIsSwapsChain); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBridgeChain = useSelector(getIsBridgeChain); @@ -114,6 +122,18 @@ const TokenButtons = ({ } }, [token.isERC721, token.address, dispatch]); + const setCorrectChain = async () => { + if (currentChainId !== token.chainId) { + const networkConfigurationId = networks[token.chainId]; + await dispatch(setActiveNetwork(networkConfigurationId)); + await dispatch( + setSwitchedNetworkDetails({ + networkClientId: networkConfigurationId, + }), + ); + } + }; + return ( { @@ -137,7 +157,7 @@ const TokenButtons = ({ properties: { location: 'Token Overview', text: 'Buy', - chain_id: chainId, + chain_id: currentChainId, token_symbol: token.symbol, }, }); @@ -206,12 +226,13 @@ const TokenButtons = ({ token_symbol: token.symbol, location: MetaMetricsSwapsEventSource.TokenView, text: 'Send', - chain_id: chainId, + chain_id: token.chainId, }, }, { excludeMetaMetricsId: false }, ); try { + await setCorrectChain(); await dispatch( startNewDraftTransaction({ type: AssetType.token, @@ -248,7 +269,9 @@ const TokenButtons = ({ size={IconSize.Sm} /> } - onClick={() => { + onClick={async () => { + await setCorrectChain(); + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) global.platform.openTab({ url: `${mmiPortfolioUrl}/swap`, @@ -263,16 +286,16 @@ const TokenButtons = ({ token_symbol: token.symbol, location: MetaMetricsSwapsEventSource.TokenView, text: 'Swap', - chain_id: chainId, + chain_id: currentChainId, }, }); dispatch( setSwapsFromToken({ ...token, - address: token.address.toLowerCase(), + address: token.address?.toLowerCase(), iconUrl: token.image, - balance: token.balance.value, - string: token.balance.display, + balance: token?.balance?.value, + string: token?.balance?.display, }), ); if (usingHardwareWallet) { @@ -309,8 +332,8 @@ const TokenButtons = ({ openBridgeExperience(MetaMetricsSwapsEventSource.TokenView, { ...token, iconUrl: token.image, - balance: token.balance.value, - string: token.balance.display, + balance: token?.balance?.value, + string: token?.balance?.display, name: token.name ?? '', }); }} diff --git a/ui/pages/asset/util.test.ts b/ui/pages/asset/util.test.ts index ecf4f2de5381..e5d4f6964567 100644 --- a/ui/pages/asset/util.test.ts +++ b/ui/pages/asset/util.test.ts @@ -28,7 +28,7 @@ describe('findAssetByAddress', () => { }); it('should return undefined if address is not provided and no token without address is found', () => { - expect(findAssetByAddress(mockTokens, undefined, '0x1')).toBeNull(); + expect(findAssetByAddress(mockTokens, undefined, '0x1')).toBeUndefined(); }); it('should return the token without address if address is not provided and a token without address exists', () => { @@ -37,9 +37,9 @@ describe('findAssetByAddress', () => { { address: '', decimals: 18, symbol: 'NULL', name: 'Token NULL' }, ], }; - expect( - findAssetByAddress(tokensWithNullAddress, undefined, '0x1'), - ).toBeNull(); + expect(findAssetByAddress(tokensWithNullAddress, undefined, '0x1')).toEqual( + { address: '', decimals: 18, name: 'Token NULL', symbol: 'NULL' }, + ); }); it('should return the correct token when address and chainId are provided', () => { diff --git a/ui/pages/asset/util.ts b/ui/pages/asset/util.ts index 479c69015f35..c18d6e92dcfa 100644 --- a/ui/pages/asset/util.ts +++ b/ui/pages/asset/util.ts @@ -83,8 +83,7 @@ export const findAssetByAddress = ( } if (!address) { - console.warn(`No token found for address: ${address}`); - return null; + return tokens.find((token) => !token.address); } return tokens.find( diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index ef0ebbfa9ee3..edf1ff8bbe22 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -363,8 +363,15 @@ export default class Routes extends Component { component={NftFullImage} /> - - + + + balance + */ +export function getSelectedAccountNativeTokenCachedBalanceByChainId(state) { + const { accountsByChainId } = state.metamask; + const { address: selectedAddress } = getSelectedInternalAccount(state); + + const balancesByChainId = {}; + for (const [chainId, accounts] of Object.entries(accountsByChainId || {})) { + if (accounts[selectedAddress]) { + balancesByChainId[chainId] = accounts[selectedAddress].balance; + } + } + return balancesByChainId; +} + +/** + * Based on the current account address, query for all tokens across all chain networks on that account, + * including the native tokens, without hardcoding any native token information. + * + * @param {object} state - Redux state + * @returns {object} An object mapping chain IDs to arrays of tokens (including native tokens) with balances. + */ +export function getSelectedAccountTokensAcrossChains(state) { + const { allTokens } = state.metamask; + const selectedAddress = getSelectedInternalAccount(state).address; + + const tokensByChain = {}; + + const nativeTokenBalancesByChainId = + getSelectedAccountNativeTokenCachedBalanceByChainId(state); + + const chainIds = new Set([ + ...Object.keys(allTokens || {}), + ...Object.keys(nativeTokenBalancesByChainId || {}), + ]); + + chainIds.forEach((chainId) => { + if (!tokensByChain[chainId]) { + tokensByChain[chainId] = []; + } + + if (allTokens[chainId]?.[selectedAddress]) { + allTokens[chainId][selectedAddress].forEach((token) => { + const tokenWithChain = { ...token, chainId, isNative: false }; + tokensByChain[chainId].push(tokenWithChain); + }); + } + + const nativeBalance = nativeTokenBalancesByChainId[chainId]; + if (nativeBalance) { + const nativeTokenInfo = getNativeTokenInfo(state, chainId); + tokensByChain[chainId].push({ + ...nativeTokenInfo, + address: '', + balance: nativeBalance, + chainId, + isNative: true, + }); + } + }); + + return tokensByChain; +} + +/** + * Retrieves native token information (symbol, decimals, name) for a given chainId from the state, + * without hardcoding any values. + * + * @param {object} state - Redux state + * @param {string} chainId - Chain ID + * @returns {object} Native token information + */ +function getNativeTokenInfo(state, chainId) { + const { networkConfigurationsByChainId } = state.metamask; + + const networkConfig = networkConfigurationsByChainId?.[chainId]; + + if (networkConfig) { + const symbol = networkConfig.nativeCurrency || AssetType.native; + const decimals = 18; + const name = networkConfig.name || 'Native Token'; + + return { + symbol, + decimals, + name, + }; + } + + const { provider } = state.metamask; + if (provider?.chainId === chainId) { + const symbol = provider.ticker || AssetType.native; + const decimals = provider.nativeCurrency?.decimals || 18; + const name = provider.nickname || 'Native Token'; + + return { + symbol, + decimals, + name, + }; + } + + return { symbol: AssetType.native, decimals: 18, name: 'Native Token' }; +} + /** * @typedef {import('./selectors.types').InternalAccountWithBalance} InternalAccountWithBalance */ @@ -619,12 +729,6 @@ export const getTokensMarketData = (state) => { return state.metamask.marketData?.[chainId]; }; -/** - * Get market data for tokens across all chains - * - * @param state - * @returns {Record>} - */ export const getMarketData = (state) => { return state.metamask.marketData; }; @@ -742,6 +846,20 @@ export const getNetworkConfigurationsByChainId = createDeepEqualSelector( (networkConfigurationsByChainId) => networkConfigurationsByChainId, ); +export const getNetworkConfigurationIdByChainId = createDeepEqualSelector( + (state) => state.metamask.networkConfigurationsByChainId, + (networkConfigurationsByChainId) => + Object.entries(networkConfigurationsByChainId).reduce( + (acc, [_chainId, network]) => { + const selectedRpcEndpoint = + network.rpcEndpoints[network.defaultRpcEndpointIndex]; + acc[_chainId] = selectedRpcEndpoint.networkClientId; + return acc; + }, + {}, + ), +); + /** * @type (state: any, chainId: string) => import('@metamask/network-controller').NetworkConfiguration */ @@ -1370,6 +1488,10 @@ export function getUSDConversionRate(state) { ?.usdConversionRate; } +export function getCurrencyRates(state) { + return state.metamask.currencyRates; +} + export function getWeb3ShimUsageStateForOrigin(state, origin) { return state.metamask.web3ShimUsageOrigins[origin]; } @@ -1460,6 +1582,10 @@ export function getNativeCurrencyImage(state) { return CHAIN_ID_TOKEN_IMAGE_MAP[chainId]; } +export function getNativeCurrencyForChain(chainId) { + return CHAIN_ID_TOKEN_IMAGE_MAP[chainId] ?? undefined; +} + export function getNextSuggestedNonce(state) { return Number(state.metamask.nextNonce); } @@ -1507,10 +1633,11 @@ export const selectERC20Tokens = createDeepEqualSelector( export const getTokenList = createSelector( selectERC20Tokens, getIsTokenDetectionInactiveOnMainnet, - (remoteTokenList, isTokenDetectionInactiveOnMainnet) => - isTokenDetectionInactiveOnMainnet + (remoteTokenList, isTokenDetectionInactiveOnMainnet) => { + return isTokenDetectionInactiveOnMainnet ? STATIC_MAINNET_TOKEN_LIST - : remoteTokenList, + : remoteTokenList; + }, ); export const getMemoizedMetadataContract = createSelector( diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 01c34dc2fe3d..a339ff856058 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -2257,7 +2257,7 @@ export function automaticallySwitchNetwork( */ export function setSwitchedNetworkDetails(switchedNetworkDetails: { networkClientId: string; - selectedTabOrigin: string; + selectedTabOrigin?: string; }): ThunkAction { return async (dispatch: MetaMaskReduxDispatch) => { await submitRequestToBackground('setSwitchedNetworkDetails', [ From df9e07d188f4a7024995f5e9c5eb41270f655b99 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 21 Nov 2024 11:33:13 -0700 Subject: [PATCH 21/28] feat(SwapsController): Remove reliance on global network (#28275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Currently, SwapsController relies on the global network: it uses the global provider from the NetworkController and feeds it into an Ethers contract object to get the allowance for a user's token, and it uses the chain ID of the global network (again, coming from NetworkController) to retrieve parameters that will ultimately be used to control when and how swaps are requested and how to transform the resulting data. This does not work long-term, as we want to remove the concept of the global network from the extension. So fundamentally, we need to have the parts of the API which rely on the global provider to take a network client ID, which will be used to fetch a provider and chain ID. One important note about SwapsController is that there is a kind of entry point to the API via `fetchAndSetQuotes`. This establishes the network that a polling loop will use, and it starts that loop. The controller also operates on one network at a time: it does not distinguish between one network or another when capturing the quote data in state, so when calling `fetchAndSetQuotes` for a different network, if the polling loop was already started for a different network, it will be reset, and all quotes captured for that network will be overwritten. We plan on keeping this behavior. Given these constraints, here is the list of changes: - Update the `FetchTradesInfoParams` type to replace `chainId` with `networkClientId`. - Update `fetchAndSetQuotes` to take a `networkClientId` instead of `chainId` within the `fetchParamsMetaData` (second argument). When called for the first time, this ID will be resolved to a network client. - Update `fetchAndSetQuotes` to no longer rely on the global provider proxy from NetworkController to get the allowance for a token, but use the network client obtained in the previous step. - Update signature for `getTopQuoteWithCalculatedSavings`: instead of taking a single quotes object, it now takes an options bag with keys `quotes` and `networkClientId`. Additionally, instead of relying on the global network to get the chain ID, it will resolve the network client ID to a network client and get the chain ID off of that. - Add new selector `getSelectedNetwork` which can be used to get information about the global network configuration + network client ID in a Redux action in one go (`getCurrentNetwork` was not sufficient to do this). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28275?quickstart=1) ## **Related issues** Progresses #23566. ## **Manual testing steps** 1. Switch to Ethereum Mainnet if you haven't already. 2. Click on the "Swap" button. 3. Open Dev Tools and go to the Network tab. 4. Fill in a monetary value for ETH. 5. Choose a destination token (it doesn't matter). 6. Wait for quotes to appear. 7. Observe that the text above the quotes says "Confirmed by X sources. Verify on Etherscan." 8. In the Dev Tools pane, search for "swap.api". Find any request that starts with `https://swap.api.cx.metamask.io/networks/`. Observe that the chain ID in the URL matches Ethereum Mainnet. 9. Now search for "gas.api". Find any request that starts with `https://gas.api.cx.metamask.io/networks/`. Observe that the chain ID in the URL matches Ethereum Mainnet. 10. Cancel the swap. 11. Switch to Linea Mainnet. 12. Fill in a monetary value for ETH. 13. Choose a destination token. 14. Wait for quotes to appear. 15. Observe that the text above the quotes says "Confirmed by X sources. Verify on LineaScan." 16. In the Dev Tools pane, search for "swap.api". Find any request that starts with `https://swap.api.cx.metamask.io/networks/`. Observe that the chain ID in the URL matches Linea Mainnet. 17. Now search for "gas.api". Find any request that starts with `https://gas.api.cx.metamask.io/networks/`. Observe that the chain ID in the URL matches Linea Mainnet. ## **Screenshots/Recordings** No screenshots/recordings necessary. Everything should work exactly the same as it does today. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- app/scripts/controllers/swaps/index.ts | 141 +++--- app/scripts/controllers/swaps/swaps.test.ts | 478 +++++++++++++++---- app/scripts/controllers/swaps/swaps.types.ts | 8 +- app/scripts/metamask-controller.js | 1 - lavamoat/browserify/beta/policy.json | 18 +- lavamoat/browserify/flask/policy.json | 18 +- lavamoat/browserify/main/policy.json | 18 +- lavamoat/browserify/mmi/policy.json | 18 +- package.json | 1 + test/stub/provider.js | 13 +- ui/ducks/swaps/swaps.js | 26 +- ui/selectors/selectors.js | 29 ++ yarn.lock | 7 +- 13 files changed, 557 insertions(+), 219 deletions(-) diff --git a/app/scripts/controllers/swaps/index.ts b/app/scripts/controllers/swaps/index.ts index c2a947686ad3..308021cd14cf 100644 --- a/app/scripts/controllers/swaps/index.ts +++ b/app/scripts/controllers/swaps/index.ts @@ -1,17 +1,14 @@ import { Contract } from '@ethersproject/contracts'; -import { - ExternalProvider, - JsonRpcFetchFunc, - Web3Provider, -} from '@ethersproject/providers'; +import { Web3Provider } from '@ethersproject/providers'; import { BaseController, StateMetadata } from '@metamask/base-controller'; -import type { ChainId } from '@metamask/controller-utils'; import { GasFeeState } from '@metamask/gas-fee-controller'; import { TransactionParams } from '@metamask/transaction-controller'; import { captureException } from '@sentry/browser'; import { BigNumber } from 'bignumber.js'; import abi from 'human-standard-token-abi'; import { cloneDeep, mapValues } from 'lodash'; +import { NetworkClient, NetworkClientId } from '@metamask/network-controller'; +import { Hex } from '@metamask/utils'; import { EtherDenomination } from '../../../../shared/constants/common'; import { GasEstimateTypes } from '../../../../shared/constants/gas'; import { @@ -71,6 +68,13 @@ import type { Trade, } from './swaps.types'; +type Network = { + client: NetworkClient; + clientId: NetworkClientId; + chainId: Hex; + ethersProvider: Web3Provider; +}; + const metadata: StateMetadata = { swapsState: { persist: false, @@ -103,28 +107,27 @@ export default class SwapsController extends BaseController< properties: Record; }) => void; - #ethersProvider: Web3Provider; - - #ethersProviderChainId: ChainId; - #indexOfNewestCallInFlight: number; #pollCount: number; #pollingTimeout: ReturnType | null = null; - #provider: ExternalProvider | JsonRpcFetchFunc; - - #getEIP1559GasFeeEstimates: () => Promise; + #getEIP1559GasFeeEstimates: (options?: { + networkClientId?: NetworkClientId; + shouldUpdateState?: boolean; + }) => Promise; #getLayer1GasFee: (params: { transactionParams: TransactionParams; - chainId: ChainId; + networkClientId: NetworkClientId; }) => Promise; + #network: Network | undefined; + private _fetchTradesInfo: ( fetchParams: FetchTradesInfoParams, - fetchMetadata: { chainId: ChainId }, + fetchMetadata: { chainId: Hex }, ) => Promise<{ [aggId: string]: Quote; }> = defaultFetchTradesInfo; @@ -267,11 +270,8 @@ export default class SwapsController extends BaseController< this.#getEIP1559GasFeeEstimates = opts.getEIP1559GasFeeEstimates; this.#getLayer1GasFee = opts.getLayer1GasFee; - this.#ethersProvider = new Web3Provider(opts.provider); - this.#ethersProviderChainId = this._getCurrentChainId(); this.#indexOfNewestCallInFlight = 0; this.#pollCount = 0; - this.#provider = opts.provider; // TODO: this should be private, but since a lot of tests depends on spying on it // we cannot enforce privacy 100% @@ -295,11 +295,11 @@ export default class SwapsController extends BaseController< return null; } - const { chainId } = fetchParamsMetaData; - - if (chainId !== this.#ethersProviderChainId) { - this.#ethersProvider = new Web3Provider(this.#provider); - this.#ethersProviderChainId = chainId; + let network; + if (this.#network?.clientId === fetchParamsMetaData.networkClientId) { + network = this.#network; + } else { + network = this.#setNetwork(fetchParamsMetaData.networkClientId); } const { quotesPollingLimitEnabled, saveFetchedQuotes } = @@ -327,8 +327,8 @@ export default class SwapsController extends BaseController< } let [newQuotes] = await Promise.all([ - this._fetchTradesInfo(fetchParams, { ...fetchParamsMetaData }), - this._setSwapsNetworkConfig(), + this._fetchTradesInfo(fetchParams, { chainId: network.chainId }), + this._setSwapsNetworkConfig(network), ]); const { saveFetchedQuotes: saveFetchedQuotesAfterResponse } = @@ -349,8 +349,8 @@ export default class SwapsController extends BaseController< destinationTokenInfo: fetchParamsMetaData?.destinationTokenInfo, })); - const isOptimism = chainId === CHAIN_IDS.OPTIMISM.toString(); - const isBase = chainId === CHAIN_IDS.BASE.toString(); + const isOptimism = network.chainId === CHAIN_IDS.OPTIMISM.toString(); + const isBase = network.chainId === CHAIN_IDS.BASE.toString(); if ((isOptimism || isBase) && Object.values(newQuotes).length > 0) { await Promise.all( @@ -358,7 +358,7 @@ export default class SwapsController extends BaseController< if (quote.trade) { const multiLayerL1TradeFeeTotal = await this.#getLayer1GasFee({ transactionParams: quote.trade, - chainId, + networkClientId: network.clientId, }); quote.multiLayerL1TradeFeeTotal = multiLayerL1TradeFeeTotal; @@ -372,13 +372,13 @@ export default class SwapsController extends BaseController< let approvalRequired = false; if ( - !isSwapsDefaultTokenAddress(fetchParams.sourceToken, chainId) && + !isSwapsDefaultTokenAddress(fetchParams.sourceToken, network.chainId) && Object.values(newQuotes).length ) { const allowance = await this._getERC20Allowance( fetchParams.sourceToken, fetchParams.fromAddress, - chainId, + network, ); const [firstQuote] = Object.values(newQuotes); @@ -428,9 +428,9 @@ export default class SwapsController extends BaseController< if (Object.values(newQuotes).length === 0) { this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR); } else { - const topQuoteAndSavings = await this.getTopQuoteWithCalculatedSavings( - newQuotes, - ); + const topQuoteAndSavings = await this.getTopQuoteWithCalculatedSavings({ + quotes: newQuotes, + }); if (Array.isArray(topQuoteAndSavings)) { topAggId = topQuoteAndSavings[0]; newQuotes = topQuoteAndSavings[1]; @@ -476,11 +476,27 @@ export default class SwapsController extends BaseController< return [newQuotes, topAggId]; } - public async getTopQuoteWithCalculatedSavings( - quotes: Record = {}, - ): Promise<[string | null, Record] | Record> { + public async getTopQuoteWithCalculatedSavings({ + quotes, + networkClientId, + }: { + quotes: Record; + networkClientId?: NetworkClientId; + }): Promise<[string | null, Record] | Record> { + let chainId; + if (networkClientId) { + const networkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + chainId = networkClient.configuration.chainId; + } else if (this.#network === undefined) { + throw new Error('There is no network set'); + } else { + chainId = this.#network.chainId; + } + const { marketData } = this._getTokenRatesState(); - const chainId = this._getCurrentChainId(); const tokenConversionRates = marketData?.[chainId] ?? {}; const { customGasPrice, customMaxPriorityFeePerGas } = @@ -494,7 +510,7 @@ export default class SwapsController extends BaseController< const newQuotes = cloneDeep(quotes); const { gasFeeEstimates, gasEstimateType } = - await this.#getEIP1559GasFeeEstimates(); + await this.#getEIP1559GasFeeEstimates({ networkClientId }); let usedGasPrice = '0x0'; @@ -911,9 +927,9 @@ export default class SwapsController extends BaseController< }; // Private Methods - private async _fetchSwapsNetworkConfig(chainId: ChainId) { + private async _fetchSwapsNetworkConfig(network: Network) { const response = await fetchWithCache({ - url: getBaseApi('network', chainId), + url: getBaseApi('network', network.chainId), fetchOptions: { method: 'GET' }, cacheOptions: { cacheRefreshTime: 600000 }, functionName: '_fetchSwapsNetworkConfig', @@ -983,29 +999,16 @@ export default class SwapsController extends BaseController< return newQuotes; } - private _getCurrentChainId(): ChainId { - const { selectedNetworkClientId } = this.messagingSystem.call( - 'NetworkController:getState', - ); - const { - configuration: { chainId }, - } = this.messagingSystem.call( - 'NetworkController:getNetworkClientById', - selectedNetworkClientId, - ); - return chainId as ChainId; - } - private async _getERC20Allowance( contractAddress: string, walletAddress: string, - chainId: ChainId, + network: Network, ) { - const contract = new Contract(contractAddress, abi, this.#ethersProvider); + const contract = new Contract(contractAddress, abi, network.ethersProvider); return await contract.allowance( walletAddress, SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[ - chainId as keyof typeof SWAPS_CHAINID_CONTRACT_ADDRESS_MAP + network.chainId as keyof typeof SWAPS_CHAINID_CONTRACT_ADDRESS_MAP ], ); } @@ -1053,8 +1056,7 @@ export default class SwapsController extends BaseController< } // Sets the network config from the MetaSwap API. - private async _setSwapsNetworkConfig() { - const chainId = this._getCurrentChainId(); + private async _setSwapsNetworkConfig(network: Network) { let swapsNetworkConfig: { quotes: number; quotesPrefetching: number; @@ -1065,7 +1067,7 @@ export default class SwapsController extends BaseController< } | null = null; try { - swapsNetworkConfig = await this._fetchSwapsNetworkConfig(chainId); + swapsNetworkConfig = await this._fetchSwapsNetworkConfig(network); } catch (e) { console.error('Request for Swaps network config failed: ', e); } @@ -1142,4 +1144,25 @@ export default class SwapsController extends BaseController< }); }); } + + #setNetwork(networkClientId: NetworkClientId) { + const networkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + const { chainId } = networkClient.configuration; + // Web3Provider (via JsonRpcProvider) creates two extra network requests, so + // we cache the object so that we can reuse it for subsequent contract + // interactions for the same network + const ethersProvider = new Web3Provider(networkClient.provider); + + const network = { + client: networkClient, + clientId: networkClientId, + chainId, + ethersProvider, + }; + this.#network = network; + return network; + } } diff --git a/app/scripts/controllers/swaps/swaps.test.ts b/app/scripts/controllers/swaps/swaps.test.ts index 4ed1b545f170..1a71e3c995ae 100644 --- a/app/scripts/controllers/swaps/swaps.test.ts +++ b/app/scripts/controllers/swaps/swaps.test.ts @@ -1,18 +1,24 @@ import { BigNumber } from '@ethersproject/bignumber'; -import { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers'; -import { ChainId } from '@metamask/controller-utils'; +import { ChainId, InfuraNetworkType } from '@metamask/controller-utils'; import BigNumberjs from 'bignumber.js'; import { mapValues } from 'lodash'; +import * as ethersProviders from '@ethersproject/providers'; +import { Hex } from '@metamask/utils'; +import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { NetworkClientId } from '@metamask/network-controller'; import { GasEstimateTypes } from '../../../../shared/constants/gas'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps'; import { createTestProviderTools } from '../../../../test/stub/provider'; +import * as fetchWithCacheModule from '../../../../shared/lib/fetch-with-cache'; import { getDefaultSwapsControllerState } from './swaps.constants'; import { FetchTradesInfoParams, FetchTradesInfoParamsMetadata, Quote, SwapsControllerMessenger, + SwapsControllerOptions, + SwapsControllerState, } from './swaps.types'; import { getMedianEthValueQuote } from './swaps.utils'; import SwapsController from '.'; @@ -38,7 +44,12 @@ const TEST_AGG_ID_6 = 'TEST_AGG_6'; const TEST_AGG_ID_BEST = 'TEST_AGG_BEST'; const TEST_AGG_ID_APPROVAL = 'TEST_AGG_APPROVAL'; -// const POLLING_TIMEOUT = SECOND * 1000; +const MOCK_PROVIDER_RESULT_STUB = { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', +}; const MOCK_APPROVAL_NEEDED = { data: '0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00', @@ -87,7 +98,7 @@ const MOCK_FETCH_METADATA: FetchTradesInfoParamsMetadata = { decimals: 18, address: '0xSomeOtherAddress', }, - chainId: CHAIN_IDS.MAINNET, + networkClientId: InfuraNetworkType.mainnet, }; const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({ @@ -117,63 +128,95 @@ const networkControllerGetStateCallbackMock = jest .fn() .mockReturnValue({ selectedNetworkClientId: 'metamask' }); -const networkControllerGetNetworkClientByIdCallbackMock = jest - .fn() - .mockReturnValue({ configuration: { chainId: CHAIN_IDS.MAINNET } }); +const networkControllerGetNetworkClientByIdCallbackMock = jest.fn(); const tokenRatesControllerGetStateCallbackMock = jest.fn().mockReturnValue({ marketData: { - '0x1': { + [CHAIN_IDS.MAINNET]: { + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { price: 2 }, + '0x1111111111111111111111111111111111111111': { price: 0.1 }, + }, + [CHAIN_IDS.OPTIMISM]: { + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { price: 2 }, + '0x1111111111111111111111111111111111111111': { price: 0.1 }, + }, + [CHAIN_IDS.BASE]: { '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { price: 2 }, '0x1111111111111111111111111111111111111111': { price: 0.1 }, }, }, }); -messengerMock.call.mockImplementation((actionName, ..._rest) => { +messengerMock.call.mockImplementation((actionName, ...args) => { if (actionName === 'NetworkController:getState') { - return networkControllerGetStateCallbackMock(); + return networkControllerGetStateCallbackMock(...args); } if (actionName === 'NetworkController:getNetworkClientById') { - return networkControllerGetNetworkClientByIdCallbackMock(); + return networkControllerGetNetworkClientByIdCallbackMock(...args); } if (actionName === 'TokenRatesController:getState') { - return tokenRatesControllerGetStateCallbackMock(); + return tokenRatesControllerGetStateCallbackMock(...args); } return undefined; }); +function mockNetworkControllerGetNetworkClientById( + networkClientsById: Record< + NetworkClientId, + { + provider: SafeEventEmitterProvider; + configuration: { + chainId: Hex; + }; + } + >, +) { + networkControllerGetNetworkClientByIdCallbackMock.mockImplementation( + (networkClientId) => { + const foundNetworkClient = networkClientsById[networkClientId]; + if (foundNetworkClient === undefined) { + throw new Error(`Unknown network client ID '${networkClientId}'`); + } + return foundNetworkClient; + }, + ); +} + describe('SwapsController', function () { - let provider: ExternalProvider | JsonRpcFetchFunc; - const getSwapsController = ( - _provider: ExternalProvider | JsonRpcFetchFunc = provider, - ) => { + const getSwapsController = ({ + options, + state, + }: { + options?: Partial; + state?: Partial; + } = {}) => { return new SwapsController( { getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, - provider: _provider, fetchTradesInfo: fetchTradesInfoStub, getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub, getLayer1GasFee: getLayer1GasFeeStub, trackMetaMetricsEvent: trackMetaMetricsEventStub, messenger: messengerMock, + ...options, + }, + { + ...getDefaultSwapsControllerState(), + ...state, }, - getDefaultSwapsControllerState(), ); }; beforeEach(function () { - const providerResultStub = { - // 1 gwei - eth_gasPrice: '0x0de0b6b3a7640000', - // by default, all accounts are external accounts (not contracts) - eth_getCode: '0x', - }; - provider = createTestProviderTools({ - scaffold: providerResultStub, + const { provider } = createTestProviderTools({ + scaffold: MOCK_PROVIDER_RESULT_STUB, networkId: 1, chainId: CHAIN_IDS.MAINNET as ChainId, - }).provider; + }); + networkControllerGetNetworkClientByIdCallbackMock.mockReturnValue({ + provider, + configuration: { chainId: CHAIN_IDS.MAINNET }, + }); jest.useFakeTimers(); }); @@ -317,21 +360,37 @@ describe('SwapsController', function () { }); }); - it('returns empty object if passed undefined or empty object', async function () { - expect( - await swapsController.getTopQuoteWithCalculatedSavings(), - ).toStrictEqual({}); + it('returns an empty object if passed an empty set of quotes', async function () { + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ chainId }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); expect( - await swapsController.getTopQuoteWithCalculatedSavings({}), + await swapsController.getTopQuoteWithCalculatedSavings({ + quotes: {}, + networkClientId, + }), ).toStrictEqual({}); }); it('returns the top aggId and quotes with savings and fee values if passed necessary data and an even number of quotes', async function () { + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ chainId }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + const topQuoteAndSavings = - await swapsController.getTopQuoteWithCalculatedSavings( - getTopQuoteAndSavingsMockQuotes(), - ); + await swapsController.getTopQuoteWithCalculatedSavings({ + quotes: getTopQuoteAndSavingsMockQuotes(), + networkClientId, + }); const topAggId = topQuoteAndSavings[0]; const resultQuotes = topQuoteAndSavings[1]; @@ -342,6 +401,21 @@ describe('SwapsController', function () { }); it('returns the top aggId and quotes with savings and fee values if passed necessary data and an odd number of quotes', async function () { + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const provider = createTestProviderTools({ chainId }); + const networkClient = { provider, configuration: { chainId } }; + networkControllerGetNetworkClientByIdCallbackMock.mockImplementation( + (givenNetworkClientId) => { + if (givenNetworkClientId === networkClientId) { + return networkClient; + } + throw new Error( + `Unknown network client ID '${givenNetworkClientId}'`, + ); + }, + ); + const completeTestInput = getTopQuoteAndSavingsMockQuotes(); const partialTestInput = { [TEST_AGG_ID_1]: completeTestInput[TEST_AGG_ID_1], @@ -370,9 +444,10 @@ describe('SwapsController', function () { }; const topQuoteAndSavings = - await swapsController.getTopQuoteWithCalculatedSavings( - partialTestInput, - ); + await swapsController.getTopQuoteWithCalculatedSavings({ + quotes: partialTestInput, + networkClientId, + }); const topAggId = topQuoteAndSavings[0]; const resultQuotes = topQuoteAndSavings[1]; @@ -381,6 +456,14 @@ describe('SwapsController', function () { }); it('returns the top aggId, without best quote flagged, and quotes with fee values if passed necessary data but no custom convert rate exists', async function () { + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ chainId }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + const testInput = mapValues( getTopQuoteAndSavingsMockQuotes(), (quote) => ({ @@ -416,7 +499,10 @@ describe('SwapsController', function () { }; const topQuoteAndSavings = - await swapsController.getTopQuoteWithCalculatedSavings(testInput); + await swapsController.getTopQuoteWithCalculatedSavings({ + quotes: testInput, + networkClientId, + }); const topAggId = topQuoteAndSavings[0]; const resultQuotes = topQuoteAndSavings[1]; expect(topAggId).toStrictEqual(TEST_AGG_ID_1); @@ -424,6 +510,14 @@ describe('SwapsController', function () { }); it('returns the top aggId and quotes with savings and fee values if passed necessary data and the source token is ETH', async function () { + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ chainId }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + const testInput = mapValues( getTopQuoteAndSavingsMockQuotes(), (quote) => ({ @@ -483,9 +577,10 @@ describe('SwapsController', function () { }; const topQuoteAndSavings = - await swapsController.getTopQuoteWithCalculatedSavings( - testInput as Record, - ); + await swapsController.getTopQuoteWithCalculatedSavings({ + quotes: testInput as Record, + networkClientId, + }); const topAggId = topQuoteAndSavings[0]; const resultQuotes = topQuoteAndSavings[1]; expect(topAggId).toStrictEqual(TEST_AGG_ID_1); @@ -493,6 +588,14 @@ describe('SwapsController', function () { }); it('returns the top aggId and quotes with savings and fee values if passed necessary data and the source token is ETH and an ETH fee is included in the trade value of what would be the best quote', async function () { + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ chainId }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + const testInput = mapValues( getTopQuoteAndSavingsMockQuotes(), (quote) => ({ @@ -567,9 +670,10 @@ describe('SwapsController', function () { delete expectedResultQuotes[TEST_AGG_ID_1].savings; const topQuoteAndSavings = - await swapsController.getTopQuoteWithCalculatedSavings( - testInput as Record, - ); + await swapsController.getTopQuoteWithCalculatedSavings({ + quotes: testInput as Record, + networkClientId, + }); const topAggId = topQuoteAndSavings[0]; const resultQuotes = topQuoteAndSavings[1]; @@ -578,6 +682,14 @@ describe('SwapsController', function () { }); it('returns the top aggId and quotes with savings and fee values if passed necessary data and the source token is not ETH and an ETH fee is included in the trade value of what would be the best quote', async function () { + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ chainId }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + const testInput = getTopQuoteAndSavingsMockQuotes(); // 0.04 ETH fee included in trade value // @ts-expect-error - trade can be undefined but in this case since its mocked it will always be defined @@ -610,7 +722,10 @@ describe('SwapsController', function () { delete expectedResultQuotes[TEST_AGG_ID_1].savings; const topQuoteAndSavings = - await swapsController.getTopQuoteWithCalculatedSavings(testInput); + await swapsController.getTopQuoteWithCalculatedSavings({ + quotes: testInput, + networkClientId, + }); const topAggId = topQuoteAndSavings[0]; const resultQuotes = topQuoteAndSavings[1]; @@ -628,24 +743,25 @@ describe('SwapsController', function () { it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () { fetchTradesInfoStub.mockReset(); - const providerResultStub = { - // 1 gwei - eth_gasPrice: '0x0de0b6b3a7640000', - // by default, all accounts are external accounts (not contracts) - eth_getCode: '0x', - }; - const mainnetProvider = createTestProviderTools({ - scaffold: providerResultStub, + const chainId = CHAIN_IDS.MAINNET; + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ + scaffold: { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', + }, networkId: 1, - chainId: CHAIN_IDS.MAINNET as ChainId, - }).provider; - - swapsController = getSwapsController(mainnetProvider); + chainId: chainId as ChainId, + }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); - const fetchTradesInfoSpy = jest - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(swapsController as any, '_fetchTradesInfo') - .mockReturnValue(getMockQuotes()); + const fetchTradesInfo = jest.fn().mockReturnValue(getMockQuotes()); + swapsController = getSwapsController({ options: { fetchTradesInfo } }); // Make it so approval is not required jest @@ -661,7 +777,7 @@ describe('SwapsController', function () { const fetchResponse = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, - MOCK_FETCH_METADATA, + { ...MOCK_FETCH_METADATA, networkClientId }, ); if (!fetchResponse?.[0]) { @@ -699,38 +815,118 @@ describe('SwapsController', function () { }, }); - expect(fetchTradesInfoSpy).toHaveBeenCalledTimes(1); - expect(fetchTradesInfoSpy).toHaveBeenCalledWith(MOCK_FETCH_PARAMS, { - ...MOCK_FETCH_METADATA, + expect(fetchTradesInfo).toHaveBeenCalledTimes(1); + expect(fetchTradesInfo).toHaveBeenCalledWith(MOCK_FETCH_PARAMS, { + chainId, }); }); - it('calls returns the correct quotes on the optimism chain', async function () { + it('returns the correct quotes on the Optimism chain', async function () { fetchTradesInfoStub.mockReset(); - const OPTIMISM_MOCK_FETCH_METADATA = { - ...MOCK_FETCH_METADATA, - chainId: CHAIN_IDS.OPTIMISM as ChainId, - }; - const optimismProviderResultStub = { - // 1 gwei - eth_gasPrice: '0x0de0b6b3a7640000', - // by default, all accounts are external accounts (not contracts) - eth_getCode: '0x', - eth_call: - '0x000000000000000000000000000000000000000000000000000103c18816d4e8', - }; - const optimismProvider = createTestProviderTools({ - scaffold: optimismProviderResultStub, + const chainId = CHAIN_IDS.OPTIMISM; + const networkClientId = 'AAAA-BBBB-CCCC-DDDD'; + const { provider } = createTestProviderTools({ + scaffold: { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', + eth_call: + '0x000000000000000000000000000000000000000000000000000103c18816d4e8', + }, networkId: 10, - chainId: CHAIN_IDS.OPTIMISM as ChainId, - }).provider; + chainId: chainId as ChainId, + }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); - swapsController = getSwapsController(optimismProvider); + const fetchTradesInfo = jest.fn().mockReturnValue(getMockQuotes()); + swapsController = getSwapsController({ options: { fetchTradesInfo } }); - const fetchTradesInfoSpy = jest + // Make it so approval is not required + jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(swapsController as any, '_fetchTradesInfo') - .mockReturnValue(getMockQuotes()); + .spyOn(swapsController as any, '_getERC20Allowance') + .mockReturnValue(BigNumber.from(1)); + + // Make the network fetch error message disappear + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); + + const fetchResponse = await swapsController.fetchAndSetQuotes( + MOCK_FETCH_PARAMS, + { ...MOCK_FETCH_METADATA, networkClientId }, + ); + + if (!fetchResponse?.[0]) { + throw new Error('Quotes should be defined'); + } + + const [newQuotes] = fetchResponse; + + expect(newQuotes[TEST_AGG_ID_BEST]).toStrictEqual({ + ...getMockQuotes()[TEST_AGG_ID_BEST], + destinationTokenInfo: { + address: '0xSomeAddress', + symbol: 'FOO', + decimals: 18, + }, + isBestQuote: true, + // TODO: find a way to calculate these values dynamically + gasEstimate: '2000000', + gasEstimateWithRefund: '0xb8cae', + savings: { + fee: '-0.061067', + metaMaskFee: '0.50505050505050505050505050505050505', + performance: '6', + total: '5.43388249494949494949494949494949495', + medianMetaMaskFee: '0.444444444444444444444444444444444444', + }, + ethFee: '0.113536', + multiLayerL1TradeFeeTotal: '0x1', + overallValueOfQuote: '49.886464', + metaMaskFeeInEth: '0.50505050505050505050505050505050505', + ethValueOfTokens: '50', + sourceTokenInfo: { + address: '0xSomeOtherAddress', + decimals: 18, + symbol: 'BAR', + }, + }); + + expect(fetchTradesInfo).toHaveBeenCalledTimes(1); + expect(fetchTradesInfo).toHaveBeenCalledWith(MOCK_FETCH_PARAMS, { + chainId, + }); + }); + + it('returns the correct quotes on the Base chain', async function () { + fetchTradesInfoStub.mockReset(); + const chainId = CHAIN_IDS.BASE; + const networkClientId = 'AAAA-BBBB-CCCC-DDDD'; + const { provider } = createTestProviderTools({ + scaffold: { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', + eth_call: + '0x000000000000000000000000000000000000000000000000000103c18816d4e8', + }, + networkId: 8453, + chainId: chainId as ChainId, + }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + + const fetchTradesInfo = jest.fn().mockReturnValue(getMockQuotes()); + swapsController = getSwapsController({ options: { fetchTradesInfo } }); // Make it so approval is not required jest @@ -746,7 +942,7 @@ describe('SwapsController', function () { const fetchResponse = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, - OPTIMISM_MOCK_FETCH_METADATA, + { ...MOCK_FETCH_METADATA, networkClientId }, ); if (!fetchResponse?.[0]) { @@ -785,13 +981,98 @@ describe('SwapsController', function () { }, }); - expect(fetchTradesInfoSpy).toHaveBeenCalledTimes(1); - expect(fetchTradesInfoSpy).toHaveBeenCalledWith(MOCK_FETCH_PARAMS, { - ...OPTIMISM_MOCK_FETCH_METADATA, + expect(fetchTradesInfo).toHaveBeenCalledTimes(1); + expect(fetchTradesInfo).toHaveBeenCalledWith(MOCK_FETCH_PARAMS, { + chainId, + }); + }); + + it('copies network config from the Swaps API into state', async () => { + const chainId = '0x64' as const; // Gnosis + const networkClientId = InfuraNetworkType.mainnet; + const { provider } = createTestProviderTools({ + scaffold: { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', + }, + networkId: 100, + chainId: chainId as ChainId, + }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + const fetchWithCacheSpy = jest + .spyOn(fetchWithCacheModule, 'default') + .mockResolvedValue({ + refreshRates: { + quotes: 1, + quotesPrefetching: 2, + stxGetTransactions: 3, + stxBatchStatus: 4, + stxStatusDeadline: 5, + }, + parameters: { + stxMaxFeeMultiplier: 6, + }, + }); + + swapsController = getSwapsController(); + + await swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { + ...MOCK_FETCH_METADATA, + networkClientId, + }); + + expect(fetchWithCacheSpy).toHaveBeenCalledWith({ + url: 'https://swap.api.cx.metamask.io/networks/100', + fetchOptions: { + method: 'GET', + }, + cacheOptions: { + cacheRefreshTime: 600000, + }, + functionName: '_fetchSwapsNetworkConfig', + }); + expect(swapsController.state.swapsState).toMatchObject({ + swapsQuoteRefreshTime: 1000, + swapsQuotePrefetchingRefreshTime: 2000, + swapsStxGetTransactionsRefreshTime: 3000, + swapsStxBatchStatusRefreshTime: 4000, + swapsStxMaxFeeMultiplier: 6, + swapsStxStatusDeadline: 5, }); }); it('performs the allowance check', async function () { + const chainId = CHAIN_IDS.OPTIMISM; + const networkClientId = 'AAAA-BBBB-CCCC-DDDD'; + const { provider } = createTestProviderTools({ + scaffold: MOCK_PROVIDER_RESULT_STUB, + networkId: 10, + chainId: chainId as ChainId, + }); + const networkClient = { provider, configuration: { chainId } }; + mockNetworkControllerGetNetworkClientById({ + [networkClientId]: networkClient, + }); + const ethersProvider = new ethersProviders.Web3Provider(provider); + jest + .spyOn(ethersProviders, 'Web3Provider') + .mockImplementation((givenProvider) => { + if (givenProvider === provider) { + return ethersProvider; + } + throw new Error('Could not create a Web3Provider'); + }); + + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_fetchTradesInfo') + .mockReturnValue(getMockQuotes()); + // Make it so approval is not required const getERC20AllowanceSpy = jest // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -804,16 +1085,21 @@ describe('SwapsController', function () { .spyOn(swapsController as any, '_setSwapsNetworkConfig') .mockReturnValue(undefined); - await swapsController.fetchAndSetQuotes( - MOCK_FETCH_PARAMS, - MOCK_FETCH_METADATA, - ); + await swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { + ...MOCK_FETCH_METADATA, + networkClientId, + }); expect(getERC20AllowanceSpy).toHaveBeenCalledTimes(1); expect(getERC20AllowanceSpy).toHaveBeenCalledWith( MOCK_FETCH_PARAMS.sourceToken, MOCK_FETCH_PARAMS.fromAddress, - CHAIN_IDS.MAINNET, + { + client: networkClient, + clientId: networkClientId, + chainId, + ethersProvider, + }, ); }); diff --git a/app/scripts/controllers/swaps/swaps.types.ts b/app/scripts/controllers/swaps/swaps.types.ts index ca059723277a..76da1fb7a6b9 100644 --- a/app/scripts/controllers/swaps/swaps.types.ts +++ b/app/scripts/controllers/swaps/swaps.types.ts @@ -1,13 +1,12 @@ -import { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers'; import { TokenRatesControllerGetStateAction } from '@metamask/assets-controllers'; import { ControllerGetStateAction, ControllerStateChangeEvent, RestrictedControllerMessenger, } from '@metamask/base-controller'; -import type { ChainId } from '@metamask/controller-utils'; import { GasFeeState } from '@metamask/gas-fee-controller'; import { + NetworkClientId, NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, } from '@metamask/network-controller'; @@ -312,7 +311,7 @@ export type FetchTradesInfoParams = { }; export type FetchTradesInfoParamsMetadata = { - chainId: ChainId; + networkClientId: NetworkClientId; sourceTokenInfo: { address: string; symbol: string; @@ -348,11 +347,10 @@ export type SwapsControllerOptions = { }, factor: number, ) => Promise<{ gasLimit: string; simulationFails: boolean }>; - provider: ExternalProvider | JsonRpcFetchFunc; fetchTradesInfo: typeof defaultFetchTradesInfo; getLayer1GasFee: (params: { transactionParams: TransactionParams; - chainId: ChainId; + networkClientId: NetworkClientId; }) => Promise; getEIP1559GasFeeEstimates: () => Promise; trackMetaMetricsEvent: (event: { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 676595dfbff3..ac633bbc8e5d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2143,7 +2143,6 @@ export default class MetamaskController extends EventEmitter { this.swapsController = new SwapsController( { messenger: swapsControllerMessenger, - provider: this.provider, // TODO: Remove once TransactionController exports this action type getBufferedGasLimit: async (txMeta, multiplier) => { const { gas: gasLimit, simulationFails } = diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index f87946a09c0f..a5581cd16a85 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -893,6 +893,14 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "uuid": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1780,11 +1788,11 @@ "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-provider": true, "@metamask/eth-query": true, "@metamask/network-controller>@metamask/eth-block-tracker": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, @@ -1917,14 +1925,6 @@ "semver": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/json-rpc-engine": true, - "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "uuid": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index f87946a09c0f..a5581cd16a85 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -893,6 +893,14 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "uuid": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1780,11 +1788,11 @@ "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-provider": true, "@metamask/eth-query": true, "@metamask/network-controller>@metamask/eth-block-tracker": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, @@ -1917,14 +1925,6 @@ "semver": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/json-rpc-engine": true, - "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "uuid": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index f87946a09c0f..a5581cd16a85 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -893,6 +893,14 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "uuid": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1780,11 +1788,11 @@ "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-provider": true, "@metamask/eth-query": true, "@metamask/network-controller>@metamask/eth-block-tracker": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, @@ -1917,14 +1925,6 @@ "semver": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/json-rpc-engine": true, - "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "uuid": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 1bad9f3288a2..672c34d8faea 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -985,6 +985,14 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "uuid": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1872,11 +1880,11 @@ "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-provider": true, "@metamask/eth-query": true, "@metamask/network-controller>@metamask/eth-block-tracker": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, @@ -2009,14 +2017,6 @@ "semver": true } }, - "@metamask/network-controller>@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/json-rpc-engine": true, - "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "uuid": true - } - }, "@metamask/network-controller>@metamask/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, diff --git a/package.json b/package.json index fb6218ebb5c6..2f7498a1d537 100644 --- a/package.json +++ b/package.json @@ -471,6 +471,7 @@ "@metamask/eslint-config-nodejs": "^9.0.0", "@metamask/eslint-config-typescript": "^9.0.1", "@metamask/eslint-plugin-design-tokens": "^1.1.0", + "@metamask/eth-json-rpc-provider": "^4.1.6", "@metamask/forwarder": "^1.1.0", "@metamask/phishing-warning": "^4.1.0", "@metamask/preferences-controller": "^13.0.2", diff --git a/test/stub/provider.js b/test/stub/provider.js index f86762218adf..ff5e09944be5 100644 --- a/test/stub/provider.js +++ b/test/stub/provider.js @@ -3,7 +3,9 @@ import { createScaffoldMiddleware, } from '@metamask/json-rpc-engine'; import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware'; +import { providerFromEngine } from '@metamask/eth-json-rpc-provider'; import Ganache from 'ganache'; +import { CHAIN_IDS } from '../../shared/constants/network'; export function getTestSeed() { return 'people carpet cluster attract ankle motor ozone mass dove original primary mask'; @@ -39,22 +41,17 @@ export function createEngineForTestData() { return new JsonRpcEngine(); } -export function providerFromEngine(engine) { - const provider = { sendAsync: engine.handle.bind(engine) }; - return provider; -} - export function createTestProviderTools(opts = {}) { const engine = createEngineForTestData(); // handle provided hooks - engine.push(createScaffoldMiddleware(opts.scaffold || {})); + engine.push(createScaffoldMiddleware(opts.scaffold ?? {})); // handle block tracker methods engine.push( providerAsMiddleware( Ganache.provider({ mnemonic: getTestSeed(), - network_id: opts.networkId, - chain: { chainId: opts.chainId }, + network_id: opts.networkId ?? 1, + chain: { chainId: opts.chainId ?? CHAIN_IDS.MAINNET }, hardfork: 'muirGlacier', }), ), diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index d23c0ce69381..4886d1dbdad7 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -65,8 +65,8 @@ import { isHardwareWallet, getHardwareWalletType, checkNetworkAndAccountSupports1559, - getSelectedNetworkClientId, getSelectedInternalAccount, + getSelectedNetwork, } from '../../selectors'; import { getSmartTransactionsEnabled, @@ -628,14 +628,14 @@ export const fetchQuotesAndSetQuoteState = ( ) => { return async (dispatch, getState) => { const state = getState(); - const chainId = getCurrentChainId(state); + const selectedNetwork = getSelectedNetwork(state); let swapsLivenessForNetwork = { swapsFeatureIsLive: false, }; try { const swapsFeatureFlags = await fetchSwapsFeatureFlags(); swapsLivenessForNetwork = getSwapsLivenessForNetwork( - chainId, + selectedNetwork.configuration.chainId, swapsFeatureFlags, ); } catch (error) { @@ -650,7 +650,6 @@ export const fetchQuotesAndSetQuoteState = ( const fetchParams = getFetchParams(state); const selectedAccount = getSelectedAccount(state); - const networkClientId = getSelectedNetworkClientId(state); const balanceError = getBalanceError(state); const swapsDefaultToken = getSwapsDefaultToken(state); const fetchParamsFromToken = @@ -697,7 +696,7 @@ export const fetchQuotesAndSetQuoteState = ( symbol: toTokenSymbol, decimals: toTokenDecimals, image: toTokenIconUrl, - networkClientId, + networkClientId: selectedNetwork.clientId, }, true, ), @@ -725,7 +724,7 @@ export const fetchQuotesAndSetQuoteState = ( symbol: fromTokenSymbol, decimals: fromTokenDecimals, image: fromTokenIconUrl, - networkClientId, + networkClientId: selectedNetwork.clientId, }, true, ), @@ -791,7 +790,7 @@ export const fetchQuotesAndSetQuoteState = ( sourceTokenInfo, destinationTokenInfo, accountBalance: selectedAccount.balance, - chainId, + networkClientId: selectedNetwork.clientId, }, ), ); @@ -897,7 +896,7 @@ export const signAndSendSwapsSmartTransaction = ({ const { sourceTokenInfo = {}, destinationTokenInfo = {} } = metaData; const usedQuote = getUsedQuote(state); const swapsNetworkConfig = getSwapsNetworkConfig(state); - const chainId = getCurrentChainId(state); + const selectedNetwork = getSelectedNetwork(state); dispatch( setSmartTransactionsRefreshInterval( @@ -948,7 +947,12 @@ export const signAndSendSwapsSmartTransaction = ({ sensitiveProperties: swapMetaData, }); - if (!isContractAddressValid(usedTradeTxParams.to, chainId)) { + if ( + !isContractAddressValid( + usedTradeTxParams.to, + selectedNetwork.configuration.chainId, + ) + ) { captureMessage('Invalid contract address', { extra: { token_from: swapMetaData.token_from, @@ -993,7 +997,7 @@ export const signAndSendSwapsSmartTransaction = ({ updatedApproveTxParams.gas = `0x${decimalToHex( fees.approvalTxFees?.gasLimit || 0, )}`; - updatedApproveTxParams.chainId = chainId; + updatedApproveTxParams.chainId = selectedNetwork.configuration.chainId; approvalTxUuid = await dispatch( signAndSendSmartTransaction({ unsignedTransaction: updatedApproveTxParams, @@ -1004,7 +1008,7 @@ export const signAndSendSwapsSmartTransaction = ({ unsignedTransaction.gas = `0x${decimalToHex( fees.tradeTxFees?.gasLimit || 0, )}`; - unsignedTransaction.chainId = chainId; + unsignedTransaction.chainId = selectedNetwork.configuration.chainId; const uuid = await dispatch( signAndSendSmartTransaction({ unsignedTransaction, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 9852d4ae7d25..96e880d52aef 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2139,6 +2139,35 @@ export const getCurrentNetwork = createDeepEqualSelector( }, ); +export const getSelectedNetwork = createDeepEqualSelector( + getSelectedNetworkClientId, + getNetworkConfigurationsByChainId, + (selectedNetworkClientId, networkConfigurationsByChainId) => { + if (selectedNetworkClientId === undefined) { + throw new Error('No network is selected'); + } + + // TODO: Add `networkConfigurationsByNetworkClientId` to NetworkController state so this is easier to do + const possibleNetworkConfiguration = Object.values( + networkConfigurationsByChainId, + ).find((networkConfiguration) => { + return networkConfiguration.rpcEndpoints.some((rpcEndpoint) => { + return rpcEndpoint.networkClientId === selectedNetworkClientId; + }); + }); + if (possibleNetworkConfiguration === undefined) { + throw new Error( + 'Could not find network configuration for selected network client', + ); + } + + return { + configuration: possibleNetworkConfiguration, + clientId: selectedNetworkClientId, + }; + }, +); + export const getConnectedSitesListWithNetworkInfo = createDeepEqualSelector( getConnectedSitesList, getAllDomains, diff --git a/yarn.lock b/yarn.lock index 527af97c08f5..adf55eaa9e85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6460,9 +6460,9 @@ __metadata: linkType: hard "@metamask/slip44@npm:^4.0.0": - version: 4.1.0 - resolution: "@metamask/slip44@npm:4.1.0" - checksum: 10/4265254a1800a24915bd1de15f86f196737132f9af2a084c2efc885decfc5dd87ad8f0687269d90b35e2ec64d3ea4fbff0caa793bcea6e585b1f3a290952b750 + version: 4.0.0 + resolution: "@metamask/slip44@npm:4.0.0" + checksum: 10/3e47e8834b0fbdabe1f126fd78665767847ddc1f9ccc8defb23007dd71fcd2e4899c8ca04857491be3630668a3765bad1e40fdfca9a61ef33236d8d08e51535e languageName: node linkType: hard @@ -26838,6 +26838,7 @@ __metadata: "@metamask/eslint-plugin-design-tokens": "npm:^1.1.0" "@metamask/eth-json-rpc-filters": "npm:^9.0.0" "@metamask/eth-json-rpc-middleware": "patch:@metamask/eth-json-rpc-middleware@npm%3A14.0.1#~/.yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch" + "@metamask/eth-json-rpc-provider": "npm:^4.1.6" "@metamask/eth-ledger-bridge-keyring": "npm:^5.0.1" "@metamask/eth-query": "npm:^4.0.0" "@metamask/eth-sig-util": "npm:^7.0.1" From 08cc205443af87abe258849a92cd7b2959eec9f9 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 21 Nov 2024 20:09:07 +0100 Subject: [PATCH 22/28] fix: fix test networks display for portfolio view (#28601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes the display in the asset page and in the main token list when the price checker setting is off [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28601?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28594 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/a1213b6a-3f23-49f5-be15-a0b369b9016e ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../app/assets/token-cell/token-cell.tsx | 17 +++++++---------- .../app/assets/token-list/token-list.tsx | 18 +++++++++++++++++- ui/pages/asset/components/asset-page.tsx | 17 ++++++++++++++++- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx index 56d6555258bd..d470ef3dc21a 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -84,17 +84,14 @@ export default function TokenCell({ image; const secondaryThreshold = 0.01; - // Format for fiat balance with currency style - const secondary = formatWithThreshold( - tokenFiatAmount, - secondaryThreshold, - locale, - { - style: 'currency', - currency: currentCurrency.toUpperCase(), - }, - ); + const secondary = + tokenFiatAmount === null + ? undefined + : formatWithThreshold(tokenFiatAmount, secondaryThreshold, locale, { + style: 'currency', + currency: currentCurrency.toUpperCase(), + }); const primary = formatAmount( locale, diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 08b5675dfe94..3f094634743e 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -7,6 +7,7 @@ import { sortAssets } from '../util/sort'; import { getCurrencyRates, getCurrentNetwork, + getIsTestnet, getMarketData, getNetworkConfigurationIdByChainId, getNewTokensImported, @@ -14,6 +15,7 @@ import { getSelectedAccount, getSelectedAccountNativeTokenCachedBalanceByChainId, getSelectedAccountTokensAcrossChains, + getShowFiatInTestnets, getTokenExchangeRates, } from '../../../../selectors'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; @@ -24,6 +26,8 @@ import { endTrace, TraceName } from '../../../../../shared/lib/trace'; import { useTokenBalances } from '../../../../hooks/useTokenBalances'; import { setTokenNetworkFilter } from '../../../../store/actions'; import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; +import { getMultichainShouldShowFiat } from '../../../../selectors/multichain'; type TokenListProps = { onTokenClick: (chainId: string, address: string) => void; @@ -212,6 +216,18 @@ export default function TokenList({ console.log(t('loadingTokens')); } + // Check if testnet + const isTestnet = useSelector(getIsTestnet); + const shouldShowFiat = useMultichainSelector( + getMultichainShouldShowFiat, + selectedAccount, + ); + const isMainnet = !isTestnet; + // Check if show conversion is enabled + const showFiatInTestnets = useSelector(getShowFiatInTestnets); + const showFiat = + shouldShowFiat && (isMainnet || (isTestnet && showFiatInTestnets)); + return (
{sortedFilteredTokens.map((tokenData) => ( @@ -220,7 +236,7 @@ export default function TokenList({ chainId={tokenData.chainId} address={tokenData.address} symbol={tokenData.symbol} - tokenFiatAmount={tokenData.tokenFiatAmount} + tokenFiatAmount={showFiat ? tokenData.tokenFiatAmount : null} image={tokenData?.image} isNative={tokenData.isNative} string={tokenData.string} diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index d7ad8e568f4e..19ce592f0071 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -18,6 +18,8 @@ import { getCurrencyRates, getSelectedAccountNativeTokenCachedBalanceByChainId, getSelectedAccount, + getIsTestnet, + getShowFiatInTestnets, } from '../../../selectors'; import { Display, @@ -49,6 +51,8 @@ import CoinButtons from '../../../components/app/wallet-overview/coin-buttons'; import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import { calculateTokenBalance } from '../../../components/app/assets/util/calculateTokenBalance'; import { useTokenBalances } from '../../../hooks/useTokenBalances'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; +import { getMultichainShouldShowFiat } from '../../../selectors/multichain'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; @@ -109,6 +113,17 @@ const AssetPage = ({ const marketData = useSelector(getMarketData); const currencyRates = useSelector(getCurrencyRates); + const isTestnet = useSelector(getIsTestnet); + const shouldShowFiat = useMultichainSelector( + getMultichainShouldShowFiat, + selectedAccount, + ); + const isMainnet = !isTestnet; + // Check if show conversion is enabled + const showFiatInTestnets = useSelector(getShowFiatInTestnets); + const showFiat = + shouldShowFiat && (isMainnet || (isTestnet && showFiatInTestnets)); + const nativeBalances: Record = useSelector( getSelectedAccountNativeTokenCachedBalanceByChainId, ) as Record; @@ -254,7 +269,7 @@ const AssetPage = ({ chainId={chainId} symbol={symbol} image={image} - tokenFiatAmount={tokenFiatAmount} + tokenFiatAmount={showFiat ? tokenFiatAmount : null} string={balance?.toString()} /> Date: Thu, 21 Nov 2024 21:44:49 +0100 Subject: [PATCH 23/28] feat: multichain token detection (#28380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** TokenDetectionController is responsible for detecting and keeping an updated list of all tokens across supported chains. This dataset is stored in the detectedTokens state variable within Metamask’s state. After completing this task, token detection will be enhanced by implementing periodic polling across all networks linked to the wallet, resulting in a more comprehensive dataset available to users. For each network added to the wallet, the polling loop will receive the network as a parameter and execute token autodetection for it. Once results are available, they will be stored in detectedTokensAllChains, organized by chainId. This approach enables us to retrieve a comprehensive list of detected tokens across all networks. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28258?quickstart=1) ## **Related issues** Fixes: [#3431](https://github.com/MetaMask/MetaMask-planning/issues/3431) ## **Manual testing steps** 1. install dependencies using `yarn` 2. start the project using` PORTFOLIO_VIEW=1 yarn start` 3. add other networks where you have tokens 4. the autodetection should be multichain ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/a0db910c-b3a3-456d-b1b9-0c6a3e472976 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .storybook/test-data.js | 7 + ...ts-controllers-npm-44.1.0-012aa448d8.patch | 71 +++++++ package.json | 2 +- .../app/assets/asset-list/asset-list.tsx | 44 ++++- .../detected-token-details.js | 6 +- .../detected-token-selection-popover.js | 85 ++++++-- ...etected-token-selection-popover.stories.js | 5 + .../detected-token-values.js | 24 ++- .../detected-token-values.stories.js | 27 ++- .../app/detected-token/detected-token.js | 184 +++++++++++++++--- .../app/detected-token/detected-token.test.js | 14 ++ .../hide-token-confirmation-modal.js | 37 +++- .../hide-token-confirmation-modal.test.js | 1 + .../detected-token-banner.js | 39 +++- .../import-nfts-modal/import-nfts-modal.js | 1 + ui/hooks/useTokenFiatAmount.js | 37 +++- ui/selectors/selectors.js | 36 ++++ ui/store/actions.test.js | 4 +- ui/store/actions.ts | 8 +- yarn.lock | 57 +----- 20 files changed, 556 insertions(+), 133 deletions(-) create mode 100644 .yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch diff --git a/.storybook/test-data.js b/.storybook/test-data.js index a36cbf944981..717109b77dac 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -525,6 +525,13 @@ const state = { decimals: 18, }, ], + tokenBalances: { + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { + '0x1': { + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0x25e4bc', + }, + }, + }, allDetectedTokens: { '0xaa36a7': { '0x9d0ba4ddac06032527b140912ec808ab9451b788': [ diff --git a/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch b/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch new file mode 100644 index 000000000000..faf388c89741 --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch @@ -0,0 +1,71 @@ +diff --git a/dist/assetsUtil.cjs b/dist/assetsUtil.cjs +index 48571b8c1b78e94d88e1837e986b5f8735ac651b..61246f51500c8cab48f18296a73629fb73454caa 100644 +--- a/dist/assetsUtil.cjs ++++ b/dist/assetsUtil.cjs +@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); ++function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } + exports.fetchTokenContractExchangeRates = exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedStakedBalanceNetworks = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.hasNewCollectionFields = exports.compareNftMetadata = exports.TOKEN_PRICES_BATCH_SIZE = void 0; + const controller_utils_1 = require("@metamask/controller-utils"); + const utils_1 = require("@metamask/utils"); +@@ -233,7 +234,7 @@ async function getIpfsCIDv1AndPath(ipfsUrl) { + const index = url.indexOf('/'); + const cid = index !== -1 ? url.substring(0, index) : url; + const path = index !== -1 ? url.substring(index) : undefined; +- const { CID } = await import("multiformats"); ++ const { CID } = _interopRequireWildcard(require("multiformats")); + // We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) + // because most cid v0s appear to be incompatible with IPFS subdomains + return { +diff --git a/dist/token-prices-service/codefi-v2.mjs b/dist/token-prices-service/codefi-v2.mjs +index e7eaad2cfa8b233c4fd42a51f745233a1cc5c387..bf8ec7819f678c2f185d6a85d7e3ea81f055a309 100644 +--- a/dist/token-prices-service/codefi-v2.mjs ++++ b/dist/token-prices-service/codefi-v2.mjs +@@ -12,8 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function ( + var _CodefiTokenPricesServiceV2_tokenPricePolicy; + import { handleFetch } from "@metamask/controller-utils"; + import { hexToNumber } from "@metamask/utils"; +-import $cockatiel from "cockatiel"; +-const { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } = $cockatiel; ++import { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } from "cockatiel" + /** + * The list of currencies that can be supplied as the `vsCurrency` parameter to + * the `/spot-prices` endpoint, in lowercase form. +diff --git a/dist/TokenDetectionController.cjs b/dist/TokenDetectionController.cjs +index 8fd5efde7a3c24080f8a43f79d10300e8c271245..a3c334ac7dd2e5698e6b54a73491b7145c2a9010 100644 +--- a/dist/TokenDetectionController.cjs ++++ b/dist/TokenDetectionController.cjs +@@ -250,17 +250,20 @@ _TokenDetectionController_intervalId = new WeakMap(), _TokenDetectionController_ + } + }); + this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange', +- // TODO: Either fix this lint violation or explain why it's necessary to ignore. +- // eslint-disable-next-line @typescript-eslint/no-misused-promises +- async (selectedAccount) => { +- const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id; +- if (isSelectedAccountIdChanged) { +- __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f"); +- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { +- selectedAddress: selectedAccount.address, +- }); +- } +- }); ++ // TODO: Either fix this lint violation or explain why it's necessary to ignore. ++ // eslint-disable-next-line @typescript-eslint/no-misused-promises ++ async (selectedAccount) => { ++ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState'); ++ const chainIds = Object.keys(networkConfigurationsByChainId); ++ const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id; ++ if (isSelectedAccountIdChanged) { ++ __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f"); ++ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { ++ selectedAddress: selectedAccount.address, ++ chainIds, ++ }); ++ } ++ }); + }, _TokenDetectionController_stopPolling = function _TokenDetectionController_stopPolling() { + if (__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f")) { + clearInterval(__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f")); diff --git a/package.json b/package.json index 2f7498a1d537..c12b8b40cdfb 100644 --- a/package.json +++ b/package.json @@ -293,7 +293,7 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@patch%3A@metamask/assets-controllers@npm%253A44.0.0%23~/.yarn/patches/@metamask-assets-controllers-npm-44.0.0-c223d56176.patch%3A%3Aversion=44.0.0&hash=5a94c2#~/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A44.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.8.2", "@metamask/browser-passworder": "^4.3.0", diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 068813ee71c8..114ced0496ca 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -4,8 +4,11 @@ import TokenList from '../token-list'; import { PRIMARY } from '../../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; import { + getAllDetectedTokensForSelectedAddress, getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, + getNetworkConfigurationsByChainId, + getPreferences, getSelectedAccount, } from '../../../../selectors'; import { @@ -76,6 +79,17 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, ); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const { tokenNetworkFilter } = useSelector(getPreferences); + const allOpts: Record = {}; + Object.keys(allNetworks || {}).forEach((chainId) => { + allOpts[chainId] = true; + }); + + const allNetworksFilterShown = + Object.keys(tokenNetworkFilter || {}).length !== + Object.keys(allOpts || {}).length; + const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); const [showReceiveModal, setShowReceiveModal] = useState(false); @@ -98,16 +112,30 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { // for EVM assets const shouldShowTokensLinks = showTokensLinks ?? isEvm; + const detectedTokensMultichain = useSelector( + getAllDetectedTokensForSelectedAddress, + ); + + const totalTokens = + process.env.PORTFOLIO_VIEW && !allNetworksFilterShown + ? (Object.values(detectedTokensMultichain).reduce( + // @ts-expect-error TS18046: 'tokenArray' is of type 'unknown' + (count, tokenArray) => count + tokenArray.length, + 0, + ) as number) + : detectedTokens.length; + return ( <> - {detectedTokens.length > 0 && - !isTokenDetectionInactiveOnNonMainnetSupportedNetwork && ( - setShowDetectedTokens(true)} - margin={4} - /> - )} + {totalTokens && + totalTokens > 0 && + !isTokenDetectionInactiveOnNonMainnetSupportedNetwork ? ( + setShowDetectedTokens(true)} + margin={4} + /> + ) : null} { const tokenList = useSelector(getTokenList); const tokenData = tokenList[token.address?.toLowerCase()]; const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); const currentNetwork = useSelector(getCurrentNetwork); - return ( } @@ -84,6 +85,7 @@ DetectedTokenDetails.propTypes = { }), handleTokenSelection: PropTypes.func.isRequired, tokensListDetected: PropTypes.object, + chainId: PropTypes.string, }; export default DetectedTokenDetails; diff --git a/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js b/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js index 0229173050d8..5974d8ba71b6 100644 --- a/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js +++ b/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; @@ -10,8 +10,12 @@ import { MetaMetricsTokenEventSource, } from '../../../../../shared/constants/metametrics'; import { + getAllDetectedTokensForSelectedAddress, getCurrentChainId, + getCurrentNetwork, getDetectedTokensInCurrentNetwork, + getNetworkConfigurationsByChainId, + getPreferences, } from '../../../../selectors'; import Popover from '../../../ui/popover'; @@ -34,10 +38,38 @@ const DetectedTokenSelectionPopover = ({ const chainId = useSelector(getCurrentChainId); const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const { tokenNetworkFilter } = useSelector(getPreferences); + const allOpts = {}; + Object.keys(allNetworks || {}).forEach((networkId) => { + allOpts[networkId] = true; + }); + + const allNetworksFilterShown = + Object.keys(tokenNetworkFilter || {}).length !== + Object.keys(allOpts || {}).length; + + const currentNetwork = useSelector(getCurrentNetwork); + + const detectedTokensMultichain = useSelector( + getAllDetectedTokensForSelectedAddress, + ); + + const totalTokens = useMemo(() => { + return process.env.PORTFOLIO_VIEW && !allNetworksFilterShown + ? Object.values(detectedTokensMultichain).reduce( + (count, tokenArray) => count + tokenArray.length, + 0, + ) + : detectedTokens.length; + }, [detectedTokensMultichain, detectedTokens, allNetworksFilterShown]); + const { selected: selectedTokens = [] } = sortingBasedOnTokenSelection(tokensListDetected); const onClose = () => { + const chainIds = Object.keys(detectedTokensMultichain); + setShowDetectedTokens(false); const eventTokensDetails = detectedTokens.map( ({ address, symbol }) => `${symbol} - ${address}`, @@ -47,8 +79,10 @@ const DetectedTokenSelectionPopover = ({ category: MetaMetricsEventCategory.Wallet, properties: { source_connection_method: MetaMetricsTokenEventSource.Detected, - chain_id: chainId, tokens: eventTokensDetails, + ...(process.env.PORTFOLIO_VIEW + ? { chain_ids: chainIds } + : { chain_id: chainId }), }, }); }; @@ -81,25 +115,44 @@ const DetectedTokenSelectionPopover = ({ - - {detectedTokens.map((token, index) => { - return ( - - ); - })} - + {process.env.PORTFOLIO_VIEW && !allNetworksFilterShown ? ( + + {Object.entries(detectedTokensMultichain).map( + ([networkId, tokens]) => { + return tokens.map((token, index) => ( + + )); + }, + )} + + ) : ( + + {detectedTokens.map((token, index) => { + return ( + + ); + })} + + )} ); }; diff --git a/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.stories.js b/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.stories.js index 525e88fc2785..5d7def0f28e9 100644 --- a/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.stories.js +++ b/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.stories.js @@ -11,6 +11,11 @@ const store = configureStore({ ...testData, metamask: { ...testData.metamask, + currencyRates: { + SepoliaETH: { + conversionRate: 3910.28, + }, + }, ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), }, }); diff --git a/ui/components/app/detected-token/detected-token-values/detected-token-values.js b/ui/components/app/detected-token/detected-token-values/detected-token-values.js index 667c356b0e1f..07c70edcf196 100644 --- a/ui/components/app/detected-token/detected-token-values/detected-token-values.js +++ b/ui/components/app/detected-token/detected-token-values/detected-token-values.js @@ -7,10 +7,14 @@ import { TextColor, TextVariant, } from '../../../../helpers/constants/design-system'; -import { useTokenTracker } from '../../../../hooks/useTokenTracker'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; -import { getUseCurrencyRateCheck } from '../../../../selectors'; +import { + getCurrentChainId, + getSelectedAddress, + getUseCurrencyRateCheck, +} from '../../../../selectors'; import { Box, Checkbox, Text } from '../../../component-library'; +import { useTokenTracker } from '../../../../hooks/useTokenBalances'; const DetectedTokenValues = ({ token, @@ -21,12 +25,25 @@ const DetectedTokenValues = ({ return tokensListDetected[token.address]?.selected; }); - const { tokensWithBalances } = useTokenTracker({ tokens: [token] }); + const selectedAddress = useSelector(getSelectedAddress); + const currentChainId = useSelector(getCurrentChainId); + const chainId = token.chainId ?? currentChainId; + + const { tokensWithBalances } = useTokenTracker({ + chainId, + tokens: [token], + address: selectedAddress, + hideZeroBalanceTokens: false, + }); + const balanceString = tokensWithBalances[0]?.string; const formattedFiatBalance = useTokenFiatAmount( token.address, balanceString, token.symbol, + {}, + false, + chainId, ); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); @@ -73,6 +90,7 @@ DetectedTokenValues.propTypes = { symbol: PropTypes.string, iconUrl: PropTypes.string, aggregators: PropTypes.array, + chainId: PropTypes.string, }), handleTokenSelection: PropTypes.func.isRequired, tokensListDetected: PropTypes.object, diff --git a/ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js b/ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js index cf5039a7c2a6..32402631b7bf 100644 --- a/ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js +++ b/ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js @@ -1,14 +1,16 @@ import React from 'react'; - +import { Provider } from 'react-redux'; +import testData from '../../../../../.storybook/test-data'; +import configureStore from '../../../../store/store'; import DetectedTokenValues from './detected-token-values'; export default { title: 'Components/App/DetectedToken/DetectedTokenValues', - + component: DetectedTokenValues, argTypes: { token: { control: 'object' }, - handleTokenSelection: { control: 'func' }, - tokensListDetected: { control: 'array' }, + handleTokenSelection: { action: 'handleTokenSelection' }, // Action for interactions + tokensListDetected: { control: 'object' }, }, args: { token: { @@ -73,10 +75,21 @@ export default { }, }; -const Template = (args) => { - return ; +// Mock store data +const customData = { + ...testData, + metamask: { + ...testData.metamask, + }, }; -export const DefaultStory = Template.bind({}); +const customStore = configureStore(customData); + +const Template = (args) => ( + + + +); +export const DefaultStory = Template.bind({}); DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/detected-token/detected-token.js b/ui/components/app/detected-token/detected-token.js index 3d9038cc52b9..31e100547e12 100644 --- a/ui/components/app/detected-token/detected-token.js +++ b/ui/components/app/detected-token/detected-token.js @@ -1,4 +1,4 @@ -import React, { useState, useContext } from 'react'; +import React, { useState, useContext, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useSelector, useDispatch } from 'react-redux'; import { chain } from 'lodash'; @@ -9,8 +9,11 @@ import { setNewTokensImported, } from '../../../store/actions'; import { + getAllDetectedTokensForSelectedAddress, getCurrentChainId, getDetectedTokensInCurrentNetwork, + getNetworkConfigurationsByChainId, + getPreferences, getSelectedNetworkClientId, } from '../../../selectors'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -38,8 +41,8 @@ const sortingBasedOnTokenSelection = (tokensDetected) => { // ditch the 'selected' property and get just the tokens' .mapValues((group) => group.map(({ token }) => { - const { address, symbol, decimals, aggregators } = token; - return { address, symbol, decimals, aggregators }; + const { address, symbol, decimals, aggregators, chainId } = token; + return { address, symbol, decimals, aggregators, chainId }; }), ) // Exit the chain and get the underlying value, an object. @@ -51,16 +54,68 @@ const DetectedToken = ({ setShowDetectedTokens }) => { const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); - const chainId = useSelector(getCurrentChainId); const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); const networkClientId = useSelector(getSelectedNetworkClientId); - - const [tokensListDetected, setTokensListDetected] = useState(() => - detectedTokens.reduce((tokenObj, token) => { - tokenObj[token.address] = { token, selected: true }; - return tokenObj; - }, {}), + const detectedTokensMultichain = useSelector( + getAllDetectedTokensForSelectedAddress, ); + const currentChainId = useSelector(getCurrentChainId); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const { tokenNetworkFilter } = useSelector(getPreferences); + const allOpts = {}; + Object.keys(allNetworks || {}).forEach((chainId) => { + allOpts[chainId] = true; + }); + + const allNetworksFilterShown = + Object.keys(tokenNetworkFilter || {}).length !== + Object.keys(allOpts || {}).length; + + const totalDetectedTokens = useMemo(() => { + return process.env.PORTFOLIO_VIEW && !allNetworksFilterShown + ? Object.values(detectedTokensMultichain).flat().length + : detectedTokens.length; + }, [detectedTokens, detectedTokensMultichain, allNetworksFilterShown]); + + const [tokensListDetected, setTokensListDetected] = useState({}); + + useEffect(() => { + const newTokensList = () => { + if (process.env.PORTFOLIO_VIEW && !allNetworksFilterShown) { + return Object.entries(detectedTokensMultichain).reduce( + (acc, [chainId, tokens]) => { + if (Array.isArray(tokens)) { + tokens.forEach((token) => { + acc[token.address] = { + token: { ...token, chainId }, + selected: tokensListDetected[token.address]?.selected ?? true, + }; + }); + } + return acc; + }, + {}, + ); + } + + return detectedTokens.reduce((tokenObj, token) => { + tokenObj[token.address] = { + token, + selected: tokensListDetected[token.address]?.selected ?? true, + chainId: currentChainId, + }; + return tokenObj; + }, {}); + }; + + setTokensListDetected(newTokensList()); + }, [ + allNetworksFilterShown, + detectedTokensMultichain, + detectedTokens, + currentChainId, + ]); + const [showDetectedTokenIgnoredPopover, setShowDetectedTokenIgnoredPopover] = useState(false); const [partiallyIgnoreDetectedTokens, setPartiallyIgnoreDetectedTokens] = @@ -79,22 +134,53 @@ const DetectedToken = ({ setShowDetectedTokens }) => { token_standard: TokenStandard.ERC20, asset_type: AssetType.token, token_added_type: 'detected', - chain_id: chainId, + chain_id: importedToken.chainId, }, }); }); - await dispatch(addImportedTokens(selectedTokens, networkClientId)); - const tokenSymbols = selectedTokens.map(({ symbol }) => symbol); - dispatch(setNewTokensImported(tokenSymbols.join(', '))); + + if (process.env.PORTFOLIO_VIEW && !allNetworksFilterShown) { + const tokensByChainId = selectedTokens.reduce((acc, token) => { + const { chainId } = token; + + if (!acc[chainId]) { + acc[chainId] = { tokens: [] }; + } + + acc[chainId].tokens.push(token); + + return acc; + }, {}); + + const importPromises = Object.entries(tokensByChainId).map( + async ([networkId, { tokens }]) => { + const chainConfig = allNetworks[networkId]; + const { defaultRpcEndpointIndex } = chainConfig; + const { networkClientId: networkInstanceId } = + chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; + + await dispatch(addImportedTokens(tokens, networkInstanceId)); + const tokenSymbols = tokens.map(({ symbol }) => symbol); + dispatch(setNewTokensImported(tokenSymbols.join(', '))); + }, + ); + + await Promise.all(importPromises); + } else { + await dispatch(addImportedTokens(selectedTokens, networkClientId)); + const tokenSymbols = selectedTokens.map(({ symbol }) => symbol); + dispatch(setNewTokensImported(tokenSymbols.join(', '))); + } }; const handleClearTokensSelection = async () => { const { selected: selectedTokens = [], deselected: deSelectedTokens = [] } = sortingBasedOnTokenSelection(tokensListDetected); - if (deSelectedTokens.length < detectedTokens.length) { + if (deSelectedTokens.length < totalDetectedTokens) { await importSelectedTokens(selectedTokens); } + const tokensDetailsList = deSelectedTokens.map( ({ symbol, address }) => `${symbol} - ${address}`, ); @@ -108,17 +194,53 @@ const DetectedToken = ({ setShowDetectedTokens }) => { asset_type: AssetType.token, }, }); - const deSelectedTokensAddresses = deSelectedTokens.map( - ({ address }) => address, - ); - await dispatch( - ignoreTokens({ - tokensToIgnore: deSelectedTokensAddresses, - dontShowLoadingIndicator: true, - }), - ); - setShowDetectedTokens(false); - setPartiallyIgnoreDetectedTokens(false); + + if (process.env.PORTFOLIO_VIEW && !allNetworksFilterShown) { + // group deselected tokens by chainId + const groupedByChainId = deSelectedTokens.reduce((acc, token) => { + const { chainId } = token; + if (!acc[chainId]) { + acc[chainId] = []; + } + acc[chainId].push(token); + return acc; + }, {}); + + const promises = Object.entries(groupedByChainId).map( + async ([chainId, tokens]) => { + const chainConfig = allNetworks[chainId]; + const { defaultRpcEndpointIndex } = chainConfig; + const { networkClientId: networkInstanceId } = + chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; + + await dispatch( + ignoreTokens({ + tokensToIgnore: tokens, + dontShowLoadingIndicator: true, + networkClientId: networkInstanceId, + }), + ); + }, + ); + + await Promise.all(promises); + setShowDetectedTokens(false); + setPartiallyIgnoreDetectedTokens(false); + } else { + const deSelectedTokensAddresses = deSelectedTokens.map( + ({ address }) => address, + ); + + await dispatch( + ignoreTokens({ + tokensToIgnore: deSelectedTokensAddresses, + dontShowLoadingIndicator: true, + }), + ); + + setShowDetectedTokens(false); + setPartiallyIgnoreDetectedTokens(false); + } }; const handleTokenSelection = (token) => { @@ -135,7 +257,7 @@ const DetectedToken = ({ setShowDetectedTokens }) => { const { selected: selectedTokens = [] } = sortingBasedOnTokenSelection(tokensListDetected); - if (selectedTokens.length < detectedTokens.length) { + if (selectedTokens.length < totalDetectedTokens) { setShowDetectedTokenIgnoredPopover(true); setPartiallyIgnoreDetectedTokens(true); } else { @@ -169,9 +291,13 @@ const DetectedToken = ({ setShowDetectedTokens }) => { partiallyIgnoreDetectedTokens={partiallyIgnoreDetectedTokens} /> )} - {detectedTokens.length > 0 && ( + {totalDetectedTokens > 0 && ( { ...testData, metamask: { ...testData.metamask, + currencyRates: { + SepoliaETH: { + conversionDate: 1620710825.03, + conversionRate: 3910.28, + usdConversionRate: 3910.28, + }, + }, ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), + tokenBalances: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + [CHAIN_IDS.SEPOLIA]: { + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0x25e4bc', + }, + }, + }, }, }); const props = { diff --git a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js index f9f15d211806..e1662a641ae7 100644 --- a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js +++ b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js @@ -9,27 +9,31 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; -import { getCurrentChainId } from '../../../../selectors'; +import { + getCurrentChainId, + getNetworkConfigurationsByChainId, +} from '../../../../selectors'; function mapStateToProps(state) { return { chainId: getCurrentChainId(state), token: state.appState.modal.modalState.props.token, history: state.appState.modal.modalState.props.history, + networkConfigurationsByChainId: getNetworkConfigurationsByChainId(state), }; } function mapDispatchToProps(dispatch) { return { hideModal: () => dispatch(actions.hideModal()), - hideToken: (address) => { - dispatch( + hideToken: async (address, networkClientId) => { + await dispatch( actions.ignoreTokens({ tokensToIgnore: address, + networkClientId, }), - ).then(() => { - dispatch(actions.hideModal()); - }); + ); + dispatch(actions.hideModal()); }, }; } @@ -44,10 +48,12 @@ class HideTokenConfirmationModal extends Component { hideToken: PropTypes.func.isRequired, hideModal: PropTypes.func.isRequired, chainId: PropTypes.string.isRequired, + networkConfigurationsByChainId: PropTypes.object.isRequired, token: PropTypes.shape({ symbol: PropTypes.string, address: PropTypes.string, image: PropTypes.string, + chainId: PropTypes.string, }), history: PropTypes.object, }; @@ -55,8 +61,21 @@ class HideTokenConfirmationModal extends Component { state = {}; render() { - const { chainId, token, hideToken, hideModal, history } = this.props; - const { symbol, address, image } = token; + const { + chainId, + token, + hideToken, + hideModal, + history, + networkConfigurationsByChainId, + } = this.props; + const { symbol, address, image, chainId: tokenChainId } = token; + const chainIdToUse = tokenChainId || chainId; + + const chainConfig = networkConfigurationsByChainId[chainIdToUse]; + const { defaultRpcEndpointIndex } = chainConfig; + const { networkClientId: networkInstanceId } = + chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; return (
@@ -96,7 +115,7 @@ class HideTokenConfirmationModal extends Component { token_symbol: symbol, }, }); - hideToken(address); + hideToken(address, networkInstanceId); history.push(DEFAULT_ROUTE); }} > diff --git a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.test.js b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.test.js index 6a53c80a378d..982a6a272943 100644 --- a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.test.js +++ b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.test.js @@ -79,6 +79,7 @@ describe('Hide Token Confirmation Modal', () => { expect(mockHideModal).toHaveBeenCalled(); expect(actions.ignoreTokens).toHaveBeenCalledWith({ tokensToIgnore: tokenState.address, + networkClientId: 'goerli', }); }); }); diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.js b/ui/components/multichain/detected-token-banner/detected-token-banner.js index 8851614ec948..59db889d11ce 100644 --- a/ui/components/multichain/detected-token-banner/detected-token-banner.js +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.js @@ -7,6 +7,9 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentChainId, getDetectedTokensInCurrentNetwork, + getAllDetectedTokensForSelectedAddress, + getPreferences, + getNetworkConfigurationsByChainId, } from '../../../selectors'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { @@ -23,14 +26,40 @@ export const DetectedTokensBanner = ({ }) => { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); + const { tokenNetworkFilter } = useSelector(getPreferences); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + + const allOpts = {}; + Object.keys(allNetworks || {}).forEach((chainId) => { + allOpts[chainId] = true; + }); + + const allNetworksFilterShown = + Object.keys(tokenNetworkFilter || {}).length !== + Object.keys(allOpts || {}).length; const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); - const detectedTokensDetails = detectedTokens.map( - ({ address, symbol }) => `${symbol} - ${address}`, - ); + const detectedTokensMultichain = useSelector( + getAllDetectedTokensForSelectedAddress, + ); const chainId = useSelector(getCurrentChainId); + const detectedTokensDetails = + process.env.PORTFOLIO_VIEW && !allNetworksFilterShown + ? Object.values(detectedTokensMultichain) + .flat() + .map(({ address, symbol }) => `${symbol} - ${address}`) + : detectedTokens.map(({ address, symbol }) => `${symbol} - ${address}`); + + const totalTokens = + process.env.PORTFOLIO_VIEW && !allNetworksFilterShown + ? Object.values(detectedTokensMultichain).reduce( + (count, tokenArray) => count + tokenArray.length, + 0, + ) + : detectedTokens.length; + const handleOnClick = () => { actionButtonOnClick(); trackEvent({ @@ -51,9 +80,9 @@ export const DetectedTokensBanner = ({ data-testid="detected-token-banner" {...props} > - {detectedTokens.length === 1 + {totalTokens === 1 ? t('numberOfNewTokensDetectedSingular') - : t('numberOfNewTokensDetectedPlural', [detectedTokens.length])} + : t('numberOfNewTokensDetectedPlural', [totalTokens])} ); }; diff --git a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js index 8b85e5cf1a4b..549f0ab5c70d 100644 --- a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js +++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js @@ -76,6 +76,7 @@ export const ImportNftsModal = ({ onClose }) => { const [disabled, setDisabled] = useState(true); const [nftAddFailed, setNftAddFailed] = useState(false); const trackEvent = useContext(MetaMetricsContext); + const [nftAddressValidationError, setNftAddressValidationError] = useState(null); const [duplicateTokenIdError, setDuplicateTokenIdError] = useState(null); diff --git a/ui/hooks/useTokenFiatAmount.js b/ui/hooks/useTokenFiatAmount.js index dfa4144b90e3..5abbbc608de0 100644 --- a/ui/hooks/useTokenFiatAmount.js +++ b/ui/hooks/useTokenFiatAmount.js @@ -5,6 +5,9 @@ import { getCurrentCurrency, getShouldShowFiat, getConfirmationExchangeRates, + getMarketData, + getCurrencyRates, + getNetworkConfigurationsByChainId, } from '../selectors'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; import { getConversionRate } from '../ducks/metamask/metamask'; @@ -22,6 +25,7 @@ import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; * @param {boolean} [overrides.showFiat] - If truthy, ensures the fiat value is shown even if the showFiat value from state is falsey * @param {boolean} hideCurrencySymbol - Indicates whether the returned formatted amount should include the trailing currency symbol * @returns {string} The formatted token amount in the user's chosen fiat currency + * @param {string} [chainId] - The chain id */ export function useTokenFiatAmount( tokenAddress, @@ -29,17 +33,44 @@ export function useTokenFiatAmount( tokenSymbol, overrides = {}, hideCurrencySymbol, + chainId = null, ) { + const allMarketData = useSelector(getMarketData); + const contractExchangeRates = useSelector( getTokenExchangeRates, shallowEqual, ); + + const contractMarketData = chainId + ? Object.entries(allMarketData[chainId]).reduce( + (acc, [address, marketData]) => { + acc[address] = marketData?.price ?? null; + return acc; + }, + {}, + ) + : null; + + const tokenMarketData = chainId ? contractMarketData : contractExchangeRates; + const confirmationExchangeRates = useSelector(getConfirmationExchangeRates); const mergedRates = { - ...contractExchangeRates, + ...tokenMarketData, ...confirmationExchangeRates, }; + + const currencyRates = useSelector(getCurrencyRates); const conversionRate = useSelector(getConversionRate); + const networkConfigurationsByChainId = useSelector( + getNetworkConfigurationsByChainId, + ); + + const tokenConversionRate = chainId + ? currencyRates[networkConfigurationsByChainId[chainId].nativeCurrency] + .conversionRate + : conversionRate; + const currentCurrency = useSelector(getCurrentCurrency); const userPrefersShownFiat = useSelector(getShouldShowFiat); const showFiat = overrides.showFiat ?? userPrefersShownFiat; @@ -53,7 +84,7 @@ export function useTokenFiatAmount( () => getTokenFiatAmount( tokenExchangeRate, - conversionRate, + tokenConversionRate, currentCurrency, tokenAmount, tokenSymbol, @@ -61,8 +92,8 @@ export function useTokenFiatAmount( hideCurrencySymbol, ), [ + tokenConversionRate, tokenExchangeRate, - conversionRate, currentCurrency, tokenAmount, tokenSymbol, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 96e880d52aef..27b3a878042b 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -86,6 +86,7 @@ import { getLedgerTransportType, isAddressLedger, getIsUnlocked, + getCompletedOnboarding, } from '../ducks/metamask/metamask'; import { getLedgerWebHidConnectedStatus, @@ -2554,6 +2555,41 @@ export function getDetectedTokensInCurrentNetwork(state) { return state.metamask.allDetectedTokens?.[currentChainId]?.[selectedAddress]; } +export function getAllDetectedTokens(state) { + return state.metamask.allDetectedTokens; +} + +/** + * To retrieve the list of tokens detected across all chains. + * + * @param {*} state + * @returns list of token objects on all networks + */ +export function getAllDetectedTokensForSelectedAddress(state) { + const completedOnboarding = getCompletedOnboarding(state); + + if (!completedOnboarding) { + return {}; + } + + const { address: selectedAddress } = getSelectedInternalAccount(state); + + const tokensByChainId = Object.entries( + state.metamask.allDetectedTokens || {}, + ).reduce((acc, [chainId, chainTokens]) => { + const tokensForAddress = chainTokens[selectedAddress]; + if (tokensForAddress) { + acc[chainId] = tokensForAddress.map((token) => ({ + ...token, + chainId, + })); + } + return acc; + }, {}); + + return tokensByChainId; +} + /** * To fetch the name of the tokens that are imported from tokens found page * diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 10391adad3df..c16df9838916 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1013,7 +1013,9 @@ describe('Actions', () => { const store = mockStore(); background.getApi.returns({ - ignoreTokens: sinon.stub().callsFake((_, cb) => cb(new Error('error'))), + ignoreTokens: sinon + .stub() + .callsFake((_, __, cb) => cb(new Error('error'))), getStatePatches: sinon.stub().callsFake((cb) => cb(null, [])), }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index a339ff856058..66962d46161d 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -1989,14 +1989,17 @@ export function addImportedTokens( * * @param options * @param options.tokensToIgnore + * @param options.networkClientId * @param options.dontShowLoadingIndicator */ export function ignoreTokens({ tokensToIgnore, dontShowLoadingIndicator = false, + networkClientId = null, }: { tokensToIgnore: string[]; dontShowLoadingIndicator: boolean; + networkClientId?: NetworkClientId; }): ThunkAction { const _tokensToIgnore = Array.isArray(tokensToIgnore) ? tokensToIgnore @@ -2007,7 +2010,10 @@ export function ignoreTokens({ dispatch(showLoadingIndication()); } try { - await submitRequestToBackground('ignoreTokens', [_tokensToIgnore]); + await submitRequestToBackground('ignoreTokens', [ + _tokensToIgnore, + networkClientId, + ]); } catch (error) { logErrorWithMessage(error); dispatch(displayWarning(error)); diff --git a/yarn.lock b/yarn.lock index adf55eaa9e85..65dc70847c4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4934,9 +4934,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:44.0.0": - version: 44.0.0 - resolution: "@metamask/assets-controllers@npm:44.0.0" +"@metamask/assets-controllers@npm:44.1.0": + version: 44.1.0 + resolution: "@metamask/assets-controllers@npm:44.1.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/abi": "npm:^5.7.0" @@ -4969,13 +4969,13 @@ __metadata: "@metamask/keyring-controller": ^18.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^14.0.0 - checksum: 10/6f3d8712a90aa322aabd38d43663d299ad7ee98a6d838d72bfc3b426ea0e4e925bb78c1aaaa3c75d43e95d46993c47583a4a03f4c58aee155525424fa86207ae + checksum: 10/924c67fba204711ddde4be6615359318ed0fbdd05ebd8e5d98ae9d9ae288adad5cb6fc901b91d8e84f92a6ab62f0bfb25601b03c676044009f81a7fffa8087e7 languageName: node linkType: hard -"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A44.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.0.0-c223d56176.patch::version=44.0.0&hash=5a94c2": - version: 44.0.0 - resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A44.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.0.0-c223d56176.patch::version=44.0.0&hash=5a94c2" +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A44.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch": + version: 44.1.0 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A44.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch::version=44.1.0&hash=423db2" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/abi": "npm:^5.7.0" @@ -5008,46 +5008,7 @@ __metadata: "@metamask/keyring-controller": ^18.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^14.0.0 - checksum: 10/0d6c386a1f1e68ab339340fd8fa600827f55f234bc54b2224069a1819ab037641daa9696a0d62f187c0649317393efaeeb119a7852af51da3bb340e0e98cf9f6 - languageName: node - linkType: hard - -"@metamask/assets-controllers@patch:@metamask/assets-controllers@patch%3A@metamask/assets-controllers@npm%253A44.0.0%23~/.yarn/patches/@metamask-assets-controllers-npm-44.0.0-c223d56176.patch%3A%3Aversion=44.0.0&hash=5a94c2#~/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch": - version: 44.0.0 - resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@patch%3A@metamask/assets-controllers@npm%253A44.0.0%23~/.yarn/patches/@metamask-assets-controllers-npm-44.0.0-c223d56176.patch%3A%3Aversion=44.0.0&hash=5a94c2#~/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch::version=44.0.0&hash=c4e407" - dependencies: - "@ethereumjs/util": "npm:^8.1.0" - "@ethersproject/abi": "npm:^5.7.0" - "@ethersproject/address": "npm:^5.7.0" - "@ethersproject/bignumber": "npm:^5.7.0" - "@ethersproject/contracts": "npm:^5.7.0" - "@ethersproject/providers": "npm:^5.7.0" - "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/base-controller": "npm:^7.0.2" - "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.4.3" - "@metamask/eth-query": "npm:^4.0.0" - "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/polling-controller": "npm:^12.0.1" - "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/utils": "npm:^10.0.0" - "@types/bn.js": "npm:^5.1.5" - "@types/uuid": "npm:^8.3.0" - async-mutex: "npm:^0.5.0" - bn.js: "npm:^5.2.1" - cockatiel: "npm:^3.1.2" - immer: "npm:^9.0.6" - lodash: "npm:^4.17.21" - multiformats: "npm:^13.1.0" - single-call-balance-checker-abi: "npm:^1.0.0" - uuid: "npm:^8.3.2" - peerDependencies: - "@metamask/accounts-controller": ^19.0.0 - "@metamask/approval-controller": ^7.0.0 - "@metamask/keyring-controller": ^18.0.0 - "@metamask/network-controller": ^22.0.0 - "@metamask/preferences-controller": ^14.0.0 - checksum: 10/11e8920bdf8ffce4a534c6aadfe768176c4e461a00bc06e6ece52f085755ff252194881d9edd308097186a05057075fd9812b6e4b1fd97dd731814ad205013da + checksum: 10/5e3b0109e6b5c0d65338a18b2c590d15229003e05c55cf0013d8e32687bbe774de05872a7b61038aa90177a6ce01b32814c3c680ee3c10cbad8cba9db2d796aa languageName: node linkType: hard @@ -26819,7 +26780,7 @@ __metadata: "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@patch%3A@metamask/assets-controllers@npm%253A44.0.0%23~/.yarn/patches/@metamask-assets-controllers-npm-44.0.0-c223d56176.patch%3A%3Aversion=44.0.0&hash=5a94c2#~/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch" + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A44.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.8.2" From 3eb37d9380b3d8554122ec5467ad0f491506f551 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 21 Nov 2024 17:57:37 -0330 Subject: [PATCH 24/28] test: Fix flakiness caused by display of newly switched to network modal (#28625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes the flakiness seen, for example, in https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/111818/workflows/f974462c-8208-4189-8592-928b21f0cfde/jobs/4189413/tests See this screenshot: ![Screenshot from 2024-11-21 16-39-14](https://github.com/user-attachments/assets/e5523440-82a8-462e-9ad3-fa2280e97826) The test failure error is: `ElementClickInterceptedError: Element
is not clickable at point (866,482) because another element
obscures it` So the test is attempting to clikc the "Import tokens" button in the opened menu behind the "You are now using Binance" modal, but the Import Tokens button cannot be clicked because the modal intercepts the click So there is a race condition whereby the test assumes that no modal will interfere with the click, and the test will pass if the click can occur before the modal is rendered, but it will fail the the click is attempted after the modal is rendered. Prior to the PR in question (https://github.com/MetaMask/metamask-extension/pull/28575) there was no menu, and the "Import tokens" button could be clicked directly. The PR added the menu and move "Import tokens" into that menu, so now the test has to wait for that menu to open before the "Import tokens" button can be clicked, exacerbating the race condition. That new network modal should not be shown in that scenario this is the logic that controls whether that network modal should be shown: ``` const shouldShowNetworkInfo = isUnlocked && currentChainId && !isTestNet && !isSendRoute && !isNetworkUsed && !isCurrentProviderCustom && completedOnboarding && allAccountsOnNetworkAreEmpty && switchedNetworkDetails === null; ``` (I think that is what folks are above referring to as the "Got it" modal) Meanwhile, in fixture-builder.js we have: ``` usedNetworks: { [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: true, [CHAIN_IDS.GOERLI]: true, [CHAIN_IDS.LOCALHOST]: true, }, ``` So for any test that sets the network to something other than those four networks, !isNetworkUsed will evaluate to true, which will result in that modal being shown. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28625?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** e2e tests should ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../transactions/contract-interaction-redesign.spec.ts | 3 +++ test/e2e/tests/tokens/add-hide-token.spec.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/test/e2e/tests/confirmations/transactions/contract-interaction-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/contract-interaction-redesign.spec.ts index 9b37e2ee5066..a668539d24dc 100644 --- a/test/e2e/tests/confirmations/transactions/contract-interaction-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/contract-interaction-redesign.spec.ts @@ -146,6 +146,9 @@ describe('Confirmation Redesign Contract Interaction Component', function () { }, useTransactionSimulations: false, }) + .withAppStateController({ + [CHAIN_IDS.OPTIMISM]: true, + }) .withNetworkControllerOnOptimism() .build(), ganacheOptions: { diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js index c9a1f26ad9eb..011d52849feb 100644 --- a/test/e2e/tests/tokens/add-hide-token.spec.js +++ b/test/e2e/tests/tokens/add-hide-token.spec.js @@ -118,6 +118,9 @@ describe('Add existing token using search', function () { }, ], }) + .withAppStateController({ + [CHAIN_IDS.OPTIMISM]: true, + }) .build(), ganacheOptions: { ...defaultGanacheOptions, From 175d42939d8cfca6a876e83b109723c3da433dcb Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:27:48 -0800 Subject: [PATCH 25/28] fix: Network filter must respect `PORTFOLIO_VIEW` feature flag (#28626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** When feature flag is omitted, we should not show tokens across all chains, only the `currentNetwork.chainId` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28626?quickstart=1) ## **Related issues** Fixes: Multichain tokens being rendered when `PORTFOLIO_VIEW` feature is off (unexpected) ## **Manual testing steps** With feature flag off: `yarn webpack --watch` When running the app, the tokens displayed should always belong to the globally selected network. With feature flag on: `PORTFOLIO_VIEW=1 yarn webpack --watch` When running the app, you should see all tokens from all added networks, when "All Networks" filter is selected. When "Current filter" is selected, you should see only tokens for the globally selected network. ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-11-21 at 8 18 08 PM](https://github.com/user-attachments/assets/91305b19-8614-4bf1-a5c4-ec6a37633369) ### **After** https://github.com/user-attachments/assets/40fc641b-e8df-4eb8-beb9-9d907fd9888b ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../errors-after-init-opt-in-background-state.json | 4 ---- .../errors-after-init-opt-in-ui-state.json | 4 ---- ...errors-before-init-opt-in-background-state.json | 3 +-- .../asset-list-control-bar.tsx | 7 ++++++- ui/components/app/assets/token-list/token-list.tsx | 14 ++++++++------ .../network-list-menu/network-list-menu.tsx | 2 +- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 6252c91e73a2..e8ca8579a7a4 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -238,10 +238,6 @@ "redesignedTransactionsEnabled": "boolean", "tokenSortConfig": "object", "tokenNetworkFilter": { - "0x1": "boolean", - "0xaa36a7": "boolean", - "0xe705": "boolean", - "0xe708": "boolean", "0x539": "boolean" }, "shouldShowAggregatedBalancePopover": "boolean" diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 3bc7057435c8..cbc9ff5b74c4 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -39,10 +39,6 @@ "redesignedTransactionsEnabled": "boolean", "tokenSortConfig": "object", "tokenNetworkFilter": { - "0x1": "boolean", - "0xaa36a7": "boolean", - "0xe705": "boolean", - "0xe708": "boolean", "0x539": "boolean" }, "shouldShowAggregatedBalancePopover": "boolean" diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 5d6be06b692e..07b292d33b3b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -120,8 +120,7 @@ "tokenSortConfig": "object", "tokenNetworkFilter": {}, "showMultiRpcModal": "boolean", - "shouldShowAggregatedBalancePopover": "boolean", - "tokenNetworkFilter": {} + "shouldShowAggregatedBalancePopover": "boolean" }, "selectedAddress": "string", "theme": "light", diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx index e5c43aad281d..a09b5128396a 100644 --- a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx @@ -87,8 +87,13 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { // We need to set the default filter for all users to be all included networks, rather than defaulting to empty object // This effect is to unblock and derisk in the short-term useEffect(() => { - if (Object.keys(tokenNetworkFilter || {}).length === 0) { + if ( + process.env.PORTFOLIO_VIEW && + Object.keys(tokenNetworkFilter || {}).length === 0 + ) { dispatch(setTokenNetworkFilter(allOpts)); + } else { + dispatch(setTokenNetworkFilter({ [currentNetwork.chainId]: true })); } }, []); diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 3f094634743e..5e25c7ff5f41 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -112,12 +112,14 @@ export default function TokenList({ // Ensure newly added networks are included in the tokenNetworkFilter useEffect(() => { - const allNetworkFilters = Object.fromEntries( - Object.keys(allNetworks).map((chainId) => [chainId, true]), - ); - - if (Object.keys(tokenNetworkFilter).length > 1) { - dispatch(setTokenNetworkFilter(allNetworkFilters)); + if (process.env.PORTFOLIO_VIEW) { + const allNetworkFilters = Object.fromEntries( + Object.keys(allNetworks).map((chainId) => [chainId, true]), + ); + + if (Object.keys(tokenNetworkFilter).length > 1) { + dispatch(setTokenNetworkFilter(allNetworkFilters)); + } } }, [Object.keys(allNetworks).length]); diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 4dcecbf696e7..9c99b3048a95 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -292,7 +292,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { // however, if I am already filtered on "Current Network", we'll want to filter by the selected network when the network changes if (Object.keys(tokenNetworkFilter).length <= 1) { dispatch(setTokenNetworkFilter({ [network.chainId]: true })); - } else { + } else if (process.env.PORTFOLIO_VIEW) { dispatch(setTokenNetworkFilter(allOpts)); } From e748576e927a3c853c2f3662b207988c54e71387 Mon Sep 17 00:00:00 2001 From: Harika <153644847+hjetpoluru@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:31:07 -0500 Subject: [PATCH 26/28] test: Fixed artifacts issue due to switching window title (#28469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR addresses the issue where artifacts are not generated when a timeout occurs during the switching window and the dialog does not appear, making it difficult to determine what has happened during the test scenario. The fix ensures that an error is generated when a timeout occurs and is correctly returned. If the dialog is found, no error is generated. Special thanks to @davidmurdoch for pair programming, understanding the issue and suggesting the fix. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28469?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28116 ## **Manual testing steps** All the artifacts needs to be generated. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../server-mocha-to-background.ts | 18 +++++++++++++++--- test/e2e/background-socket/types.ts | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/test/e2e/background-socket/server-mocha-to-background.ts b/test/e2e/background-socket/server-mocha-to-background.ts index 3b4cc1c88b2f..ef8cc446a60c 100644 --- a/test/e2e/background-socket/server-mocha-to-background.ts +++ b/test/e2e/background-socket/server-mocha-to-background.ts @@ -83,9 +83,14 @@ class ServerMochaToBackground { if (message.command === 'openTabs' && message.tabs) { this.eventEmitter.emit('openTabs', message.tabs); } else if (message.command === 'notFound') { - throw new Error( + const error = new Error( `No window found by background script with ${message.property}: ${message.value}`, ); + if (this.eventEmitter.listenerCount('error') > 0) { + this.eventEmitter.emit('error', error); + } else { + throw error; + } } } @@ -108,8 +113,15 @@ class ServerMochaToBackground { // This is a way to wait for an event async, without timeouts or polling async waitForResponse() { - return new Promise((resolve) => { - this.eventEmitter.once('openTabs', resolve); + return new Promise((resolve, reject) => { + this.eventEmitter.once('error', (error) => { + this.eventEmitter.removeListener('openTabs', resolve); + reject(error); + }); + this.eventEmitter.once('openTabs', (result) => { + this.eventEmitter.removeListener('error', reject); + resolve(result); + }); }); } } diff --git a/test/e2e/background-socket/types.ts b/test/e2e/background-socket/types.ts index 00a734374c90..eba69ebc4873 100644 --- a/test/e2e/background-socket/types.ts +++ b/test/e2e/background-socket/types.ts @@ -19,6 +19,7 @@ export type Handle = { export type WindowProperties = 'title' | 'url'; export type ServerMochaEventEmitterType = { + error: [error: Error]; openTabs: [openTabs: chrome.tabs.Tab[]]; notFound: [openTabs: chrome.tabs.Tab[]]; }; From 5116c3249876a8098d8b96cbed8369500805b2b7 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:01:48 -0500 Subject: [PATCH 27/28] refactor: move `getProviderConfig` out of `ducks/metamask.js` to `shared/selectors/networks.ts` (#27646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts some from functions from JS to TS. Also updates some functions to match the actual expect return values. Why only these functions? I'm trying to solve circular dependency issues. `getProviderConfig` is so widely used in the codebase, and makes use of multiple selectors itself, it makes it very complicated to untangle. I've put it in a new file (`seelctors/networks.ts`) just to, hopefully temporarily, simplify untangling other circular dependency issues. --- app/scripts/lib/ppom/ppom-middleware.ts | 3 +- app/scripts/lib/ppom/ppom-util.ts | 2 +- app/scripts/metamask-controller.js | 6 +- shared/constants/bridge.ts | 2 + shared/constants/multichain/networks.ts | 2 + shared/constants/network.ts | 2 + shared/modules/selectors/networks.ts | 114 ++++++++++++++++ .../modules/selectors/smart-transactions.ts | 9 +- {ui => shared/modules}/selectors/util.js | 0 test/data/mock-state.json | 8 +- .../asset-list-control-bar.tsx | 7 +- .../app/assets/asset-list/asset-list.tsx | 2 +- .../network-filter/network-filter.tsx | 2 +- .../app/currency-input/currency-input.js | 6 +- .../detected-token-selection-popover.js | 2 +- .../app/detected-token/detected-token.js | 6 +- .../loading-network-screen.container.js | 6 +- .../confirm-delete-network.container.js | 2 +- .../hide-token-confirmation-modal.js | 6 +- .../transaction-already-confirmed.tsx | 9 +- .../multi-rpc-edit-modal.tsx | 2 +- .../selected-account-component.test.js | 7 - .../selected-account.container.js | 2 +- ...-percentage-overview-cross-chains.test.tsx | 7 +- .../wrong-network-notification.tsx | 9 +- .../address-copy-button.js | 2 +- .../asset-picker-modal-network.tsx | 6 +- .../asset-picker/asset-picker.tsx | 6 +- .../detected-token-banner.js | 2 +- .../import-tokens-modal.js | 2 +- .../network-list-menu/network-list-menu.tsx | 2 +- ...ification-detail-block-explorer-button.tsx | 2 +- .../review-permissions-page.tsx | 2 +- .../components/recipient-content.test.tsx | 2 +- .../deprecated-networks.js | 6 +- .../ui/new-network-info/new-network-info.js | 2 +- ui/ducks/bridge/selectors.ts | 18 +-- ui/ducks/metamask/metamask.js | 43 +----- ui/ducks/send/helpers.test.js | 1 - ui/ducks/send/send.js | 6 +- ui/hooks/bridge/useBridging.ts | 2 +- ...eAccountTotalCrossChainFiatBalance.test.ts | 7 +- .../useAccountTotalCrossChainFiatBalance.ts | 2 +- ui/hooks/useAccountTrackerPolling.ts | 6 +- ui/hooks/useCurrencyRatePolling.ts | 6 +- ui/hooks/useGasFeeEstimates.js | 2 +- ui/hooks/useGasFeeEstimates.test.js | 9 +- ui/hooks/useMMICustodySendTransaction.ts | 2 +- ui/hooks/useMultichainSelector.test.ts | 2 +- ui/hooks/useTokenBalances.ts | 2 +- ui/hooks/useTokenDetectionPolling.ts | 7 +- ui/hooks/useTokenFiatAmount.js | 2 +- ui/hooks/useTokenListPolling.ts | 2 +- ui/hooks/useTokenRatesPolling.ts | 2 +- ui/hooks/useTokenTracker.js | 2 +- .../asset/components/chart/asset-chart.tsx | 3 +- ui/pages/asset/components/native-asset.tsx | 2 +- ui/pages/asset/components/token-asset.tsx | 2 +- ui/pages/asset/useHistoricalPrices.ts | 3 +- ui/pages/asset/util.ts | 3 +- ui/pages/bridge/hooks/useAddToken.ts | 6 +- .../hooks/useSubmitBridgeTransaction.test.tsx | 23 +++- ui/pages/bridge/index.tsx | 2 +- .../bridge/prepare/prepare-bridge-page.tsx | 2 +- .../confirm/info/hooks/useSupportsEIP1559.ts | 6 +- .../nft-send-heading/nft-send-heading.tsx | 2 +- .../token-transfer/token-details-section.tsx | 2 +- .../simulation-details/asset-pill.tsx | 2 +- .../confirm-transaction-base.container.js | 2 +- .../confirm-transaction.component.js | 2 +- .../confirmation/confirmation.js | 2 +- ui/pages/confirmations/selectors/confirm.ts | 2 +- ui/pages/home/home.container.js | 3 +- .../privacy-settings/privacy-settings.js | 2 +- .../connect-page/connect-page.tsx | 2 +- ui/pages/routes/routes.container.js | 10 +- .../edit-contact/edit-contact.container.js | 2 +- .../networks-form/networks-form.tsx | 2 +- .../security-tab/security-tab.container.js | 2 +- .../settings-tab/settings-tab.container.js | 2 +- ui/pages/settings/settings.container.js | 2 +- ui/pages/swaps/hooks/useUpdateSwapsState.ts | 3 +- ui/selectors/approvals.ts | 2 +- ui/selectors/confirm-transaction.js | 2 +- ui/selectors/institutional/selectors.test.ts | 17 ++- ui/selectors/institutional/selectors.ts | 9 +- .../metamask-notifications.ts | 2 +- ui/selectors/multichain.test.ts | 6 +- ui/selectors/multichain.ts | 12 +- ui/selectors/networks.test.ts | 129 ++++++++++++++++++ ui/selectors/permissions.js | 2 +- ui/selectors/selectors.js | 46 +------ ui/selectors/selectors.test.js | 59 +------- ui/selectors/signatures.ts | 2 +- ui/selectors/snaps/accounts.ts | 2 +- ui/selectors/snaps/address-book.ts | 2 +- ui/selectors/transactions.js | 10 +- ui/store/actions.ts | 10 +- 98 files changed, 462 insertions(+), 326 deletions(-) create mode 100644 shared/modules/selectors/networks.ts rename {ui => shared/modules}/selectors/util.js (100%) create mode 100644 ui/selectors/networks.test.ts diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 44c7a8a965c4..f99cf675f534 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -14,8 +14,7 @@ import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import { PreferencesController } from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state-controller'; import { SECURITY_ALERT_RESPONSE_CHECKING_CHAIN } from '../../../../shared/constants/security-provider'; -// eslint-disable-next-line import/no-restricted-paths -import { getProviderConfig } from '../../../../ui/ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { trace, TraceContext, TraceName } from '../../../../shared/lib/trace'; import { generateSecurityAlertId, diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index b0bf6b03b4d9..d27ec6c8e505 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -158,7 +158,7 @@ export async function isChainSupported(chainId: Hex): Promise { `Error fetching supported chains from security alerts API`, ); } - return supportedChainIds.includes(chainId); + return supportedChainIds.includes(chainId as Hex); } function normalizePPOMRequest( diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ac633bbc8e5d..548b2f9d940c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -239,10 +239,8 @@ import { } from '../../shared/lib/transactions-controller-utils'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { getCurrentChainId } from '../../ui/selectors'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { getProviderConfig } from '../../ui/ducks/metamask/metamask'; +import { getCurrentChainId } from '../../ui/selectors/selectors'; +import { getProviderConfig } from '../../shared/modules/selectors/networks'; import { endTrace, trace } from '../../shared/lib/trace'; // eslint-disable-next-line import/no-restricted-paths import { isSnapId } from '../../ui/helpers/utils/snaps'; diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index ed3b21c6a581..10f2587d3fbd 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -13,6 +13,8 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.BASE, ]; +export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; + export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index 04ee1134c0b6..f5a45138d88a 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -22,6 +22,8 @@ export type MultichainProviderConfig = ProviderConfigWithImageUrl & { isAddressCompatible: (address: string) => boolean; }; +export type MultichainNetworkIds = `${MultichainNetworks}`; + export enum MultichainNetworks { BITCOIN = 'bip122:000000000019d6689c085ae165831e93', BITCOIN_TESTNET = 'bip122:000000000933ea01ad0ee984209779ba', diff --git a/shared/constants/network.ts b/shared/constants/network.ts index aef122ea67bc..3fca971c338e 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -87,6 +87,8 @@ export const NETWORK_TYPES = { LINEA_MAINNET: 'linea-mainnet', } as const; +export type NetworkTypes = (typeof NETWORK_TYPES)[keyof typeof NETWORK_TYPES]; + /** * An object containing shortcut names for any non-builtin network. We need * this to be able to differentiate between networks that require custom diff --git a/shared/modules/selectors/networks.ts b/shared/modules/selectors/networks.ts new file mode 100644 index 000000000000..41aad6da6948 --- /dev/null +++ b/shared/modules/selectors/networks.ts @@ -0,0 +1,114 @@ +import { + RpcEndpointType, + type NetworkConfiguration, + type NetworkState as _NetworkState, +} from '@metamask/network-controller'; +import { createSelector } from 'reselect'; +import { NetworkStatus } from '../../constants/network'; +import { createDeepEqualSelector } from './util'; + +export type NetworkState = { metamask: _NetworkState }; + +export type NetworkConfigurationsState = { + metamask: { + networkConfigurations: Record< + string, + MetaMaskExtensionNetworkConfiguration + >; + }; +}; + +export type SelectedNetworkClientIdState = { + metamask: { + selectedNetworkClientId: string; + }; +}; + +export type MetaMaskExtensionNetworkConfiguration = NetworkConfiguration; + +export type NetworkConfigurationsByChainIdState = { + metamask: Pick<_NetworkState, 'networkConfigurationsByChainId'>; +}; + +export type ProviderConfigState = NetworkConfigurationsByChainIdState & + SelectedNetworkClientIdState; + +export const getNetworkConfigurationsByChainId = createDeepEqualSelector( + (state: NetworkConfigurationsByChainIdState) => + state.metamask.networkConfigurationsByChainId, + (networkConfigurationsByChainId) => networkConfigurationsByChainId, +); + +export function getSelectedNetworkClientId( + state: SelectedNetworkClientIdState, +) { + return state.metamask.selectedNetworkClientId; +} + +/** + * Get the provider configuration for the current selected network. + * + * @param state - Redux state object. + */ +export const getProviderConfig = createSelector( + (state: ProviderConfigState) => getNetworkConfigurationsByChainId(state), + getSelectedNetworkClientId, + (networkConfigurationsByChainId, selectedNetworkClientId) => { + for (const network of Object.values(networkConfigurationsByChainId)) { + for (const rpcEndpoint of network.rpcEndpoints) { + if (rpcEndpoint.networkClientId === selectedNetworkClientId) { + const blockExplorerUrl = + network.defaultBlockExplorerUrlIndex === undefined + ? undefined + : network.blockExplorerUrls?.[ + network.defaultBlockExplorerUrlIndex + ]; + + return { + chainId: network.chainId, + ticker: network.nativeCurrency, + rpcPrefs: { ...(blockExplorerUrl && { blockExplorerUrl }) }, + type: + rpcEndpoint.type === RpcEndpointType.Custom + ? 'rpc' + : rpcEndpoint.networkClientId, + ...(rpcEndpoint.type === RpcEndpointType.Custom && { + id: rpcEndpoint.networkClientId, + nickname: network.name, + rpcUrl: rpcEndpoint.url, + }), + }; + } + } + } + return undefined; // should not be reachable + }, +); + +export function getNetworkConfigurations( + state: NetworkConfigurationsState, +): Record { + return state.metamask.networkConfigurations; +} + +/** + * Returns true if the currently selected network is inaccessible or whether no + * provider has been set yet for the currently selected network. + * + * @param state - Redux state object. + */ +export function isNetworkLoading(state: NetworkState) { + const selectedNetworkClientId = getSelectedNetworkClientId(state); + return ( + selectedNetworkClientId && + state.metamask.networksMetadata[selectedNetworkClientId].status !== + NetworkStatus.Available + ); +} + +export function getInfuraBlocked(state: NetworkState) { + return ( + state.metamask.networksMetadata[getSelectedNetworkClientId(state)] + .status === NetworkStatus.Blocked + ); +} diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts index 4fb6d56fc87d..b88d5f7c029b 100644 --- a/shared/modules/selectors/smart-transactions.ts +++ b/shared/modules/selectors/smart-transactions.ts @@ -12,6 +12,7 @@ import { // eslint-disable-next-line import/no-restricted-paths } from '../../../ui/selectors/selectors'; // TODO: Migrate shared selectors to this file. import { isProduction } from '../environment'; +import { NetworkState } from './networks'; type SmartTransactionsMetaMaskState = { metamask: { @@ -113,9 +114,7 @@ export const getCurrentChainSupportsSmartTransactions = ( return getAllowedSmartTransactionsChainIds().includes(chainId); }; -const getIsAllowedRpcUrlForSmartTransactions = ( - state: SmartTransactionsMetaMaskState, -) => { +const getIsAllowedRpcUrlForSmartTransactions = (state: NetworkState) => { const chainId = getCurrentChainId(state); if (!isProduction() || SKIP_STX_RPC_URL_CHECK_CHAIN_IDS.includes(chainId)) { // Allow any STX RPC URL in development and testing environments or for specific chain IDs. @@ -131,7 +130,7 @@ const getIsAllowedRpcUrlForSmartTransactions = ( }; export const getSmartTransactionsEnabled = ( - state: SmartTransactionsMetaMaskState, + state: SmartTransactionsMetaMaskState & NetworkState, ): boolean => { const supportedAccount = accountSupportsSmartTx(state); // TODO: Create a new proxy service only for MM feature flags. @@ -150,7 +149,7 @@ export const getSmartTransactionsEnabled = ( }; export const getIsSmartTransaction = ( - state: SmartTransactionsMetaMaskState, + state: SmartTransactionsMetaMaskState & NetworkState, ): boolean => { const smartTransactionsPreferenceEnabled = getSmartTransactionsPreferenceEnabled(state); diff --git a/ui/selectors/util.js b/shared/modules/selectors/util.js similarity index 100% rename from ui/selectors/util.js rename to shared/modules/selectors/util.js diff --git a/test/data/mock-state.json b/test/data/mock-state.json index d2b66cee3108..734845f0ca9a 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -402,26 +402,30 @@ "name": "Custom Mainnet RPC", "nativeCurrency": "ETH", "defaultRpcEndpointIndex": 0, + "ticker": "ETH", "rpcEndpoints": [ { "type": "custom", "url": "https://testrpc.com", "networkClientId": "testNetworkConfigurationId" } - ] + ], + "blockExplorerUrls": [] }, "0x5": { "chainId": "0x5", "name": "Goerli", "nativeCurrency": "ETH", "defaultRpcEndpointIndex": 0, + "ticker": "ETH", "rpcEndpoints": [ { "type": "custom", "url": "https://goerli.com", "networkClientId": "goerli" } - ] + ], + "blockExplorerUrls": [] } }, "internalAccounts": { diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx index a09b5128396a..dede9004d29e 100644 --- a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx @@ -1,10 +1,7 @@ import React, { useEffect, useRef, useState, useContext, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - getCurrentNetwork, - getNetworkConfigurationsByChainId, - getPreferences, -} from '../../../../../selectors'; +import { getCurrentNetwork, getPreferences } from '../../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../../shared/modules/selectors/networks'; import { Box, ButtonBase, diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 114ced0496ca..6a3036f88764 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -7,10 +7,10 @@ import { getAllDetectedTokensForSelectedAddress, getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, - getNetworkConfigurationsByChainId, getPreferences, getSelectedAccount, } from '../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; import { getMultichainIsEvm, getMultichainSelectedAccountCachedBalance, diff --git a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx index 29e4c97e2c82..4e9aa14eea25 100644 --- a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx +++ b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx @@ -5,11 +5,11 @@ import { getCurrentChainId, getCurrentNetwork, getPreferences, - getNetworkConfigurationsByChainId, getChainIdsToPoll, getShouldHideZeroBalanceTokens, getSelectedAccount, } from '../../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../../shared/modules/selectors/networks'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { SelectableListItem } from '../sort-control/sort-control'; import { Text } from '../../../../component-library/text/text'; diff --git a/ui/components/app/currency-input/currency-input.js b/ui/components/app/currency-input/currency-input.js index 43da00ad3ab0..3efcecb35150 100644 --- a/ui/components/app/currency-input/currency-input.js +++ b/ui/components/app/currency-input/currency-input.js @@ -5,10 +5,8 @@ import { Box } from '../../component-library'; import { BlockSize } from '../../../helpers/constants/design-system'; import UnitInput from '../../ui/unit-input'; import CurrencyDisplay from '../../ui/currency-display'; -import { - getNativeCurrency, - getProviderConfig, -} from '../../../ducks/metamask/metamask'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { getCurrentChainId, getCurrentCurrency, diff --git a/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js b/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js index 5974d8ba71b6..ff205f7844f0 100644 --- a/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js +++ b/ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js @@ -14,9 +14,9 @@ import { getCurrentChainId, getCurrentNetwork, getDetectedTokensInCurrentNetwork, - getNetworkConfigurationsByChainId, getPreferences, } from '../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; import Popover from '../../../ui/popover'; import Box from '../../../ui/box'; diff --git a/ui/components/app/detected-token/detected-token.js b/ui/components/app/detected-token/detected-token.js index 31e100547e12..bb215b4f7301 100644 --- a/ui/components/app/detected-token/detected-token.js +++ b/ui/components/app/detected-token/detected-token.js @@ -12,10 +12,12 @@ import { getAllDetectedTokensForSelectedAddress, getCurrentChainId, getDetectedTokensInCurrentNetwork, - getNetworkConfigurationsByChainId, getPreferences, - getSelectedNetworkClientId, } from '../../../selectors'; +import { + getSelectedNetworkClientId, + getNetworkConfigurationsByChainId, +} from '../../../../shared/modules/selectors/networks'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { diff --git a/ui/components/app/loading-network-screen/loading-network-screen.container.js b/ui/components/app/loading-network-screen/loading-network-screen.container.js index 34a626bc1b5e..77920ad68032 100644 --- a/ui/components/app/loading-network-screen/loading-network-screen.container.js +++ b/ui/components/app/loading-network-screen/loading-network-screen.container.js @@ -4,9 +4,11 @@ import * as actions from '../../../store/actions'; import { getAllEnabledNetworks, getNetworkIdentifier, - isNetworkLoading, } from '../../../selectors'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { + getProviderConfig, + isNetworkLoading, +} from '../../../../shared/modules/selectors/networks'; import LoadingNetworkScreen from './loading-network-screen.component'; const DEPRECATED_TEST_NET_CHAINIDS = ['0x3', '0x2a', '0x4']; diff --git a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js index ac4c444692d2..ccca2935bf4d 100644 --- a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js +++ b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'; import { removeNetwork } from '../../../../store/actions'; -import { getNetworkConfigurationsByChainId } from '../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; import ConfirmDeleteNetwork from './confirm-delete-network.component'; const mapStateToProps = (state, ownProps) => { diff --git a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js index e1662a641ae7..c34e4897d50a 100644 --- a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js +++ b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js @@ -9,10 +9,8 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../../../../selectors'; +import { getCurrentChainId } from '../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; function mapStateToProps(state) { return { diff --git a/ui/components/app/modals/transaction-already-confirmed/transaction-already-confirmed.tsx b/ui/components/app/modals/transaction-already-confirmed/transaction-already-confirmed.tsx index 1bd2afc75926..031ffcf175c8 100644 --- a/ui/components/app/modals/transaction-already-confirmed/transaction-already-confirmed.tsx +++ b/ui/components/app/modals/transaction-already-confirmed/transaction-already-confirmed.tsx @@ -2,7 +2,6 @@ import React, { useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { type TransactionMeta } from '@metamask/transaction-controller'; -import { type NetworkClientConfiguration } from '@metamask/network-controller'; import { getRpcPrefsForCurrentProvider, getTransaction, @@ -38,9 +37,7 @@ export default function TransactionAlreadyConfirmed() { // eslint-disable-next-line @typescript-eslint/no-explicit-any (getTransaction as any)(state, originalTransactionId), ); - const rpcPrefs: NetworkClientConfiguration = useSelector( - getRpcPrefsForCurrentProvider, - ); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const viewTransaction = () => { // TODO: Fix getBlockExplorerLink arguments compatible with the actual controller types @@ -48,9 +45,7 @@ export default function TransactionAlreadyConfirmed() { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any transaction as any, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - rpcPrefs as any, + rpcPrefs, ); global.platform.openTab({ url: blockExplorerLink, diff --git a/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.tsx b/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.tsx index 0d6985dbfb4d..3136c92993dd 100644 --- a/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.tsx +++ b/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.tsx @@ -24,7 +24,7 @@ import { setShowMultiRpcModal } from '../../../store/actions'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../../app/scripts/lib/util'; -import { getNetworkConfigurationsByChainId } from '../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import NetworkListItem from './network-list-item/network-list-item'; diff --git a/ui/components/app/selected-account/selected-account-component.test.js b/ui/components/app/selected-account/selected-account-component.test.js index 9545580bebbb..257ee44aa46a 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -52,13 +52,6 @@ jest.mock('../../../selectors', () => { getAccountType: mockGetAccountType, getSelectedInternalAccount: mockGetSelectedAccount, getCurrentChainId: jest.fn(() => '0x5'), - getSelectedNetworkClientId: jest.fn(() => 'goerli'), - getNetworkConfigurationsByChainId: jest.fn(() => ({ - '0x5': { - chainId: '0x5', - rpcEndpoints: [{ networkClientId: 'goerli' }], - }, - })), }; }); diff --git a/ui/components/app/selected-account/selected-account.container.js b/ui/components/app/selected-account/selected-account.container.js index a6e0c03a347a..2ba78fa4c2de 100644 --- a/ui/components/app/selected-account/selected-account.container.js +++ b/ui/components/app/selected-account/selected-account.container.js @@ -10,7 +10,7 @@ import { getCustodyAccountDetails, getIsCustodianSupportedChain, } from '../../../selectors/institutional/selectors'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; ///: END:ONLY_INCLUDE_IF import SelectedAccount from './selected-account.component'; diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx index 1a335de9c14b..72b2e89e49c6 100644 --- a/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.test.tsx @@ -8,11 +8,11 @@ import { getShouldHideZeroBalanceTokens, getPreferences, getMarketData, - getNetworkConfigurationsByChainId, getAllTokens, getChainIdsToPoll, } from '../../../selectors'; import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountTotalCrossChainFiatBalance'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { AggregatedPercentageOverviewCrossChains } from './aggregated-percentage-overview-cross-chains'; jest.mock('react-redux', () => ({ @@ -36,11 +36,14 @@ jest.mock('../../../selectors', () => ({ getPreferences: jest.fn(), getShouldHideZeroBalanceTokens: jest.fn(), getMarketData: jest.fn(), - getNetworkConfigurationsByChainId: jest.fn(), getAllTokens: jest.fn(), getChainIdsToPoll: jest.fn(), })); +jest.mock('../../../../shared/modules/selectors/networks', () => ({ + getNetworkConfigurationsByChainId: jest.fn(), +})); + jest.mock('../../../hooks/useAccountTotalCrossChainFiatBalance', () => ({ useAccountTotalCrossChainFiatBalance: jest.fn(), })); diff --git a/ui/components/institutional/wrong-network-notification/wrong-network-notification.tsx b/ui/components/institutional/wrong-network-notification/wrong-network-notification.tsx index 217ce2e575c0..f5c412260bb2 100644 --- a/ui/components/institutional/wrong-network-notification/wrong-network-notification.tsx +++ b/ui/components/institutional/wrong-network-notification/wrong-network-notification.tsx @@ -11,16 +11,13 @@ import { import { getSelectedAccountCachedBalance } from '../../../selectors'; import { getIsCustodianSupportedChain } from '../../../selectors/institutional/selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { Icon, IconName, IconSize, Box, Text } from '../../component-library'; const WrongNetworkNotification: React.FC = () => { const t = useI18nContext(); - const providerConfig = useSelector< - object, - { nickname?: string; type: string } | undefined - >(getProviderConfig); - const balance = useSelector(getSelectedAccountCachedBalance); + const providerConfig = useSelector(getProviderConfig); + const balance = useSelector(getSelectedAccountCachedBalance); const isCustodianSupportedChain = useSelector(getIsCustodianSupportedChain); diff --git a/ui/components/multichain/address-copy-button/address-copy-button.js b/ui/components/multichain/address-copy-button/address-copy-button.js index fa8b0803ee0b..8fb73c299fd2 100644 --- a/ui/components/multichain/address-copy-button/address-copy-button.js +++ b/ui/components/multichain/address-copy-button/address-copy-button.js @@ -8,7 +8,7 @@ import { getIsCustodianSupportedChain, getCustodianIconForAddress, } from '../../../selectors/institutional/selectors'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; ///: END:ONLY_INCLUDE_IF import { ButtonBase, IconName, Box } from '../../component-library'; import { diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx index d674fbef528e..f64209543557 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx @@ -19,8 +19,10 @@ import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constan import { useI18nContext } from '../../../../hooks/useI18nContext'; ///: END:ONLY_INCLUDE_IF import { NetworkListItem } from '../../network-list-item'; -import { getNetworkConfigurationsByChainId } from '../../../../selectors'; -import { getProviderConfig } from '../../../../ducks/metamask/metamask'; +import { + getNetworkConfigurationsByChainId, + getProviderConfig, +} from '../../../../../shared/modules/selectors/networks'; /** * AssetPickerModalNetwork component displays a modal for selecting a network in the asset picker. diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx index ff665ce0f8f8..cf87ee3ba7f1 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx @@ -24,10 +24,8 @@ import { } from '../../../../helpers/constants/design-system'; import { AssetType } from '../../../../../shared/constants/transaction'; import { AssetPickerModal } from '../asset-picker-modal/asset-picker-modal'; -import { - getCurrentNetwork, - getNetworkConfigurationsByChainId, -} from '../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; +import { getCurrentNetwork } from '../../../../selectors'; import Tooltip from '../../../ui/tooltip'; import { LARGE_SYMBOL_LENGTH } from '../constants'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.js b/ui/components/multichain/detected-token-banner/detected-token-banner.js index 59db889d11ce..4f76fa22fb35 100644 --- a/ui/components/multichain/detected-token-banner/detected-token-banner.js +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.js @@ -9,8 +9,8 @@ import { getDetectedTokensInCurrentNetwork, getAllDetectedTokensForSelectedAddress, getPreferences, - getNetworkConfigurationsByChainId, } from '../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal.js b/ui/components/multichain/import-tokens-modal/import-tokens-modal.js index b9ef596b8f07..a1194f2285f2 100644 --- a/ui/components/multichain/import-tokens-modal/import-tokens-modal.js +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal.js @@ -21,13 +21,13 @@ import { getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getRpcPrefsForCurrentProvider, getSelectedInternalAccount, - getSelectedNetworkClientId, getTokenDetectionSupportNetworkByChainId, getTokenList, getCurrentNetwork, getTestNetworkBackgroundColor, getTokenExchangeRates, } from '../../../selectors'; +import { getSelectedNetworkClientId } from '../../../../shared/modules/selectors/networks'; import { addImportedTokens, clearPendingTokens, diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 9c99b3048a95..8b18170adc6c 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -36,6 +36,7 @@ import { FEATURED_RPCS, TEST_CHAINS, } from '../../../../shared/constants/network'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { getCurrentChainId, getShowTestNetworks, @@ -44,7 +45,6 @@ import { getOriginOfCurrentTab, getUseRequestQueue, getEditedNetwork, - getNetworkConfigurationsByChainId, getOrderedNetworksList, getIsAddingNewNetwork, getIsMultiRpcOnboarding, diff --git a/ui/components/multichain/notification-detail-block-explorer-button/notification-detail-block-explorer-button.tsx b/ui/components/multichain/notification-detail-block-explorer-button/notification-detail-block-explorer-button.tsx index 6ca6e27c491f..8f0395abf230 100644 --- a/ui/components/multichain/notification-detail-block-explorer-button/notification-detail-block-explorer-button.tsx +++ b/ui/components/multichain/notification-detail-block-explorer-button/notification-detail-block-explorer-button.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import type { NotificationServicesController } from '@metamask/notification-services-controller'; import { toHex } from '@metamask/controller-utils'; -import { getNetworkConfigurationsByChainId } from '../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { ButtonVariant } from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx index 66e7cadd7546..f65dd7a662cf 100644 --- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx +++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx @@ -11,10 +11,10 @@ import { FlexDirection, } from '../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; import { getConnectedSitesList, getInternalAccounts, - getNetworkConfigurationsByChainId, getPermissionSubjects, getPermittedAccountsForSelectedTab, getPermittedChainsForSelectedTab, diff --git a/ui/components/multichain/pages/send/components/recipient-content.test.tsx b/ui/components/multichain/pages/send/components/recipient-content.test.tsx index d738aee3cb8a..fe3f1b539ea3 100644 --- a/ui/components/multichain/pages/send/components/recipient-content.test.tsx +++ b/ui/components/multichain/pages/send/components/recipient-content.test.tsx @@ -21,7 +21,7 @@ jest.mock('reselect', () => ({ createSelector: jest.fn(), })); -jest.mock('../../../../../selectors/util', () => ({ +jest.mock('../../../../../../shared/modules/selectors/util', () => ({ createDeepEqualSelector: jest.fn(), })); diff --git a/ui/components/ui/deprecated-networks/deprecated-networks.js b/ui/components/ui/deprecated-networks/deprecated-networks.js index 736b5badb6a0..de7bbec1fd28 100644 --- a/ui/components/ui/deprecated-networks/deprecated-networks.js +++ b/ui/components/ui/deprecated-networks/deprecated-networks.js @@ -7,10 +7,8 @@ import { Severity, } from '../../../helpers/constants/design-system'; -import { - getCurrentNetwork, - getNetworkConfigurationsByChainId, -} from '../../../selectors'; +import { getCurrentNetwork } from '../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { getCompletedOnboarding } from '../../../ducks/metamask/metamask'; import { BannerAlert, Box } from '../../component-library'; import { diff --git a/ui/components/ui/new-network-info/new-network-info.js b/ui/components/ui/new-network-info/new-network-info.js index 51b96bdc5b36..15a11918afc1 100644 --- a/ui/components/ui/new-network-info/new-network-info.js +++ b/ui/components/ui/new-network-info/new-network-info.js @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'; import { TOKEN_API_METASWAP_CODEFI_URL } from '../../../../shared/constants/tokens'; import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { I18nContext } from '../../../contexts/i18n'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { AlignItems, BackgroundColor, diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 5624a0ec5569..86f4c8155b17 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -1,11 +1,7 @@ -import { - NetworkConfiguration, - NetworkState, -} from '@metamask/network-controller'; +import { NetworkConfiguration } from '@metamask/network-controller'; import { uniqBy } from 'lodash'; import { createSelector } from 'reselect'; import { - getNetworkConfigurationsByChainId, getIsBridgeEnabled, getSwapsDefaultToken, SwapsEthToken, @@ -17,8 +13,12 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../app/scripts/controllers/bridge/types'; -import { createDeepEqualSelector } from '../../selectors/util'; -import { getProviderConfig } from '../metamask/metamask'; +import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; +import { + NetworkState, + getProviderConfig, + getNetworkConfigurationsByChainId, +} from '../../../shared/modules/selectors/networks'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; // TODO: Remove restricted import @@ -26,8 +26,8 @@ import { calcTokenAmount } from '../../../shared/lib/transactions-controller-uti import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { BridgeState } from './bridge'; -export type BridgeAppState = { - metamask: NetworkState & { bridgeState: BridgeControllerState } & { +type BridgeAppState = NetworkState & { + metamask: { bridgeState: BridgeControllerState } & { useExternalServices: boolean; }; bridge: BridgeState; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index af456e29acbc..93550d45fc10 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -1,7 +1,6 @@ import { addHexPrefix, isHexString } from 'ethereumjs-util'; import { createSelector } from 'reselect'; import { mergeGasFeeEstimates } from '@metamask/transaction-controller'; -import { RpcEndpointType } from '@metamask/network-controller'; import { AlertTypes } from '../../../shared/constants/alerts'; import { GasEstimateTypes, @@ -16,9 +15,11 @@ import { accountsWithSendEtherInfoSelector, checkNetworkAndAccountSupports1559, getAddressBook, - getSelectedNetworkClientId, - getNetworkConfigurationsByChainId, } from '../../selectors/selectors'; +import { + getProviderConfig, + getSelectedNetworkClientId, +} from '../../../shared/modules/selectors/networks'; import { getSelectedInternalAccount } from '../../selectors/accounts'; import * as actionConstants from '../../store/actionConstants'; import { updateTransactionGasFees } from '../../store/actions'; @@ -274,42 +275,6 @@ export function updateGasFees({ export const getAlertEnabledness = (state) => state.metamask.alertEnabledness; -/** - * Get the provider configuration for the current selected network. - * - * @param {object} state - Redux state object. - */ -export const getProviderConfig = createSelector( - (state) => getNetworkConfigurationsByChainId(state), - (state) => getSelectedNetworkClientId(state), - (networkConfigurationsByChainId, selectedNetworkClientId) => { - for (const network of Object.values(networkConfigurationsByChainId)) { - for (const rpcEndpoint of network.rpcEndpoints) { - if (rpcEndpoint.networkClientId === selectedNetworkClientId) { - const blockExplorerUrl = - network.blockExplorerUrls?.[network.defaultBlockExplorerUrlIndex]; - - return { - chainId: network.chainId, - ticker: network.nativeCurrency, - rpcPrefs: { ...(blockExplorerUrl && { blockExplorerUrl }) }, - type: - rpcEndpoint.type === RpcEndpointType.Custom - ? 'rpc' - : rpcEndpoint.networkClientId, - ...(rpcEndpoint.type === RpcEndpointType.Custom && { - id: rpcEndpoint.networkClientId, - nickname: network.name, - rpcUrl: rpcEndpoint.url, - }), - }; - } - } - } - return undefined; // should not be reachable - }, -); - export const getUnconnectedAccountAlertEnabledness = (state) => getAlertEnabledness(state)[AlertTypes.unconnectedAccount]; diff --git a/ui/ducks/send/helpers.test.js b/ui/ducks/send/helpers.test.js index 74f660991cd7..7129ffca8194 100644 --- a/ui/ducks/send/helpers.test.js +++ b/ui/ducks/send/helpers.test.js @@ -64,7 +64,6 @@ jest.mock('../metamask/metamask', () => ({ ...jest.requireActual('../metamask/metamask'), getGasFeeEstimates: jest.fn(), getNativeCurrency: jest.fn(), - getProviderConfig: jest.fn(), })); jest.mock('../swaps/swaps', () => ({ diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 30cbc6eeb5dd..15acc3355d8f 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -53,10 +53,13 @@ import { getSelectedInternalAccount, getSelectedInternalAccountWithBalance, getUnapprovedTransactions, - getSelectedNetworkClientId, getIsSwapsChain, getUseExternalServices, } from '../../selectors'; +import { + getSelectedNetworkClientId, + getProviderConfig, +} from '../../../shared/modules/selectors/networks'; import { displayWarning, hideLoadingIndication, @@ -101,7 +104,6 @@ import { import { getGasEstimateType, getNativeCurrency, - getProviderConfig, getTokens, } from '../metamask/metamask'; diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index fe7a21e2206f..c4ae1cca57a3 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -30,7 +30,7 @@ import { isHardwareKeyring } from '../../helpers/utils/hardware'; import { getPortfolioUrl } from '../../helpers/utils/portfolio'; import { setSwapsFromToken } from '../../ducks/swaps/swaps'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; -import { getProviderConfig } from '../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../shared/modules/selectors/networks'; ///: END:ONLY_INCLUDE_IF const useBridging = () => { diff --git a/ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts b/ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts index 9fe819d92171..dd5b8aaab579 100644 --- a/ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts +++ b/ui/hooks/useAccountTotalCrossChainFiatBalance.test.ts @@ -3,11 +3,11 @@ import { renderHook } from '@testing-library/react-hooks'; import { act } from 'react-dom/test-utils'; import { getCurrentCurrency, - getNetworkConfigurationsByChainId, getCrossChainTokenExchangeRates, getCrossChainMetaMaskCachedBalances, } from '../selectors'; import { getCurrencyRates } from '../ducks/metamask/metamask'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; import { FormattedTokensWithBalances, useAccountTotalCrossChainFiatBalance, @@ -19,13 +19,16 @@ jest.mock('react-redux', () => ({ jest.mock('../selectors', () => ({ getCurrentCurrency: jest.fn(), - getNetworkConfigurationsByChainId: jest.fn(), getCrossChainTokenExchangeRates: jest.fn(), getCrossChainMetaMaskCachedBalances: jest.fn(), })); jest.mock('../ducks/metamask/metamask', () => ({ getCurrencyRates: jest.fn(), })); +jest.mock('../../shared/modules/selectors/networks', () => ({ + getSelectedNetworkClientId: jest.fn(), + getNetworkConfigurationsByChainId: jest.fn(), +})); const mockGetCurrencyRates = getCurrencyRates as jest.Mock; const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock; diff --git a/ui/hooks/useAccountTotalCrossChainFiatBalance.ts b/ui/hooks/useAccountTotalCrossChainFiatBalance.ts index d63328e4fbcf..ac5658278946 100644 --- a/ui/hooks/useAccountTotalCrossChainFiatBalance.ts +++ b/ui/hooks/useAccountTotalCrossChainFiatBalance.ts @@ -2,7 +2,6 @@ import { shallowEqual, useSelector } from 'react-redux'; import { toChecksumAddress } from 'ethereumjs-util'; import { getCurrentCurrency, - getNetworkConfigurationsByChainId, getCrossChainTokenExchangeRates, getCrossChainMetaMaskCachedBalances, } from '../selectors'; @@ -13,6 +12,7 @@ import { import { getCurrencyRates } from '../ducks/metamask/metamask'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; import { TokenWithBalance } from '../components/app/assets/asset-list/asset-list'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; type AddressBalances = { [address: string]: number; diff --git a/ui/hooks/useAccountTrackerPolling.ts b/ui/hooks/useAccountTrackerPolling.ts index cc7f9aee3818..5dffe5dfc852 100644 --- a/ui/hooks/useAccountTrackerPolling.ts +++ b/ui/hooks/useAccountTrackerPolling.ts @@ -1,8 +1,5 @@ import { useSelector } from 'react-redux'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../selectors'; +import { getCurrentChainId } from '../selectors'; import { accountTrackerStartPolling, accountTrackerStopPollingByPollingToken, @@ -11,6 +8,7 @@ import { getCompletedOnboarding, getIsUnlocked, } from '../ducks/metamask/metamask'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; import useMultiPolling from './useMultiPolling'; const useAccountTrackerPolling = () => { diff --git a/ui/hooks/useCurrencyRatePolling.ts b/ui/hooks/useCurrencyRatePolling.ts index 34772a94a501..cd8c616cd4b9 100644 --- a/ui/hooks/useCurrencyRatePolling.ts +++ b/ui/hooks/useCurrencyRatePolling.ts @@ -1,8 +1,6 @@ import { useSelector } from 'react-redux'; -import { - getNetworkConfigurationsByChainId, - getUseCurrencyRateCheck, -} from '../selectors'; +import { getUseCurrencyRateCheck } from '../selectors'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; import { currencyRateStartPolling, currencyRateStopPollingByPollingToken, diff --git a/ui/hooks/useGasFeeEstimates.js b/ui/hooks/useGasFeeEstimates.js index abbaf0db0bb9..b1b686256059 100644 --- a/ui/hooks/useGasFeeEstimates.js +++ b/ui/hooks/useGasFeeEstimates.js @@ -12,7 +12,7 @@ import { gasFeeStopPollingByPollingToken, getNetworkConfigurationByNetworkClientId, } from '../store/actions'; -import { getSelectedNetworkClientId } from '../selectors'; +import { getSelectedNetworkClientId } from '../../shared/modules/selectors/networks'; import usePolling from './usePolling'; /** diff --git a/ui/hooks/useGasFeeEstimates.test.js b/ui/hooks/useGasFeeEstimates.test.js index dd63e10581d0..c09785ac36ab 100644 --- a/ui/hooks/useGasFeeEstimates.test.js +++ b/ui/hooks/useGasFeeEstimates.test.js @@ -36,13 +36,16 @@ jest.mock('../ducks/metamask/metamask', () => ({ .mockReturnValue('getIsNetworkBusyByChainId'), })); +jest.mock('../../shared/modules/selectors/networks', () => ({ + getSelectedNetworkClientId: jest + .fn() + .mockReturnValue('getSelectedNetworkClientId'), +})); + jest.mock('../selectors', () => ({ checkNetworkAndAccountSupports1559: jest .fn() .mockReturnValue('checkNetworkAndAccountSupports1559'), - getSelectedNetworkClientId: jest - .fn() - .mockReturnValue('getSelectedNetworkClientId'), })); jest.mock('react-redux', () => { diff --git a/ui/hooks/useMMICustodySendTransaction.ts b/ui/hooks/useMMICustodySendTransaction.ts index 49634fbf0174..ac434bb5d8db 100644 --- a/ui/hooks/useMMICustodySendTransaction.ts +++ b/ui/hooks/useMMICustodySendTransaction.ts @@ -19,7 +19,7 @@ import { getConfirmationSender } from '../pages/confirmations/components/confirm import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { getSmartTransactionsEnabled } from '../../shared/modules/selectors'; import { CHAIN_ID_TO_RPC_URL_MAP } from '../../shared/constants/network'; -import { getProviderConfig } from '../ducks/metamask/metamask'; +import { getProviderConfig } from '../../shared/modules/selectors/networks'; type MMITransactionMeta = TransactionMeta & { txParams: { from: string }; diff --git a/ui/hooks/useMultichainSelector.test.ts b/ui/hooks/useMultichainSelector.test.ts index 35ed5cf7b155..e5d7f197ffcb 100644 --- a/ui/hooks/useMultichainSelector.test.ts +++ b/ui/hooks/useMultichainSelector.test.ts @@ -1,7 +1,7 @@ import { InternalAccount } from '@metamask/keyring-api'; import { createMockInternalAccount } from '../../test/jest/mocks'; import { renderHookWithProvider } from '../../test/lib/render-helpers'; -import { getSelectedNetworkClientId } from '../selectors'; +import { getSelectedNetworkClientId } from '../../shared/modules/selectors/networks'; import { MultichainState, getMultichainIsEvm } from '../selectors/multichain'; import { CHAIN_IDS } from '../../shared/constants/network'; import { mockNetworkState } from '../../test/stub/networks'; diff --git a/ui/hooks/useTokenBalances.ts b/ui/hooks/useTokenBalances.ts index 8d3a078f8d07..52add5a99592 100644 --- a/ui/hooks/useTokenBalances.ts +++ b/ui/hooks/useTokenBalances.ts @@ -2,7 +2,7 @@ import { useSelector } from 'react-redux'; import BN from 'bn.js'; import { Token } from '@metamask/assets-controllers'; import { Hex } from '@metamask/utils'; -import { getNetworkConfigurationsByChainId } from '../selectors'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; import { tokenBalancesStartPolling, tokenBalancesStopPollingByPollingToken, diff --git a/ui/hooks/useTokenDetectionPolling.ts b/ui/hooks/useTokenDetectionPolling.ts index d2e08d01892d..66e027b5af6b 100644 --- a/ui/hooks/useTokenDetectionPolling.ts +++ b/ui/hooks/useTokenDetectionPolling.ts @@ -1,9 +1,6 @@ import { useSelector } from 'react-redux'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, - getUseTokenDetection, -} from '../selectors'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; +import { getCurrentChainId, getUseTokenDetection } from '../selectors'; import { tokenDetectionStartPolling, tokenDetectionStopPollingByPollingToken, diff --git a/ui/hooks/useTokenFiatAmount.js b/ui/hooks/useTokenFiatAmount.js index 5abbbc608de0..2728b2994781 100644 --- a/ui/hooks/useTokenFiatAmount.js +++ b/ui/hooks/useTokenFiatAmount.js @@ -7,8 +7,8 @@ import { getConfirmationExchangeRates, getMarketData, getCurrencyRates, - getNetworkConfigurationsByChainId, } from '../selectors'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; import { getConversionRate } from '../ducks/metamask/metamask'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; diff --git a/ui/hooks/useTokenListPolling.ts b/ui/hooks/useTokenListPolling.ts index 7f7de517c304..98ea0c324da4 100644 --- a/ui/hooks/useTokenListPolling.ts +++ b/ui/hooks/useTokenListPolling.ts @@ -1,12 +1,12 @@ import { useSelector } from 'react-redux'; import { getCurrentChainId, - getNetworkConfigurationsByChainId, getPetnamesEnabled, getUseExternalServices, getUseTokenDetection, getUseTransactionSimulations, } from '../selectors'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; import { tokenListStartPolling, tokenListStopPollingByPollingToken, diff --git a/ui/hooks/useTokenRatesPolling.ts b/ui/hooks/useTokenRatesPolling.ts index 37864ec89b82..bd57da835a99 100644 --- a/ui/hooks/useTokenRatesPolling.ts +++ b/ui/hooks/useTokenRatesPolling.ts @@ -2,11 +2,11 @@ import { useSelector } from 'react-redux'; import { getCurrentChainId, getMarketData, - getNetworkConfigurationsByChainId, getTokenExchangeRates, getTokensMarketData, getUseCurrencyRateCheck, } from '../selectors'; +import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks'; import { tokenRatesStartPolling, tokenRatesStopPollingByPollingToken, diff --git a/ui/hooks/useTokenTracker.js b/ui/hooks/useTokenTracker.js index 0ce2c9cbcac2..c38b3c295f58 100644 --- a/ui/hooks/useTokenTracker.js +++ b/ui/hooks/useTokenTracker.js @@ -2,9 +2,9 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import TokenTracker from '@metamask/eth-token-tracker'; import { shallowEqual, useSelector } from 'react-redux'; import { getSelectedInternalAccount } from '../selectors'; +import { getProviderConfig } from '../../shared/modules/selectors/networks'; import { SECOND } from '../../shared/constants/time'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; -import { getProviderConfig } from '../ducks/metamask/metamask'; import { useEqualityCheck } from './useEqualityCheck'; export function useTokenTracker({ diff --git a/ui/pages/asset/components/chart/asset-chart.tsx b/ui/pages/asset/components/chart/asset-chart.tsx index efa7adad03ad..b49eaf7ced90 100644 --- a/ui/pages/asset/components/chart/asset-chart.tsx +++ b/ui/pages/asset/components/chart/asset-chart.tsx @@ -14,6 +14,7 @@ import { import { Line } from 'react-chartjs-2'; import classnames from 'classnames'; import { brandColor } from '@metamask/design-tokens'; +import { Hex } from '@metamask/utils'; import { useTheme } from '../../../../hooks/useTheme'; import { BackgroundColor, @@ -80,7 +81,7 @@ const AssetChart = ({ currentPrice, currency, }: { - chainId: `0x${string}`; + chainId: Hex; address: string; currentPrice?: number; currency: string; diff --git a/ui/pages/asset/components/native-asset.tsx b/ui/pages/asset/components/native-asset.tsx index abd571b231a7..38f24f8d5bea 100644 --- a/ui/pages/asset/components/native-asset.tsx +++ b/ui/pages/asset/components/native-asset.tsx @@ -8,7 +8,7 @@ import { getSelectedInternalAccount, getNativeCurrencyForChain, } from '../../../selectors'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { AssetType } from '../../../../shared/constants/transaction'; import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; diff --git a/ui/pages/asset/components/token-asset.tsx b/ui/pages/asset/components/token-asset.tsx index ef91b92aae5d..80ff721a216c 100644 --- a/ui/pages/asset/components/token-asset.tsx +++ b/ui/pages/asset/components/token-asset.tsx @@ -6,7 +6,6 @@ import { useHistory } from 'react-router-dom'; import { Hex } from '@metamask/utils'; import { NetworkConfiguration } from '@metamask/network-controller'; import { - getNetworkConfigurationsByChainId, getSelectedInternalAccount, getTokenList, selectERC20TokensByChain, @@ -22,6 +21,7 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; import { showModal } from '../../../store/actions'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import AssetOptions from './asset-options'; import AssetPage from './asset-page'; diff --git a/ui/pages/asset/useHistoricalPrices.ts b/ui/pages/asset/useHistoricalPrices.ts index febf99a9daed..38c76fcf9de1 100644 --- a/ui/pages/asset/useHistoricalPrices.ts +++ b/ui/pages/asset/useHistoricalPrices.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; // @ts-expect-error suppress CommonJS vs ECMAScript error import { Point } from 'chart.js'; import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { MINUTE } from '../../../shared/constants/time'; import { getShouldShowFiat } from '../../selectors'; @@ -20,7 +21,7 @@ export const useHistoricalPrices = ({ currency, timeRange, }: { - chainId: `0x${string}`; + chainId: Hex; address: string; currency: string; timeRange: TimeRange; diff --git a/ui/pages/asset/util.ts b/ui/pages/asset/util.ts index c18d6e92dcfa..2f4a41df6cc9 100644 --- a/ui/pages/asset/util.ts +++ b/ui/pages/asset/util.ts @@ -1,4 +1,5 @@ import { SUPPORTED_CHAIN_IDS, Token } from '@metamask/assets-controllers'; +import { Hex } from '@metamask/utils'; /** Formats a datetime in a short human readable format like 'Feb 8, 12:11 PM' */ export const getShortDateFormatter = () => @@ -59,7 +60,7 @@ export const getPricePrecision = (price: number) => { * * @param chainId - The hexadecimal chain id. */ -export const chainSupportsPricing = (chainId: `0x${string}`) => +export const chainSupportsPricing = (chainId: Hex) => (SUPPORTED_CHAIN_IDS as readonly string[]).includes(chainId); /** The opacity components should set during transition */ diff --git a/ui/pages/bridge/hooks/useAddToken.ts b/ui/pages/bridge/hooks/useAddToken.ts index 597149b16e49..d0d29847bf57 100644 --- a/ui/pages/bridge/hooks/useAddToken.ts +++ b/ui/pages/bridge/hooks/useAddToken.ts @@ -2,12 +2,12 @@ import { useDispatch, useSelector } from 'react-redux'; import { NetworkConfiguration } from '@metamask/network-controller'; import { Numeric } from '../../../../shared/modules/Numeric'; import { QuoteResponse } from '../types'; +import { FEATURED_RPCS } from '../../../../shared/constants/network'; +import { addToken, addNetwork } from '../../../store/actions'; import { getNetworkConfigurationsByChainId, getSelectedNetworkClientId, -} from '../../../selectors'; -import { FEATURED_RPCS } from '../../../../shared/constants/network'; -import { addToken, addNetwork } from '../../../store/actions'; +} from '../../../../shared/modules/selectors/networks'; export default function useAddToken() { const dispatch = useDispatch(); diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx index 20f471b1065b..54d7b9e96700 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import { MemoryRouter, useHistory } from 'react-router-dom'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import * as actions from '../../../store/actions'; -import * as selectors from '../../../selectors'; +import * as networks from '../../../../shared/modules/selectors/networks'; import { DummyQuotesNoApproval, DummyQuotesWithApproval, @@ -41,13 +41,12 @@ jest.mock('../../../store/actions', () => { }; }); -jest.mock('../../../selectors', () => { - const original = jest.requireActual('../../../selectors'); +jest.mock('../../../../shared/modules/selectors/networks', () => { + const original = jest.requireActual( + '../../../../shared/modules/selectors/networks', + ); return { ...original, - getIsBridgeEnabled: () => true, - getIsBridgeChain: () => true, - checkNetworkAndAccountSupports1559: () => true, getSelectedNetworkClientId: () => 'mainnet', getNetworkConfigurationsByChainId: jest.fn(() => ({ '0x1': { @@ -84,6 +83,16 @@ jest.mock('../../../selectors', () => { }; }); +jest.mock('../../../selectors', () => { + const original = jest.requireActual('../../../selectors'); + return { + ...original, + getIsBridgeEnabled: () => true, + getIsBridgeChain: () => true, + checkNetworkAndAccountSupports1559: () => true, + }; +}); + const middleware = [thunk]; const makeMockStore = () => { @@ -402,7 +411,7 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { ); const mockedGetNetworkConfigurationsByChainId = // @ts-expect-error this is a jest mock - selectors.getNetworkConfigurationsByChainId as jest.Mock; + networks.getNetworkConfigurationsByChainId as jest.Mock; mockedGetNetworkConfigurationsByChainId.mockImplementationOnce(() => ({ '0x1': { blockExplorerUrls: ['https://etherscan.io'], diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index 2c9f082519e9..687057094005 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -17,13 +17,13 @@ import { IconName, } from '../../components/component-library'; import { getIsBridgeChain, getIsBridgeEnabled } from '../../selectors'; +import { getProviderConfig } from '../../../shared/modules/selectors/networks'; import useBridging from '../../hooks/bridge/useBridging'; import { Content, Footer, Header, } from '../../components/multichain/pages/page'; -import { getProviderConfig } from '../../ducks/metamask/metamask'; import { resetBridgeState, setFromChain } from '../../ducks/bridge/actions'; import { useSwapsFeatureFlags } from '../swaps/hooks/useSwapsFeatureFlags'; import PrepareBridgePage from './prepare/prepare-bridge-page'; diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 46fbdd77786b..b0553407686d 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -41,7 +41,7 @@ import { QuoteRequest } from '../types'; import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; import { BridgeQuoteCard } from '../quotes/bridge-quote-card'; import { isValidQuoteRequest } from '../utils/quote'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { BridgeInputGroup } from './bridge-input-group'; const PrepareBridgePage = () => { diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useSupportsEIP1559.ts b/ui/pages/confirmations/components/confirm/info/hooks/useSupportsEIP1559.ts index 57fbd78de9fc..d492c5030354 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useSupportsEIP1559.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useSupportsEIP1559.ts @@ -4,10 +4,8 @@ import { } from '@metamask/transaction-controller'; import { useSelector } from 'react-redux'; import { isLegacyTransaction } from '../../../../../../helpers/utils/transactions.util'; -import { - checkNetworkAndAccountSupports1559, - getSelectedNetworkClientId, -} from '../../../../../../selectors'; +import { checkNetworkAndAccountSupports1559 } from '../../../../../../selectors'; +import { getSelectedNetworkClientId } from '../../../../../../../shared/modules/selectors/networks'; export function useSupportsEIP1559(transactionMeta: TransactionMeta) { const isLegacyTxn = diff --git a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx index 405236fe66da..09c5ffa922c6 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx @@ -17,9 +17,9 @@ import { TextVariant, } from '../../../../../../../helpers/constants/design-system'; import { getNftImageAlt } from '../../../../../../../helpers/utils/nfts'; -import { getNetworkConfigurationsByChainId } from '../../../../../../../selectors'; import { useConfirmContext } from '../../../../../context/confirm'; import { useAssetDetails } from '../../../../../hooks/useAssetDetails'; +import { getNetworkConfigurationsByChainId } from '../../../../../../../../shared/modules/selectors/networks'; const NFTSendHeading = () => { const { currentConfirmation: transactionMeta } = diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx index b2ee988adde9..dc394fb1442b 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx @@ -26,7 +26,7 @@ import { TextVariant, } from '../../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; -import { getNetworkConfigurationsByChainId } from '../../../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../../../shared/modules/selectors/networks'; import { useConfirmContext } from '../../../../context/confirm'; import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; import { useBalanceChanges } from '../../../simulation-details/useBalanceChanges'; diff --git a/ui/pages/confirmations/components/simulation-details/asset-pill.tsx b/ui/pages/confirmations/components/simulation-details/asset-pill.tsx index 99bd2b3af8ef..0b7efdf13282 100644 --- a/ui/pages/confirmations/components/simulation-details/asset-pill.tsx +++ b/ui/pages/confirmations/components/simulation-details/asset-pill.tsx @@ -19,7 +19,7 @@ import { } from '../../../../helpers/constants/design-system'; import Name from '../../../../components/app/name'; import { TokenStandard } from '../../../../../shared/constants/transaction'; -import { getNetworkConfigurationsByChainId } from '../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../../../shared/constants/network'; import { AssetIdentifier } from './types'; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index c34025e2f0c5..d9a04ace67a9 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -54,7 +54,6 @@ import { getUnapprovedTransactions, getInternalAccountByAddress, getApprovedAndSignedTransactions, - getSelectedNetworkClientId, getPrioritizedUnapprovedTemplatedConfirmations, } from '../../../selectors'; import { @@ -72,6 +71,7 @@ import { getSendToAccounts, findKeyringForAddress, } from '../../../ducks/metamask/metamask'; +import { getSelectedNetworkClientId } from '../../../../shared/modules/selectors/networks'; import { addHexPrefix, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js index 12971f21a2af..ef08fb6bbba1 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js @@ -13,6 +13,7 @@ import { } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getSendTo } from '../../../ducks/send'; +import { getSelectedNetworkClientId } from '../../../../shared/modules/selectors/networks'; import { CONFIRM_DEPLOY_CONTRACT_PATH, CONFIRM_SEND_ETHER_PATH, @@ -27,7 +28,6 @@ import { isTokenMethodAction } from '../../../helpers/utils/transactions.util'; import usePolling from '../../../hooks/usePolling'; import { usePrevious } from '../../../hooks/usePrevious'; import { - getSelectedNetworkClientId, unconfirmedTransactionsHashSelector, unconfirmedTransactionsListSelector, use4ByteResolutionSelector, diff --git a/ui/pages/confirmations/confirmation/confirmation.js b/ui/pages/confirmations/confirmation/confirmation.js index 30816acea1f2..c2d42be40e47 100644 --- a/ui/pages/confirmations/confirmation/confirmation.js +++ b/ui/pages/confirmations/confirmation/confirmation.js @@ -32,9 +32,9 @@ import { getTotalUnapprovedCount, useSafeChainsListValidationSelector, getSnapsMetadata, - getNetworkConfigurationsByChainId, getHideSnapBranding, } from '../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import Callout from '../../../components/ui/callout'; import { Box, Icon, IconName } from '../../../components/component-library'; import Loading from '../../../components/ui/loading-screen'; diff --git a/ui/pages/confirmations/selectors/confirm.ts b/ui/pages/confirmations/selectors/confirm.ts index 57107bdf3021..e1a3282532d6 100644 --- a/ui/pages/confirmations/selectors/confirm.ts +++ b/ui/pages/confirmations/selectors/confirm.ts @@ -3,7 +3,7 @@ import { ApprovalType } from '@metamask/controller-utils'; import { createSelector } from 'reselect'; import { getPendingApprovals } from '../../../selectors/approvals'; import { getPreferences } from '../../../selectors/selectors'; -import { createDeepEqualSelector } from '../../../selectors/util'; +import { createDeepEqualSelector } from '../../../../shared/modules/selectors/util'; import { ConfirmMetamaskState } from '../types/confirm'; const ConfirmationApprovalTypes = [ diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 9d4511021529..da4677111e63 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -26,7 +26,6 @@ import { getTotalUnapprovedCount, getUnapprovedTemplatedConfirmations, getWeb3ShimUsageStateForOrigin, - getInfuraBlocked, getShowWhatsNewPopup, getSortedAnnouncementsToShow, getShowRecoveryPhraseReminder, @@ -51,7 +50,7 @@ import { getAccountType, ///: END:ONLY_INCLUDE_IF } from '../../selectors'; - +import { getInfuraBlocked } from '../../../shared/modules/selectors/networks'; import { closeNotificationPopup, setConnectedStatusPopoverHasBeenShown, diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index aed08f196957..3e0d2e19de48 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -51,8 +51,8 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getPetnamesEnabled, getExternalServicesOnboardingToggleState, - getNetworkConfigurationsByChainId, } from '../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; import { setIpfsGateway, diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index e002e54ef34e..ba9bcc6bf674 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -5,10 +5,10 @@ import { NetworkConfiguration } from '@metamask/network-controller'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getInternalAccounts, - getNetworkConfigurationsByChainId, getSelectedInternalAccount, getUpdatedAndSortedAccounts, } from '../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { Box, Button, diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index e0fe5575e2c2..bb02e0ebaaa9 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -6,7 +6,6 @@ import { getIsNetworkUsed, getNetworkIdentifier, getPreferences, - isNetworkLoading, getTheme, getIsTestnet, getCurrentChainId, @@ -26,6 +25,10 @@ import { getUnapprovedTransactions, getPendingApprovals, } from '../../selectors'; +import { + isNetworkLoading, + getProviderConfig, +} from '../../../shared/modules/selectors/networks'; import { lockMetamask, hideImportNftsModal, @@ -47,10 +50,7 @@ import { import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; import { getSendStage } from '../../ducks/send'; -import { - getIsUnlocked, - getProviderConfig, -} from '../../ducks/metamask/metamask'; +import { getIsUnlocked } from '../../ducks/metamask/metamask'; import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; import { selectSwitchedNetworkNeverShowMessage } from '../../components/app/toast-master/selectors'; import Routes from './routes.component'; diff --git a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js index ff10d850345e..259c503ef5eb 100644 --- a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js +++ b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js @@ -7,7 +7,7 @@ import { getInternalAccountByAddress, getInternalAccounts, } from '../../../../selectors'; -import { getProviderConfig } from '../../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../../shared/modules/selectors/networks'; import { CONTACT_VIEW_ROUTE, CONTACT_LIST_ROUTE, diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.tsx b/ui/pages/settings/networks-tab/networks-form/networks-form.tsx index 6c5ecdad8c58..db3a86ce0ed4 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.tsx +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.tsx @@ -28,7 +28,7 @@ import { import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { getNetworkConfigurationsByChainId } from '../../../../selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks'; import { addNetwork, setEditedNetwork, diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index 676a53097d4a..c70640c624e9 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -24,10 +24,10 @@ import { } from '../../../store/actions'; import { getIsSecurityAlertsEnabled, - getNetworkConfigurationsByChainId, getMetaMetricsDataDeletionId, getPetnamesEnabled, } from '../../../selectors/selectors'; +import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { openBasicFunctionalityModal } from '../../../ducks/app/app'; import SecurityTab from './security-tab.component'; diff --git a/ui/pages/settings/settings-tab/settings-tab.container.js b/ui/pages/settings/settings-tab/settings-tab.container.js index e6ad25f0df92..ec46a2f3cd5b 100644 --- a/ui/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/pages/settings/settings-tab/settings-tab.container.js @@ -14,7 +14,7 @@ import { getTheme, getSelectedInternalAccount, } from '../../../selectors'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import SettingsTab from './settings-tab.component'; const mapStateToProps = (state) => { diff --git a/ui/pages/settings/settings.container.js b/ui/pages/settings/settings.container.js index 58a35f37e616..db500f8b1d46 100644 --- a/ui/pages/settings/settings.container.js +++ b/ui/pages/settings/settings.container.js @@ -32,7 +32,7 @@ import { ADD_NETWORK_ROUTE, ADD_POPULAR_CUSTOM_NETWORK, } from '../../helpers/constants/routes'; -import { getProviderConfig } from '../../ducks/metamask/metamask'; +import { getProviderConfig } from '../../../shared/modules/selectors/networks'; import { toggleNetworkMenu } from '../../store/actions'; import Settings from './settings.component'; diff --git a/ui/pages/swaps/hooks/useUpdateSwapsState.ts b/ui/pages/swaps/hooks/useUpdateSwapsState.ts index ea72c8e273ab..dc44c51a8489 100644 --- a/ui/pages/swaps/hooks/useUpdateSwapsState.ts +++ b/ui/pages/swaps/hooks/useUpdateSwapsState.ts @@ -18,6 +18,7 @@ import { getIsSwapsChain, getUseExternalServices, } from '../../../selectors'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../../shared/constants/swaps'; export default function useUpdateSwapsState() { const dispatch = useDispatch(); @@ -35,7 +36,7 @@ export default function useUpdateSwapsState() { return undefined; } - fetchTokens(chainId) + fetchTokens(chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP) .then((tokens) => { dispatch(setSwapsTokens(tokens)); }) diff --git a/ui/selectors/approvals.ts b/ui/selectors/approvals.ts index 9ee42f769482..14e2362d0708 100644 --- a/ui/selectors/approvals.ts +++ b/ui/selectors/approvals.ts @@ -1,7 +1,7 @@ import { ApprovalControllerState } from '@metamask/approval-controller'; import { ApprovalType } from '@metamask/controller-utils'; import { createSelector } from 'reselect'; -import { createDeepEqualSelector } from './util'; +import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; export type ApprovalsMetaMaskState = { metamask: { diff --git a/ui/selectors/confirm-transaction.js b/ui/selectors/confirm-transaction.js index 9a1457e2bc66..b68d598a0839 100644 --- a/ui/selectors/confirm-transaction.js +++ b/ui/selectors/confirm-transaction.js @@ -11,7 +11,6 @@ import { getGasEstimateType, getGasFeeEstimates, getNativeCurrency, - getProviderConfig, } from '../ducks/metamask/metamask'; import { GasEstimateTypes, @@ -29,6 +28,7 @@ import { subtractHexes, sumHexes, } from '../../shared/modules/conversion.utils'; +import { getProviderConfig } from '../../shared/modules/selectors/networks'; import { getAveragePriceEstimateInHexWEI } from './custom-gas'; import { checkNetworkAndAccountSupports1559, diff --git a/ui/selectors/institutional/selectors.test.ts b/ui/selectors/institutional/selectors.test.ts index 52b27bb8871f..4c398b6fab0b 100644 --- a/ui/selectors/institutional/selectors.test.ts +++ b/ui/selectors/institutional/selectors.test.ts @@ -4,7 +4,11 @@ import { Hex } from '@metamask/utils'; import { toHex } from '@metamask/controller-utils'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../test/stub/networks'; -import { CHAIN_IDS } from '../../../shared/constants/network'; +import { + CHAIN_IDS, + CURRENCY_SYMBOLS, + NETWORK_TO_NAME_MAP, +} from '../../../shared/constants/network'; import { getConfiguredCustodians, getCustodianIconForAddress, @@ -71,10 +75,15 @@ const custodianMock = { function buildState(overrides = {}) { const defaultState = { metamask: { + selectedNetworkClientId: '0x1', networkConfigurationsByChainId: { - [toHex(1)]: { - chainId: toHex(1), - rpcEndpoints: [{}], + [CHAIN_IDS.MAINNET]: { + chainId: CHAIN_IDS.MAINNET, + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + name: NETWORK_TO_NAME_MAP[CHAIN_IDS.MAINNET], + nativeCurrency: CURRENCY_SYMBOLS.ETH, + rpcEndpoints: [], }, }, internalAccounts: { diff --git a/ui/selectors/institutional/selectors.ts b/ui/selectors/institutional/selectors.ts index 05bd13b52509..eb70e0fcd72c 100644 --- a/ui/selectors/institutional/selectors.ts +++ b/ui/selectors/institutional/selectors.ts @@ -1,7 +1,10 @@ import { toChecksumAddress } from 'ethereumjs-util'; import { getAccountType } from '../selectors'; import { getSelectedInternalAccount } from '../accounts'; -import { getProviderConfig } from '../../ducks/metamask/metamask'; +import { + ProviderConfigState, + getProviderConfig, +} from '../../../shared/modules/selectors/networks'; import { hexToDecimal } from '../../../shared/modules/conversion.utils'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -165,7 +168,9 @@ export function getCustodianIconForAddress(state: State, address: string) { return custodianIcon; } -export function getIsCustodianSupportedChain(state: State) { +export function getIsCustodianSupportedChain( + state: State & ProviderConfigState, +) { try { // @ts-expect-error state types don't match const selectedAccount = getSelectedInternalAccount(state); diff --git a/ui/selectors/metamask-notifications/metamask-notifications.ts b/ui/selectors/metamask-notifications/metamask-notifications.ts index ae71adaa8d36..10f4f337eabf 100644 --- a/ui/selectors/metamask-notifications/metamask-notifications.ts +++ b/ui/selectors/metamask-notifications/metamask-notifications.ts @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; import { NotificationServicesController } from '@metamask/notification-services-controller'; -import { createDeepEqualSelector } from '../util'; +import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; const { TRIGGER_TYPES } = NotificationServicesController.Constants; diff --git a/ui/selectors/multichain.test.ts b/ui/selectors/multichain.test.ts index 3097d61f9549..2fa47a8db9e8 100644 --- a/ui/selectors/multichain.test.ts +++ b/ui/selectors/multichain.test.ts @@ -2,10 +2,7 @@ import { Cryptocurrency } from '@metamask/assets-controllers'; import { InternalAccount } from '@metamask/keyring-api'; import { Hex } from '@metamask/utils'; import { NetworkConfiguration } from '@metamask/network-controller'; -import { - getNativeCurrency, - getProviderConfig, -} from '../ducks/metamask/metamask'; +import { getNativeCurrency } from '../ducks/metamask/metamask'; import { MULTICHAIN_PROVIDER_CONFIGS, MultichainNetworks, @@ -24,6 +21,7 @@ import { } from '../../shared/constants/network'; import { MultichainNativeAssets } from '../../shared/constants/multichain/assets'; import { mockNetworkState } from '../../test/stub/networks'; +import { getProviderConfig } from '../../shared/modules/selectors/networks'; import { AccountsState } from './accounts'; import { MultichainState, diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 08078be52f03..1914dbce2dd8 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -14,7 +14,6 @@ import { getCompletedOnboarding, getConversionRate, getNativeCurrency, - getProviderConfig, } from '../ducks/metamask/metamask'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -25,6 +24,11 @@ import { TEST_NETWORK_IDS, CHAIN_IDS, } from '../../shared/constants/network'; +import { + getProviderConfig, + NetworkState, + getNetworkConfigurationsByChainId, +} from '../../shared/modules/selectors/networks'; import { AccountsState, getSelectedInternalAccount } from './accounts'; import { getCurrentChainId, @@ -32,7 +36,6 @@ import { getIsMainnet, getMaybeSelectedInternalAccount, getNativeCurrencyImage, - getNetworkConfigurationsByChainId, getSelectedAccountCachedBalance, getShouldShowFiat, getShowFiatInTestnets, @@ -46,7 +49,10 @@ export type BalancesState = { metamask: BalancesControllerState; }; -export type MultichainState = AccountsState & RatesState & BalancesState; +export type MultichainState = AccountsState & + RatesState & + BalancesState & + NetworkState; // TODO: Remove after updating to @metamask/network-controller 20.0.0 export type ProviderConfigWithImageUrlAndExplorerUrl = { diff --git a/ui/selectors/networks.test.ts b/ui/selectors/networks.test.ts new file mode 100644 index 000000000000..2df8a23f302c --- /dev/null +++ b/ui/selectors/networks.test.ts @@ -0,0 +1,129 @@ +import { NetworkStatus, RpcEndpointType } from '@metamask/network-controller'; +import mockState from '../../test/data/mock-state.json'; +import { mockNetworkState } from '../../test/stub/networks'; +import { CHAIN_IDS } from '../../shared/constants/network'; +import * as networks from '../../shared/modules/selectors/networks'; + +describe('Network Selectors', () => { + describe('#getNetworkConfigurationsByChainId', () => { + it('returns networkConfigurationsByChainId', () => { + const networkConfigurationsByChainId = { + '0x1351': { + name: 'TEST', + chainId: '0x1351' as const, + nativeCurrency: 'TEST', + defaultRpcEndpointUrl: 'https://mock-rpc-url-1', + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + type: RpcEndpointType.Custom as const, + networkClientId: 'testNetworkConfigurationId1', + url: 'https://mock-rpc-url-1', + }, + ], + blockExplorerUrls: [], + }, + '0x1337': { + name: 'RPC', + chainId: '0x1337' as const, + nativeCurrency: 'RPC', + defaultRpcEndpointUrl: 'https://mock-rpc-url-2', + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + type: RpcEndpointType.Custom as const, + networkClientId: 'testNetworkConfigurationId2', + url: 'https://mock-rpc-url-2', + }, + ], + blockExplorerUrls: [], + }, + }; + + expect( + networks.getNetworkConfigurationsByChainId({ + metamask: { + networkConfigurationsByChainId, + }, + }), + ).toStrictEqual(networkConfigurationsByChainId); + }); + }); + + describe('#getNetworkConfigurations', () => { + it('returns undefined if state.metamask.networkConfigurations is undefined', () => { + expect( + networks.getNetworkConfigurations({ + metamask: { + // @ts-expect-error the types forbid `undefined`. this is a strange test. + networkConfigurations: undefined, + }, + }), + ).toBeUndefined(); + }); + + it('returns networkConfigurations', () => { + const networkConfigurations = { + '0x1351': { + name: 'TEST', + chainId: '0x1351' as const, + nativeCurrency: 'TEST', + defaultRpcEndpointUrl: 'https://mock-rpc-url-1', + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + type: RpcEndpointType.Custom as const, + networkClientId: 'testNetworkConfigurationId1', + url: 'https://mock-rpc-url-1', + }, + ], + blockExplorerUrls: [], + }, + '0x1337': { + name: 'RPC', + chainId: '0x1337' as const, + nativeCurrency: 'RPC', + defaultRpcEndpointUrl: 'https://mock-rpc-url-2', + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + type: RpcEndpointType.Custom as const, + networkClientId: 'testNetworkConfigurationId2', + url: 'https://mock-rpc-url-2', + }, + ], + blockExplorerUrls: [], + }, + }; + expect( + networks.getNetworkConfigurations({ + metamask: { + networkConfigurations, + }, + }), + ).toStrictEqual(networkConfigurations); + }); + }); + + describe('#getInfuraBlocked', () => { + it('returns getInfuraBlocked', () => { + let isInfuraBlocked = networks.getInfuraBlocked( + mockState as networks.NetworkState, + ); + expect(isInfuraBlocked).toBe(false); + + const modifiedMockState = { + ...mockState, + metamask: { + ...mockState.metamask, + ...mockNetworkState({ + chainId: CHAIN_IDS.GOERLI, + metadata: { status: NetworkStatus.Blocked, EIPS: {} }, + }), + }, + }; + isInfuraBlocked = networks.getInfuraBlocked(modifiedMockState); + expect(isInfuraBlocked).toBe(true); + }); + }); +}); diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index 00468f2f948d..a0d69b7dc75e 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -4,8 +4,8 @@ import { isEvmAccountType } from '@metamask/keyring-api'; import { CaveatTypes } from '../../shared/constants/permissions'; // eslint-disable-next-line import/no-restricted-paths import { PermissionNames } from '../../app/scripts/controllers/permissions'; +import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { getApprovalRequestsByType } from './approvals'; -import { createDeepEqualSelector } from './util'; import { getInternalAccount, getMetaMaskAccountsOrdered, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 27b3a878042b..e4e9c7e27fa7 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -24,7 +24,6 @@ import { CHAIN_ID_TO_RPC_URL_MAP, CHAIN_IDS, NETWORK_TYPES, - NetworkStatus, SEPOLIA_DISPLAY_NAME, GOERLI_DISPLAY_NAME, LINEA_GOERLI_DISPLAY_NAME, @@ -79,7 +78,6 @@ import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens'; import { DAY } from '../../shared/constants/time'; import { TERMS_OF_USE_LAST_UPDATED } from '../../shared/constants/terms'; import { - getProviderConfig, getConversionRate, isNotEIP1559Network, isEIP1559Network, @@ -106,6 +104,12 @@ import { MultichainNativeAssets } from '../../shared/constants/multichain/assets import { BridgeFeatureFlagsKey } from '../../app/scripts/controllers/bridge/types'; import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; +import { + getProviderConfig, + getSelectedNetworkClientId, + getNetworkConfigurationsByChainId, +} from '../../shared/modules/selectors/networks'; +import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -120,28 +124,8 @@ import { getSubjectMetadata, } from './permissions'; import { getSelectedInternalAccount, getInternalAccounts } from './accounts'; -import { createDeepEqualSelector } from './util'; import { getMultichainBalances, getMultichainNetwork } from './multichain'; -/** - * Returns true if the currently selected network is inaccessible or whether no - * provider has been set yet for the currently selected network. - * - * @param {object} state - Redux state object. - */ -export function isNetworkLoading(state) { - const selectedNetworkClientId = getSelectedNetworkClientId(state); - return ( - selectedNetworkClientId && - state.metamask.networksMetadata[selectedNetworkClientId].status !== - NetworkStatus.Available - ); -} - -export function getSelectedNetworkClientId(state) { - return state.metamask.selectedNetworkClientId; -} - export function getNetworkIdentifier(state) { const { type, nickname, rpcUrl } = getProviderConfig(state); @@ -838,15 +822,6 @@ export function getGasIsLoading(state) { return state.appState.gasIsLoading; } -export const getNetworkConfigurationsByChainId = createDeepEqualSelector( - (state) => state.metamask.networkConfigurationsByChainId, - /** - * @param networkConfigurationsByChainId - * @returns { import('@metamask/network-controller').NetworkState['networkConfigurationsByChainId']} - */ - (networkConfigurationsByChainId) => networkConfigurationsByChainId, -); - export const getNetworkConfigurationIdByChainId = createDeepEqualSelector( (state) => state.metamask.networkConfigurationsByChainId, (networkConfigurationsByChainId) => @@ -1437,7 +1412,7 @@ export const getMultipleTargetsSubjectMetadata = createDeepEqualSelector( export function getRpcPrefsForCurrentProvider(state) { const { rpcPrefs } = getProviderConfig(state); - return rpcPrefs || {}; + return rpcPrefs; } export function getKnownMethodData(state, data) { @@ -1477,13 +1452,6 @@ export function getUseExternalServices(state) { return state.metamask.useExternalServices; } -export function getInfuraBlocked(state) { - return ( - state.metamask.networksMetadata[getSelectedNetworkClientId(state)] - .status === NetworkStatus.Blocked - ); -} - export function getUSDConversionRate(state) { return state.metamask.currencyRates[getProviderConfig(state).ticker] ?.usdConversionRate; diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index d3799885eaf6..e70e9cd7ff29 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -10,10 +10,10 @@ import { KeyringType } from '../../shared/constants/keyring'; import mockState from '../../test/data/mock-state.json'; import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; import { createMockInternalAccount } from '../../test/jest/mocks'; -import { getProviderConfig } from '../ducks/metamask/metamask'; import { mockNetworkState } from '../../test/stub/networks'; import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import { selectSwitchedNetworkNeverShowMessage } from '../components/app/toast-master/selectors'; +import { getProviderConfig } from '../../shared/modules/selectors/networks'; import * as selectors from './selectors'; jest.mock('../../app/scripts/lib/util', () => ({ @@ -646,45 +646,6 @@ describe('Selectors', () => { }); }); - describe('#getNetworkConfigurationsByChainId', () => { - it('returns networkConfigurationsByChainId', () => { - const networkConfigurationsByChainId = { - '0xtest': { - chainId: '0xtest', - nativeCurrency: 'TEST', - defaultRpcEndpointUrl: 'https://mock-rpc-url-1', - defaultRpcEndpointIndex: 0, - rpcEndpoints: [ - { - networkClientId: 'testNetworkConfigurationId1', - url: 'https://mock-rpc-url-1', - }, - ], - }, - '0x1337': { - chainId: '0x1337', - nativeCurrency: 'RPC', - defaultRpcEndpointUrl: 'https://mock-rpc-url-2', - defaultRpcEndpointIndex: 0, - rpcEndpoints: [ - { - networkClientId: 'testNetworkConfigurationId2', - url: 'https://mock-rpc-url-2', - }, - ], - }, - }; - - expect( - selectors.getNetworkConfigurationsByChainId({ - metamask: { - networkConfigurationsByChainId, - }, - }), - ).toStrictEqual(networkConfigurationsByChainId); - }); - }); - describe('#getCurrentNetwork', () => { it('returns built-in network configuration', () => { const modifiedMockState = { @@ -1317,24 +1278,6 @@ describe('Selectors', () => { expect(selectors.getSnapsInstallPrivacyWarningShown(mockState)).toBe(false); }); - it('#getInfuraBlocked', () => { - let isInfuraBlocked = selectors.getInfuraBlocked(mockState); - expect(isInfuraBlocked).toBe(false); - - const modifiedMockState = { - ...mockState, - metamask: { - ...mockState.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.GOERLI, - metadata: { status: 'blocked' }, - }), - }, - }; - isInfuraBlocked = selectors.getInfuraBlocked(modifiedMockState); - expect(isInfuraBlocked).toBe(true); - }); - it('#getSnapRegistryData', () => { const mockSnapId = 'npm:@metamask/test-snap-bip44'; expect(selectors.getSnapRegistryData(mockState, mockSnapId)).toStrictEqual( diff --git a/ui/selectors/signatures.ts b/ui/selectors/signatures.ts index 7325e5c038c0..0fd99eb4361a 100644 --- a/ui/selectors/signatures.ts +++ b/ui/selectors/signatures.ts @@ -1,10 +1,10 @@ import { createSelector } from 'reselect'; import { DefaultRootState } from 'react-redux'; +import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { unapprovedPersonalMsgsSelector, unapprovedTypedMessagesSelector, } from './transactions'; -import { createDeepEqualSelector } from './util'; export const selectUnapprovedMessages = createSelector( unapprovedPersonalMsgsSelector, diff --git a/ui/selectors/snaps/accounts.ts b/ui/selectors/snaps/accounts.ts index 55a30f0c72eb..0b0559b7d1dd 100644 --- a/ui/selectors/snaps/accounts.ts +++ b/ui/selectors/snaps/accounts.ts @@ -2,7 +2,7 @@ import { createSelector } from 'reselect'; import { AccountsControllerState } from '@metamask/accounts-controller'; import { getAccountName } from '../selectors'; import { getInternalAccounts } from '../accounts'; -import { createDeepEqualSelector } from '../util'; +import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; /** * The Metamask state for the accounts controller. diff --git a/ui/selectors/snaps/address-book.ts b/ui/selectors/snaps/address-book.ts index e002153d9e61..da2e4ac802c9 100644 --- a/ui/selectors/snaps/address-book.ts +++ b/ui/selectors/snaps/address-book.ts @@ -1,5 +1,5 @@ import { AddressBookController } from '@metamask/address-book-controller'; -import { createDeepEqualSelector } from '../util'; +import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; /** diff --git a/ui/selectors/transactions.js b/ui/selectors/transactions.js index 3074fd4bfde4..9428a6fbd8c3 100644 --- a/ui/selectors/transactions.js +++ b/ui/selectors/transactions.js @@ -12,14 +12,14 @@ import { import txHelper from '../helpers/utils/tx-helper'; import { SmartTransactionStatus } from '../../shared/constants/transaction'; import { hexToDecimal } from '../../shared/modules/conversion.utils'; -import { getProviderConfig } from '../ducks/metamask/metamask'; -import { getCurrentChainId } from './selectors'; -import { getSelectedInternalAccount } from './accounts'; -import { hasPendingApprovals, getApprovalRequestsByType } from './approvals'; +import { getProviderConfig } from '../../shared/modules/selectors/networks'; import { createDeepEqualSelector, filterAndShapeUnapprovedTransactions, -} from './util'; +} from '../../shared/modules/selectors/util'; +import { getCurrentChainId } from './selectors'; +import { getSelectedInternalAccount } from './accounts'; +import { hasPendingApprovals, getApprovalRequestsByType } from './approvals'; const INVALID_INITIAL_TRANSACTION_TYPES = [ TransactionType.cancel, diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 66962d46161d..f3f4e712acf5 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -69,8 +69,11 @@ import { getInternalAccountByAddress, getSelectedInternalAccount, getInternalAccounts, - getSelectedNetworkClientId, } from '../selectors'; +import { + getSelectedNetworkClientId, + getProviderConfig, +} from '../../shared/modules/selectors/networks'; import { computeEstimatedGasLimit, initializeSendState, @@ -82,10 +85,7 @@ import { SEND_STAGES, } from '../ducks/send'; import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account'; -import { - getProviderConfig, - getUnconnectedAccountAlertEnabledness, -} from '../ducks/metamask/metamask'; +import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { HardwareDeviceNames, From aee0aa82d7a476ed70e16cf260bb7a092894e1b3 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 21 Nov 2024 19:33:52 -0330 Subject: [PATCH 28/28] chore: Branch off of "New Crowdin translations by Github Action" (#28390) This is a branch off of https://github.com/MetaMask/metamask-extension/pull/26964, as of commit 3ec2e61b06 This will make it easier to merge the branch, which regularly has new commits pushed to it This adds new translations from crowdin --------- Co-authored-by: metamaskbot Co-authored-by: Desi McAdam --- app/_locales/de/messages.json | 459 ++++++++++++++++++++++++++++++- app/_locales/el/messages.json | 459 ++++++++++++++++++++++++++++++- app/_locales/es/messages.json | 458 +++++++++++++++++++++++++++++- app/_locales/fr/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/hi/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/id/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/ja/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/ko/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/pt/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/ru/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/tl/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/tr/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/vi/messages.json | 455 +++++++++++++++++++++++++++++- app/_locales/zh_CN/messages.json | 455 +++++++++++++++++++++++++++++- 14 files changed, 6363 insertions(+), 18 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index e8de6564304b..172fd05a5a73 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Verbinden Sie Ihre QR-basierte Hardware-Wallet." }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "Die Adresse in der Anmeldeanfrage entspricht nicht der Adresse des Kontos, mit dem Sie sich anmelden." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Wählen Sie die Konten aus, über die Sie benachrichtigt werden möchten:" }, + "accountBalance": { + "message": "Kontostand" + }, "accountDetails": { "message": "Kontodetails" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Kontooptionen" }, + "accountPermissionToast": { + "message": "Kontogenehmigungen aktualisiert" + }, "accountSelectionRequired": { "message": "Sie müssen ein Konto auswählen!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Konten wurden verbunden" }, + "accountsPermissionsTitle": { + "message": "Ihre Konten einsehen und Transaktionen vorschlagen" + }, + "accountsSmallCase": { + "message": "Konten" + }, "active": { "message": "Aktiv" }, @@ -180,12 +195,18 @@ "add": { "message": "Hinzufügen" }, + "addACustomNetwork": { + "message": "Benutzerdefiniertes Netzwerk hinzufügen" + }, "addANetwork": { "message": "Ein neues Netzwerk hinzufügen" }, "addANickname": { "message": "Spitznamen hinzufügen" }, + "addAUrl": { + "message": "URL hinzufügen" + }, "addAccount": { "message": "Konto hinzufügen" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Einen Block-Explorer hinzufügen" }, + "addBlockExplorerUrl": { + "message": "Eine Block-Explorer-URL hinzufügen" + }, "addContact": { "message": "Kontakt hinzufügen" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Sie fügen einen neuen RPC-Anbieter für das Ethereum-Hauptnetz hinzu." }, + "addEthereumWatchOnlyAccount": { + "message": "Ein Ethereum-Konto ansehen (Beta)" + }, "addFriendsAndAddresses": { "message": "Freunde und Adressen hinzufügen, welchen Sie vertrauen" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Netzwerk hinzufügen" }, + "addNetworkConfirmationTitle": { + "message": "$1 hinzufügen", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Ein neues Konto hinzufügen" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Adresse wurde kopiert!" }, + "addressMismatch": { + "message": "Nichtübereinstimmung der Website-Adresse" + }, + "addressMismatchOriginal": { + "message": "Aktuelle URL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Punycode-Version: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Erweitert" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Prioritätsgebühr (alias „Miner Tip“) geht direkt an Miner und veranlasst sie, Ihre Transaktion zu priorisieren." }, + "aggregatedBalancePopover": { + "message": "Dies spiegelt den Wert aller Tokens wider, über die Sie in einem bestimmten Netzwerk verfügen. Sollten Sie diesen Wert lieber in ETH oder anderen Währungen angezeigt bekommen wollen, wechseln Sie zu $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Ich stimme MetaMasks $1 zu", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Dies kann in „Einstellungen > Benachrichtigungen“ geändert werden." }, + "alertMessageAddressMismatchWarning": { + "message": "Angreifer imitieren manchmal Websites, indem sie kleine Änderungen an der Adresse der Website vornehmen. Vergewissern Sie sich, dass Sie mit der beabsichtigten Website interagieren, bevor Sie fortfahren." + }, "alertMessageGasEstimateFailed": { "message": "Wir sind nicht in der Lage, eine genaue Gebühr anzugeben, und diese Schätzung könnte zu hoch sein. Wir schlagen vor, dass Sie ein individuelles Gas-Limit eingeben, aber es besteht das Risiko, dass die Transaktion trotzdem fehlschlägt." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Um mit dieser Transaktion fortzufahren, müssen Sie das Gas-Limit auf 21.000 oder mehr erhöhen." }, + "alertMessageInsufficientBalance2": { + "message": "Sie haben nicht genug ETH auf Ihrem Konto, um die Netzwerk-Gebühren zu bezahlen." + }, "alertMessageNetworkBusy": { "message": "Die Gas-Preise sind hoch und die Schätzungen sind weniger genau." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Asset-Optionen" }, + "assets": { + "message": "Assets" + }, + "assetsDescription": { + "message": "Automatische Erkennung von Tokens in Ihrer Wallet, Anzeige von NFTs und stapelweise Aktualisierung des Kontostands" + }, "attemptSendingAssets": { "message": "Wenn Sie versuchen, Assets direkt von einem Netzwerk in ein anderes zu senden, kann dies zu einem dauerhaften Asset-Verlust führen. Verwenden Sie unbedingt eine Bridge." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Bridge, nicht senden" }, + "bridgeFrom": { + "message": "Bridge von" + }, + "bridgeSelectNetwork": { + "message": "Netzwerk wählen" + }, + "bridgeTo": { + "message": "Bridge nach" + }, "browserNotSupported": { "message": "Ihr Browser wird nicht unterstützt …" }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Geheime Wiederherstellungsphrase bestätigen" }, + "confirmTitleApproveTransaction": { + "message": "Bewilligungsanfrage" + }, + "confirmTitleDeployContract": { + "message": "Einen Kontrakt nutzen" + }, + "confirmTitleDescApproveTransaction": { + "message": "Diese Website möchte die Genehmigung, Ihre NFTs abzuheben." + }, + "confirmTitleDescDeployContract": { + "message": "Diese Website möchte, dass Sie einen Kontrakt nutzen." + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Diese Website möchte die Genehmigung, Ihre Tokens abzuheben." + }, "confirmTitleDescPermitSignature": { "message": "Diese Website möchte die Genehmigung, Ihre Tokens auszugeben." }, "confirmTitleDescSIWESignature": { "message": "Eine Website möchte, dass Sie sich anmelden, um zu beweisen, dass Sie dieses Konto besitzen." }, + "confirmTitleDescSign": { + "message": "Überprüfen Sie vor der Bestätigung die Details der Anfrage." + }, "confirmTitlePermitTokens": { "message": "Antrag auf Ausgabenobergrenze" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Genehmigung entfernen" + }, "confirmTitleSIWESignature": { "message": "Anmeldeanfrage" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Genehmigung entfernen" + }, "confirmTitleSignature": { "message": "Signaturanfrage" }, "confirmTitleTransaction": { "message": "Transaktionsanfrage" }, + "confirmationAlertModalDetails": { + "message": "Um Ihre Assets und Anmeldedaten zu schützen, empfehlen wir die Ablehnung der Anfrage." + }, + "confirmationAlertModalTitle": { + "message": "Diese Anfrage scheint verdächtig" + }, "confirmed": { "message": "Bestätigt" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "Wir haben ein missverständliches Zeichen im ENS-Namen entdeckt. Prüfen Sie den ENS-Namen, um möglichen Betrug zu vermeiden." }, + "congratulations": { + "message": "Glückwunsch!" + }, "connect": { "message": "Verbinden" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Verbundene Seiten" }, + "connectedSitesAndSnaps": { + "message": "Verbundene Websites und Snaps" + }, "connectedSitesDescription": { "message": "$1 ist mit diesen Seiten verbunden. Sie können Ihre Konto-Adresse einsehen.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask ist mit dieser Seite verbunden, aber es sind noch keine Konten verbunden" }, + "connectedSnaps": { + "message": "Verbundene Snaps" + }, + "connectedWithAccount": { + "message": "$1 Konten verbunden", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Verbunden mit $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Verbinden" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Verbindung zum Sepolia-Testnetzwerk wird hergestellt" }, + "connectionDescription": { + "message": "Diese Website möchte" + }, "connectionFailed": { "message": "Verbindung fehlgeschlagen" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Adresse in die Zwischenablage kopieren" }, + "copyAddressShort": { + "message": "Adresse kopieren" + }, "copyPrivateKey": { "message": "Privaten Schlüssel kopieren" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "Standard-RPC-URL" }, + "defaultSettingsSubTitle": { + "message": "MetaMask verwendet Standardeinstellungen, um ein optimales Gleichgewicht zwischen Sicherheit und Benutzerfreundlichkeit herzustellen. Ändern Sie diese Einstellungen, um Ihre Privatsphäre weiter zu verbessern." + }, + "defaultSettingsTitle": { + "message": "Standard-Datenschutzeinstellungen" + }, "delete": { "message": "Löschen" }, "deleteContact": { "message": "Kontakt löschen" }, + "deleteMetaMetricsData": { + "message": "MetaMetrics-Daten löschen" + }, + "deleteMetaMetricsDataDescription": { + "message": "Dadurch werden historische, mit Ihrer Nutzung auf diesem Gerät verbundene MetaMetrics-Daten gelöscht. Ihre Wallet und Ihre Konten bleiben nach dem Löschen dieser Daten unverändert. Dieser Vorgang kann bis zu 30 Tage dauern. Lesen Sie unsere $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Diese Anfrage kann aufgrund eines Serverproblems des Analysesystems derzeit nicht bearbeitet werden. Bitte versuchen Sie es später erneut." + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Wir können diese Daten im Moment nicht löschen" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Wir sind dabei, Ihre gesamten MetaMetrics-Daten zu löschen. Sind Sie sicher?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "MetaMetrics-Daten löschen?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Sie haben diese Aktion am $1 initiiert. Dieser Vorgang kann bis zu 30 Tage dauern. $2 anzeigen", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Wenn Sie dieses Netzwerk löschen, müssen Sie es erneut hinzufügen, um Ihre Assets in diesem Netzwerk anzuzeigen." }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Einzahlung" }, + "depositCrypto": { + "message": "Zahlen Sie Krypto von einem anderen Konto über eine Wallet-Adresse oder einen QR-Code ein." + }, "deprecatedGoerliNtwrkMsg": { "message": "Aufgrund von Aktualisierungen des Ethereum-Systems wird das Goerli-Testnetzwerk bald eingestellt." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "Konten" }, + "disconnectAllDescriptionText": { + "message": "Falls Sie die Verbindung zu dieser Seite trennen, müssen Sie Ihre Konten und Netzwerke bei einer weiteren Nutzung dieser Website erneut verbinden." + }, "disconnectAllSnapsText": { "message": "Snaps" }, + "disconnectMessage": { + "message": "Dadurch wird die Verbindung zu dieser Seite getrennt" + }, "disconnectPrompt": { "message": "$1 trennen" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Spitznamen bearbeiten" }, + "editAccounts": { + "message": "Konten bearbeiten" + }, "editAddressNickname": { "message": "Adressenspitznamen bearbeiten" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "Das ursprüngliche Netzwerk bearbeiten" }, + "editNetworksTitle": { + "message": "Netzwerke bearbeiten" + }, "editNonceField": { "message": "Nonce bearbeiten" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Genehmigung bearbeiten" }, + "editPermissions": { + "message": "Genehmigungen bearbeiten" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Beschleunigung der Gasgebühr bearbeiten" }, + "editSpendingCap": { + "message": "Ausgabenobergrenze bearbeiten" + }, + "editSpendingCapAccountBalance": { + "message": "Kontostand: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Geben Sie den von Ihnen gewünschten Betrag ein, der in Ihrem Namen ausgegeben werden soll." + }, + "editSpendingCapError": { + "message": "Die Ausgabenobergrenze darf nicht mehr als $1 Dezimalstellen überschreiten. Entfernen Sie die Dezimalstellen, um fortzufahren." + }, "enableAutoDetect": { "message": " Automatische Erkennung aktivieren" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "ENS-Lookup fehlgeschlagen." }, + "enterANameToIdentifyTheUrl": { + "message": "Geben Sie zur Identifizierung der URL einen Namen ein" + }, "enterANumber": { "message": "Nummer eingeben" }, + "enterChainId": { + "message": "Chain-ID eingeben" + }, "enterCustodianToken": { "message": "$1-Token eingeben oder neues Token hinzufügen" }, "enterMaxSpendLimit": { "message": "Max. Ausgabenlimit eingeben" }, + "enterNetworkName": { + "message": "Netzwerkname eingeben" + }, "enterOptionalPassword": { "message": "Optionales Passwort eingeben" }, "enterPasswordContinue": { "message": "Zum Fortfahren Passwort eingeben" }, + "enterRpcUrl": { + "message": "RPC-URL eingeben" + }, + "enterSymbol": { + "message": "Symbol eingeben" + }, "enterTokenNameOrAddress": { "message": "Tokennamen eingeben oder Adresse einfügen" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Experimentell" }, + "exportYourData": { + "message": "Ihre Daten exportieren" + }, + "exportYourDataButton": { + "message": "Herunterladen" + }, + "exportYourDataDescription": { + "message": "Sie können Daten wie Ihre Kontakte und Einstellungen exportieren." + }, "extendWalletWithSnaps": { "message": "Erkunden Sie die von der Community erstellten Snaps, um Ihr web3-Erlebnis individuell zu gestalten.", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Diese Gas-Gebühr wurde von $1 vorgeschlagen. Dies kann ein Problem mit Ihrer Transaktion verursachen. Bei Fragen wenden Sie sich bitte an $1.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Gas-Gebühr" + }, "gasIsETH": { "message": "Gas ist $1" }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Etwas ist schiefgelaufen ...." }, + "generalDescription": { + "message": "Synchronisieren Sie Einstellungen geräteübergreifend, wählen Sie Netzwerkeinstellungen aus und verfolgen Sie Token-Daten" + }, "genericExplorerView": { "message": "Konto auf $1 ansehen" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "Wenn Sie aus der App ausgesperrt werden oder ein neues Gerät erhalten, verlieren Sie Ihre Gelder. Sichern Sie unbedingt Ihre geheime Wiederherstellungsphrase in $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Alle ignorieren" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "in Ihren Einstellungen" }, + "included": { + "message": "einschließlich" + }, "infuraBlockedNotification": { "message": "MetaMask kann sich nicht mit dem Blockchain Host verbinden. Überprüfen Sie mögliche Gründe $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "JSON Datei", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Bewahren Sie eine Erinnerung an Ihre geheime Wiederherstellungsphrase an einem sicheren Ort auf. Wenn Sie sie verlieren, kann Ihnen niemand helfen, sie wiederzubekommen. Schlimmer noch, Sie werden nie wieder Zugang zu Ihrer Wallet haben. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Kontoname" }, @@ -2402,6 +2622,9 @@ "message": "Erfahren Sie, wie Sie $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Erfahren Sie, wie es funktioniert" + }, "learnMore": { "message": "Mehr erfahren" }, @@ -2409,6 +2632,9 @@ "message": "Wollen Sie $1 über Gas?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Erfahren Sie mehr über bewährte Datenschutzpraktiken." + }, "learnMoreKeystone": { "message": "Mehr erfahren" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Link" }, + "linkCentralizedExchanges": { + "message": "Verlinken Sie Ihre Coinbase- oder Binance-Konten, um Kryptos kostenfrei an MetaMask zu übweisen." + }, "links": { "message": "Links" }, @@ -2557,6 +2786,9 @@ "message": "Stellen Sie sicher, dass niemand zuschaut.", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Standard-Datenschutzeinstellungen verwalten" + }, "marketCap": { "message": "Marktkapitalisierung" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Die Schaltfläche Verbindungsstatus zeigt an, ob die Webseite, die Sie besuchen, mit Ihrem aktuell ausgewählten Konto verbunden ist." }, + "metaMetricsIdNotAvailableError": { + "message": "Da Sie sich noch nie für MetaMetrics angemeldet haben, gibt es hier keine Daten zu löschen." + }, "metadataModalSourceTooltip": { "message": "$1 wird auf npm gehostet und $2 ist die einzige Kennung dieses Snaps.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "mehr" }, + "moreAccounts": { + "message": "Über $1 mehr Konten", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "Über $1 mehr Netzwerke", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Sie fügen dieses Netzwerk zu MetaMask hinzu und geben dieser Website die Genehmigung, es zu nutzen." + }, "multipleSnapConnectionWarning": { "message": "$1 möchte $2 Snaps verwenden", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Netzwerkdetails bearbeiten" }, "nativeTokenScamWarningDescription": { - "message": "Dieses Netzwerk passt nicht zu seiner zugehörigen Chain-ID oder seinem Namen. Viele beliebte Tokens verwenden den Namen $1, was sie zu einem Ziel für Betrüger macht. Betrüger könnten Sie dazu verleiten, ihnen im Gegenzug wertvollere Währung zu schicken. Überprüfen Sie alles, bevor Sie fortfahren.", + "message": "Das native Token-Symbol stimmt nicht mit dem erwarteten Symbol des nativen Tokens für das Netzwerk mit der zugehörigen Chain-ID überein. Sie haben $1 eingegeben, während das erwartete Token-Symbol $2 ist. Überprüfen Sie bitte, ob Sie mit der richtigen Chain verbunden sind.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "etwas anderes", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Dies ist ein möglicher Betrug", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Netzwerkdetails" }, + "networkFee": { + "message": "Netzwerkgebühr" + }, "networkIsBusy": { "message": "Das Netzwerk ist ausgelastet. Die Gas-Preise sind hoch und die Schätzungen sind weniger genau." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Netzwerkoptionen" }, + "networkPermissionToast": { + "message": "Netzwerkgenehmigungen aktualisiert" + }, "networkProvider": { "message": "Netzwerkanbieter" }, @@ -2865,15 +3121,26 @@ "message": "Wir können keine Verbindung zu $1 aufbauen.", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Netzwerk zu $1 gewechselt", + "description": "$1 represents the network name" + }, "networkURL": { "message": "Netzwerk-URL" }, "networkURLDefinition": { "message": "Die URL, die für den Zugriff auf dieses Netzwerk verwendet wird." }, + "networkUrlErrorWarning": { + "message": "Angreifer imitieren manchmal Websites, indem sie kleine Änderungen an der Adresse der Website vornehmen. Vergewissern Sie sich, dass Sie mit der beabsichtigten Website interagieren, bevor Sie fortfahren. Punycode-Version: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Netzwerke" }, + "networksSmallCase": { + "message": "Netzwerke" + }, "nevermind": { "message": "Schon gut" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Wir haben unsere Datenschutzrichtlinie aktualisiert" }, + "newRpcUrl": { + "message": "Neue RPC-URL" + }, "newTokensImportedMessage": { "message": "Sie haben $1 erfolgreich importiert.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask ist nicht mit dieser Website verbunden" }, + "noConnectionDescription": { + "message": "Um eine Verbindung zu einer Website herzustellen, suchen und wählen Sie die Schaltfläche „Verbinden“. Beachten Sie, dass MetaMask nur Verbindungen zu Websites auf Web3 herstellen kann" + }, "noConversionRateAvailable": { "message": "Kein Umrechnungskurs verfügbar" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Eigene Nonce" }, + "none": { + "message": "Keine" + }, "notBusy": { "message": "Nicht ausgelastet" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Genehmigungsdetails" }, + "permissionFor": { + "message": "Genehmigung für" + }, + "permissionFrom": { + "message": "Genehmigung von" + }, "permissionRequest": { "message": "Genehmigungsanfrage" }, @@ -3593,6 +3875,14 @@ "message": "Lassen Sie $1 über Ihre MetaMask-Einstellungen auf Ihre bevorzugte Sprache zugreifen. Dies kann verwendet werden, um den Inhalt von $1 in Ihrer Sprache zu lokalisieren und anzuzeigen.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Sehen Sie Informationen wie Ihre bevorzugte Sprache und Fiat-Währung.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Geben Sie $1 in Ihren MetaMask-Einstellungen Zugriff auf Informationen wie die von Ihnen bevorzugte Sprache und Fiat-Währung. Auf diese Weise kann $1 die auf Ihre Präferenzen zugeschnittenen Inhalte anzeigen. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Anzeige eines benutzerdefinierten Bildschirms", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Sie erteilen dem Spender die Genehmigung, diese Menge an Tokens von Ihrem Konto auszugeben." }, + "permittedChainToastUpdate": { + "message": "$1 hat Zugang zu $2." + }, "personalAddressDetected": { "message": "Personalisierte Adresse identifiziert. Bitte füge die Token-Contract-Adresse ein." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Empfangen" }, + "receiveCrypto": { + "message": "Krypto empfangen" + }, + "recipientAddressPlaceholderNew": { + "message": "Öffentliche Adresse (0x) oder Domainname eingeben" + }, "recommendedGasLabel": { "message": "Empfohlen" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Abgelehnt" }, + "rememberSRPIfYouLooseAccess": { + "message": "Denken Sie daran: Sollten Sie Ihre geheime Wiederherstellungsphrase verlieren, verlieren Sie auch den Zugriff auf Ihr Wallet. $1, um diese Wörter sicher zu verwahren, damit Sie jederzeit auf Ihr Geld zugreifen können." + }, + "reminderSet": { + "message": "Erinnerung eingestellt!" + }, "remove": { "message": "Entfernen" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Aufgrund eines Fehlers wurde diese Anfrage nicht vom Sicherheitsanbieter verifiziert. Gehen Sie behutsam vor." }, + "requestingFor": { + "message": "Anfordern für" + }, + "requestingForAccount": { + "message": "Anfordern für $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "Anfragen warten auf Bestätigung" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Seed-Phrase anzeigen" }, + "review": { + "message": "Überprüfen" + }, + "reviewAlert": { + "message": "Benachrichtigung überprüfen" + }, "reviewAlerts": { "message": "Benachrichtigungen überprüfen" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Genehmigung widerrufen" }, + "revokeSimulationDetailsDesc": { + "message": "Sie entziehen einer Person die Genehmigung, Tokens von Ihrem Konto auszugeben." + }, "revokeSpendingCap": { "message": "Ausgabenobergrenze für Ihr $1 widerrufen", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Dieser Drittanbieter kann keine weiteren Ihrer aktuellen oder zukünftigen Tokens ausgeben." }, + "rpcNameOptional": { + "message": "RPC-Name (Optional)" + }, "rpcUrl": { "message": "Neue RPC-URL" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Sicherheit und Datenschutz" }, + "securityDescription": { + "message": "Verringern Sie das Risiko, sich mit unsicheren Netzwerken zu verbinden und sichern Sie Ihre Konten" + }, + "securityMessageLinkForNetworks": { + "message": "Netzwerk-Betrügereien und Sicherheitsrisiken" + }, + "securityPrivacyPath": { + "message": "Einstellungen > Sicherheit und Datenschutz." + }, "securityProviderPoweredBy": { "message": "Unterstützt durch $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Alle Genehmigungen ansehen", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Details anzeigen" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Wenn Sie nicht die Konten sehen, die Sie erwarten, versuchen Sie, den HD-Pfad oder das aktuell ausgewählte Netzwerk zu ändern." }, + "selectRpcUrl": { + "message": "RPC-URL auswählen" + }, "selectType": { "message": "Typ auswählen" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Erlaubnis für alle erteilen" }, + "setApprovalForAllRedesignedTitle": { + "message": "Auszahlungsanfrage" + }, "setApprovalForAllTitle": { "message": "$1 ohne Ausgabenlimit genehmigen", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Einstellungen" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Die Einstellungen sind für Benutzerfreundlichkeit und Sicherheit optimiert. Sie können diese jederzeit ändern." + }, "settingsSearchMatchingNotFound": { "message": "Keine passenden Ergebnisse gefunden." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Mehr anzeigen" }, + "showNativeTokenAsMainBalance": { + "message": "Natives Token als Hauptsaldo anzeigen" + }, "showNft": { "message": "NFT anzeigen" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Anmelden mit" }, + "simulationApproveHeading": { + "message": "Abheben" + }, + "simulationDetailsApproveDesc": { + "message": "Sie erteilen einer anderen Person die Genehmigung, NFTs von Ihrem Konto abzuheben." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Sie erteilen einer anderen Person die Genehmigung, diesen Betrag von Ihrem Konto auszugeben." + }, "simulationDetailsFiatNotAvailable": { "message": "Nicht verfügbar" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Sie senden" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Sie entziehen einer anderen Person die Genehmigung, NFTs von Ihrem Konto abzuheben." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Sie erteilen einer anderen Person die Genehmigung, NFTs von Ihrem Konto abzuheben." + }, "simulationDetailsTitle": { "message": "Geschätzte Änderungen" }, @@ -4755,7 +5119,7 @@ "message": "Snaps wurden verbunden" }, "snapsNoInsight": { - "message": "Der Snap brachte keine Einsicht." + "message": "Keine Einsichten möglich" }, "snapsPrivacyWarningFirstMessage": { "message": "Sie erkennen an, dass es sich – sofern nicht anders angegeben – bei jedem von Ihnen installierten Snap um einen Drittanbieter-Service handelt, gemäß der in Consensys $1 genannten Definition. Ihre Nutzung von Drittanbieter-Services unterliegt separaten Bedingungen, die vom Anbieter des jeweiligen Drittanbieter-Service festgelegt werden. Consensys empfiehlt keiner bestimmten Person aus irgendeinem bestimmten Grund die Verwendung eines Snaps. Sie nehmen Zugriff auf, verlassen sich auf und verwenden die Dienste Dritter auf eigenes Risiko. Consensys lehnt jede Verantwortung und Haftung für Verluste ab, die sich aus Ihrer Nutzung von Drittanbieter-Diensten ergeben.", @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Hoppla! Etwas ist schiefgelaufen." }, + "sortBy": { + "message": "Sortieren nach" + }, + "sortByAlphabetically": { + "message": "In alphabetischer Reihenfolge (A–Z)" + }, + "sortByDecliningBalance": { + "message": "Abnehmender Saldo ($1 Hoch-Tief)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Quelle" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Spender" }, + "spenderTooltipDesc": { + "message": "Dies ist die Adresse, an die Sie Ihre NFTs abheben können." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Dies ist die Adresse, die Ihre Tokens in Ihrem Namen ausgeben kann." + }, "spendingCap": { "message": "Ausgabenobergrenze" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Ausgabenobergrenze-Anfrage für $1" }, + "spendingCapTooltipDesc": { + "message": "Dies ist die Menge an Tokens, auf die der Spender in Ihrem Namen zugreifen kann." + }, "srpInputNumberOfWords": { "message": "Ich habe eine $1-Wort-Phrase.", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Vorgeschlagen von $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Empfohlenes Währungssymbol:" + }, "suggestedTokenName": { "message": "Vorgeschlagener Name:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Besuchen Sie unser Support Center." }, + "supportMultiRpcInformation": { + "message": "Wir unterstützen nun mehrere RPCs für ein einzelnes Netzwerk. Ihr aktuellster RPC (ferngesteuerter Prozeduraufruf) wurde standardmäßig ausgewählt, um widersprüchliche Informationen aufzulösen." + }, "surveyConversion": { "message": "Nehmen Sie an unserer Umfrage teil" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Die Gas-Gebühren werden geschätzt und werden aufgrund der Komplexität des Netzwerk-Traffics und der Transaktionskomplexität schwanken." }, + "swapGasFeesExplanation": { + "message": "MetaMask verdient kein Geld mit Gas-Gebühren. Bei diesen Gebühren handelt es sich um Schätzwerte, die sich je nach Auslastung des Netzwerks und der Komplexität einer Transaktion ändern können. Erfahren Sie mehr über $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "hier", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Erfahren Sie mehr über Gasgebühren" }, @@ -5186,9 +5583,19 @@ "message": "Gasgebühren werden an Krypto-Miner gezahlt, die Transaktionen im $1-Netzwerk verarbeiten. MetaMask profitiert nicht von den Gasgebühren.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "In diesem Angebot sind die Gas-Gebühren enthalten, indem der gesendete bzw. empfangene Tokenbetrag entsprechend angepasst wird. Sie können ETH in einer separaten Transaktion auf Ihrer Aktivitätsliste erhalten." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Erfahren Sie mehr über Gas-Gebühren" + }, "swapHighSlippage": { "message": "Hohe Slippage" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Enthält Gas und eine MetaMask-Gebühr von $1%", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Enthält eine MetaMask-Gebühr von $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Unsere Nutzungsbedingungen wurden aktualisiert." }, + "testnets": { + "message": "Testnets" + }, "theme": { "message": "Motiv" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Transaktionsgebühr" }, + "transactionFlowNetwork": { + "message": "Netzwerk" + }, "transactionHistoryBaseFee": { "message": "Grundgebühr (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Übertragung" }, + "transferCrypto": { + "message": "Krypto überweisen" + }, "transferFrom": { "message": "Übertragung von" }, + "transferRequest": { + "message": "Überweisungsanfrage" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5766,7 +6185,7 @@ "message": "Bleiben Sie mit Benachrichtigungen auf dem Laufenden darüber, was in Ihrer Wallet passiert." }, "turnOnMetamaskNotificationsMessagePrivacyBold": { - "message": "Einstellungen > Benachrichtigungen." + "message": "Benachrichtigungseinstellungen." }, "turnOnMetamaskNotificationsMessagePrivacyLink": { "message": "Erfahren Sie, wie wir Ihre Privatsphäre bei der Nutzung dieser Funktion schützen." @@ -5844,12 +6263,22 @@ "update": { "message": "Update" }, + "updateEthereumChainConfirmationDescription": { + "message": "Diese Website fordert Sie zur Aktualisierung Ihrer Standard-Netzwerk-URL auf. Sie können die Standardeinstellungen und Netzwerkinformationen jederzeit ändern." + }, + "updateNetworkConfirmationTitle": { + "message": "$1 aktualisieren", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Aktualisieren Sie Ihre Informationen oder" }, "updateRequest": { "message": "Aktualisierungsanfrage" }, + "updatedRpcForNetworks": { + "message": "Netzwerk-RPCs aktualisiert" + }, "uploadDropFile": { "message": "Legen Sie Ihre Datei hier ab" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "unsere Hardware-Wallet-Verbindungsanleitung" }, + "walletProtectedAndReadyToUse": { + "message": "Ihr Wallet ist geschützt und einsatzbereit. Sie finden Ihre geheime Wiederherstellungsphrase unter $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Möchten Sie dieses Netzwerk hinzufügen?" }, @@ -5991,6 +6424,17 @@ "message": "$1 Der Dritte könnte Ihr gesamtes Token-Guthaben ohne weitere Benachrichtigung oder Zustimmung ausgeben. Schützen Sie sich, indem Sie eine niedrigere Ausgabenobergrenze festlegen.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Durch das Einschalten dieser Option können Sie Ethereum-Konten über eine öffentliche Adresse oder einen ENS-Namen ansehen. Für Feedback zu dieser Beta-Funktion füllen Sie bitte diese $1 aus.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Ethereum-Konten ansehen (Beta)" + }, + "watchOutMessage": { + "message": "Vorsicht vor $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Schwach" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Was ist das?" }, + "withdrawing": { + "message": "Auszahlung" + }, "wrongNetworkName": { "message": "Laut unseren Aufzeichnungen stimmt dieser Netzwerkname nicht mit dieser Chain-ID überein." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Ihr Kontostand" }, + "yourBalanceIsAggregated": { + "message": "Ihr Kontostand wird aggregiert" + }, "yourNFTmayBeAtRisk": { "message": "Ihr NFT könnte gefährdet sein" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Wir waren nicht in der Lage, Ihre Transaktion zu stornieren, bevor sie in der Blockchain bestätigt wurde." }, + "yourWalletIsReady": { + "message": "Ihre Wallet ist bereit" + }, "zeroGasPriceOnSpeedUpError": { "message": "Keine Gas-Kosten bei Beschleunigung" } diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 5d2ba61516fa..132265ee4167 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Συνδέστε το πορτοφόλι υλικού μέσω QR" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "Η διεύθυνση στο αίτημα σύνδεσης δεν ταιριάζει με τη διεύθυνση του λογαριασμού που χρησιμοποιείτε για να συνδεθείτε." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Επιλέξτε τους λογαριασμούς για τους οποίους θέλετε να λαμβάνετε ειδοποιήσεις:" }, + "accountBalance": { + "message": "Υπόλοιπο λογαριασμού" + }, "accountDetails": { "message": "Στοιχεία λογαριασμού" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Επιλογές λογαριασμού" }, + "accountPermissionToast": { + "message": "Ενημέρωση αδειών λογαριασμού" + }, "accountSelectionRequired": { "message": "Πρέπει να επιλέξετε έναν λογαριασμό!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Συνδεδεμένοι λογαριασμοί" }, + "accountsPermissionsTitle": { + "message": "Δείτε τους λογαριασμούς σας και προτείνετε συναλλαγές" + }, + "accountsSmallCase": { + "message": "λογαριασμοί" + }, "active": { "message": "Ενεργό" }, @@ -180,12 +195,18 @@ "add": { "message": "Προσθήκη" }, + "addACustomNetwork": { + "message": "Προσθήκη προσαρμοσμένου δικτύου" + }, "addANetwork": { "message": "Προσθήκη ενός δικτύου" }, "addANickname": { "message": "Προσθήκη ενός ψευδωνύμου" }, + "addAUrl": { + "message": "Προσθήκη διεύθυνσης URL" + }, "addAccount": { "message": "Προσθήκη λογαριασμού" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Προσθήκη ενός block explorer" }, + "addBlockExplorerUrl": { + "message": "Προσθήκη διεύθυνσης URL του block explorer" + }, "addContact": { "message": "Προσθήκη επαφής" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Προσθέτετε έναν νέο πάροχο RPC για το Ethereum Mainnet" }, + "addEthereumWatchOnlyAccount": { + "message": "Παρακολουθήστε έναν λογαριασμό Ethereum (Beta)" + }, "addFriendsAndAddresses": { "message": "Προσθέστε φίλους και διευθύνσεις που εμπιστεύεστε" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Προσθήκη δικτύου" }, + "addNetworkConfirmationTitle": { + "message": "Προσθήκη $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Προσθήκη νέου λογαριασμού Ethereum" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Η διεύθυνση αντιγράφηκε!" }, + "addressMismatch": { + "message": "Αναντιστοιχία διεύθυνσης ιστότοπου" + }, + "addressMismatchOriginal": { + "message": "Τρέχουσα διεύθυνση URL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Έκδοση Punycode: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Σύνθετες" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Το τέλος προτεραιότητας (γνωστό και ως “miner tip”) πηγαίνει άμεσα στους miner και τους ενθαρρύνει να δώσουν προτεραιότητα στη συναλλαγή σας." }, + "aggregatedBalancePopover": { + "message": "Αυτό αντικατοπτρίζει την αξία όλων των tokens που έχετε στην κατοχή σας σε ένα συγκεκριμένο δίκτυο. Εάν προτιμάτε να βλέπετε αυτή την αξία σε ETH ή άλλα νομίσματα, μεταβείτε στις $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Συμφωνώ με το $1 του MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Αυτό μπορεί να αλλάξει στις \"Ρυθμίσεις > Ειδοποιήσεις\"" }, + "alertMessageAddressMismatchWarning": { + "message": "Οι εισβολείς μερικές φορές αντιγράφουν ιστότοπους κάνοντας μικρές αλλαγές στη διεύθυνση του ιστότοπου. Βεβαιωθείτε ότι αλληλεπιδράτε με τον ιστότοπο που θέλετε πριν συνεχίσετε." + }, "alertMessageGasEstimateFailed": { "message": "Δεν μπορούμε να παράσχουμε τα τέλη με ακρίβεια και αυτή η εκτίμηση μπορεί να είναι υψηλή. Σας προτείνουμε να εισάγετε ένα προσαρμοσμένο όριο τελών συναλλαγών, αλλά υπάρχει κίνδυνος η συναλλαγή να αποτύχει και πάλι." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Για να συνεχίσετε με αυτή τη συναλλαγή, θα πρέπει να αυξήσετε το όριο των τελών συναλλαγών σε 21000 ή περισσότερο." }, + "alertMessageInsufficientBalance2": { + "message": "Δεν έχετε αρκετά ETH στον λογαριασμό σας για να πληρώσετε τα τέλη δικτύου." + }, "alertMessageNetworkBusy": { "message": "Οι τιμές των τελών συναλλαγών είναι υψηλές και οι εκτιμήσεις είναι λιγότερο ακριβείς." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Επιλογές περιουσιακών στοιχείων" }, + "assets": { + "message": "Περιουσιακά στοιχεία" + }, + "assetsDescription": { + "message": "Αυτόματος εντοπισμός tokens στο πορτοφόλι σας, εμφάνιση NFT και ομαδοποιημένες ενημερώσεις υπολοίπων λογαριασμών" + }, "attemptSendingAssets": { "message": "Ενδέχεται να χάσετε τα περιουσιακά σας στοιχεία εάν προσπαθήσετε να τα στείλετε από άλλο δίκτυο. Μεταφέρετε κεφάλαια με ασφάλεια μεταξύ δικτύων χρησιμοποιώντας μια διασύνδεση." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Μην στείλετε χωρίς διασύνδεση" }, + "bridgeFrom": { + "message": "Γέφυρα από" + }, + "bridgeSelectNetwork": { + "message": "Επιλέξτε δίκτυο" + }, + "bridgeTo": { + "message": "Γέφυρα σε" + }, "browserNotSupported": { "message": "Το Πρόγραμμα Περιήγησής σας δεν υποστηρίζεται..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Επιβεβαιώστε τη Μυστική Φράση Ανάκτησης" }, + "confirmTitleApproveTransaction": { + "message": "Αίτημα χορήγησης άδειας" + }, + "confirmTitleDeployContract": { + "message": "Ανάπτυξη συμβολαίου" + }, + "confirmTitleDescApproveTransaction": { + "message": "Αυτός ο ιστότοπος ζητάει άδεια για να αποσύρει τα NFT σας" + }, + "confirmTitleDescDeployContract": { + "message": "Αυτός ο ιστότοπος θέλει να αναπτύξετε ένα συμβόλαιο" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Αυτός ο ιστότοπος ζητάει άδεια για την απόσυρση των tokens σας" + }, "confirmTitleDescPermitSignature": { "message": "Αυτός ο ιστότοπος ζητάει άδεια για να δαπανήσει τα tokens σας." }, "confirmTitleDescSIWESignature": { "message": "Ένας ιστότοπος θέλει να συνδεθείτε για να αποδείξετε ότι είστε ο κάτοχος αυτού του λογαριασμού." }, + "confirmTitleDescSign": { + "message": "Ελέγξτε τις λεπτομέρειες του αιτήματος πριν επιβεβαιώσετε." + }, "confirmTitlePermitTokens": { "message": "Αίτημα ανώτατου ορίου δαπανών" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Κατάργηση άδειας" + }, "confirmTitleSIWESignature": { "message": "Αίτημα σύνδεσης" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Κατάργηση άδειας" + }, "confirmTitleSignature": { "message": "Αίτημα υπογραφής" }, "confirmTitleTransaction": { "message": "Αίτημα συναλλαγής" }, + "confirmationAlertModalDetails": { + "message": "Για να προστατεύσετε τα περιουσιακά σας στοιχεία και τις πληροφορίες σύνδεσης, σας προτείνουμε να απορρίψετε το αίτημα." + }, + "confirmationAlertModalTitle": { + "message": "Αυτό το αίτημα είναι ύποπτο" + }, "confirmed": { "message": "Επιβεβαιωμένο" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "Εντοπίσαμε έναν παράξενο χαρακτήρα στο όνομα ENS. Ελέγξτε το όνομα ENS για να αποφύγετε μια πιθανή απάτη." }, + "congratulations": { + "message": "Συγχαρητήρια!" + }, "connect": { "message": "Σύνδεση" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Συνδεδεμένοι ιστότοποι" }, + "connectedSitesAndSnaps": { + "message": "Συνδεδεμένοι ιστότοποι και Snaps" + }, "connectedSitesDescription": { "message": "Το $1 είναι συνδεδεμένο σε αυτές τις ιστοσελίδες. Μπορούν να δουν τη διεύθυνση του λογαριασμού σας.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "Το MetaMask είναι συνδεδεμένο σε αυτόν τον ιστότοπο, αλλά δεν έχουν συνδεθεί ακόμα λογαριασμοί" }, + "connectedSnaps": { + "message": "Συνδεδεμένα Snaps" + }, + "connectedWithAccount": { + "message": "$1 λογαριασμοί συνδεδεμένοι", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Συνδέεται με $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Σύνδεση" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Σύνδεση στο δίκτυο δοκιμών Sepolia" }, + "connectionDescription": { + "message": "Αυτός ο ιστότοπος θέλει να" + }, "connectionFailed": { "message": "Η σύνδεση απέτυχε" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Αντιγραφή διεύθυνσης στο πρόχειρο" }, + "copyAddressShort": { + "message": "Αντιγραφή διεύθυνσης" + }, "copyPrivateKey": { "message": "Αντιγραφή ιδιωτικού κλειδιού" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "Προεπιλεγμένη διεύθυνση URL RPC" }, + "defaultSettingsSubTitle": { + "message": "Το MetaMask χρησιμοποιεί προεπιλεγμένες ρυθμίσεις για την καλύτερη δυνατή εξισορρόπηση της ασφάλειας και της ευκολίας χρήσης. Αλλάξτε αυτές τις ρυθμίσεις για να ενισχύσετε ακόμη περισσότερο το απόρρητό σας." + }, + "defaultSettingsTitle": { + "message": "Προεπιλεγμένες ρυθμίσεις απορρήτου" + }, "delete": { "message": "Διαγραφή" }, "deleteContact": { "message": "Διαγραφή επαφής" }, + "deleteMetaMetricsData": { + "message": "Διαγραφή δεδομένων από το MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Αυτό θα διαγράψει τα ιστορικά δεδομένα από το MetaMetrics που σχετίζονται με τη χρήση σας σε αυτή τη συσκευή. Το πορτοφόλι και οι λογαριασμοί σας θα παραμείνουν ακριβώς όπως είναι τώρα μετά τη διαγραφή αυτών των δεδομένων. Η διαδικασία αυτή μπορεί να διαρκέσει έως και 30 ημέρες. Δείτε την $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Αυτό το αίτημα δεν μπορεί να ολοκληρωθεί αυτή τη στιγμή λόγω προβλήματος του διακομιστή στο σύστημα ανάλυσης, προσπαθήστε ξανά αργότερα" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Δεν μπορούμε να διαγράψουμε αυτά τα δεδομένα προς το παρόν" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Πρόκειται να αφαιρέσουμε όλα τα δεδομένα σας από το MetaMetrics. Είστε σίγουροι;" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Διαγραφή των δεδομένων από το MetaMetrics;" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Ξεκινήσατε αυτή την ενέργεια στις $1. Αυτή η διαδικασία μπορεί να διαρκέσει έως και 30 ημέρες. Δείτε την $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Εάν διαγράψετε αυτό το δίκτυο, θα πρέπει να το προσθέσετε ξανά για να δείτε τα περιουσιακά σας στοιχεία σε αυτό το δίκτυο" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Κατάθεση" }, + "depositCrypto": { + "message": "Κατάθεση κρυπτονομισμάτων από άλλο λογαριασμό με διεύθυνση πορτοφολιού ή κωδικό QR." + }, "deprecatedGoerliNtwrkMsg": { "message": "Εξαιτίας των ενημερώσεων στο σύστημα Ethereum, το δίκτυο δοκιμών Goerli θα καταργηθεί σύντομα." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "λογαριασμοί" }, + "disconnectAllDescriptionText": { + "message": "Εάν αποσυνδεθείτε από αυτόν τον ιστότοπο, θα πρέπει να επανασυνδέσετε τους λογαριασμούς και τα δίκτυά σας για να χρησιμοποιήσετε ξανά αυτόν τον ιστότοπο." + }, "disconnectAllSnapsText": { "message": "Snaps" }, + "disconnectMessage": { + "message": "Αυτό θα σας αποσυνδέσει από αυτόν το ιστότοπο" + }, "disconnectPrompt": { "message": "Αποσύνδεση $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Επεξεργασία ψευδώνυμου" }, + "editAccounts": { + "message": "Επεξεργασία λογαριασμών" + }, "editAddressNickname": { "message": "Επεξεργασία διεύθυνσης ψευδώνυμου" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "επεξεργασία του αρχικού δικτύου" }, + "editNetworksTitle": { + "message": "Επεξεργασία δικτύων" + }, "editNonceField": { "message": "Επεξεργασία Nonce" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Επεξεργασία αδειών" }, + "editPermissions": { + "message": "Επεξεργασία αδειών" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Επεξεργασία τελών επίσπευσης συναλλαγής" }, + "editSpendingCap": { + "message": "Επεξεργασία ανώτατου ορίου δαπανών" + }, + "editSpendingCapAccountBalance": { + "message": "Υπόλοιπο λογαριασμού: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Εισαγάγετε το ποσό που αισθάνεστε άνετα να δαπανήσει για λογαριασμό σας." + }, + "editSpendingCapError": { + "message": "Το ανώτατο όριο δαπανών δεν μπορεί να υπερβαίνει τα $1 δεκαδικά ψηφία. Αφαιρέστε τα δεκαδικά ψηφία για να συνεχίσετε." + }, "enableAutoDetect": { "message": " Ενεργοποίηση αυτόματου εντοπισμού" }, @@ -1650,7 +1829,7 @@ "message": "Αίτημα δημόσιου κλειδιού κρυπτογράφησης" }, "endpointReturnedDifferentChainId": { - "message": "Η διεύθυνση URL του RPC που εισαγάγατε επέστρεψε ένα διαφορετικό αναγνωριστικό αλυσίδας ($1). Ενημερώστε το αναγνωριστικό αλυσίδας ώστε να ταιριάζει με την διεύθυνση URL του RPC του δικτύου που προσπαθείτε να προσθέσετε.", + "message": "Η διεύθυνση URL του RPC που εισαγάγατε επέστρεψε ένα διαφορετικό αναγνωριστικό αλυσίδας ($1).", "description": "$1 is the return value of eth_chainId from an RPC endpoint" }, "enhancedTokenDetectionAlertMessage": { @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "Η αναζήτηση ENS απέτυχε." }, + "enterANameToIdentifyTheUrl": { + "message": "Εισαγάγετε ένα όνομα για τον προσδιορισμό της διεύθυνσης URL" + }, "enterANumber": { "message": "Εισάγετε έναν αριθμό" }, + "enterChainId": { + "message": "Εισαγάγετε το αναγνωριστικό αλυσίδας" + }, "enterCustodianToken": { "message": "Πληκτρολογήστε το token $1 ή προσθέστε ένα νέο token" }, "enterMaxSpendLimit": { "message": "Εισάγετε το μέγιστο όριο δαπανών" }, + "enterNetworkName": { + "message": "Εισαγάγετε το όνομα δικτύου" + }, "enterOptionalPassword": { "message": "Πληκτρολογήστε προαιρετικό κωδικό πρόσβασης" }, "enterPasswordContinue": { "message": "Πληκτρολογήστε τον κωδικό πρόσβασης για να συνεχίσετε" }, + "enterRpcUrl": { + "message": "Εισαγάγετε τη διεύθυνση URL του RPC" + }, + "enterSymbol": { + "message": "Εισάγετε το σύμβολο" + }, "enterTokenNameOrAddress": { "message": "Πληκτρολογήστε το όνομα του token ή επικολλήστε τη διεύθυνση" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Πειραματικά" }, + "exportYourData": { + "message": "Εξαγωγή των δεδομένων σας" + }, + "exportYourDataButton": { + "message": "Λήψη" + }, + "exportYourDataDescription": { + "message": "Μπορείτε να εξάγετε δεδομένα όπως οι επαφές και οι προτιμήσεις σας." + }, "extendWalletWithSnaps": { "message": "Προσαρμόστε την εμπειρία του πορτοφολιού σας.", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Αυτό το τέλος συναλλαγής έχει προταθεί από το $1. Η παράκαμψη μπορεί να προκαλέσει προβλήματα με τη συναλλαγή σας. Εάν έχετε απορίες, επικοινωνήστε με $1.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Τέλη συναλλαγών" + }, "gasIsETH": { "message": "Τέλη συναλλαγής $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Κάτι πήγε στραβά...." }, + "generalDescription": { + "message": "Συγχρονισμός ρυθμίσεων σε όλες τις συσκευές, επιλογή προτιμήσεων δικτύου και παρακολούθηση δεδομένων των tokens" + }, "genericExplorerView": { "message": "Προβολή λογαριασμού σε $1" }, @@ -2093,6 +2302,10 @@ "id": { "message": "Αναγνωριστικό" }, + "ifYouGetLockedOut": { + "message": "Εάν δεν μπορείτε να χρησιμοποιήσετε την εφαρμογή ή αποκτήσατε νέα συσκευή, θα χάσετε τα κεφάλαιά σας. Βεβαιωθείτε ότι έχετε δημιουργήσει αντίγραφα ασφαλείας της Μυστικής σας Φράσης Ανάκτησης στο $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Αγνόηση όλων" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "στις Ρυθμίσεις σας" }, + "included": { + "message": "περιλαμβάνεται" + }, "infuraBlockedNotification": { "message": "Το MetaMask δεν μπορεί να συνδεθεί με τον διακομιστή του blockchain. Εξετάστε τους πιθανούς λόγους $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "Αρχείο JSON", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Κρατήστε μια υπενθύμιση της Μυστικής σας Φράσης Ανάκτησης σε ασφαλές μέρος. Εάν την χάσετε, κανείς δεν μπορεί να σας βοηθήσει να την επαναφέρετε. Ακόμα χειρότερα, δεν θα έχετε ποτέ ξανά πρόσβαση στο πορτοφόλι σας. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Όνομα λογαριασμού" }, @@ -2402,6 +2622,9 @@ "message": "Μάθετε πώς να $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Μάθετε πως" + }, "learnMore": { "message": "μάθετε περισσότερα" }, @@ -2409,6 +2632,9 @@ "message": "Θέλετε να $1 για το τέλος συναλλαγής;", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": " Μάθετε περισσότερα για τις βέλτιστες πρακτικές απορρήτου." + }, "learnMoreKeystone": { "message": "Μάθετε περισσότερα" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Σύνδεσμος" }, + "linkCentralizedExchanges": { + "message": "Συνδέστε τους λογαριασμούς σας στο Coinbase ή Binance για να μεταφέρετε κρυπτονομίσματα στο MetaMask δωρεάν." + }, "links": { "message": "Σύνδεσμοι" }, @@ -2557,6 +2786,9 @@ "message": "Βεβαιωθείτε ότι κανείς δεν κοιτάει", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Διαχείριση προεπιλεγμένων ρυθμίσεων απορρήτου" + }, "marketCap": { "message": "Κεφαλαιοποίηση αγοράς" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Το κουμπί Κατάστασης Σύνδεσης δείχνει αν ο ιστότοπος που επισκέπτεστε είναι συνδεδεμένος με τον τρέχοντα επιλεγμένο λογαριασμό σας." }, + "metaMetricsIdNotAvailableError": { + "message": "Εφόσον δεν έχετε επιλέξει ποτέ το MetaMetrics, δεν υπάρχουν δεδομένα προς διαγραφή εδώ." + }, "metadataModalSourceTooltip": { "message": "Το $1 φιλοξενείται στο npm και το $2 είναι το μοναδικό αναγνωριστικό αυτού του Snap.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "περισσότερα" }, + "moreAccounts": { + "message": "+ $1 περισσότεροι λογαριασμοί", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 περισσότερα δίκτυα", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Προσθέτετε αυτό το δίκτυο στο MetaMask και δίνετε σε αυτόν τον ιστότοπο την άδεια να το χρησιμοποιεί." + }, "multipleSnapConnectionWarning": { "message": "Το $1 θέλει να χρησιμοποιήσει $2 Snaps", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Επεξεργασία λεπτομερειών δικτύου" }, "nativeTokenScamWarningDescription": { - "message": "Αυτό το δίκτυο δεν ταιριάζει με το αναγνωριστικό ή το όνομα της σχετικής αλυσίδας. Πολλά δημοφιλή tokens χρησιμοποιούν το όνομα $1, καθιστώντας το στόχο για απάτες. Οι απατεώνες μπορεί να σας ξεγελάσουν για να τους στείλετε πιο πολύτιμα νομίσματα σε αντάλλαγμα. Επαληθεύστε τα πάντα προτού συνεχίσετε.", + "message": "Το σύμβολο του εγγενούς token δεν ταιριάζει με το αναμενόμενο σύμβολο του εγγενούς token για το δίκτυο με το σχετικό αναγνωριστικό αλυσίδας. Έχετε εισαγάγει $1 ενώ το αναμενόμενο σύμβολο του token είναι $2. Επαληθεύστε ότι έχετε συνδεθεί στη σωστή αλυσίδα.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "κάτι άλλο", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Πρόκειται για πιθανή απάτη", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Λεπτομέρειες Δικτύου" }, + "networkFee": { + "message": "Τέλη δικτύου" + }, "networkIsBusy": { "message": "Το δίκτυο είναι απασχολημένο. Τα τέλη συναλλαγής είναι υψηλά και οι εκτιμήσεις λιγότερο ακριβείς." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Επιλογές δικτύου" }, + "networkPermissionToast": { + "message": "Ενημέρωση αδειών δικτύου" + }, "networkProvider": { "message": "Πάροχος δικτύου" }, @@ -2865,15 +3121,26 @@ "message": "Δεν μπορούμε να συνδεθούμε στο $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Το δίκτυο άλλαξε σε $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "Διεύθυνση URL του δικτύου" }, "networkURLDefinition": { "message": "Η διεύθυνση URL που χρησιμοποιείται για την πρόσβαση σε αυτό το δίκτυο." }, + "networkUrlErrorWarning": { + "message": "Οι εισβολείς μερικές φορές αντιγράφουν ιστότοπους κάνοντας μικρές αλλαγές στη διεύθυνση του ιστότοπου. Βεβαιωθείτε ότι αλληλεπιδράτε με τον ιστότοπο που θέλετε πριν συνεχίσετε. Έκδοση Punycode: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Δίκτυα" }, + "networksSmallCase": { + "message": "δίκτυα" + }, "nevermind": { "message": "Δεν πειράζει" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Ενημερώσαμε την πολιτική απορρήτου μας" }, + "newRpcUrl": { + "message": "Νέα διεύθυνση URL του RPC" + }, "newTokensImportedMessage": { "message": "Έχετε εισάγει με επιτυχία το $1.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "Το MetaMask δεν συνδέεται με αυτόν τον ιστότοπο" }, + "noConnectionDescription": { + "message": "Για να συνδεθείτε σε έναν ιστότοπο, βρείτε και επιλέξτε το κουμπί \"Σύνδεση\". Να θυμάστε ότι το MetaMask μπορεί να συνδεθεί μόνο με ιστότοπους στο web3" + }, "noConversionRateAvailable": { "message": "Δεν υπάρχει διαθέσιμη ισοτιμία μετατροπής" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Προσαρμοσμένο Nonce" }, + "none": { + "message": "Κανένα" + }, "notBusy": { "message": "Δεν είναι απασχολημένο" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Λεπτομέρειες άδειας χρήσης" }, + "permissionFor": { + "message": "Άδεια για" + }, + "permissionFrom": { + "message": "Άδεια από" + }, "permissionRequest": { "message": "Αίτημα άδειας" }, @@ -3593,6 +3875,14 @@ "message": "Επιτρέψτε στο $1 να έχει πρόσβαση στη γλώσσα προτίμησής σας από τις ρυθμίσεις του MetaMask. Αυτό μπορεί να χρησιμεύει για την τοπική προσαρμογή και την εμφάνιση του περιεχομένου του $1 στη γλώσσα σας.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Δείτε πληροφορίες όπως η προτιμώμενη γλώσσα και το νόμισμα.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Επιτρέψτε στο $1 να έχει πρόσβαση σε πληροφορίες όπως η προτιμώμενη γλώσσα και το νόμισμα στις ρυθμίσεις του MetaMask. Αυτό βοηθά το $1 να εμφανίζει περιεχόμενο προσαρμοσμένο στις προτιμήσεις σας. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Εμφάνιση προσαρμοσμένης οθόνης", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Δίνετε στον διαθέτη την άδεια να δαπανήσει τα tokens από τον λογαριασμό σας." }, + "permittedChainToastUpdate": { + "message": "Το $1 έχει πρόσβαση στο $2." + }, "personalAddressDetected": { "message": "Η προσωπική διεύθυνση εντοπίστηκε. Καταχωρίστε τη διεύθυνση συμβολαίου του token." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Λήψη" }, + "receiveCrypto": { + "message": "Λάβετε κρυπτονομίσματα" + }, + "recipientAddressPlaceholderNew": { + "message": "Εισαγάγετε τη δημόσια διεύθυνση (0x) ή το όνομα τομέα" + }, "recommendedGasLabel": { "message": "Προτεινόμενο" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Απορρίφθηκε" }, + "rememberSRPIfYouLooseAccess": { + "message": "Να θυμάστε, αν χάσετε τη Μυστική Φράση Ανάκτησης, δεν θα έχετε πρόσβαση στο πορτοφόλι σας. $1 για να κρατήσετε αυτό το σύνολο λέξεων ασφαλές, ώστε να έχετε πάντα πρόσβαση στα κεφάλαιά σας." + }, + "reminderSet": { + "message": "Ορισμός υπενθύμισης!" + }, "remove": { "message": "Κατάργηση" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Λόγω σφάλματος, αυτό το αίτημα δεν επαληθεύτηκε από τον πάροχο ασφαλείας. Προχωρήστε με προσοχή." }, + "requestingFor": { + "message": "Ζητήθηκε για" + }, + "requestingForAccount": { + "message": "Ζητήθηκε για $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "αιτήματα που περιμένουν να επιβεβαιωθούν" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Αποκάλυψη φράσης ανάκτησης" }, + "review": { + "message": "Επανέλεγχος" + }, + "reviewAlert": { + "message": "Ειδοποίηση επανέλεγχου" + }, "reviewAlerts": { "message": "Έλεγχος ειδοποιήσεων" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Ανάκληση άδειας" }, + "revokeSimulationDetailsDesc": { + "message": "Αφαιρείτε την άδεια κάποιου να ξοδεύει tokens από τον λογαριασμό σας." + }, "revokeSpendingCap": { "message": "Ανάκληση του ανώτατου ορίου δαπανών για το $1", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Αυτός ο τρίτος δεν θα μπορεί να ξοδέψει άλλα από τα τρέχοντα ή μελλοντικά σας tokens." }, + "rpcNameOptional": { + "message": "Όνομα RPC (προαιρετικά)" + }, "rpcUrl": { "message": "Νέα διεύθυνση URL του RPC" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Ασφάλεια και απόρρητο" }, + "securityDescription": { + "message": "Μειώστε τις πιθανότητες σύνδεσης σε μη ασφαλή δίκτυα και προστατέψτε τους λογαριασμούς σας" + }, + "securityMessageLinkForNetworks": { + "message": "διαδικτυακές απάτες και κίνδυνοι ασφαλείας" + }, + "securityPrivacyPath": { + "message": "Ρυθμίσεις > Ασφάλεια & Απόρρητο." + }, "securityProviderPoweredBy": { "message": "Με την υποστήριξη του $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Δείτε όλες τις άδειες", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Δείτε λεπτομέρειες" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Αν δεν βλέπετε τους λογαριασμούς που περιμένετε, δοκιμάστε να αλλάξετε τη διαδρομή HD ή το τρέχον επιλεγμένο δίκτυο." }, + "selectRpcUrl": { + "message": "Επιλέξτε την διεύθυνση URL του RPC" + }, "selectType": { "message": "Επιλέξτε Τύπο" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Ρύθμιση έγκρισης για όλους" }, + "setApprovalForAllRedesignedTitle": { + "message": "Αίτημα ανάληψης" + }, "setApprovalForAllTitle": { "message": "Έγκριση $1 χωρίς όριο δαπανών", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Ρυθμίσεις" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Οι ρυθμίσεις έχουν βελτιστοποιηθεί για ευκολία χρήσης και ασφάλεια. Μπορείτε να τις αλλάξετε ανά πάσα στιγμή." + }, "settingsSearchMatchingNotFound": { "message": "Δε βρέθηκαν αποτελέσματα που να ταιριάζουν." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Εμφάνιση περισσότερων" }, + "showNativeTokenAsMainBalance": { + "message": "Εμφάνιση των εγγενών tokens ως κύριο υπόλοιπο" + }, "showNft": { "message": "Εμφάνιση των NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Σύνδεση με" }, + "simulationApproveHeading": { + "message": "Ανάληψη" + }, + "simulationDetailsApproveDesc": { + "message": "Δίνετε σε κάποιον άλλο την άδεια να κάνει ανάληψη τα NFT από τον λογαριασμό σας." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Δίνετε σε κάποιον άλλον την άδεια να ξοδέψει αυτό το ποσό από τον λογαριασμό σας." + }, "simulationDetailsFiatNotAvailable": { "message": "Μη διαθέσιμο" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Στέλνετε" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Αφαιρείτε την άδεια από κάποιον άλλο να αποσύρει NFT από τον λογαριασμό σας." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Δίνετε την άδεια σε κάποιον άλλο να κάνει ανάληψη NFT από τον λογαριασμό σας." + }, "simulationDetailsTitle": { "message": "Εκτιμώμενες αλλαγές" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Ουπς! Κάτι πήγε στραβά." }, + "sortBy": { + "message": "Ταξινόμηση κατά" + }, + "sortByAlphabetically": { + "message": "Αλφαβητικά (Α-Ω)" + }, + "sortByDecliningBalance": { + "message": "Φθίνουσα απόσβεση ($1 υψηλό-χαμηλό)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Πηγή" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Διαθέτης" }, + "spenderTooltipDesc": { + "message": "Αυτή είναι η διεύθυνση που θα μπορεί να αποσύρει τα NFT σας." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Αυτή είναι η διεύθυνση που θα μπορεί να ξοδεύει τα tokens για λογαριασμό σας." + }, "spendingCap": { "message": "Ανώτατο όριο δαπανών" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Αίτημα ανώτατου ορίου δαπανών για το $1" }, + "spendingCapTooltipDesc": { + "message": "Αυτό είναι το ποσό των tokens που θα μπορεί να έχει πρόσβαση ο διαθέτης για λογαριασμό σας." + }, "srpInputNumberOfWords": { "message": "Έχω μια φράση $1 λέξεων", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Προτείνεται από $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Προτεινόμενο σύμβολο νομίσματος:" + }, "suggestedTokenName": { "message": "Προτεινόμενο όνομα:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Επισκεφθείτε το Κέντρο Υποστήριξής μας" }, + "supportMultiRpcInformation": { + "message": "Υποστηρίζουμε πλέον πολλαπλά RPC για ένα μόνο δίκτυο. Το πιο πρόσφατο RPC έχει επιλεχθεί ως το προεπιλεγμένο για την επίλυση αντικρουόμενων πληροφοριών." + }, "surveyConversion": { "message": "Πάρτε μέρος στην έρευνά μας" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Τα τέλη συναλλαγών είναι ενδεικτικά και θα αυξομειώνονται ανάλογα με την κίνηση του δικτύου και την πολυπλοκότητα των συναλλαγών." }, + "swapGasFeesExplanation": { + "message": "Το MetaMask δεν κερδίζει χρήματα από τα τέλη συναλλαγών. Αυτές οι χρεώσεις είναι εκτιμήσεις και μπορούν να αλλάξουν ανάλογα με το πόσο απασχολημένο είναι το δίκτυο και πόσο περίπλοκη είναι μια συναλλαγή. Μάθετε περισσότερα $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "εδώ", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Μάθετε περισσότερα σχετικά με τα τέλη συναλλαγών" }, @@ -5186,9 +5583,19 @@ "message": "Τα τέλη συναλλαγών καταβάλλονται σε κρυπτονομίσματα στους αναλυτές που επεξεργάζονται συναλλαγές στο δίκτυο $1. Το MetaMask δεν επωφελείται από τα τέλη συναλλαγών.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Αυτή η τιμή ενσωματώνει τα τέλη συναλλαγών προσαρμόζοντας το ποσό των tokens που αποστέλλονται ή λαμβάνονται. Μπορείτε να λάβετε ETH σε ξεχωριστή συναλλαγή στη λίστα δραστηριοτήτων σας." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Μάθετε περισσότερα για τα τέλη συναλλαγών" + }, "swapHighSlippage": { "message": "Υψηλή ολίσθηση" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Περιλαμβάνει τα τέλη και μια χρέωση $1% του MetaMask", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Περιλαμβάνει μια χρέωση $1% στο MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Οι Όροι Χρήσης μας έχουν ενημερωθεί" }, + "testnets": { + "message": "Testnets" + }, "theme": { "message": "Θέμα" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Χρέωση συναλλαγής" }, + "transactionFlowNetwork": { + "message": "Δίκτυο" + }, "transactionHistoryBaseFee": { "message": "Βασική χρέωση (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Μεταφορά" }, + "transferCrypto": { + "message": "Μεταφορά κρυπτονομισμάτων" + }, "transferFrom": { "message": "Μεταφορά από" }, + "transferRequest": { + "message": "Μεταβίβαση αιτήματος" + }, "trillionAbbreviation": { "message": "Τ", "description": "Shortened form of 'trillion'" @@ -5766,7 +6185,7 @@ "message": "Μείνετε ενήμεροι για ό,τι συμβαίνει στο πορτοφόλι σας με τις ειδοποιήσεις." }, "turnOnMetamaskNotificationsMessagePrivacyBold": { - "message": "Ρυθμίσεις > Ειδοποιήσεις." + "message": "ρυθμίσεις ειδοποιήσεων." }, "turnOnMetamaskNotificationsMessagePrivacyLink": { "message": "Μάθετε πώς προστατεύουμε το απόρρητό σας κατά τη χρήση αυτής της λειτουργίας." @@ -5844,12 +6263,22 @@ "update": { "message": "Ενημέρωση" }, + "updateEthereumChainConfirmationDescription": { + "message": "Αυτός ο ιστότοπος ζητά να ενημερώσει την προεπιλεγμένη διεύθυνση URL του δικτύου σας. Μπορείτε να επεξεργαστείτε τις προεπιλογές και τις πληροφορίες δικτύου ανά πάσα στιγμή." + }, + "updateNetworkConfirmationTitle": { + "message": "Ενημέρωση του $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Ενημερώστε τα στοιχεία σας ή" }, "updateRequest": { "message": "Αίτημα ενημέρωσης" }, + "updatedRpcForNetworks": { + "message": "Ενημέρωση του RPC δικτύου" + }, "uploadDropFile": { "message": "Αφήστε το αρχείο σας εδώ" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "ο οδηγός μας σύνδεσης πορτοφολιού υλικού" }, + "walletProtectedAndReadyToUse": { + "message": "Το πορτοφόλι σας είναι προστατευμένο και έτοιμο προς χρήση. Μπορείτε να βρείτε τη Μυστική σας Φράση Ανάκτησης στο $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Θέλετε να προσθέσετε αυτό το δίκτυο;" }, @@ -5991,6 +6424,17 @@ "message": "$1 Ο τρίτος θα μπορούσε να ξοδέψει ολόκληρο το υπόλοιπο των tokens σας χωρίς περαιτέρω ειδοποίηση ή συγκατάθεση. Προστατέψτε τον εαυτό σας προσαρμόζοντας ένα χαμηλότερο όριο δαπανών.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Η ενεργοποίηση αυτής της επιλογής θα σας δώσει τη δυνατότητα να παρακολουθείτε λογαριασμούς Ethereum μέσω μιας δημόσιας διεύθυνσης ή ενός ονόματος ENS. Για σχόλια σχετικά με αυτή τη λειτουργία Beta συμπληρώστε αυτή την $1.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Παρακολούθηση λογαριασμών Ethereum (Beta)" + }, + "watchOutMessage": { + "message": "Προσοχή στο $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Αδύναμο" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Τι είναι αυτό;" }, + "withdrawing": { + "message": "Ανάληψη" + }, "wrongNetworkName": { "message": "Σύμφωνα με τα αρχεία μας, το όνομα του δικτύου ενδέχεται να μην αντιστοιχεί με αυτό το αναγνωριστικό αλυσίδας." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Το υπόλοιπό σας" }, + "yourBalanceIsAggregated": { + "message": "Το υπόλοιπό σας είναι συγκεντρωτικό" + }, "yourNFTmayBeAtRisk": { "message": "Τα NFT μπορεί να κινδυνεύουν" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Δεν μπορέσαμε να ακυρώσουμε τη συναλλαγή σας πριν επιβεβαιωθεί από το blockchain." }, + "yourWalletIsReady": { + "message": "Το πορτοφόλι σας είναι έτοιμο" + }, "zeroGasPriceOnSpeedUpError": { "message": "Μηδενική τιμή συναλλαγών για επίσπευση" } diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 604396f2c6eb..746c099ae75e 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Conecte su monedero físico QR" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "La dirección de la solicitud de inicio de sesión no coincide con la dirección de la cuenta que está utilizando para iniciar sesión." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Seleccione las cuentas sobre las que desea recibir notificaciones:" }, + "accountBalance": { + "message": "Saldo de la cuenta" + }, "accountDetails": { "message": "Detalles de la cuenta" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Opciones de la cuenta" }, + "accountPermissionToast": { + "message": "Se actualizaron los permisos de la cuenta" + }, "accountSelectionRequired": { "message": "Debe seleccionar una cuenta." }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Cuentas conectadas" }, + "accountsPermissionsTitle": { + "message": "Ver sus cuentas y sugerir transacciones" + }, + "accountsSmallCase": { + "message": "cuentas" + }, "active": { "message": "Activo" }, @@ -180,12 +195,18 @@ "add": { "message": "Agregar" }, + "addACustomNetwork": { + "message": "Agregar una red personalizada" + }, "addANetwork": { "message": "Agregar una red" }, "addANickname": { "message": "Añadir un apodo" }, + "addAUrl": { + "message": "Agregar una URL" + }, "addAccount": { "message": "Añadir cuenta" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Agregar un explorador de bloque" }, + "addBlockExplorerUrl": { + "message": "Agregar una URL del explorador de bloques" + }, "addContact": { "message": "Agregar contacto" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Va a agregar un nuevo proveedor de RPC para la red principal de Ethereum" }, + "addEthereumWatchOnlyAccount": { + "message": "Observar una cuenta Ethereum (Beta)" + }, "addFriendsAndAddresses": { "message": "Agregue amigos y direcciones de confianza" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Agregar red" }, + "addNetworkConfirmationTitle": { + "message": "Agregar $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Añadir una cuenta nueva de Ethereum" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "¡Dirección copiada!" }, + "addressMismatch": { + "message": "La dirección del sitio no coincide" + }, + "addressMismatchOriginal": { + "message": "URL actual: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Versión de Punycode: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Avanzado" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "La tarifa de prioridad (también llamada “propina del minero”) va directamente a los mineros para incentivarlos a priorizar su transacción." }, + "aggregatedBalancePopover": { + "message": "Esto refleja el valor de todos los tokens que posee en una red determinada. Si prefiere ver este valor en ETH u otras monedas, vaya a $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Acepto los $1 de MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Esto se puede modificar en \"Configuración > Alertas\"" }, + "alertMessageAddressMismatchWarning": { + "message": "A veces, los atacantes imitan sitios web haciendo pequeños cambios en la dirección del sitio. Asegúrese de estar interactuando con el sitio deseado antes de continuar." + }, "alertMessageGasEstimateFailed": { "message": "No podemos proporcionar una tarifa exacta y esta estimación podría ser alta. Le sugerimos que ingrese un límite de gas personalizado, pero existe el riesgo de que la transacción aún falle." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Para continuar con esta transacción, deberá aumentar el límite de gas a 21000 o más." }, + "alertMessageInsufficientBalance2": { + "message": "No tiene suficiente ETH en su cuenta para pagar las tarifas de red." + }, "alertMessageNetworkBusy": { "message": "Los precios del gas son altos y las estimaciones son menos precisas." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Opciones de activos" }, + "assets": { + "message": "Activos" + }, + "assetsDescription": { + "message": "Detectar automáticamente tokens en su monedero, mostrar NFT y recibir actualizaciones de saldo de cuenta por lotes" + }, "attemptSendingAssets": { "message": "Puede perder sus activos si intenta enviarlos desde otra red. Transfiera fondos de forma segura entre redes mediante el uso de un puente." }, @@ -621,6 +679,9 @@ "backupKeyringSnapReminder": { "message": "Asegúrese de poder acceder a cualquier cuenta creada por este Snap por si mismo antes de eliminarlo" }, + "backupNow": { + "message": "Respaldar ahora" + }, "balance": { "message": "Saldo" }, @@ -779,6 +840,15 @@ "bridgeDontSend": { "message": "Puente, no enviar" }, + "bridgeFrom": { + "message": "Puentear desde" + }, + "bridgeSelectNetwork": { + "message": "Seleccionar red" + }, + "bridgeTo": { + "message": "Puentear hacia" + }, "browserNotSupported": { "message": "Su explorador no es compatible..." }, @@ -942,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Confirmar frase secreta de recuperación" }, + "confirmTitleApproveTransaction": { + "message": "Solicitud de asignación" + }, + "confirmTitleDeployContract": { + "message": "Implementar un contrato" + }, + "confirmTitleDescApproveTransaction": { + "message": "Este sitio solicita permiso para retirar sus NFT" + }, + "confirmTitleDescDeployContract": { + "message": "Este sitio quiere que implemente un contrato" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Este sitio solicita permiso para retirar sus tokens" + }, "confirmTitleDescPermitSignature": { "message": "Este sitio solicita permiso para gastar sus tokens." }, "confirmTitleDescSIWESignature": { "message": "Un sitio quiere que inicie sesión para demostrar que es el propietario de esta cuenta." }, + "confirmTitleDescSign": { + "message": "Revise los detalles de la solicitud antes de confirmar." + }, "confirmTitlePermitTokens": { "message": "Solicitud de límite de gasto" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Eliminar permiso" + }, "confirmTitleSIWESignature": { "message": "Solicitud de inicio de sesión" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Eliminar permiso" + }, "confirmTitleSignature": { "message": "Solicitud de firma" }, "confirmTitleTransaction": { "message": "Solicitud de transacción" }, + "confirmationAlertModalDetails": { + "message": "Para proteger sus activos e información de inicio de sesión, le sugerimos que rechace la solicitud." + }, + "confirmationAlertModalTitle": { + "message": "Esta solicitud es sospechosa" + }, "confirmed": { "message": "Confirmado" }, @@ -972,6 +1072,9 @@ "confusingEnsDomain": { "message": "Se detectó un carácter que puede confundirse con otro similar en el nombre de ENS. Verifique el nombre de ENS para evitar una posible estafa." }, + "congratulations": { + "message": "¡Felicidades!" + }, "connect": { "message": "Conectar" }, @@ -1032,6 +1135,9 @@ "connectedSites": { "message": "Sitios conectados" }, + "connectedSitesAndSnaps": { + "message": "Sitios conectados y Snaps" + }, "connectedSitesDescription": { "message": "$1 está conectado a estos sitios. Pueden ver la dirección de su cuenta.", "description": "$1 is the account name" @@ -1043,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask está conectado a este sitio, pero aún no hay cuentas conectadas" }, + "connectedSnaps": { + "message": "Snaps conectados" + }, + "connectedWithAccount": { + "message": "$1 cuentas conectadas", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Conectado con $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Conectando" }, @@ -1070,6 +1187,9 @@ "connectingToSepolia": { "message": "Conectando a la red de prueba Sepolia" }, + "connectionDescription": { + "message": "Este sitio quiere" + }, "connectionFailed": { "message": "Conexión fallida" }, @@ -1150,6 +1270,9 @@ "copyAddress": { "message": "Copiar dirección al Portapapeles" }, + "copyAddressShort": { + "message": "Copiar dirección" + }, "copyPrivateKey": { "message": "Copiar clave privada" }, @@ -1399,12 +1522,41 @@ "defaultRpcUrl": { "message": "URL RPC por defecto" }, + "defaultSettingsSubTitle": { + "message": "MetaMask utiliza la configuración por defecto para equilibrar mejor la seguridad y la facilidad de uso. Cambie esta configuración para aumentar aún más su privacidad." + }, + "defaultSettingsTitle": { + "message": "Configuración de privacidad por defecto" + }, "delete": { "message": "Eliminar" }, "deleteContact": { "message": "Eliminar contacto" }, + "deleteMetaMetricsData": { + "message": "Eliminar datos de MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Esto eliminará los datos históricos de MetaMetrics asociados con su uso en este dispositivo. Su monedero y cuentas permanecerán exactamente como están ahora después de que se eliminen estos datos. Este proceso puede tardar hasta 30 días. Consulte nuestra $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Esta solicitud no se puede ejecutar en este momento por un problema con el servidor del sistema de análisis. Vuelva a intentarlo más tarde" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "No podemos eliminar estos datos en este momento" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Estamos a punto de eliminar todos sus datos de MetaMetrics. ¿Está seguro?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "¿Eliminar datos de MetaMetrics?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Usted inició esta acción el $1. Este proceso puede tardar hasta 30 días. Consulte la $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Si elimina esta red, deberá volver a agregarla para ver sus activos en esta red" }, @@ -1415,6 +1567,9 @@ "deposit": { "message": "Depositar" }, + "depositCrypto": { + "message": "Deposite criptomonedas desde otra cuenta con una dirección de monedero o un código QR." + }, "deprecatedGoerliNtwrkMsg": { "message": "Debido a las actualizaciones del sistema Ethereum, la red de prueba Goerli se eliminará pronto." }, @@ -1456,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "cuentas" }, + "disconnectAllDescriptionText": { + "message": "Si se desconecta de este sitio, tendrá que volver a conectar sus cuentas y redes para volver a utilizarlo." + }, "disconnectAllSnapsText": { "message": "Snaps" }, + "disconnectMessage": { + "message": "Esto lo desconectará de este sitio" + }, "disconnectPrompt": { "message": "Desconectar $1" }, @@ -1528,6 +1689,9 @@ "editANickname": { "message": "Editar alias" }, + "editAccounts": { + "message": "Editar cuentas" + }, "editAddressNickname": { "message": "Editar apodo de dirección" }, @@ -1608,6 +1772,9 @@ "editNetworkLink": { "message": "editar la red original" }, + "editNetworksTitle": { + "message": "Editar redes" + }, "editNonceField": { "message": "Editar nonce" }, @@ -1617,9 +1784,24 @@ "editPermission": { "message": "Editar permiso" }, + "editPermissions": { + "message": "Editar permisos" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Editar la tarifa de aceleración de gas" }, + "editSpendingCap": { + "message": "Editar límite de gasto" + }, + "editSpendingCapAccountBalance": { + "message": "Saldo de la cuenta: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Ingrese la cantidad que considere conveniente que se gaste en su nombre." + }, + "editSpendingCapError": { + "message": "El límite de gasto no puede superar $1 dígitos decimales. Elimine los dígitos decimales para continuar." + }, "enableAutoDetect": { "message": " Activar autodetección" }, @@ -1671,21 +1853,36 @@ "ensUnknownError": { "message": "Error al buscar ENS." }, + "enterANameToIdentifyTheUrl": { + "message": "Ingrese un nombre para identificar la URL" + }, "enterANumber": { "message": "Ingrese un número" }, + "enterChainId": { + "message": "Ingrese el ID de cadena" + }, "enterCustodianToken": { "message": "Ingrese su token $1 o agregue un token nuevo" }, "enterMaxSpendLimit": { "message": "Escribir límite máximo de gastos" }, + "enterNetworkName": { + "message": "Ingrese el nombre de la red" + }, "enterOptionalPassword": { "message": "Ingrese la contraseña opcional" }, "enterPasswordContinue": { "message": "Escribir contraseña para continuar" }, + "enterRpcUrl": { + "message": "Ingrese el URL de RPC" + }, + "enterSymbol": { + "message": "Ingrese el símbolo" + }, "enterTokenNameOrAddress": { "message": "Ingrese el nombre del token o pegue la dirección" }, @@ -1759,6 +1956,15 @@ "experimental": { "message": "Experimental" }, + "exportYourData": { + "message": "Exporte sus datos" + }, + "exportYourDataButton": { + "message": "Descargar" + }, + "exportYourDataDescription": { + "message": "Puede exportar datos como sus contactos y preferencias." + }, "extendWalletWithSnaps": { "message": "Explore los Snaps creados por la comunidad para personalizar su experiencia web3", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1874,6 +2080,9 @@ "message": "Esta tarifa de gas ha sido sugerida por $1. Anularla puede causar un problema con su transacción. Comuníquese con $1 si tiene preguntas.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Tarifa de gas" + }, "gasIsETH": { "message": "El gas es $1 " }, @@ -1944,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Algo salió mal..." }, + "generalDescription": { + "message": "Sincronizar configuraciones entre dispositivos, seleccionar preferencias de red y dar seguimiento a los datos de tokens" + }, "genericExplorerView": { "message": "Ver cuenta en $1" }, @@ -2090,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "Si se le bloquea el acceso a la aplicación o adquiere un nuevo dispositivo, perderá sus fondos. Asegúrese de hacer una copia de seguridad de su frase secreta de recuperación en $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Ignorar todo" }, @@ -2171,6 +2387,9 @@ "inYourSettings": { "message": "en su Configuración" }, + "included": { + "message": "incluido" + }, "infuraBlockedNotification": { "message": "MetaMask no se pudo conectar al host de la cadena de bloques. Revise las razones posibles $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2341,6 +2560,10 @@ "message": "Archivo JSON", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Guarde un recordatorio de su frase secreta de recuperación en algún lugar seguro. Si la pierde, nadie podrá ayudarlo a recuperarla. Y lo que es peor, no podrá volver a acceder a su monedero. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Nombre de la cuenta" }, @@ -2399,6 +2622,9 @@ "message": "Aprenda cómo $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Más información" + }, "learnMore": { "message": "Más información" }, @@ -2406,6 +2632,9 @@ "message": "¿Quiere $1 sobre gas?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Obtenga más información sobre las mejores prácticas de privacidad." + }, "learnMoreKeystone": { "message": "Más información" }, @@ -2494,6 +2723,9 @@ "link": { "message": "Vínculo" }, + "linkCentralizedExchanges": { + "message": "Vincule sus cuentas de Coinbase o Binance para transferir criptomonedas a MetaMask de forma gratuita." + }, "links": { "message": "Vínculos" }, @@ -2554,6 +2786,9 @@ "message": "Asegúrese de que nadie esté mirando", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Gestionar la configuración de privacidad por defecto" + }, "marketCap": { "message": "Capitalización bursátil" }, @@ -2597,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "El botón de estado de la conexión muestra si el sitio web que visita está conectado a la cuenta seleccionada actualmente." }, + "metaMetricsIdNotAvailableError": { + "message": "Como nunca ha utilizado MetaMetrics, no hay datos que eliminar aquí." + }, "metadataModalSourceTooltip": { "message": "$1 está alojado en npm y $2 es el identificador único de este Snap.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2673,6 +2911,17 @@ "more": { "message": "más" }, + "moreAccounts": { + "message": "Más de $1 cuentas adicionales", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "Más de $1 redes adicionales", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Está agregando esta red a MetaMask y otorgando permiso a este sitio para usarla." + }, "multipleSnapConnectionWarning": { "message": "$1 quiere conectarse a $2", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2751,9 +3000,13 @@ "message": "Editar detalles de la red" }, "nativeTokenScamWarningDescription": { - "message": "Esta red no coincide con su ID de cadena o nombre asociado. Muchos tokens populares usan el nombre $1, lo que los convierte en blanco de estafas. Los estafadores pueden engañarlo para que les envíe dinero más valioso a cambio. Verifique todo antes de continuar.", + "message": "El símbolo del token nativo no coincide con el símbolo esperado del token nativo para la red con el ID de cadena asociado. Ingresó $1 mientras que el símbolo del token esperado es $2. Verifique que esté conectado a la cadena correcta.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "algo más", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Esto es una estafa potencial", "description": "Title for nativeTokenScamWarningDescription" @@ -2787,6 +3040,9 @@ "networkDetails": { "message": "Detalles de la red" }, + "networkFee": { + "message": "Tarifa de red" + }, "networkIsBusy": { "message": "La red está ocupada. Los precios del gas son altos y las estimaciones son menos precisas." }, @@ -2841,6 +3097,9 @@ "networkOptions": { "message": "Opciones de red" }, + "networkPermissionToast": { + "message": "Se actualizaron los permisos de la red" + }, "networkProvider": { "message": "Proveedor de red" }, @@ -2862,15 +3121,26 @@ "message": "No podemos conectar a $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "La red cambió a $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "Dirección URL de la red" }, "networkURLDefinition": { "message": "La dirección URL que se utilizó para acceder a esta red." }, + "networkUrlErrorWarning": { + "message": "A veces, los atacantes imitan sitios web haciendo pequeños cambios en la dirección del sitio. Asegúrese de estar interactuando con el sitio deseado antes de continuar. Versión de Punycode: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Redes" }, + "networksSmallCase": { + "message": "redes" + }, "nevermind": { "message": "No es importante" }, @@ -2921,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Hemos actualizado nuestra política de privacidad" }, + "newRpcUrl": { + "message": "Nueva URL de RPC" + }, "newTokensImportedMessage": { "message": "Ha importado $1 exitosamente.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2983,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask no está conectado a este sitio" }, + "noConnectionDescription": { + "message": "Para conectarse a un sitio, busque y seleccione el botón \"conectar\". Recuerde que MetaMask solo puede conectarse a sitios en Web3" + }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -3028,6 +3304,9 @@ "nonceFieldHeading": { "message": "Nonce personalizado" }, + "none": { + "message": "Ninguna" + }, "notBusy": { "message": "No ocupado" }, @@ -3509,6 +3788,12 @@ "permissionDetails": { "message": "Detalles del permiso" }, + "permissionFor": { + "message": "Permiso para" + }, + "permissionFrom": { + "message": "Permiso de" + }, "permissionRequest": { "message": "Solicitud de permiso" }, @@ -3590,6 +3875,14 @@ "message": "Permita que $1 acceda a su idioma preferido desde la configuración de MetaMask. Esto se puede usar para localizar y mostrar el contenido de $1 usando su idioma.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Vea información como su idioma preferido y moneda fiduciaria.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Permita que $1 acceda a información como su idioma preferido y moneda fiduciaria en su configuración de MetaMask. Esto ayuda a que $1 muestre contenido adaptado a sus preferencias. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Mostrar una pantalla personalizada", "description": "The description for the `endowment:page-home` permission" @@ -3748,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Le está dando permiso al gastador para gastar esta cantidad de tokens de su cuenta." }, + "permittedChainToastUpdate": { + "message": "$1 tiene acceso a $2." + }, "personalAddressDetected": { "message": "Se detectó una dirección personal. Ingrese la dirección de contrato del token." }, @@ -3960,6 +4256,12 @@ "receive": { "message": "Recibir" }, + "receiveCrypto": { + "message": "Recibir criptomonedas" + }, + "recipientAddressPlaceholderNew": { + "message": "Ingrese la dirección pública (0x) o el nombre del dominio" + }, "recommendedGasLabel": { "message": "Recomendado" }, @@ -4023,6 +4325,12 @@ "rejected": { "message": "Rechazado" }, + "rememberSRPIfYouLooseAccess": { + "message": "Recuerde que si pierde su frase secreta de recuperación, perderá el acceso a su monedero. $1 para mantener seguro este conjunto de palabras y poder acceder siempre a sus fondos." + }, + "reminderSet": { + "message": "¡Recordatorio creado!" + }, "remove": { "message": "Quitar" }, @@ -4102,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Debido a un error, el proveedor de seguridad no verificó esta solicitud. Proceda con precaución." }, + "requestingFor": { + "message": "Solicitando para" + }, + "requestingForAccount": { + "message": "Solicitando para $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "solicitudes en espera de confirmación" }, @@ -4190,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Revelar frase semilla" }, + "review": { + "message": "Revisar" + }, + "reviewAlert": { + "message": "Alerta de revisión" + }, "reviewAlerts": { "message": "Revisar alertas" }, @@ -4215,6 +4536,9 @@ "revokePermission": { "message": "Revocar permiso" }, + "revokeSimulationDetailsDesc": { + "message": "Está eliminando el permiso de una persona para gastar tokens de su cuenta." + }, "revokeSpendingCap": { "message": "Revocar un límite de gasto para su $1", "description": "$1 is a token symbol" @@ -4222,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Este tercero no podrá gastar más de sus tokens actuales o futuros." }, + "rpcNameOptional": { + "message": "Nombre de RPC (opcional)" + }, "rpcUrl": { "message": "Nueva dirección URL de RPC" }, @@ -4274,10 +4601,23 @@ "securityAndPrivacy": { "message": "Seguridad y privacidad" }, + "securityDescription": { + "message": "Reduzca sus posibilidades de unirse a redes inseguras y proteja sus cuentas" + }, + "securityMessageLinkForNetworks": { + "message": "estafas en la red y riesgos de seguridad" + }, + "securityPrivacyPath": { + "message": "Configuración > Seguridad y privacidad." + }, "securityProviderPoweredBy": { "message": "Impulsado por $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Ver todos los permisos", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Ver detalles" }, @@ -4371,6 +4711,9 @@ "selectPathHelp": { "message": "Si no ve las cuentas previstas, intente cambiar la ruta HD o la red seleccionada actualmente." }, + "selectRpcUrl": { + "message": "Seleccionar URL de RPC" + }, "selectType": { "message": "Seleccionar tipo" }, @@ -4433,6 +4776,9 @@ "setApprovalForAll": { "message": "Establecer aprobación para todos" }, + "setApprovalForAllRedesignedTitle": { + "message": "Solicitud de retiro" + }, "setApprovalForAllTitle": { "message": "Aprobar $1 sin límite preestablecido", "description": "The token symbol that is being approved" @@ -4443,6 +4789,9 @@ "settings": { "message": "Configuración" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "La configuración está optimizada para facilitar su uso y garantizar la seguridad. Cámbiela en cualquier momento." + }, "settingsSearchMatchingNotFound": { "message": "No se encontraron resultados coincidentes." }, @@ -4489,6 +4838,9 @@ "showMore": { "message": "Mostrar más" }, + "showNativeTokenAsMainBalance": { + "message": "Mostrar el token nativo como saldo principal" + }, "showNft": { "message": "Mostrar NFT" }, @@ -4528,6 +4880,15 @@ "signingInWith": { "message": "Iniciar sesión con" }, + "simulationApproveHeading": { + "message": "Retirar" + }, + "simulationDetailsApproveDesc": { + "message": "Le está dando permiso a otra persona para que retire NFT de su cuenta." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Le está dando permiso a otra persona para que gaste este monto de su cuenta." + }, "simulationDetailsFiatNotAvailable": { "message": "No disponible" }, @@ -4537,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Envía" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Está eliminando el permiso de otra persona para retirar NFT de su cuenta." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Está dando permiso a otra persona para que retire NFT de su cuenta." + }, "simulationDetailsTitle": { "message": "Cambios estimados" }, @@ -4789,6 +5156,16 @@ "somethingWentWrong": { "message": "Lo lamentamos, se produjo un error." }, + "sortBy": { + "message": "Ordenar por" + }, + "sortByAlphabetically": { + "message": "Alfabéticamente (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Saldo decreciente ($1 alto-bajo)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Fuente" }, @@ -4832,6 +5209,12 @@ "spender": { "message": "Gastador" }, + "spenderTooltipDesc": { + "message": "Esta es la dirección que podrá retirar sus NFT." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Esta es la dirección que podrá gastar sus tokens en su nombre." + }, "spendingCap": { "message": "Límite de gasto" }, @@ -4845,6 +5228,9 @@ "spendingCapRequest": { "message": "Solicitud de límite de gastos para su $1" }, + "spendingCapTooltipDesc": { + "message": "Esta es la cantidad de tokens a la que el gastador podrá acceder en su nombre." + }, "srpInputNumberOfWords": { "message": "Tengo una frase de $1 palabras", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5048,6 +5434,9 @@ "message": "Sugerido por $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Símbolo de moneda sugerido:" + }, "suggestedTokenName": { "message": "Nombre sugerido:" }, @@ -5057,6 +5446,9 @@ "supportCenter": { "message": "Visite nuestro Centro de soporte técnico" }, + "supportMultiRpcInformation": { + "message": "Ahora admitimos varias RPC para una sola red. Su RPC más reciente se ha seleccionado como predeterminada para resolver información conflictiva." + }, "surveyConversion": { "message": "Responda a nuestra encuesta" }, @@ -5173,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Las tarifas de gas son estimadas y fluctuarán en función del tráfico de la red y la complejidad de las transacciones." }, + "swapGasFeesExplanation": { + "message": "MetaMask no obtiene dinero de las tarifas de gas. Estas tarifas son estimaciones y pueden cambiar según el nivel de tráfico de la red y la complejidad de la transacción. Más información $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "aquí", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Obtenga más información sobre las tarifas de gas" }, @@ -5183,9 +5583,19 @@ "message": "Las tarifas de gas se pagan a los mineros de criptomonedas que procesan transacciones en la red $1. MetaMask no se beneficia de las tarifas de gas.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Esta cotización incorpora las tarifas de gas al ajustar la cantidad de tokens enviada o recibida. Puede recibir ETH en una transacción separada en su lista de actividades." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Más información sobre las tarifas de gas" + }, "swapHighSlippage": { "message": "Deslizamiento alto" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Incluye gas y una tasa de MetaMask del $1%", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Incluye una tasa de MetaMask del $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5482,6 +5892,9 @@ "termsOfUseTitle": { "message": "Nuestros Términos de uso han sido actualizados" }, + "testnets": { + "message": "Red de pruebas" + }, "theme": { "message": "Tema" }, @@ -5663,6 +6076,9 @@ "transactionFee": { "message": "Tarifa de transacción" }, + "transactionFlowNetwork": { + "message": "Red" + }, "transactionHistoryBaseFee": { "message": "Tarifa base (GWEI)" }, @@ -5705,9 +6121,15 @@ "transfer": { "message": "Transferir" }, + "transferCrypto": { + "message": "Transferir criptomonedas" + }, "transferFrom": { "message": "Transferir desde" }, + "transferRequest": { + "message": "Solicitud de transferencia" + }, "trillionAbbreviation": { "message": "b", "description": "Shortened form of 'trillion'" @@ -5841,12 +6263,22 @@ "update": { "message": "Actualizar" }, + "updateEthereumChainConfirmationDescription": { + "message": "Este sitio solicita actualizar la URL de su red predeterminada. Puede editar los valores predeterminados y la información de la red en cualquier momento." + }, + "updateNetworkConfirmationTitle": { + "message": "Actualizar $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Actualice su información o" }, "updateRequest": { "message": "Solicitud de actualización" }, + "updatedRpcForNetworks": { + "message": "RPC de red actualizadas" + }, "uploadDropFile": { "message": "Ingrese su archivo aquí" }, @@ -5971,6 +6403,10 @@ "walletConnectionGuide": { "message": "nuestra guía de conexión del monedero físico" }, + "walletProtectedAndReadyToUse": { + "message": "Su monedero está protegido y listo para usar. Puede encontrar su frase secreta de recuperación en $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "¿Desea añadir esta red?" }, @@ -5988,6 +6424,17 @@ "message": "$1 El tercero podría gastar todo su saldo de tokens sin previo aviso o consentimiento. Protéjase personalizando un límite de gasto más bajo.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Al activar esta opción, podrá ver las cuentas de Ethereum a través de una dirección pública o un nombre de ENS. Para recibir comentarios sobre esta función Beta, complete este $1.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Observar cuentas Ethereum (Beta)" + }, + "watchOutMessage": { + "message": "Cuidado con $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Débil" }, @@ -6034,6 +6481,9 @@ "whatsThis": { "message": "¿Qué es esto?" }, + "withdrawing": { + "message": "Retirando" + }, "wrongNetworkName": { "message": "Según nuestros registros, es posible que el nombre de la red no coincida correctamente con este ID de cadena." }, @@ -6062,6 +6512,9 @@ "yourBalance": { "message": "Su saldo" }, + "yourBalanceIsAggregated": { + "message": "Su saldo es un acumulado" + }, "yourNFTmayBeAtRisk": { "message": "Sus NFT podrían estar en riesgo" }, @@ -6074,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "No pudimos cancelar su transacción antes de que se confirmara en la cadena de bloques." }, + "yourWalletIsReady": { + "message": "Su monedero está listo" + }, "zeroGasPriceOnSpeedUpError": { "message": "No hay entradas sobre el precio del gas al acelerar la transacción" } diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 7c4db83af9e8..bcfa71f5661b 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Connectez votre portefeuille électronique QR" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "L’adresse figurant dans la demande de connexion ne correspond pas à l’adresse du compte que vous utilisez pour vous connecter." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Sélectionnez les comptes pour lesquels vous souhaitez recevoir des notifications :" }, + "accountBalance": { + "message": "Solde du compte" + }, "accountDetails": { "message": "Détails du compte" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Options du compte" }, + "accountPermissionToast": { + "message": "Les autorisations accordées au compte ont été mises à jour" + }, "accountSelectionRequired": { "message": "Vous devez sélectionner un compte !" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Comptes connectés" }, + "accountsPermissionsTitle": { + "message": "Consultez vos comptes et suggérez des transactions" + }, + "accountsSmallCase": { + "message": "comptes" + }, "active": { "message": "Actif" }, @@ -180,12 +195,18 @@ "add": { "message": "Ajouter" }, + "addACustomNetwork": { + "message": "Ajouter un réseau personnalisé" + }, "addANetwork": { "message": "Ajouter un réseau" }, "addANickname": { "message": "Ajouter un pseudo" }, + "addAUrl": { + "message": "Ajouter une URL" + }, "addAccount": { "message": "Ajouter un compte" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Ajouter un explorateur de blocs" }, + "addBlockExplorerUrl": { + "message": "Ajouter une URL d’explorateur de blocs" + }, "addContact": { "message": "Ajouter un contact" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Vous êtes en train d’ajouter un nouveau fournisseur de RPC pour le réseau principal de la blockchain Ethereum" }, + "addEthereumWatchOnlyAccount": { + "message": "Surveiller un compte Ethereum (Bêta)" + }, "addFriendsAndAddresses": { "message": "Ajoutez uniquement des amis et des adresses de confiance" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Ajouter un réseau" }, + "addNetworkConfirmationTitle": { + "message": "Ajouter $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Ajouter un nouveau compte Ethereum" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Adresse copiée !" }, + "addressMismatch": { + "message": "Inadéquation de l’adresse du site" + }, + "addressMismatchOriginal": { + "message": "URL actuelle : $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Version Punycode : $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Paramètres avancés" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Les frais de priorité (aussi appelés « pourboire du mineur ») vont directement aux mineurs et les incitent à accorder la priorité à votre transaction." }, + "aggregatedBalancePopover": { + "message": "Cette valeur reflète la valeur de tous les jetons que vous possédez sur un réseau donné. Si vous préférez que cette valeur soit affichée en ETH ou dans d’autres monnaies, veuillez accéder à $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "J’accepte les $1 de MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Vous pouvez modifier ceci dans « Paramètres > Alertes »" }, + "alertMessageAddressMismatchWarning": { + "message": "Les pirates informatiques imitent parfois les sites en modifiant légèrement l’adresse du site. Assurez-vous que vous interagissez avec le site voulu avant de continuer." + }, "alertMessageGasEstimateFailed": { "message": "Nous ne sommes pas en mesure de déterminer le montant exact des frais et cette estimation peut être élevée. Nous vous suggérons d’entrer une limite de gaz personnalisée, mais la transaction pourrait quand même échouer." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Pour effectuer cette transaction, vous devez augmenter la limite de gaz à 21 000 ou plus." }, + "alertMessageInsufficientBalance2": { + "message": "Vous n’avez pas assez d’ETH sur votre compte pour payer les frais de réseau." + }, "alertMessageNetworkBusy": { "message": "Les prix du gaz sont élevés et les estimations sont moins précises." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Options d’actifs" }, + "assets": { + "message": "Actifs" + }, + "assetsDescription": { + "message": "Détection automatique des jetons dans votre portefeuille, affichage des NFT et mise à jour du solde de plusieurs comptes" + }, "attemptSendingAssets": { "message": "Si vous essayez d’envoyer des actifs directement d’un réseau à un autre, une perte permanente des actifs pourrait en résulter. Assurez-vous d’utiliser une passerelle." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Passerelle, ne pas envoyer" }, + "bridgeFrom": { + "message": "Passerelle depuis" + }, + "bridgeSelectNetwork": { + "message": "Sélectionner un réseau" + }, + "bridgeTo": { + "message": "Passerelle vers" + }, "browserNotSupported": { "message": "Votre navigateur internet n’est pas compatible..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Confirmer la phrase secrète de récupération" }, + "confirmTitleApproveTransaction": { + "message": "Demande de provision" + }, + "confirmTitleDeployContract": { + "message": "Déployer un contrat" + }, + "confirmTitleDescApproveTransaction": { + "message": "Ce site demande l’autorisation de retirer vos NFT" + }, + "confirmTitleDescDeployContract": { + "message": "Ce site veut que vous déployiez un contrat" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Ce site demande l’autorisation de retirer vos jetons" + }, "confirmTitleDescPermitSignature": { "message": "Ce site demande que vous lui accordiez l'autorisation de dépenser vos jetons." }, "confirmTitleDescSIWESignature": { "message": "Un site vous demande de vous connecter pour prouver que vous êtes le titulaire de ce compte." }, + "confirmTitleDescSign": { + "message": "Vérifiez les détails de la demande avant de confirmer." + }, "confirmTitlePermitTokens": { "message": "Demande de plafonnement des dépenses" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Retirer l'autorisation" + }, "confirmTitleSIWESignature": { "message": "Demande de connexion" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Retirer l'autorisation" + }, "confirmTitleSignature": { "message": "Demande de signature" }, "confirmTitleTransaction": { "message": "Demande de transaction" }, + "confirmationAlertModalDetails": { + "message": "Pour protéger vos actifs et vos informations de connexion, nous vous suggérons de rejeter la demande." + }, + "confirmationAlertModalTitle": { + "message": "Cette demande est suspecte" + }, "confirmed": { "message": "Confirmé" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "Nous avons détecté un caractère pouvant prêter à confusion dans le nom de l’ENS. Vérifiez le nom de l’ENS pour éviter toute fraude potentielle." }, + "congratulations": { + "message": "Félicitations !" + }, "connect": { "message": "Connecter" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Sites connectés" }, + "connectedSitesAndSnaps": { + "message": "Sites et Snaps connectés" + }, "connectedSitesDescription": { "message": "$1 est connecté à ces sites. Ils peuvent voir l’adresse de votre compte.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask est connecté à ce site, mais aucun compte n’est encore connecté" }, + "connectedSnaps": { + "message": "Snaps connectés" + }, + "connectedWithAccount": { + "message": "$1 comptes connectés", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Connecté avec $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Connexion…" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Connexion au réseau de test Sepolia" }, + "connectionDescription": { + "message": "Ce site veut" + }, "connectionFailed": { "message": "Échec de la connexion" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Copier l’addresse dans le presse-papier" }, + "copyAddressShort": { + "message": "Copier l’adresse" + }, "copyPrivateKey": { "message": "Copier la clé privée" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "URL par défaut du RPC" }, + "defaultSettingsSubTitle": { + "message": "MetaMask utilise des paramètres par défaut pour établir un équilibre entre sécurité et facilité d’utilisation. Modifiez ces paramètres pour renforcer les mesures de protection de la confidentialité." + }, + "defaultSettingsTitle": { + "message": "Paramètres de confidentialité par défaut" + }, "delete": { "message": "Supprimer" }, "deleteContact": { "message": "Supprimer le contact" }, + "deleteMetaMetricsData": { + "message": "Supprimer les données MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Cela supprimera les données historiques de MetaMetrics associées à votre utilisation de cet appareil. Aucun changement ne sera apporté à votre portefeuille et à vos comptes après la suppression de ces données. Ce processus peut prendre jusqu’à 30 jours. Veuillez consulter notre $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Cette demande ne peut être traitée pour l’instant, car un problème est survenu avec le serveur du système d’analyse, veuillez réessayer plus tard" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Pour le moment, il nous est impossible de supprimer ces données" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Nous sommes sur le point de supprimer toutes vos données MetaMetrics. Êtes-vous sûr(e) de vouloir continuer ?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Voulez-vous supprimer les données MetaMetrics ?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Vous avez initié cette action le $1. Ce processus peut prendre jusqu’à 30 jours. Veuillez consulter notre $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Si vous supprimez ce réseau, vous devrez l’ajouter à nouveau pour voir vos actifs sur ce réseau" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Effectuez un dépôt" }, + "depositCrypto": { + "message": "Déposez des crypto-monnaies à partir d’un autre compte à l’aide d’une adresse de portefeuille ou d’un code QR." + }, "deprecatedGoerliNtwrkMsg": { "message": "Les mises à jour du système Ethereum rendront bientôt le testnet Goerli obsolète." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "comptes" }, + "disconnectAllDescriptionText": { + "message": "Si vous vous déconnectez de ce site, vous devrez reconnecter vos comptes et réseaux pour pouvoir l’utiliser à nouveau." + }, "disconnectAllSnapsText": { "message": "Snaps" }, + "disconnectMessage": { + "message": "Cela vous déconnectera de ce site" + }, "disconnectPrompt": { "message": "Déconnecter $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Modifier le pseudo" }, + "editAccounts": { + "message": "Modifier les comptes" + }, "editAddressNickname": { "message": "Modifier le pseudo de l’adresse" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "modifier le réseau d’origine" }, + "editNetworksTitle": { + "message": "Modifier les réseaux" + }, "editNonceField": { "message": "Modifier le nonce" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Modifier l’autorisation" }, + "editPermissions": { + "message": "Modifier les autorisations" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Modifier les gas fees d’accélération" }, + "editSpendingCap": { + "message": "Modifier le plafond des dépenses" + }, + "editSpendingCapAccountBalance": { + "message": "Solde du compte : $1 $2" + }, + "editSpendingCapDesc": { + "message": "Indiquez le montant maximum qui pourra être dépensé en votre nom." + }, + "editSpendingCapError": { + "message": "Le plafond des dépenses ne peut contenir plus de $1 chiffres décimaux. Supprimez les chiffres décimaux excédentaires pour continuer." + }, "enableAutoDetect": { "message": " Activer la détection automatique" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "La recherche d’ENS a échoué." }, + "enterANameToIdentifyTheUrl": { + "message": "Saisissez un nom pour identifier l’URL" + }, "enterANumber": { "message": "Saisissez un nombre" }, + "enterChainId": { + "message": "Saisissez l’ID de chaîne" + }, "enterCustodianToken": { "message": "Saisissez votre jeton de $1 ou ajoutez un nouveau jeton" }, "enterMaxSpendLimit": { "message": "Saisissez la limite de dépenses maximale" }, + "enterNetworkName": { + "message": "Saisissez le nom du réseau" + }, "enterOptionalPassword": { "message": "Entrez le mot de passe facultatif" }, "enterPasswordContinue": { "message": "Entrez votre mot de passe pour continuer" }, + "enterRpcUrl": { + "message": "Saisissez l’URL du RPC" + }, + "enterSymbol": { + "message": "Saisissez le symbole" + }, "enterTokenNameOrAddress": { "message": "Saisissez le nom du jeton ou copiez-collez l’adresse" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Expérimental" }, + "exportYourData": { + "message": "Exportez vos données" + }, + "exportYourDataButton": { + "message": "Télécharger" + }, + "exportYourDataDescription": { + "message": "Vous pouvez exporter des données telles que vos contacts et vos préférences." + }, "extendWalletWithSnaps": { "message": "Explorez les Snaps créés par la communauté pour personnaliser votre expérience web3", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Ce prix de carburant a été suggéré par $1. Si vous n’en tenez pas compte, vous risquez de rencontrer des difficultés lors de votre transaction. Veuillez contacter $1 pour toute question.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Frais de gaz" + }, "gasIsETH": { "message": "Le gaz est $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Quelque chose a mal tourné…" }, + "generalDescription": { + "message": "Synchronisez les paramètres entre différents appareils, sélectionnez les préférences réseau et suivez les données relatives aux jetons" + }, "genericExplorerView": { "message": "Voir le compte sur $1" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "Si vous ne pouvez plus accéder à votre compte ou si vous changez d’appareil, vous perdrez vos fonds. Veillez à sauvegarder votre phrase secrète de récupération dans $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Ignorer tout" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "dans vos paramètres" }, + "included": { + "message": "inclus" + }, "infuraBlockedNotification": { "message": "MetaMask ne peut pas se connecter à l’hôte de la blockchain. Vérifiez les éventuelles raisons $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "Fichier JSON", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Conservez votre phrase secrète de récupération dans un endroit sûr. Si vous la perdez, personne ne pourra vous aider à la récupérer. Pire encore, vous ne pourrez plus jamais accéder à votre portefeuille. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Nom du compte" }, @@ -2402,6 +2622,9 @@ "message": "Découvrir comment $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Découvrir comment" + }, "learnMore": { "message": "En savoir plus" }, @@ -2409,6 +2632,9 @@ "message": "Vous voulez $1 sur les frais de gaz ?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "En savoir plus sur les bonnes pratiques en matière de protection des données personnelles." + }, "learnMoreKeystone": { "message": "En savoir plus" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Associer" }, + "linkCentralizedExchanges": { + "message": "Liez votre compte Coinbase ou Binance pour transférer gratuitement des crypto-monnaies vers MetaMask." + }, "links": { "message": "Liens" }, @@ -2557,6 +2786,9 @@ "message": "Assurez-vous que personne ne regarde votre écran", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Gérer les paramètres de confidentialité par défaut" + }, "marketCap": { "message": "Capitalisation boursière" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Le bouton d’état de connexion indique si le site Web que vous visitez est connecté à votre compte actuellement sélectionné." }, + "metaMetricsIdNotAvailableError": { + "message": "Comme vous n’avez jamais adhéré à MetaMetrics, il n’y a pas de données à supprimer ici." + }, "metadataModalSourceTooltip": { "message": "$1 est hébergé sur npm et $2 est l’identifiant unique de ce Snap.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "plus" }, + "moreAccounts": { + "message": "+ $1 comptes supplémentaires", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 réseaux supplémentaires", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Vous allez ajouter ce réseau à MetaMask et donner à ce site l’autorisation de l’utiliser." + }, "multipleSnapConnectionWarning": { "message": "$1 veut utiliser $2 Snaps", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Modifier les détails du réseau" }, "nativeTokenScamWarningDescription": { - "message": "L’ID ou le nom de la chaîne associée à ce réseau n’est pas correct. De nombreux jetons populaires utilisent le nom $1, ce qui en fait une cible pour les escrocs. Vérifiez tout avant de continuer, car vous risquez de vous faire arnaquer.", + "message": "Le symbole du jeton natif ne correspond pas au symbole attendu pour le jeton natif du réseau associé à cet ID de chaîne. Vous avez saisi $1 alors que le symbole attendu pour le jeton est $2. Veuillez vérifier que vous êtes connecté à la bonne chaîne.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "autre chose", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Il pourrait s’agir d’une arnaque", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Détails du réseau" }, + "networkFee": { + "message": "Frais de réseau" + }, "networkIsBusy": { "message": "Le réseau est occupé. Les gas fees sont élevés et les estimations sont moins précises." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Options du réseau" }, + "networkPermissionToast": { + "message": "Les autorisations réseau ont été mises à jour" + }, "networkProvider": { "message": "Fournisseur de réseau" }, @@ -2865,15 +3121,26 @@ "message": "Nous ne pouvons pas nous connecter à $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Le réseau est passé à $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "URL du réseau" }, "networkURLDefinition": { "message": "L’URL utilisée pour accéder à ce réseau." }, + "networkUrlErrorWarning": { + "message": "Les pirates informatiques imitent parfois les sites en modifiant légèrement l’adresse du site. Assurez-vous que vous interagissez avec le site voulu avant de continuer. Version Punycode : $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Réseaux" }, + "networksSmallCase": { + "message": "réseaux" + }, "nevermind": { "message": "Abandonner" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Nous avons mis à jour notre politique de confidentialité" }, + "newRpcUrl": { + "message": "Nouvelle URL de RPC" + }, "newTokensImportedMessage": { "message": "Vous avez importé avec succès $1.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask n’est pas connecté à ce site" }, + "noConnectionDescription": { + "message": "Pour vous connecter à un site, trouvez et sélectionnez le bouton « connecter ». N’oubliez pas que MetaMask ne peut se connecter qu’à des sites Web3" + }, "noConversionRateAvailable": { "message": "Aucun taux de conversion disponible" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Nonce personnalisé" }, + "none": { + "message": "Aucun" + }, "notBusy": { "message": "Pas occupé" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Informations sur les autorisations" }, + "permissionFor": { + "message": "Autorisation pour" + }, + "permissionFrom": { + "message": "Autorisation de" + }, "permissionRequest": { "message": "Demande d’autorisation" }, @@ -3593,6 +3875,14 @@ "message": "Autoriser le Snap $1 à accéder à vos paramètres MetaMask pour qu’il puisse identifier votre langue préférée. Cela permet de localiser et d’afficher le contenu du Snap $1 dans votre langue.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Vous pouvez consulter des informations telles que votre langue et votre monnaie fiat préférées.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Autorisez $1 à accéder à des informations telles que votre langue et votre monnaie fiat préférées dans les paramètres de votre compte MetaMask. Cela permet à $1 d’afficher un contenu adapté à vos préférences. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Afficher un écran personnalisé", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Vous autorisez la dépenseur à dépenser ce nombre de jetons de votre compte." }, + "permittedChainToastUpdate": { + "message": "$1 a accès à $2." + }, "personalAddressDetected": { "message": "Votre adresse personnelle a été détectée. Veuillez saisir à la place l’adresse du contrat du jeton." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Recevoir" }, + "receiveCrypto": { + "message": "Recevoir des crypto-monnaies" + }, + "recipientAddressPlaceholderNew": { + "message": "Saisissez l’adresse publique (0x) ou le nom de domaine" + }, "recommendedGasLabel": { "message": "Recommandé" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Rejeté" }, + "rememberSRPIfYouLooseAccess": { + "message": "N’oubliez pas que si vous perdez votre phrase secrète de récupération, vous perdez l’accès à votre portefeuille. $1 pour sécuriser cet ensemble de mots et éviter de perdre l’accès à vos fonds." + }, + "reminderSet": { + "message": "Rappel défini !" + }, "remove": { "message": "Supprimer" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Suite à une erreur, cette demande n’a pas été vérifiée par le fournisseur de sécurité. Veuillez agir avec prudence." }, + "requestingFor": { + "message": "Demande de" + }, + "requestingForAccount": { + "message": "Demande de $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "demandes en attente d’un accusé de réception" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Révéler la phrase mnémonique" }, + "review": { + "message": "Examiner" + }, + "reviewAlert": { + "message": "Examiner l’alerte" + }, "reviewAlerts": { "message": "Examiner les alertes" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Retirer l'autorisation" }, + "revokeSimulationDetailsDesc": { + "message": "Vous révoquez l’autorisation que vous avez accordée à quelqu’un d’autre de dépenser des jetons à partir de votre compte." + }, "revokeSpendingCap": { "message": "Supprimez le plafond des dépenses pour vos $1", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Ce tiers ne pourra plus dépenser vos jetons actuels ou futurs." }, + "rpcNameOptional": { + "message": "Nom du RPC (facultatif)" + }, "rpcUrl": { "message": "Nouvelle URL de RPC" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Sécurité et confidentialité" }, + "securityDescription": { + "message": "Réduisez le risque de rejoindre des réseaux dangereux et protégez vos comptes" + }, + "securityMessageLinkForNetworks": { + "message": "arnaques et risques de piratage informatique" + }, + "securityPrivacyPath": { + "message": "Paramètres > Sécurité et confidentialité." + }, "securityProviderPoweredBy": { "message": "Service fourni par $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Voir toutes les autorisations", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Voir les détails" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Si vous ne voyez pas les comptes auxquels vous vous attendez, essayez de changer le chemin d’accès au portefeuille HD ou le réseau sélectionné." }, + "selectRpcUrl": { + "message": "Sélectionner l’URL du RPC" + }, "selectType": { "message": "Sélectionner le type" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Définir l’approbation pour tous" }, + "setApprovalForAllRedesignedTitle": { + "message": "Demande de retrait" + }, "setApprovalForAllTitle": { "message": "Approuver $1 sans limite de dépenses", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Paramètres" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Les paramètres sont optimisés pour sécuriser l’application et faciliter son utilisation. Vous pouvez modifier les paramètres à tout moment." + }, "settingsSearchMatchingNotFound": { "message": "Aucun résultat correspondant trouvé." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Afficher plus" }, + "showNativeTokenAsMainBalance": { + "message": "Afficher le solde principal en jeton natif" + }, "showNft": { "message": "Afficher le NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Se connecter avec" }, + "simulationApproveHeading": { + "message": "Retirer" + }, + "simulationDetailsApproveDesc": { + "message": "Vous donnez à quelqu’un d’autre l’autorisation de retirer des NFT de votre compte." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Vous accordez à quelqu’un d’autre l’autorisation de dépenser ce montant qui sera débité de votre compte." + }, "simulationDetailsFiatNotAvailable": { "message": "Non disponible" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Vous envoyez" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Vous révoquez l’autorisation que vous avez accordée à quelqu’un d’autre de retirer des NFT de votre compte." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Vous accordez à quelqu’un d’autre l’autorisation de retirer des NFT de votre compte." + }, "simulationDetailsTitle": { "message": "Changements estimés" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Oups ! Quelque chose a mal tourné. " }, + "sortBy": { + "message": "Trier par" + }, + "sortByAlphabetically": { + "message": "Ordre alphabétique (de A à Z)" + }, + "sortByDecliningBalance": { + "message": "Solde décroissant (du solde le plus élevé au solde le moins élevé en $1)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Source" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Dépenseur" }, + "spenderTooltipDesc": { + "message": "Il s’agit de l’adresse qui pourra retirer vos NFT." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Il s’agit de l’adresse qui pourra dépenser vos jetons en votre nom." + }, "spendingCap": { "message": "Plafond de dépenses" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Demande de fixer un plafond de dépenses pour votre $1" }, + "spendingCapTooltipDesc": { + "message": "Il s’agit du nombre de jetons auquel le dépenseur pourra accéder en votre nom." + }, "srpInputNumberOfWords": { "message": "J’ai une phrase de $1 mots", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Suggéré par $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Symbole monétaire suggéré :" + }, "suggestedTokenName": { "message": "Nom suggéré :" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Visitez notre centre d’aide" }, + "supportMultiRpcInformation": { + "message": "Nous prenons désormais en charge plusieurs RPC pour un même réseau. Votre RPC le plus récent a été sélectionné par défaut afin de résoudre les problèmes liés à des informations contradictoires." + }, "surveyConversion": { "message": "Répondez à notre sondage" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Les frais de carburant sont estimés et fluctueront selon le trafic réseau et la complexité de la transaction." }, + "swapGasFeesExplanation": { + "message": "MetaMask ne tire aucun profit des frais de gaz. Ces frais sont des estimations et peuvent changer en fonction de l’activité du réseau et de la complexité de la transaction. En savoir plus $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "ici", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "En savoir plus sur les frais de carburant" }, @@ -5186,9 +5583,19 @@ "message": "Les frais de carburant sont payés aux mineurs de cryptomonnaies qui traitent les transactions sur le réseau $1. MetaMask ne tire aucun profit des frais de carburant.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Cette cotation incorpore les frais de gaz en ajustant le nombre de jetons envoyés ou reçus. Vous pouvez recevoir des ETH dans une transaction séparée sur votre liste d’activités." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "En savoir plus sur les frais de gaz" + }, "swapHighSlippage": { "message": "Important effet de glissement" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Inclut les frais de gaz et les frais MetaMask qui s’élèvent à $1 %", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Comprend des frais MetaMask à hauteur de $1 %.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Nos conditions d’utilisation ont été mises à jour" }, + "testnets": { + "message": "Testnets" + }, "theme": { "message": "Thème" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Frais de transaction" }, + "transactionFlowNetwork": { + "message": "Réseau" + }, "transactionHistoryBaseFee": { "message": "Frais de base (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Transfert" }, + "transferCrypto": { + "message": "Transférer des crypto-monnaies" + }, "transferFrom": { "message": "Transfert depuis" }, + "transferRequest": { + "message": "Demande de transfert" + }, "trillionAbbreviation": { "message": "B", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "Mise à jour" }, + "updateEthereumChainConfirmationDescription": { + "message": "Ce site demande à mettre à jour l’URL par défaut de votre réseau. Vous pouvez modifier les paramètres par défaut et les informations du réseau à tout moment." + }, + "updateNetworkConfirmationTitle": { + "message": "Mettre à jour $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Mettez à jour vos informations ou" }, "updateRequest": { "message": "Demande de mise à jour" }, + "updatedRpcForNetworks": { + "message": "Les RPC du réseau ont été mis à jour" + }, "uploadDropFile": { "message": "Déposez votre fichier ici" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "notre guide de connexion des portefeuilles matériels" }, + "walletProtectedAndReadyToUse": { + "message": "Votre portefeuille est protégé et prêt à être utilisé. Vous trouverez votre phrase secrète de récupération dans $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Voulez-vous ajouter ce réseau ?" }, @@ -5991,6 +6424,17 @@ "message": "$1 L’autre partie au contrat peut dépenser la totalité de votre solde de jetons sans préavis et sans demander votre consentement. Protégez-vous en abaissant le plafond des dépenses.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "En activant cette option, vous pourrez surveiller des comptes Ethereum via une adresse publique ou un nom ENS. Pour nous faire part de vos commentaires sur cette fonctionnalité bêta, veuillez remplir ce $1.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Surveiller des comptes Ethereum (Bêta)" + }, + "watchOutMessage": { + "message": "Méfiez-vous de $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Faible" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Qu’est-ce que c’est ?" }, + "withdrawing": { + "message": "Retrait en cours" + }, "wrongNetworkName": { "message": "Selon nos informations, il se peut que le nom du réseau ne corresponde pas exactement à l’ID de chaîne." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Votre solde" }, + "yourBalanceIsAggregated": { + "message": "Votre solde est agrégé" + }, "yourNFTmayBeAtRisk": { "message": "Il peut y avoir des risques associés à votre NFT" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Nous n'avons pas pu annuler votre transaction avant qu'elle ne soit confirmée sur la blockchain." }, + "yourWalletIsReady": { + "message": "Votre portefeuille est prêt" + }, "zeroGasPriceOnSpeedUpError": { "message": "Prix de carburant zéro sur l’accélération" } diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 9f3800ef6b61..f5484e624348 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "अपने QR hardware wallet को कनेक्ट करें" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave ज़ीरो" + }, "SIWEAddressInvalid": { "message": "साइन-इन रिक्वेस्ट का एड्रेस उस अकाउंट के एड्रेस से मेल नहीं खाता जिसका इस्तेमाल आप साइन इन करने के लिए कर रहे हैं।" }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "वे अकाउंट चुनें जिनके बारे में आप सूचित होना चाहते हैं:" }, + "accountBalance": { + "message": "अकाउंट बैलेंस" + }, "accountDetails": { "message": "अकाउंट की जानकारी" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "अकाउंट विकल्प" }, + "accountPermissionToast": { + "message": "अकाउंट अनुमतियां अपडेट की गईं" + }, "accountSelectionRequired": { "message": "आपको एक अकाउंट चुनना होगा!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "अकाउंट्स कनेक्ट किए गए।" }, + "accountsPermissionsTitle": { + "message": "अपने अकाउंट्स देखें और ट्रांसेक्शन के सुझाव दें" + }, + "accountsSmallCase": { + "message": "अकाउंट" + }, "active": { "message": "एक्टिव है" }, @@ -180,12 +195,18 @@ "add": { "message": "जोड़ें" }, + "addACustomNetwork": { + "message": "एक कस्टम नेटवर्क जोड़ें" + }, "addANetwork": { "message": "एक नेटवर्क जोड़ें" }, "addANickname": { "message": "एक उपनाम जोड़ें" }, + "addAUrl": { + "message": "एक URL जोड़ें" + }, "addAccount": { "message": "अकाउंट जोड़ें" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "ब्लॉक एक्सप्लोरर जोड़ें" }, + "addBlockExplorerUrl": { + "message": "एक ब्लॉक एक्सप्लोरर URL जोड़ें" + }, "addContact": { "message": "कॉन्टेक्ट जोड़ें" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "आप Ethereum Mainnet के लिए एक नया RPC प्रोवाइडर जोड़ रहे हैं" }, + "addEthereumWatchOnlyAccount": { + "message": "कोई Ethereum अकाउंट देखें (Beta)" + }, "addFriendsAndAddresses": { "message": "उन मित्रों और पतों को जोड़ें, जिन पर आप भरोसा करते हैं" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "नेटवर्क जोड़ें" }, + "addNetworkConfirmationTitle": { + "message": "$1 जोड़ें", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "एक नया Ethereum अकाउंट जोड़ें" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "एड्रेस कॉपी किया गया!" }, + "addressMismatch": { + "message": "साइट एड्रेस का मैच न होना" + }, + "addressMismatchOriginal": { + "message": "वर्तमान URL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "पुनीकोड ​​वर्शन: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "एडवांस्ड" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "प्रायोरिटी फ़ीस (उर्फ \"माइनर टिप\") सीधे माइनरों के पास जाती है और उन्हें आपके ट्रांसेक्शन को प्राथमिकता देने के लिए प्रोत्साहित करती है।" }, + "aggregatedBalancePopover": { + "message": "यह किसी दिए गए नेटवर्क पर आपके स्वामित्व वाले सभी टोकन के मूल्य को दर्शाता है। यदि आप इस मूल्य को ETH या अन्य मुद्राओं में देखना पसंद करते हैं, तो $1 पर जाएं।", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "मैं MetaMask के $1 से सहमत हूं", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "इसे \"सेटिंग > अलर्ट\" में बदला जा सकता है" }, + "alertMessageAddressMismatchWarning": { + "message": "हमला करने वाले कभी-कभी साइट के एड्रेस में छोटे-छोटे बदलाव करके साइटों की नकल करते हैं। जारी रखने से पहले सुनिश्चित करें कि आप इच्छित साइट के साथ इंटरैक्ट कर रहे हैं।" + }, "alertMessageGasEstimateFailed": { "message": "हम सटीक शुल्क प्रदान करने में असमर्थ हैं और यह अनुमान अधिक हो सकता है। हम आपको एक कस्टम गैस लिमिट दर्ज करने का सुझाव देते हैं, लेकिन जोखिम है कि ट्रांसेक्शन अभी भी विफल हो जाएगा।" }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "इस ट्रांसेक्शन को जारी रखने के लिए, आपको गैस लिमिट को 21000 या अधिक तक बढ़ाना होगा।" }, + "alertMessageInsufficientBalance2": { + "message": "नेटवर्क फीस का भुगतान करने के लिए आपके अकाउंट में पर्याप्त ETH नहीं है।" + }, "alertMessageNetworkBusy": { "message": "गैस प्राइसें अधिक हैं और अनुमान कम सटीक हैं।" }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "एसेट के विकल्प" }, + "assets": { + "message": "एसेट्स" + }, + "assetsDescription": { + "message": "अपने वॉलेट में टोकन ऑटोडिटेक्ट करें, NFT प्रदर्शित करें, और बैच अकाउंट बैलेंस संबंधी अपडेट प्राप्त करें" + }, "attemptSendingAssets": { "message": "अगर आप एसेट्स को सीधे एक नेटवर्क से दूसरे नेटवर्क पर भेजने की कोशिश करते हैं, तो ऐसा करने से आपको एसेट्स का नुकसान हो सकता है। ब्रिज का इस्तेमाल करके नेटवर्कों के बीच फंड्स को सुरक्षित तरीके से ट्रांसफ़र करें।" }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "ब्रिज, न भेजें" }, + "bridgeFrom": { + "message": "इससे ब्रिज करें" + }, + "bridgeSelectNetwork": { + "message": "नेटवर्क को चुनें" + }, + "bridgeTo": { + "message": "इसपर ब्रिज करें" + }, "browserNotSupported": { "message": "आपका ब्राउज़र सपोर्टेड नहीं है..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "सीक्रेट रिकवरी फ्रेज कन्फर्म करें" }, + "confirmTitleApproveTransaction": { + "message": "भत्ता का अनुरोध" + }, + "confirmTitleDeployContract": { + "message": "एक कॉन्ट्रैक्ट करें" + }, + "confirmTitleDescApproveTransaction": { + "message": "यह साइट आपके NFTs को निकालने की अनुमति चाहती है" + }, + "confirmTitleDescDeployContract": { + "message": "यह साइट चाहती है कि आप एक कॉन्ट्रैक्ट करें" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "यह साइट आपके टोकन को निकालने की अनुमति चाहती है" + }, "confirmTitleDescPermitSignature": { "message": "यह साइट आपके टोकन खर्च करने की अनुमति चाहती है।" }, "confirmTitleDescSIWESignature": { "message": "एक साइट चाहती है कि आप यह साबित करने के लिए साइन इन करें कि यह आपका अकाउंट है।" }, + "confirmTitleDescSign": { + "message": "कन्फर्म करने से पहले अनुरोध के विवरण की समीक्षा करें।" + }, "confirmTitlePermitTokens": { "message": "खर्च करने की सीमा का अनुरोध" }, + "confirmTitleRevokeApproveTransaction": { + "message": "अनुमति हटाएं" + }, "confirmTitleSIWESignature": { "message": "साइन-इन अनुरोध" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "अनुमति हटाएं" + }, "confirmTitleSignature": { "message": "सिग्नेचर अनुरोध" }, "confirmTitleTransaction": { "message": "ट्रांसेक्शन अनुरोध" }, + "confirmationAlertModalDetails": { + "message": "आपकी एसेट्स और लॉगिन जानकारी की सुरक्षा के लिए, हम आपको अनुरोध को रिजेक्ट करने का सुझाव देते हैं।" + }, + "confirmationAlertModalTitle": { + "message": "यह अनुरोध संदिग्ध है" + }, "confirmed": { "message": "कन्फर्म किया गया" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "हमने ENS नाम में एक कंफ्यूज़ करने वाले करैक्टर का पता लगाया है। संभावित धोखाधड़ी से बचने के लिए ENS नाम की जाँच करें।" }, + "congratulations": { + "message": "बधाइयां!" + }, "connect": { "message": "कनेक्ट करें" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "कनेक्ट की गई साइटें" }, + "connectedSitesAndSnaps": { + "message": "कनेक्टेड साइटें और Snaps" + }, "connectedSitesDescription": { "message": "$1 इन साइटों से कनेक्ट है। वे आपके अकाउंट का एड्रेस देख सकते हैं।", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask इस साइट से कनेक्टेड है, लेकिन अभी तक कोई अकाउंट कनेक्ट नहीं किया गया है" }, + "connectedSnaps": { + "message": "कनेक्टेड Snaps" + }, + "connectedWithAccount": { + "message": "$1 अकाउंट्स कनेक्ट किए गए", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "$1 से कनेक्ट किए गए", + "description": "$1 represents account name" + }, "connecting": { "message": "कनेक्ट किया जा रहा है" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Sepolia टेस्ट नेटवर्क से कनेक्ट कर रहा है" }, + "connectionDescription": { + "message": "यह साइट निम्नलिखित करना चाहती है:" + }, "connectionFailed": { "message": "कनेक्शन नहीं हो पाया" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "क्लिपबोर्ड पर एड्रेस कॉपी करें" }, + "copyAddressShort": { + "message": "एड्रेस कॉपी करें" + }, "copyPrivateKey": { "message": "प्राइवेट की (key) को कॉपी करें" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "डिफॉल्ट RPC URL" }, + "defaultSettingsSubTitle": { + "message": "MetaMask सुरक्षा और उपयोग में आसानी को संतुलित करने के लिए डिफॉल्ट सेटिंग्स का उपयोग करता है। अपनी गोपनीयता को और बढ़ाने के लिए इन सेटिंग्स को बदलें।" + }, + "defaultSettingsTitle": { + "message": "डिफॉल्ट गोपनीयता सेटिंग्स" + }, "delete": { "message": "मिटाएं" }, "deleteContact": { "message": "कॉन्टेक्ट मिटाएं" }, + "deleteMetaMetricsData": { + "message": "MetaMetrics डेटा डिलीट करें" + }, + "deleteMetaMetricsDataDescription": { + "message": "यह इस डिवाइस पर आपके उपयोग से जुड़ा ऐतिहासिक MetaMetrics डेटा डिलीट कर देगा। इस डेटा को डिलीट किए जाने के बाद आपका वॉलेट और एकाउंट्स बिल्कुल वैसे ही रहेंगे जैसे वे अभी हैं। इस प्रक्रिया में 30 दिन तक का समय लग सकता है। हमारी $1 देखें।", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "एनालिटिक्स सिस्टम सर्वर की समस्या के कारण यह अनुरोध अभी पूरा नहीं किया जा सकता है, कृपया बाद में फिर से प्रयास करें" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "हम अभी इस डेटा को डिलीट करने में असमर्थ हैं" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "हम आपके सभी MetaMetrics डेटा को हटाने वाले हैं। क्या आप वाकई ऐसा चाहते हैं?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "MetaMetrics डेटा डिलीट करें?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "आपने यह कार्रवाई $1 पर शुरू की। इस प्रक्रिया में 30 दिन तक का समय लग सकता है। $2 देखें", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "अगर आप इस नेटवर्क को हटाते हैं, तो आपको इस नेटवर्क में अपने एसेट देखने के लिए इसे फिर से जोड़ना होगा" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "डिपॉज़िट करें" }, + "depositCrypto": { + "message": "वॉलेट एड्रेस या QR कोड के साथ किसी अन्य अकाउंट से क्रिप्टो डिपॉज़िट करना।" + }, "deprecatedGoerliNtwrkMsg": { "message": "Ethereum सिस्टम में हुए अपडेट के कारण, Goerli टेस्ट नेटवर्क को जल्द ही चरणबद्ध तरीके से हटा दिया जाएगा।" }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "अकाउंट्स" }, + "disconnectAllDescriptionText": { + "message": "यदि आप इस साइट से डिस्कनेक्ट हो जाते हैं, तो आपको इस साइट का दोबारा उपयोग करने के लिए अपने एकाउंट्स और नेटवर्क को फिर से कनेक्ट करना होगा।" + }, "disconnectAllSnapsText": { "message": "Snaps" }, + "disconnectMessage": { + "message": "यह आपको इस साइट से डिस्कनेक्ट कर देगा" + }, "disconnectPrompt": { "message": "$1 डिस्कनेक्ट करें" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "उपनाम बदलें" }, + "editAccounts": { + "message": "एकाउंट्स बदलें" + }, "editAddressNickname": { "message": "एड्रेस उपनाम बदलें" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "मूल नेटवर्क को संपादित करें" }, + "editNetworksTitle": { + "message": "नेटवर्क बदलें" + }, "editNonceField": { "message": "Nonce बदलें" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "अनुमति बदलें" }, + "editPermissions": { + "message": "अनुमतियां बदलें" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "गैस फ़ीस स्पीड अप को बदलें" }, + "editSpendingCap": { + "message": "खर्च करने की लिमिट बदलें" + }, + "editSpendingCapAccountBalance": { + "message": "अकाउंट बैलेंस: $1 $2" + }, + "editSpendingCapDesc": { + "message": "वह राशि डालें जिसे आप अपनी ओर से खर्च किए जाने में सहज महसूस करते हैं।" + }, + "editSpendingCapError": { + "message": "खर्च करने की सीमा $1 दशमलव अंक से अधिक नहीं हो सकती। जारी रखने के लिए दशमलव अंक हटाएं।" + }, "enableAutoDetect": { "message": " ऑटो डिटेक्ट इनेबल करें" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "ENS लुकअप नहीं हो पाया।" }, + "enterANameToIdentifyTheUrl": { + "message": "URL की पहचान करने के लिए नाम डालें" + }, "enterANumber": { "message": "कोई संख्या डालें" }, + "enterChainId": { + "message": "चेन ID डालें" + }, "enterCustodianToken": { "message": "अपना $1 टोकन डालें या एक नया टोकन जोड़ें" }, "enterMaxSpendLimit": { "message": "अधिकतम खर्च की लिमिट डालें" }, + "enterNetworkName": { + "message": "नेटवर्क का नाम डालें" + }, "enterOptionalPassword": { "message": "वैकल्पिक पासवर्ड डालें" }, "enterPasswordContinue": { "message": "जारी रखने के लिए पासवर्ड डालें" }, + "enterRpcUrl": { + "message": "RPC URL डालें" + }, + "enterSymbol": { + "message": "सिंबल डालें" + }, "enterTokenNameOrAddress": { "message": "टोकन नाम डालें या एड्रेस पेस्ट करें" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "एक्सपेरिमेंटल" }, + "exportYourData": { + "message": "अपना डेटा एक्सपोर्ट करें" + }, + "exportYourDataButton": { + "message": "डाउनलोड करें" + }, + "exportYourDataDescription": { + "message": "आप अपने डेटा, जैसे कि अपने कॉन्टेक्ट्स और अपनी प्रेफेरेंस को एक्सपोर्ट कर सकते हैं।" + }, "extendWalletWithSnaps": { "message": "अपने Web3 एक्सपीरियंस को कस्टमाइज़ करने के लिए कम्युनिटी-बिल्ट Snaps को एक्सप्लोर करें।", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "यह गैस फ़ीस $1 द्वारा सुझाया गया है। इसे ओवरराइड करने से आपके ट्रांसेक्शन में समस्या हो सकती है। यदि आपके पास कोई सवाल हैं तो कृपया $1 से इंटरैक्ट करें।", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "गैस फीस" + }, "gasIsETH": { "message": "गैस $1 है " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "कुछ गलत हो गया..." }, + "generalDescription": { + "message": "सभी डिवाइसों में सेटिंग्स सिंक करें, नेटवर्क प्राथमिकताएं चुनें और टोकन डेटा ट्रैक करें" + }, "genericExplorerView": { "message": "$1 पर अकाउंट देखें" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "यदि आप ऐप से बाहर हो जाते हैं या कोई नया डिवाइस प्राप्त करते हैं, तो आप अपना फंड खो देंगे। $1 में अपना सीक्रेट रिकवरी फ्रेज़ का बैकअप लेना सुनिश्चित करें ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "सभी को अनदेखा करें" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "आपके सेटिंग्स में" }, + "included": { + "message": "शामिल है" + }, "infuraBlockedNotification": { "message": "MetaMask ब्लॉकचेन होस्ट से कनेक्ट करने में असमर्थ है। संभावित कारणों की समीक्षा करें $1।", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "JSON फाइल", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "अपने सीक्रेट रिकवरी फ्रेज़ का रिमाइंडर कहीं सुरक्षित रखें। यदि आप इसे खो देते हैं, तो इसे वापस पाने में कोई आपकी मदद नहीं कर सकता। इससे भी बुरी बात यह है कि आप कभी भी अपने वॉलेट तक पहुंच नहीं पाएंगे। $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "अकाउंट का नाम" }, @@ -2402,6 +2622,9 @@ "message": "$1 करने का तरीका जानें", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "जानें कैसे" + }, "learnMore": { "message": "अधिक जानें" }, @@ -2409,6 +2632,9 @@ "message": "गैस के बारे में $1 चाहते हैं?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "गोपनीयता की सर्वोत्तम प्रथाओं के बारे में और जानें।" + }, "learnMoreKeystone": { "message": "और अधिक जानें" }, @@ -2497,6 +2723,9 @@ "link": { "message": "लिंक" }, + "linkCentralizedExchanges": { + "message": "क्रिप्टो को मुफ्त में MetaMask में ट्रांसफ़र करने के लिए अपने Coinbase या Binance अकाउंट को लिंक करें।" + }, "links": { "message": "लिंक" }, @@ -2557,6 +2786,9 @@ "message": "पक्का करें कि इसे कोई भी नहीं देख रहा है", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "डिफॉल्ट गोपनीयता सेटिंग्स मैनेज करें" + }, "marketCap": { "message": "मार्केट कैप" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "कनेक्शन स्थिति बटन दिखाता है कि आप जिस वेबसाइट पर जा रहे हैं, वह आपके वर्तमान में चुना गया अकाउंट से कनेक्ट है।" }, + "metaMetricsIdNotAvailableError": { + "message": "चूंकि आपने कभी भी MetaMetrics का विकल्प नहीं चुना है, इसलिए यहां डिलीट करने के लिए कोई डेटा नहीं है।" + }, "metadataModalSourceTooltip": { "message": "$1 को npm पर होस्ट किया गया है और $2 इस Snap का यूनीक आइडेंटिफायर है।", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "अधिक" }, + "moreAccounts": { + "message": "+ $1 और अकाउंट", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 और नेटवर्क", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "आप इस नेटवर्क को MetaMask में जोड़ रहे हैं और इस साइट को इसका उपयोग करने की अनुमति दे रहे हैं।" + }, "multipleSnapConnectionWarning": { "message": "$1 $2 Snaps का उपयोग करना चाहता है", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "नेटवर्क का ब्यौरा बदलें" }, "nativeTokenScamWarningDescription": { - "message": "यह नेटवर्क अपनी एसोसिएटेड चेन ID या नाम से मेल नहीं खाता है। कई लोकप्रिय टोकन $1 नाम का उपयोग करते हैं, जिससे इसमें स्कैम किया जा सकता है। स्कैम करने वाले आपको बदले में ज़्यादा कीमती करेंसी भेजने का झांसा दे सकते हैं। आगे बढ़ने से पहले सब कुछ वेरीफाई करें।", + "message": "मूल टोकन सिंबल, संबंधित चेन ID वाले नेटवर्क के लिए मूल टोकन के अपेक्षित सिंबल से मेल नहीं खाता। आपने $1 डाला है जबकि अपेक्षित टोकन सिंबल $2 है। कृपया वेरीफाई करें कि आप सही चेन से जुड़े हैं।", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "कुछ और", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "यह एक बड़ा स्कैम हो सकता है", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "नेटवर्क विवरण" }, + "networkFee": { + "message": "नेटवर्क फी" + }, "networkIsBusy": { "message": "नेटवर्क व्यस्त है। गैस प्राइसें अधिक हैं और अनुमान कम सटीक हैं।" }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "नेटवर्क के विकल्प" }, + "networkPermissionToast": { + "message": "नेटवर्क अनुमतियां अपडेट की गईं" + }, "networkProvider": { "message": "नेटवर्क प्रोवाइडर" }, @@ -2865,15 +3121,26 @@ "message": "हम $1 से कनेक्ट नहीं कर सकते", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "नेटवर्क $1 पर स्विच किया गया", + "description": "$1 represents the network name" + }, "networkURL": { "message": "नेटवर्क URL" }, "networkURLDefinition": { "message": "इस नेटवर्क तक पहुंचने के लिए इस्तेमाल किया जाने वाला URL।" }, + "networkUrlErrorWarning": { + "message": "हमला करने वाले कभी-कभी साइट के एड्रेस में छोटे-छोटे बदलाव करके साइटों की नकल करते हैं। जारी रखने से पहले सुनिश्चित करें कि आप इच्छित साइट के साथ इंटरैक्ट कर रहे हैं। पुनीकोड ​​वर्शन: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "नेटवर्क" }, + "networksSmallCase": { + "message": "नेटवर्क" + }, "nevermind": { "message": "कोई बात नहीं" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "हमने अपनी गोपनीयता नीति अपडेट कर दी है" }, + "newRpcUrl": { + "message": "नया RPC URL" + }, "newTokensImportedMessage": { "message": "आपने सफलतापूर्वक $1 इम्पोर्ट कर लिया है।", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask इस साइट से कनेक्टेड नहीं है।" }, + "noConnectionDescription": { + "message": "किसी साइट से कनेक्ट करने के लिए, \"कनेक्ट करें\" बटन ढूंढें और चुनें। याद रखें MetaMask केवल web3 पर साइटों से कनेक्ट हो सकता है" + }, "noConversionRateAvailable": { "message": "कोई भी कन्वर्शन दर उपलब्ध नहीं है" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "कस्टम Nonce" }, + "none": { + "message": "कुछ नहीं" + }, "notBusy": { "message": "व्यस्त नहीं" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "अनुमति का ब्यौरा" }, + "permissionFor": { + "message": "के लिए अनुमतियां" + }, + "permissionFrom": { + "message": "से अनुमतियां" + }, "permissionRequest": { "message": "अनुमति अनुरोध" }, @@ -3593,6 +3875,14 @@ "message": "$1 को आपकी MetaMask सेटिंग्स से आपकी पसंदीदा भाषा ऐक्सेस करने दें। इसे आपकी भाषा का उपयोग करके $1 के कंटेंट का स्थानीय भाषा में अनुवाद करने और उसे दिखाने के लिए इस्तेमाल किया जा सकता है।", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "अपनी पसंदीदा भाषा और फिएट करेंसी जैसी जानकारी देखें।", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "$1 को अपनी MetaMask सेटिंग्स में अपनी पसंदीदा भाषा और फिएट करेंसी जैसी जानकारी तक पहुंचने दें। यह $1 को आपकी प्राथमिकताओं के अनुरूप सामग्री प्रदर्शित करने में मदद करता है। ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "एक कस्टम स्क्रीन दिखाएं", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "आप खर्च करने वाले को अपने अकाउंट से इतने सारे टोकन खर्च करने की अनुमति दे रहे हैं।" }, + "permittedChainToastUpdate": { + "message": "$1 की पहुंच $2 तक है।" + }, "personalAddressDetected": { "message": "व्यक्तिगत एड्रेस का एड्रेस चला। टोकन कॉन्ट्रैक्ट एड्रेस डालें।" }, @@ -3963,6 +4256,12 @@ "receive": { "message": "प्राप्त करें" }, + "receiveCrypto": { + "message": "क्रिप्टो प्राप्त करें" + }, + "recipientAddressPlaceholderNew": { + "message": "पब्लिक एड्रेस (0x) या डोमेन नाम डालें" + }, "recommendedGasLabel": { "message": "अनुशंसित" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "रिजेक्ट" }, + "rememberSRPIfYouLooseAccess": { + "message": "याद रखें, यदि आप अपना सीक्रेट रिकवरी फ्रेज़ खो देते हैं, तो आप अपने वॉलेट तक पहुंच खो देते हैं। शब्दों के इस सेट को सुरक्षित रखने के लिए $1 ताकि आप हमेशा अपने फंड तक पहुंच सकें।" + }, + "reminderSet": { + "message": "रिमाइंडर सेट किया गया!" + }, "remove": { "message": "हटाएं" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "किसी गड़बड़ी के कारण, इस रिक्वेस्ट को सुरक्षा प्रोवाइडर द्वारा वेरीफ़ाई नहीं किया गया था। सावधानी के साथ आगे बढ़ना।" }, + "requestingFor": { + "message": "के लिए अनुरोध कर रहे हैं" + }, + "requestingForAccount": { + "message": "$1 के लिए अनुरोध कर रहे हैं", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "रिक्वेस्ट्स के स्वीकार किए जाने की प्रतीक्षा की जा रही है" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "सीड फ़्रेज़ दिखाएं" }, + "review": { + "message": "समीक्षा करें" + }, + "reviewAlert": { + "message": "एलर्ट की समीक्षा करें" + }, "reviewAlerts": { "message": "एलर्ट की समीक्षा करें" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "अनुमति कैंसिल करने की" }, + "revokeSimulationDetailsDesc": { + "message": "आप किसी अन्य को आपके अकाउंट से टोकन ख़र्च करने के लिए दी गई अनुमति हटा रहे हैं।" + }, "revokeSpendingCap": { "message": "अपने $1 के लिए खर्च करने की लिमिट को हटा दें", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "यह थर्ड पार्टी आपके वर्तमान या भविष्य के और ज्यादा टोकन खर्च करने में असमर्थ होगा।" }, + "rpcNameOptional": { + "message": "RPC का नाम (वैकल्पिक)" + }, "rpcUrl": { "message": "नया RPC URL" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "सुरक्षा और गोपनीयता" }, + "securityDescription": { + "message": "असुरक्षित नेटवर्क में शामिल होने की संभावना कम करें और अपने एकाउंट्स की सुरक्षा करें" + }, + "securityMessageLinkForNetworks": { + "message": "नेटवर्क संबंधी स्कैम और सुरक्षा जोखिम" + }, + "securityPrivacyPath": { + "message": "सेटिंग्स > सुरक्षा और गोपनीयता।" + }, "securityProviderPoweredBy": { "message": "$1 द्वारा पावर्ड", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "सभी अनुमतियां देखें", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "ब्यौरा देखें" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "यदि आपको अपनी पसंद के हिसाब से अकाउंट दिखाई नहीं देते हैं, तो HD पाथ या मौजूदा चुना गया नेटवर्क बदलने की कोशिश करें।" }, + "selectRpcUrl": { + "message": "RPC URL को चुनें" + }, "selectType": { "message": "प्रकार को चुनें" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "सभी के लिए स्वीकृति सेट करें" }, + "setApprovalForAllRedesignedTitle": { + "message": "विदड्रॉवल का अनुरोध" + }, "setApprovalForAllTitle": { "message": "बिना किसी खर्च की लिमिट के $1 एप्रूव करें", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "सेटिंग" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "उपयोग में आसानी और सुरक्षा के लिए सेटिंग्स को अनुकूलित किया गया है। इन्हें किसी भी समय बदलें।" + }, "settingsSearchMatchingNotFound": { "message": "कोई मेल खाने वाला परिणाम नहीं मिला।" }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "अधिक दिखाएं" }, + "showNativeTokenAsMainBalance": { + "message": "मूल टोकन को मुख्य बैलेंस के रूप में दिखाएं" + }, "showNft": { "message": "NFT दिखाएं" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "के साथ साइन इन करना" }, + "simulationApproveHeading": { + "message": "निकालें" + }, + "simulationDetailsApproveDesc": { + "message": "आप किसी अन्य को अपने अकाउंट से NFTs निकालने की अनुमति दे रहे हैं।" + }, + "simulationDetailsERC20ApproveDesc": { + "message": "आप किसी अन्य को आपके अकाउंट से यह राशि ख़र्च करने की अनुमति दे रहे हैं।" + }, "simulationDetailsFiatNotAvailable": { "message": "उपलब्ध नहीं है" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "आप भेजते हैं" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "आप किसी अन्य को आपके अकाउंट से NFTs निकालने के लिए दी गई अनुमति हटा रहे हैं।" + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "आप किसी अन्य के लिए, आपके अकाउंट से NFTs निकालने की अनुमति दे रहे हैं।" + }, "simulationDetailsTitle": { "message": "अनुमानित बदलाव" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "ओह! कुछ गलत हो गया।" }, + "sortBy": { + "message": "इसके अनुसार क्रमबद्ध करें" + }, + "sortByAlphabetically": { + "message": "वर्णानुक्रम में (A-Z)" + }, + "sortByDecliningBalance": { + "message": "घटते हुए बैलेंस के क्रम में ($1 उच्च-निम्न)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "स्त्रोत" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "खर्च करने वाला" }, + "spenderTooltipDesc": { + "message": "यही वह एड्रेस है जो आपके NFTs को निकाल पाएगा।" + }, + "spenderTooltipERC20ApproveDesc": { + "message": "यह वह एड्रेस है जो आपकी ओर से आपके टोकन खर्च करने में सक्षम होगा।" + }, "spendingCap": { "message": "खर्च करने की लिमिट" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "आपके $1 के लिए खर्च करने की लिमिट का अनुरोध" }, + "spendingCapTooltipDesc": { + "message": "यह टोकन की वह राशि है जिसे खर्च करने वाला आपकी ओर से प्राप्त कर सकेगा।" + }, "srpInputNumberOfWords": { "message": "मेरे पास एक $1-शब्द का फ़्रेज़ है", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "$1 के द्वारा सुझाव दिया गया", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "सुझाया गया करेंसी सिंबल:" + }, "suggestedTokenName": { "message": "सुझाया गया नाम:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "हमारे सहायता केंद्र पर जाएं" }, + "supportMultiRpcInformation": { + "message": "अब हम एक ही नेटवर्क के लिए कई RPC को सपोर्ट करते हैं। विरोधाभासी जानकारी को हल करने के लिए आपके सबसे हाल के RPC को डिफ़ॉल्ट के रूप में चुना गया है।" + }, "surveyConversion": { "message": "हमारे सर्वे में भाग लें" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "गैस फ़ीस का अनुमान लगाया जाता है और नेटवर्क ट्रैफिक और ट्रांसेक्शन की जटिलता के आधार पर इसमें उतार-चढ़ाव आएगा।" }, + "swapGasFeesExplanation": { + "message": "MetaMask गैस फीस से पैसे नहीं कमाता है। ये शुल्क अनुमानित हैं और इस आधार पर बदल सकते हैं कि नेटवर्क कितना व्यस्त है और ट्रांसेक्शन कितना जटिल है। अधिक जानें $1।", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "यहाँ", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "गैस फ़ीस के बारे में और अधिक जानें" }, @@ -5186,9 +5583,19 @@ "message": "क्रिप्टो माइनरों को गैस फ़ीस का पेमेंट किया जाता है जो $1 नेटवर्क पर ट्रांसेक्शन की प्रक्रिया करते हैं। MetaMask को गैस फ़ीस से लाभ नहीं होता है।", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "यह कोटेशन भेजे गए या प्राप्त टोकन राशि को एडजस्ट करके गैस फीस को शामिल करता है। आप अपनी गतिविधि सूची में एक अलग ट्रांसेक्शन में ETH प्राप्त कर सकते हैं।" + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "गैस फीस के बारे में और अधिक जानें" + }, "swapHighSlippage": { "message": "अधिक स्लिपेज" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "इसमें गैस और $1% MetaMask फीस शामिल है", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "$1% MetaMask फ़ीस शामिल है।", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "हमारी इस्तेमाल की शर्तों को अपडेट कर दिया गया है" }, + "testnets": { + "message": "Testnets" + }, "theme": { "message": "थीम" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "ट्रांसेक्शन फ़ीस" }, + "transactionFlowNetwork": { + "message": "नेटवर्क" + }, "transactionHistoryBaseFee": { "message": "बेस फ़ीस (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "ट्रांसफ़र" }, + "transferCrypto": { + "message": "क्रिप्टो ट्रांसफ़र करें" + }, "transferFrom": { "message": "इससे ट्रांसफ़र करें" }, + "transferRequest": { + "message": "अनुरोध ट्रांसफर करें" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "अपडेट करें" }, + "updateEthereumChainConfirmationDescription": { + "message": "यह साइट आपके डिफ़ॉल्ट नेटवर्क URL को अपडेट करने का अनुरोध कर रही है। आप किसी भी समय डिफ़ॉल्ट और नेटवर्क जानकारी बदल सकते हैं।" + }, + "updateNetworkConfirmationTitle": { + "message": "$1 अपडेट करें", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "अपनी जानकारी अपडेट करें या" }, "updateRequest": { "message": "अपडेट का अनुरोध" }, + "updatedRpcForNetworks": { + "message": "नेटवर्क RPC अपडेट किया गया" + }, "uploadDropFile": { "message": "अपनी फ़ाइल यहां छोड़ें" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "हमारी hardware wallet कनेक्शन गाइड" }, + "walletProtectedAndReadyToUse": { + "message": "आपका वॉलेट सुरक्षित है और उपयोग के लिए तैयार है। आप अपना सीक्रेट रिकवरी फ्रेज़ $1 में पा सकते हैं ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "इस नेटवर्क को जोड़ना चाहते हैं?" }, @@ -5991,6 +6424,17 @@ "message": "$1 थर्ड पार्टी आपकी संपूर्ण टोकन बैलेंस को बिना किसी सूचना या सहमति के खर्च कर सकता है। खर्च की कम सीमा को कस्टमाइज़ करके खुद को सुरक्षित रखें।", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "इस विकल्प को चालू करने से आपको पब्लिक एड्रेस या ENS नाम के माध्यम से Ethereum एकाउंट्स को देखने की सुविधा मिलेगी। इस Beta फीचर पर प्रतिक्रिया के लिए कृपया इस $1 को पूरा करें।", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Ethereum एकाउंट्स देखें (Beta)" + }, + "watchOutMessage": { + "message": "$1 से सावधान रहें।", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "कमज़ोर" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "यह क्या है?" }, + "withdrawing": { + "message": "निकाला जा रहा है" + }, "wrongNetworkName": { "message": "हमारे रिकॉर्ड के अनुसार, नेटवर्क का नाम इस चेन ID से ठीक से मेल नहीं खा सकता है।" }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "आपका बैलेंस" }, + "yourBalanceIsAggregated": { + "message": "आपका बैलेंस एकत्रित हो गया है" + }, "yourNFTmayBeAtRisk": { "message": "आपका NFT खतरे में हो सकता है" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "हम आपके ट्रांसेक्शन को रद्द नहीं कर सके क्योंकि ब्लॉकचेन पर यह पहले ही कन्फर्म हो चुका है।" }, + "yourWalletIsReady": { + "message": "आपका वॉलेट तैयार है" + }, "zeroGasPriceOnSpeedUpError": { "message": "जीरो गैस प्राइस में तेज़ी" } diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index b070fb14cda9..e7c719318f69 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Hubungkan dompet perangkat keras QR Anda" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "Alamat pada permintaan masuk tidak sesuai dengan alamat akun yang Anda gunakan untuk masuk." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Pilih akun yang ingin mendapatkan notifikasi:" }, + "accountBalance": { + "message": "Saldo akun" + }, "accountDetails": { "message": "Detail akun" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Opsi akun" }, + "accountPermissionToast": { + "message": "Izin akun diperbarui" + }, "accountSelectionRequired": { "message": "Anda harus memilih satu akun!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Akun terhubung" }, + "accountsPermissionsTitle": { + "message": "Melihat akun Anda dan menyarankan transaksi" + }, + "accountsSmallCase": { + "message": "akun" + }, "active": { "message": "Aktif" }, @@ -180,12 +195,18 @@ "add": { "message": "Tambah" }, + "addACustomNetwork": { + "message": "Tambahkan jaringan khusus" + }, "addANetwork": { "message": "Tambahkan jaringan" }, "addANickname": { "message": "Tambahkan nama panggilan" }, + "addAUrl": { + "message": "Tambahkan URL" + }, "addAccount": { "message": "Tambahkan akun" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Tambahkan block explorer" }, + "addBlockExplorerUrl": { + "message": "Tambahkan URL block explorer" + }, "addContact": { "message": "Tambah kontak" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Anda menambahkan penyedia RPC baru untuk Ethereum Mainnet" }, + "addEthereumWatchOnlyAccount": { + "message": "Pantau akun Ethereum (Beta)" + }, "addFriendsAndAddresses": { "message": "Tambahkan teman dan alamat yang Anda percayai" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Tambahkan jaringan" }, + "addNetworkConfirmationTitle": { + "message": "Tambahkan $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Tambahkan akun Ethereum baru" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Alamat disalin!" }, + "addressMismatch": { + "message": "Ketidakcocokan alamat situs" + }, + "addressMismatchOriginal": { + "message": "URL saat ini: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Versi Punycode: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Lanjutan" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Biaya prioritas (alias “tip penambang”) langsung masuk ke penambang dan memberi insentif kepada mereka untuk memprioritaskan transaksi Anda." }, + "aggregatedBalancePopover": { + "message": "Ini mencerminkan nilai semua token yang Anda miliki di jaringan tertentu. Jika Anda lebih suka melihat nilai ini dalam ETH atau mata uang lainnya, pilih $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Saya menyetujui $1 MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Ini dapat diubah dalam \"Pengaturan > Peringatan\"" }, + "alertMessageAddressMismatchWarning": { + "message": "Penyerang terkadang meniru situs dengan membuat perubahan kecil pada alamat situs. Pastikan Anda berinteraksi dengan situs yang dituju sebelum melanjutkan." + }, "alertMessageGasEstimateFailed": { "message": "Kami tidak dapat memberikan biaya akurat dan estimasi ini mungkin tinggi. Kami menyarankan Anda untuk memasukkan batas gas kustom, tetapi ada risiko transaksi tetap gagal." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Untuk melanjutkan transaksi ini, Anda perlu meningkatkan batas gas menjadi 21000 atau lebih tinggi." }, + "alertMessageInsufficientBalance2": { + "message": "Anda tidak memiliki cukup ETH di akun untuk membayar biaya jaringan." + }, "alertMessageNetworkBusy": { "message": "Harga gas tinggi dan estimasinya kurang akurat." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Opsi aset" }, + "assets": { + "message": "Aset" + }, + "assetsDescription": { + "message": "Autodeteksi token di dompet Anda, tampilkan NFT, dan dapatkan pembaruan saldo akun secara batch" + }, "attemptSendingAssets": { "message": "Aset Anda berpotensi hilang jika mencoba mengirimnya dari jaringan lain. Transfer dana secara aman antar jaringan menggunakan bridge." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Bridge, jangan kirim" }, + "bridgeFrom": { + "message": "Bridge dari" + }, + "bridgeSelectNetwork": { + "message": "Pilih jaringan" + }, + "bridgeTo": { + "message": "Bridge ke" + }, "browserNotSupported": { "message": "Browser Anda tidak didukung..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Konfirmasikan Frasa Pemulihan Rahasia" }, + "confirmTitleApproveTransaction": { + "message": "Permintaan izin" + }, + "confirmTitleDeployContract": { + "message": "Terapkan kontrak" + }, + "confirmTitleDescApproveTransaction": { + "message": "Situs ini meminta izin untuk menarik NFT Anda" + }, + "confirmTitleDescDeployContract": { + "message": "Situs ini meminta Anda untuk menerapkan kontrak" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Situs ini meminta izin untuk menarik token Anda" + }, "confirmTitleDescPermitSignature": { "message": "Situs ini meminta izin untuk menggunakan token Anda." }, "confirmTitleDescSIWESignature": { "message": "Sebuah situs ingin Anda masuk untuk membuktikan Anda pemilik akun ini." }, + "confirmTitleDescSign": { + "message": "Tinjau detail permintaan sebelum Anda mengonfirmasi." + }, "confirmTitlePermitTokens": { "message": "Permintaan batas penggunaan" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Hapus izin" + }, "confirmTitleSIWESignature": { "message": "Permintaan masuk" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Hapus izin" + }, "confirmTitleSignature": { "message": "Permintaan tanda tangan" }, "confirmTitleTransaction": { "message": "Permintaan transaksi" }, + "confirmationAlertModalDetails": { + "message": "Untuk melindungi aset dan informasi login Anda, sebaiknya tolak permintaan tersebut." + }, + "confirmationAlertModalTitle": { + "message": "Permintaan ini mencurigakan" + }, "confirmed": { "message": "Dikonfirmasikan" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "Kami telah mendeteksi karakter yang membingungkan di nama ENS. Periksa nama ENS untuk menghindari kemungkinan penipuan." }, + "congratulations": { + "message": "Selamat!" + }, "connect": { "message": "Hubungkan" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Situs yang terhubung" }, + "connectedSitesAndSnaps": { + "message": "Situs dan Snaps yang terhubung" + }, "connectedSitesDescription": { "message": "$1 terhubung ke situs ini. Mereka dapat melihat alamat akun Anda.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask terhubung ke situs ini, tetapi belum ada akun yang terhubung" }, + "connectedSnaps": { + "message": "Snaps yang Terhubung" + }, + "connectedWithAccount": { + "message": "$1 akun terhubung", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Terhubung dengan $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Menghubungkan" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Menghubungkan ke jaringan uji Sepolia" }, + "connectionDescription": { + "message": "Situs ini ingin" + }, "connectionFailed": { "message": "Koneksi gagal" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Salin alamat ke papan klip" }, + "copyAddressShort": { + "message": "Salin alamat" + }, "copyPrivateKey": { "message": "Salin kunci pribadi" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "URL RPC Default" }, + "defaultSettingsSubTitle": { + "message": "MetaMask menggunakan pengaturan default untuk menyeimbangkan keamanan dan kemudahan penggunaan. Ubah pengaturan ini untuk lebih meningkatkan privasi Anda." + }, + "defaultSettingsTitle": { + "message": "Pengaturan privasi default" + }, "delete": { "message": "Hapus" }, "deleteContact": { "message": "Hapus kontak" }, + "deleteMetaMetricsData": { + "message": "Hapus data MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Ini akan menghapus data MetaMetrics historis yang terkait dengan penggunaan Anda pada perangkat ini. Dompet dan akun Anda akan tetap sama seperti sekarang setelah data ini dihapus. Proses ini dapat memakan waktu hingga 30 hari. Lihat $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Permintaan ini tidak dapat diselesaikan sekarang karena masalah server sistem analitis, coba lagi nanti" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Kami tidak dapat menghapus data ini sekarang" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Kami akan menghapus semua data MetaMetrics Anda. Lanjutkan?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Hapus data MetaMetrics?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Anda memulai tindakan ini pada $1. Proses ini dapat memakan waktu hingga 30 hari. Lihat $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Jika menghapus jaringan ini, Anda harus menambahkannya lagi untuk melihat aset Anda di jaringan ini" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Deposit" }, + "depositCrypto": { + "message": "Mendeposit kripto dari akun lain dengan alamat dompet atau kode QR." + }, "deprecatedGoerliNtwrkMsg": { "message": "Karena pembaruan pada sistem Ethereum, jaringan uji Goerli akan segera dihentikan." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "akun" }, + "disconnectAllDescriptionText": { + "message": "Jika Anda memutuskan koneksi dari situs ini, Anda harus menghubungkan kembali akun dan jaringan agar dapat menggunakan situs ini lagi." + }, "disconnectAllSnapsText": { "message": "Snap" }, + "disconnectMessage": { + "message": "Ini akan memutus koneksi dari situs ini" + }, "disconnectPrompt": { "message": "Putuskan koneksi $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Edit nama panggilan" }, + "editAccounts": { + "message": "Edit akun" + }, "editAddressNickname": { "message": "Edit nama panggilan alamat" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "edit jaringan asli" }, + "editNetworksTitle": { + "message": "Edit jaringan" + }, "editNonceField": { "message": "Edit nonce" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Edit izin" }, + "editPermissions": { + "message": "Edit izin" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Edit biaya gas percepatan" }, + "editSpendingCap": { + "message": "Edit batas penggunaan" + }, + "editSpendingCapAccountBalance": { + "message": "Saldo akun: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Masukkan jumlah yang layak untuk digunakan atas nama Anda." + }, + "editSpendingCapError": { + "message": "Batas penggunaan tidak boleh melebihi $1 digit desimal. Hapus digit desimal untuk melanjutkan." + }, "enableAutoDetect": { "message": " Aktifkan deteksi otomatis" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "Pencarian ENS gagal." }, + "enterANameToIdentifyTheUrl": { + "message": "Masukkan nama untuk mengidentifikasi URL" + }, "enterANumber": { "message": "Masukkan angka" }, + "enterChainId": { + "message": "Masukkan ID Chain" + }, "enterCustodianToken": { "message": "Masukkan token $1 atau tambahkan token baru" }, "enterMaxSpendLimit": { "message": "Masukkan batas penggunaan maksimum" }, + "enterNetworkName": { + "message": "Masukkan nama jaringan" + }, "enterOptionalPassword": { "message": "Masukkan kata sandi opsional" }, "enterPasswordContinue": { "message": "Masukkan kata sandi untuk melanjutkan" }, + "enterRpcUrl": { + "message": "Masukkan URL RPC" + }, + "enterSymbol": { + "message": "Masukkan simbol" + }, "enterTokenNameOrAddress": { "message": "Masukkan nama token atau tempel alamat" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Eksperimental" }, + "exportYourData": { + "message": "Ekspor data Anda" + }, + "exportYourDataButton": { + "message": "Unduh" + }, + "exportYourDataDescription": { + "message": "Anda dapat mengekspor data seperti kontak dan preferensi Anda." + }, "extendWalletWithSnaps": { "message": "Jelajahi Snap yang dibuat oleh komunitas untuk menyesuaikan pengalaman web3 Anda", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Biaya gas ini telah disarankan oleh $1. Pengabaian dapat menyebabkan masalah pada transaksi Anda. Hubungi $1 jika ada pertanyaan.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Biaya gas" + }, "gasIsETH": { "message": "Gas bernilai $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Terjadi kesalahan...." }, + "generalDescription": { + "message": "Sinkronkan pengaturan di seluruh perangkat, pilih preferensi jaringan, dan lacak data token" + }, "genericExplorerView": { "message": "Lihat akun di $1" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "Dana akan hilang jika Anda terkunci dari aplikasi atau menggunakan perangkat baru. Pastikan untuk mencadangkan Frasa Pemulihan Rahasia di $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Abaikan semua" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "di Pengaturan Anda" }, + "included": { + "message": "termasuk" + }, "infuraBlockedNotification": { "message": "MetaMask tidak dapat terhubung ke host blockchain. Tinjau alasan yang mungkin $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "File JSON", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Simpan pengingat Frasa Pemulihan Rahasia di tempat yang aman. Jika hilang, tidak ada yang bisa membantu Anda mendapatkannya kembali. Lebih buruknya, dompet Anda tidak dapat diakses lagi. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Nama akun" }, @@ -2402,6 +2622,9 @@ "message": "Pelajari cara $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Pelajari caranya" + }, "learnMore": { "message": "pelajari selengkapnya" }, @@ -2409,6 +2632,9 @@ "message": "Ingin $1 seputar gas?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Selengkapnya seputar praktik terbaik privasi." + }, "learnMoreKeystone": { "message": "Pelajari Selengkapnya" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Tautan" }, + "linkCentralizedExchanges": { + "message": "Tautkan akun Coinbase atau Binance untuk mentransfer kripto ke MetaMask secara gratis." + }, "links": { "message": "Tautan" }, @@ -2557,6 +2786,9 @@ "message": "Pastikan tidak ada yang melihat layar Anda", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Kelola pengaturan privasi default" + }, "marketCap": { "message": "Kap pasar" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Tombol status koneksi menunjukkan apakah situs web yang Anda kunjungi terhubung ke akun Anda yang dipilih saat ini." }, + "metaMetricsIdNotAvailableError": { + "message": "Karena Anda belum pernah ikut serta dalam MetaMetrics, tidak ada data yang dapat dihapus di sini." + }, "metadataModalSourceTooltip": { "message": "$1 dihosting pada npm dan $2 merupakan pengenal unik Snap ini.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "selengkapnya" }, + "moreAccounts": { + "message": "+ $1 akun lain", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 jaringan lain", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Anda menambahkan jaringan ini ke MetaMask dan memberikan situs ini izin untuk menggunakannya." + }, "multipleSnapConnectionWarning": { "message": "$1 ingin menggunakan Snap $2", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Edit detail jaringan" }, "nativeTokenScamWarningDescription": { - "message": "Jaringan ini tidak cocok dengan ID chain atau nama yang terkait. Banyak token populer menggunakan nama $1, menjadikannya target penipuan. Penipu dapat mengelabui Anda agar mengirimkan mata uang yang lebih berharga sebagai imbalannya. Verifikasikan semuanya sebelum melanjutkan.", + "message": "Simbol token asli tidak cocok dengan simbol token asli yang diharapkan untuk jaringan dengan ID chain yang terkait. Anda telah memasukkan $1 sementara simbol token yang diharapkan adalah $2. Verifikasikan bahwa Anda terhubung ke chain yang benar.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "suatu hal lainnya", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Ini berpotensi penipuan", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Detail jaringan" }, + "networkFee": { + "message": "Biaya jaringan" + }, "networkIsBusy": { "message": "Jaringan sibuk. Harga gas tinggi dan estimasinya kurang akurat." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Opsi jaringan" }, + "networkPermissionToast": { + "message": "Izin jaringan diperbarui" + }, "networkProvider": { "message": "Penyedia jaringan" }, @@ -2865,15 +3121,26 @@ "message": "Kami tidak dapat terhubung ke $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Jaringan beralih ke $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "URL Jaringan" }, "networkURLDefinition": { "message": "URL yang digunakan untuk mengakses jaringan ini." }, + "networkUrlErrorWarning": { + "message": "Penyerang terkadang meniru situs dengan membuat perubahan kecil pada alamat situs. Pastikan Anda berinteraksi dengan situs yang dituju sebelum melanjutkan. Versi Punycode: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Jaringan" }, + "networksSmallCase": { + "message": "jaringan" + }, "nevermind": { "message": "Lupakan" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Kami telah memperbarui kebijakan privasi" }, + "newRpcUrl": { + "message": "URL RPC Baru" + }, "newTokensImportedMessage": { "message": "Anda berhasil mengimpor $1.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask tidak terhubung ke situs ini" }, + "noConnectionDescription": { + "message": "Untuk terhubung ke situs, cari dan pilih tombol \"hubungkan\". Ingat MetaMask hanya dapat terhubung ke situs di web3" + }, "noConversionRateAvailable": { "message": "Nilai konversi tidak tersedia" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Nonce kustom" }, + "none": { + "message": "Tidak ada" + }, "notBusy": { "message": "Tidak sibuk" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Detail izin" }, + "permissionFor": { + "message": "Izin untuk" + }, + "permissionFrom": { + "message": "Izin dari" + }, "permissionRequest": { "message": "Permintaan izin" }, @@ -3593,6 +3875,14 @@ "message": "Izinkan $1 mengakses bahasa pilihan Anda dari pengaturan MetaMask. Ini dapat digunakan untuk melokalisasi dan menampilkan konten $1 menggunakan bahasa Anda.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Lihat informasi seperti bahasa pilihan Anda dan mata uang fiat.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Izinkan $1 mengakses informasi seperti bahasa pilihan dan mata uang fiat di pengaturan MetaMask Anda. Ini membantu $1 menampilkan konten yang disesuaikan dengan preferensi Anda. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Tampilkan layar khusus", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Anda memberikan izin kepada pengguna untuk menggunakan token sebanyak ini dari akun." }, + "permittedChainToastUpdate": { + "message": "$1 memiliki akses ke $2." + }, "personalAddressDetected": { "message": "Alamat pribadi terdeteksi. Masukkan alamat kontrak token." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Terima" }, + "receiveCrypto": { + "message": "Terima kripto" + }, + "recipientAddressPlaceholderNew": { + "message": "Masukkan alamat publik (0x) atau nama domain" + }, "recommendedGasLabel": { "message": "Direkomendasikan" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Ditolak" }, + "rememberSRPIfYouLooseAccess": { + "message": "Ingat, jika Anda kehilangan Frasa Pemulihan Rahasia, akses ke dompet Anda akan hilang. $1 untuk menyimpan rangkaian kata-kata ini dengan aman sehingga Anda dapat mengakses dana setiap saat." + }, + "reminderSet": { + "message": "Pengingat diatur!" + }, "remove": { "message": "Hapus" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Karena terjadi kesalahan, permintaan ini tidak diverifikasi oleh penyedia keamanan. Lanjutkan dengan hati-hati." }, + "requestingFor": { + "message": "Meminta untuk" + }, + "requestingForAccount": { + "message": "Meminta untuk $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "permintaan menunggu untuk disetujui" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Ungkap frasa seed" }, + "review": { + "message": "Tinjau" + }, + "reviewAlert": { + "message": "Tinjau peringatan" + }, "reviewAlerts": { "message": "Tinjau peringatan" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Cabut izin" }, + "revokeSimulationDetailsDesc": { + "message": "Anda menghapus izin orang lain untuk menggunakan token dari akun Anda." + }, "revokeSpendingCap": { "message": "Cabut batas penggunaan untuk $1 Anda", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Pihak ketiga ini tidak akan dapat menggunakan token Anda saat ini atau di masa mendatang." }, + "rpcNameOptional": { + "message": "Nama RPC (Opsional)" + }, "rpcUrl": { "message": "URL RPC Baru" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Keamanan & privasi" }, + "securityDescription": { + "message": "Kurangi kemungkinan Anda bergabung dengan jaringan yang tidak aman dan lindungi akun Anda" + }, + "securityMessageLinkForNetworks": { + "message": "penipuan jaringan dan risiko keamanan" + }, + "securityPrivacyPath": { + "message": "Pengaturan > Keamanan & Privasi." + }, "securityProviderPoweredBy": { "message": "Didukung oleh $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Lihat semua izin", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Lihat detailnya" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Jika Anda tidak menemukan akun yang diharapkan, coba alihkan jalur HD atau jaringan yang dipilih saat ini." }, + "selectRpcUrl": { + "message": "Pilih URL RPC" + }, "selectType": { "message": "Pilih Jenis" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Atur persetujuan untuk semua" }, + "setApprovalForAllRedesignedTitle": { + "message": "Permintaan penarikan" + }, "setApprovalForAllTitle": { "message": "Setujui $1 tanpa batas penggunaan", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Pengaturan" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Pengaturan dioptimalkan untuk kemudahan penggunaan dan keamanan. Ubah pengaturan ini kapan saja." + }, "settingsSearchMatchingNotFound": { "message": "Tidak menemukan hasil yang cocok." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Tampilkan selengkapnya" }, + "showNativeTokenAsMainBalance": { + "message": "Tampilkan token asli sebagai saldo utama" + }, "showNft": { "message": "Tampilkan NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Masuk dengan" }, + "simulationApproveHeading": { + "message": "Tarik" + }, + "simulationDetailsApproveDesc": { + "message": "Anda memberi orang lain izin untuk menarik NFT dari akun Anda." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Anda memberi orang lain izin untuk menggunakan jumlah ini dari akun Anda." + }, "simulationDetailsFiatNotAvailable": { "message": "Tidak Tersedia" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Anda mengirim" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Anda menghapus izin orang lain untuk menarik NFT dari akun Anda." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Anda memberikan izin kepada orang lain untuk menarik NFT dari akun Anda." + }, "simulationDetailsTitle": { "message": "Estimasi perubahan" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Ups! Terjadi kesalahan." }, + "sortBy": { + "message": "Urutkan sesuai" + }, + "sortByAlphabetically": { + "message": "Sesuai abjad (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Saldo menurun ($1 tinggi-rendah)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Sumber" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Pengguna" }, + "spenderTooltipDesc": { + "message": "Ini adalah alamat yang dapat digunakan untuk menarik NFT Anda." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Ini adalah alamat yang dapat menggunakan token Anda atas nama Anda." + }, "spendingCap": { "message": "Batas penggunaan" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Permintaan batas penggunaan untuk $1 Anda" }, + "spendingCapTooltipDesc": { + "message": "Ini adalah jumlah token yang dapat diakses oleh pembeli atas nama Anda." + }, "srpInputNumberOfWords": { "message": "Frasa milik saya memiliki $1 kata", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Disarankan oleh $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Simbol mata uang yang disarankan:" + }, "suggestedTokenName": { "message": "Nama yang disarankan:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Kunjungi pusat dukungan kami" }, + "supportMultiRpcInformation": { + "message": "Saat ini kami mendukung beberapa RPC untuk satu jaringan. RPC terbaru Anda telah dipilih sebagai RPC default untuk mengatasi informasi yang saling bertentangan." + }, "surveyConversion": { "message": "Ikuti survei kami" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Biaya gas diperkirakan dan akan berfluktuasi berdasarkan lalu lintas jaringan dan kompleksitas transaksi." }, + "swapGasFeesExplanation": { + "message": "MetaMask tidak menghasilkan uang dari biaya gas. Biaya ini merupakan estimasi dan dapat berubah berdasarkan seberapa sibuk jaringan dan seberapa rumit transaksinya. Pelajari selengkapnya $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "di sini", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Pelajari selengkapnya seputar biaya gas" }, @@ -5186,9 +5583,19 @@ "message": "Biaya gas dibayarkan kepada penambang kripto yang memproses transaksi di jaringan $1. MetaMask tidak mengambil keuntungan dari biaya gas.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Kuotasi ini mencakup biaya gas dengan menyesuaikan jumlah token yang dikirim atau diterima. Anda bisa menerima ETH dalam transaksi terpisah pada daftar aktivitas Anda." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Pelajari selengkapnya seputar biaya gas" + }, "swapHighSlippage": { "message": "Selip tinggi" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Termasuk biaya gas dan MetaMask sebesar $1%", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Termasuk $1% biaya MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Syarat Penggunaan kami telah diperbarui" }, + "testnets": { + "message": "Testnet" + }, "theme": { "message": " Saya menyetujui Ketentuan Penggunaan, yang berlaku untuk penggunaan atas MetaMask dan semua fiturnya" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Biaya transaksi" }, + "transactionFlowNetwork": { + "message": "Jaringan" + }, "transactionHistoryBaseFee": { "message": "Biaya dasar (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Transfer" }, + "transferCrypto": { + "message": "Transfer kripto" + }, "transferFrom": { "message": "Transfer dari" }, + "transferRequest": { + "message": "Permintaan transfer" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "Perbarui" }, + "updateEthereumChainConfirmationDescription": { + "message": "Situs ini meminta pembaruan URL jaringan default Anda. Anda dapat mengedit informasi default dan jaringan setiap saat." + }, + "updateNetworkConfirmationTitle": { + "message": "Perbarui $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Perbarui informasi Anda atau" }, "updateRequest": { "message": "Permintaan pembaruan" }, + "updatedRpcForNetworks": { + "message": "RPC Jaringan Diperbarui" + }, "uploadDropFile": { "message": "Letakkan fail di sini" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "panduan koneksi dompet perangkat keras kami" }, + "walletProtectedAndReadyToUse": { + "message": "Dompet Anda terlindungi dan siap digunakan. Temukan Frasa Pemulihan Rahasia di $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Ingin menambahkan jaringan ini?" }, @@ -5991,6 +6424,17 @@ "message": "$1 Pihak ketiga dapat mempergunakan seluruh saldo token Anda tanpa pemberitahuan atau persetujuan lebih lanjut. Lindungi diri Anda dengan menyesuaikan batas penggunaan yang lebih rendah.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Mengaktifkan opsi ini akan memberi Anda kemampuan untuk memantau akun Ethereum melalui alamat publik atau nama ENS. Untuk masukan tentang fitur Beta ini, silakan isi formulir $1 ini.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Pantau Akun Ethereum (Beta)" + }, + "watchOutMessage": { + "message": "Waspadalah terhadap $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Lemah" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Apa ini?" }, + "withdrawing": { + "message": "Penarikan" + }, "wrongNetworkName": { "message": "Menurut catatan kami, nama jaringan mungkin tidak cocok dengan ID chain ini." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Saldo Anda" }, + "yourBalanceIsAggregated": { + "message": "Saldo Anda diagregasi" + }, "yourNFTmayBeAtRisk": { "message": "NFT Anda mungkin berisiko" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Kami tidak dapat membatalkan transaksi Anda sebelum dikonfirmasi di blockchain." }, + "yourWalletIsReady": { + "message": "Dompet Anda sudah siap" + }, "zeroGasPriceOnSpeedUpError": { "message": "Biaya gas nol pada percepatan" } diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index ad77733372b6..a3e917d46600 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "QRハードウェアウォレットを接続" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "サインインリクエストのアドレスが、サインインに使用しているアカウントのアドレスと一致していません。" }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "通知を受けたいアカウントを選択してください:" }, + "accountBalance": { + "message": "アカウント残高" + }, "accountDetails": { "message": "アカウントの詳細" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "アカウントのオプション" }, + "accountPermissionToast": { + "message": "アカウントのアクセス許可が更新されました" + }, "accountSelectionRequired": { "message": "アカウントを選択する必要があります!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "アカウントが接続されました" }, + "accountsPermissionsTitle": { + "message": "アカウントを確認しトランザクションを提案する" + }, + "accountsSmallCase": { + "message": "アカウント" + }, "active": { "message": "アクティブ" }, @@ -180,12 +195,18 @@ "add": { "message": "追加" }, + "addACustomNetwork": { + "message": "カスタムネットワークを追加" + }, "addANetwork": { "message": "ネットワークを追加" }, "addANickname": { "message": "ニックネームを追加" }, + "addAUrl": { + "message": "URLを追加" + }, "addAccount": { "message": "アカウントを追加" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "ブロックエクスプローラーを追加" }, + "addBlockExplorerUrl": { + "message": "ブロックエクスプローラーURLを追加" + }, "addContact": { "message": "連絡先を追加" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "イーサリアムメインネット用の新しいRPCプロバイダーを追加しようとしています" }, + "addEthereumWatchOnlyAccount": { + "message": "イーサリアムアカウントの監視 (ベータ)" + }, "addFriendsAndAddresses": { "message": "信頼できる友達とアドレスを追加する" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "ネットワークを追加" }, + "addNetworkConfirmationTitle": { + "message": "$1の追加", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "新しいイーサリアムアカウントを追加" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "アドレスがコピーされました!" }, + "addressMismatch": { + "message": "サイトアドレスの不一致" + }, + "addressMismatchOriginal": { + "message": "現在のURL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Punycodeバージョン: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "高度な設定" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "優先手数料 (別名「マイナーチップ」) はマイナーに直接支払われ、トランザクションを優先するインセンティブとなります。" }, + "aggregatedBalancePopover": { + "message": "これには特定のネットワークで所有しているすべてのトークンの価値が反映されています。この値をETHまたはその他通貨で表示したい場合は、$1に移動します。", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "MetaMaskの$1に同意します", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "これは「設定」>「アラート」で変更できます" }, + "alertMessageAddressMismatchWarning": { + "message": "攻撃者は、サイトのアドレスに若干の変更を加えてサイトを模倣することがあります。続行する前に、意図したサイトとやり取りしていることを確認してください。" + }, "alertMessageGasEstimateFailed": { "message": "正確な手数料を提供できず、この見積もりは高い可能性があります。カスタムガスリミットの入力をお勧めしますが、それでもトランザクションが失敗するリスクがあります。" }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "このトランザクションを続行するには、ガスリミットを21000以上に上げる必要があります。" }, + "alertMessageInsufficientBalance2": { + "message": "アカウントにネットワーク手数料を支払うのに十分なETHがありません。" + }, "alertMessageNetworkBusy": { "message": "ガス価格が高く、見積もりはあまり正確ではありません。" }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "アセットのオプション" }, + "assets": { + "message": "アセット" + }, + "assetsDescription": { + "message": "ウォレットのトークンを自動検出し、NFTを表示して、アカウント残高の最新情報を一括で取得します" + }, "attemptSendingAssets": { "message": "別のネットワークからアセットを送ろうとすると、アセットが失われる可能性があります。ネットワーク間で安全に資金を移動するには、必ずブリッジを使用してください。" }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "ブリッジを使用してください" }, + "bridgeFrom": { + "message": "ブリッジ元:" + }, + "bridgeSelectNetwork": { + "message": "ネットワークを選択" + }, + "bridgeTo": { + "message": "ブリッジ先:" + }, "browserNotSupported": { "message": "ご使用のブラウザはサポートされていません..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "シークレットリカバリーフレーズの確認" }, + "confirmTitleApproveTransaction": { + "message": "許容額のリクエスト" + }, + "confirmTitleDeployContract": { + "message": "コントラクトを展開" + }, + "confirmTitleDescApproveTransaction": { + "message": "このサイトがNFTの引き出し許可を求めています。" + }, + "confirmTitleDescDeployContract": { + "message": "このサイトがコントラクトの展開を求めています" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "このサイトがトークンの引き出し許可を求めています。" + }, "confirmTitleDescPermitSignature": { "message": "このサイトがトークンの使用許可を求めています。" }, "confirmTitleDescSIWESignature": { "message": "サイトがこのアカウントを所有することを証明するためにサインインを求めています。" }, + "confirmTitleDescSign": { + "message": "確定する前に、要求の詳細を確認してください。" + }, "confirmTitlePermitTokens": { "message": "使用上限リクエスト" }, + "confirmTitleRevokeApproveTransaction": { + "message": "アクセス許可を取り消す" + }, "confirmTitleSIWESignature": { "message": "サインインリクエスト" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "アクセス許可を取り消す" + }, "confirmTitleSignature": { "message": "署名要求" }, "confirmTitleTransaction": { "message": "トランザクションの要求" }, + "confirmationAlertModalDetails": { + "message": "資産とログイン情報を守るため、要求を拒否することをお勧めします。" + }, + "confirmationAlertModalTitle": { + "message": "この要求は不審です" + }, "confirmed": { "message": "確認されました" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "ENS名に混乱しやすい文字が発見されました。詐欺を防ぐためにENS名を確認して下さい。" }, + "congratulations": { + "message": "おめでとうございます!" + }, "connect": { "message": "接続" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "接続済みのサイト" }, + "connectedSitesAndSnaps": { + "message": "接続されているサイトとSnap" + }, "connectedSitesDescription": { "message": "$1はこれらのサイトに接続されています。これらのサイトは、アカウントアドレスを把握できます。", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMaskはこのサイトに接続されていますが、まだアカウントは接続されていません" }, + "connectedSnaps": { + "message": "接続されているSnap" + }, + "connectedWithAccount": { + "message": "$1個のアカウントが接続されました", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "$1と接続されました", + "description": "$1 represents account name" + }, "connecting": { "message": "接続中..." }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Sepoliaテストネットワークに接続中" }, + "connectionDescription": { + "message": "このサイトは次のことを求めています:" + }, "connectionFailed": { "message": "接続できませんでした" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "アドレスをクリップボードにコピー" }, + "copyAddressShort": { + "message": "アドレスをコピー" + }, "copyPrivateKey": { "message": "秘密鍵をコピー" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "デフォルトのRPC URL" }, + "defaultSettingsSubTitle": { + "message": "MetaMaskは、デフォルト設定を使用して安全と使いやすさのバランスを最適化しています。プライバシーをさらに強化したい場合は、これらの設定を変更してください。" + }, + "defaultSettingsTitle": { + "message": "デフォルトのプライバシー設定" + }, "delete": { "message": "削除" }, "deleteContact": { "message": "連絡先を削除" }, + "deleteMetaMetricsData": { + "message": "MetaMetricsデータを削除" + }, + "deleteMetaMetricsDataDescription": { + "message": "これにより、このデバイスでの使用に関連した過去のMetaMetricsデータが削除されます。このデータが削除された後も、ウォレットとアカウントに変化はありません。このプロセスには最長30日間かかる場合があります。$1をご覧ください。", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "アナリティクスシステムサーバーの問題により、現在このリクエストを完了させることができません。後ほど再度お試しください" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "現在このデータを削除できません" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "すべてのMetaMetricsデータを削除しようとしています。よろしいですか?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "MetaMetricsデータを削除しますか?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "このアクションは$1に開始しました。このプロセスには最長30日間かかる場合があります。$2をご覧ください", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "このネットワークを削除した場合、このネットワーク内の資産を見るには、再度ネットワークの追加が必要になります。" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "入金" }, + "depositCrypto": { + "message": "ウォレットアドレスまたはQRコードを使った別のアカウントからの仮想通貨のデポジット" + }, "deprecatedGoerliNtwrkMsg": { "message": "イーサリアムシステムのアップデートに伴い、Goerliテストネットワークはまもなく段階的に廃止される予定です。" }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "アカウント" }, + "disconnectAllDescriptionText": { + "message": "このサイトとの接続を解除すると、このサイトをもう一度使用するには、アカウントとネットワークを接続しなおす必要があります。" + }, "disconnectAllSnapsText": { "message": "Snap" }, + "disconnectMessage": { + "message": "これにより、このサイトへの接続が解除されます" + }, "disconnectPrompt": { "message": "$1を接続解除" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "ニックネームを編集" }, + "editAccounts": { + "message": "アカウントを編集" + }, "editAddressNickname": { "message": "アドレスのニックネームを編集" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "元のネットワークを編集" }, + "editNetworksTitle": { + "message": "ネットワークを編集" + }, "editNonceField": { "message": "ナンスを編集" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "アクセス許可の編集" }, + "editPermissions": { + "message": "アクセス許可の編集" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "高速化用のガス代を編集" }, + "editSpendingCap": { + "message": "使用上限の編集" + }, + "editSpendingCapAccountBalance": { + "message": "アカウント残高: $1 $2" + }, + "editSpendingCapDesc": { + "message": "代理で使用されても構わない金額を入力してください。" + }, + "editSpendingCapError": { + "message": "使用限度は小数点以下$1桁を超えることができません。続けるには、小数点以下の桁を削除してください。" + }, "enableAutoDetect": { "message": " 自動検出を有効にする" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "ENSの検索に失敗しました。" }, + "enterANameToIdentifyTheUrl": { + "message": "URLを識別するための名前を入力してください" + }, "enterANumber": { "message": "数字を入力してください" }, + "enterChainId": { + "message": "チェーンIDを入力してください" + }, "enterCustodianToken": { "message": "$1トークンを入力するか、新しいトークンを追加してください" }, "enterMaxSpendLimit": { "message": "使用限度額の最大値を入力してください" }, + "enterNetworkName": { + "message": "ネットワーク名を入力してください" + }, "enterOptionalPassword": { "message": "オプションのパスワードを入力してください" }, "enterPasswordContinue": { "message": "続行するには、パスワードを入力してください" }, + "enterRpcUrl": { + "message": "RPC URLを入力してください" + }, + "enterSymbol": { + "message": "シンボルを入力してください" + }, "enterTokenNameOrAddress": { "message": "トークン名を入力するか、アドレスを貼り付けてください" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "試験運用" }, + "exportYourData": { + "message": "データのエクスポート" + }, + "exportYourDataButton": { + "message": "ダウンロード" + }, + "exportYourDataDescription": { + "message": "連絡先やユーザー設定などのデータをエクスポートできます。" + }, "extendWalletWithSnaps": { "message": "Web3のエクスペリエンスをカスタマイズする、コミュニティが開発したSnapをご覧ください", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "このガス代は$1により提案されています。これを上書きすると、トランザクションに問題が発生する可能性があります。ご質問がございましたら、$1までお問い合わせください。", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "ガス代" + }, "gasIsETH": { "message": "ガス代は$1です" }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "問題が発生しました...." }, + "generalDescription": { + "message": "デバイス間で設定を同期して、ネットワーク設定を選択し、トークンデータを追跡します" + }, "genericExplorerView": { "message": "$1でアカウントを表示" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "アプリからロックアウトされてしまったり、新しいデバイスを入手した場合、資金が失われます。シークレットリカバリーフレーズは必ず$1にバックアップしてください ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "すべて無視" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "設定で" }, + "included": { + "message": "含む" + }, "infuraBlockedNotification": { "message": "MetaMaskがブロックチェーンのホストに接続できません。考えられる理由$1を確認してください。", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "JSONファイル", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "シークレットリカバリーフレーズは忘れないように安全な場所に保管してください。なくしてしまうと、誰にも取り戻すことはできません。さらに、ウォレットに二度とアクセスできなくなります。$1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "アカウント名" }, @@ -2402,6 +2622,9 @@ "message": "$1の方法を学ぶ", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "方法" + }, "learnMore": { "message": "詳細" }, @@ -2409,6 +2632,9 @@ "message": "ガスに関する$1をご希望ですか?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "プライバシーのベストプラクティスに関する詳細をご覧ください。" + }, "learnMoreKeystone": { "message": "詳細" }, @@ -2497,6 +2723,9 @@ "link": { "message": "リンク" }, + "linkCentralizedExchanges": { + "message": "CoinbaseまたはBinanceアカウントをリンクして、無料でMetaMaskに仮想通貨を送金します。" + }, "links": { "message": "リンク" }, @@ -2557,6 +2786,9 @@ "message": "誰にも見られていないことを確認してください", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "デフォルトのプライバシー設定の管理" + }, "marketCap": { "message": "時価総額" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "訪問しているWebサイトが現在選択しているアカウントに接続されている場合、接続ステータスボタンが表示されます。" }, + "metaMetricsIdNotAvailableError": { + "message": "MetaMetricsにオプトインしていないため、ここで削除するデータはありません。" + }, "metadataModalSourceTooltip": { "message": "$1はnpmでホストされていて、$2はこのSnapの一意のIDです。", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "他" }, + "moreAccounts": { + "message": "+ $1個のアカウント", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1個のネットワーク", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "このネットワークをMetaMaskに追加し、サイトがそれを使用することを許可しようとしています。" + }, "multipleSnapConnectionWarning": { "message": "$1が$2 Snapの使用を求めています", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "ネットワークの詳細を編集" }, "nativeTokenScamWarningDescription": { - "message": "このネットワークは関連付けられているチェーンIDまたは名前と一致していません。人気のトークンの多くが「$1」という名前を使用しているため、詐欺の対象となっています。詐欺師はより価値の高い通貨を送り返すよう仕向けてくる可能性があります。続行する前にすべてを確認してください。", + "message": "ネイティブトークンシンボルが、関連付けられているチェーンIDのネットワークで予想されるネイティブトークンのシンボルと一致していません。予想されるトークンシンボルは$2ですが、$1と入力されました。正しいチェーンに接続されていることを確認してください。", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "別のシンボル", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "これは詐欺の可能性があります", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "ネットワークの詳細" }, + "networkFee": { + "message": "ネットワーク手数料" + }, "networkIsBusy": { "message": "ネットワークが混み合っています。ガス代が高く、見積もりはあまり正確ではありません。" }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "ネットワークオプション" }, + "networkPermissionToast": { + "message": "ネットワークへのアクセス許可が更新されました" + }, "networkProvider": { "message": "ネットワークプロバイダー" }, @@ -2865,15 +3121,26 @@ "message": "$1に接続できません", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "ネットワークが$1に切り替わりました", + "description": "$1 represents the network name" + }, "networkURL": { "message": "ネットワークURL" }, "networkURLDefinition": { "message": "このネットワークへのアクセスに使用されるURLです。" }, + "networkUrlErrorWarning": { + "message": "攻撃者は、サイトのアドレスに若干の変更を加えてサイトを模倣することがあります。続行する前に、意図したサイトとやり取りしていることを確認してください。Punycodeバージョン: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "ネットワーク" }, + "networksSmallCase": { + "message": "ネットワーク" + }, "nevermind": { "message": "取り消し" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "プライバシーポリシーが更新されました" }, + "newRpcUrl": { + "message": "新しいRPC URL" + }, "newTokensImportedMessage": { "message": "$1をインポートしました。", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMaskはこのサイトに接続されていません" }, + "noConnectionDescription": { + "message": "サイトに接続するには、「接続」ボタンを見つけて選択します。MetaMaskはWeb3のサイトにしか接続できないのでご注意ください" + }, "noConversionRateAvailable": { "message": "利用可能な換算レートがありません" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "カスタムナンス" }, + "none": { + "message": "なし" + }, "notBusy": { "message": "ビジー状態ではありません" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "アクセス許可の詳細" }, + "permissionFor": { + "message": "アクセス許可:" + }, + "permissionFrom": { + "message": "次からのアクセス許可:" + }, "permissionRequest": { "message": "許可のリクエスト" }, @@ -3593,6 +3875,14 @@ "message": "$1がMetaMaskの言語設定にアクセスできるようにします。これは、$1のコンテンツをユーザーの言語にローカライズして表示するために使用されます。", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "優先言語や法定通貨などの情報の表示", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "$1によるMetaMask設定の優先言語や法定通貨などの情報へのアクセスを許可します。これにより、$1がユーザーの設定に合わせたコンテンツを表示できるようになります。", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "カスタム画面の表示", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "この数量のトークンをアカウントから転送する権限を使用者に付与しようとしています。" }, + "permittedChainToastUpdate": { + "message": "$1は$2にアクセスできます。" + }, "personalAddressDetected": { "message": "個人アドレスが検出されました。トークンコントラクトアドレスを入力してください。" }, @@ -3963,6 +4256,12 @@ "receive": { "message": "受取" }, + "receiveCrypto": { + "message": "仮想通貨を受け取る" + }, + "recipientAddressPlaceholderNew": { + "message": "パブリックアドレス (0x) またはドメイン名を入力してください" + }, "recommendedGasLabel": { "message": "推奨" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "拒否されました" }, + "rememberSRPIfYouLooseAccess": { + "message": "シークレットリカバリーフレーズをなくしてしまうと、ウォレットにアクセスできなくなります。この単語のセットを安全に保管し、いつでも資金にアクセスできるように、$1してください。" + }, + "reminderSet": { + "message": "リマインダーが設定されました!" + }, "remove": { "message": "削除" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "エラーが発生したため、このリクエストはセキュリティプロバイダーにより検証されませんでした。慎重に進めてください。" }, + "requestingFor": { + "message": "次の要求:" + }, + "requestingForAccount": { + "message": "$1の要求", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "リクエストの承認待ち" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "シードフレーズを確認" }, + "review": { + "message": "確認" + }, + "reviewAlert": { + "message": "アラートを確認" + }, "reviewAlerts": { "message": "アラートを確認する" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "許可を取り消す" }, + "revokeSimulationDetailsDesc": { + "message": "別のユーザーに付与したアカウントのトークン使用許可を取り消そうとしています。" + }, "revokeSpendingCap": { "message": "$1の使用上限を取り消す", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "このサードパーティは、現在または今後のトークンをこれ以上使用できなくなります。" }, + "rpcNameOptional": { + "message": "RPC名 (オプション)" + }, "rpcUrl": { "message": "新しいRPC URL" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "セキュリティとプライバシー" }, + "securityDescription": { + "message": "安全ではないネットワークに参加してしまう可能性を減らし、アカウントを守ります" + }, + "securityMessageLinkForNetworks": { + "message": "ネットワーク詐欺とセキュリティのリスク" + }, + "securityPrivacyPath": { + "message": "「設定」>「セキュリティとプライバシー」で確認できます。" + }, "securityProviderPoweredBy": { "message": "データソース: $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "すべてのアクセス許可を表示", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "詳細を表示" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "アカウントが見当たらない場合は、HDパスまたは現在選択されているネットワークを切り替えてみてください。" }, + "selectRpcUrl": { + "message": "RPC URLを選択" + }, "selectType": { "message": "種類を選択" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "すべてを承認に設定" }, + "setApprovalForAllRedesignedTitle": { + "message": "出金のリクエスト" + }, "setApprovalForAllTitle": { "message": "使用限度額なしで$1を承認", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "設定" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "設定は使いやすさとセキュリティを確保するために最適化されています。これらはいつでも変更できます。" + }, "settingsSearchMatchingNotFound": { "message": "一致する結果が見つかりませんでした。" }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "他を表示" }, + "showNativeTokenAsMainBalance": { + "message": "ネイティブトークンをメイン残高として表示" + }, "showNft": { "message": "NFTを表示" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "サインイン方法:" }, + "simulationApproveHeading": { + "message": "引き出し" + }, + "simulationDetailsApproveDesc": { + "message": "別のユーザーに、アカウントからのNFTの引き出しを許可しようとしています。" + }, + "simulationDetailsERC20ApproveDesc": { + "message": "別のユーザーに、アカウントからこの数量を使用する許可を与えようとしています。" + }, "simulationDetailsFiatNotAvailable": { "message": "利用できません" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "送金額" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "別のユーザーに付与したアカウントからのNFT引き出し許可を取り消そうとしています。" + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "別のユーザーに、アカウントからのNFTの引き出しを許可しようとしています。" + }, "simulationDetailsTitle": { "message": "予測される増減額" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "申し訳ありません。問題が発生しました。" }, + "sortBy": { + "message": "並べ替え基準" + }, + "sortByAlphabetically": { + "message": "アルファベット順 (A~Z)" + }, + "sortByDecliningBalance": { + "message": "残高の多い順 ($1 高~低)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "ソース" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "使用者" }, + "spenderTooltipDesc": { + "message": "これは、NFTを引き出せるようになるアドレスです。" + }, + "spenderTooltipERC20ApproveDesc": { + "message": "これが、トークンを代理で使用できるようになるアドレスです。" + }, "spendingCap": { "message": "使用上限" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "$1の使用上限のリクエスト" }, + "spendingCapTooltipDesc": { + "message": "これは、使用者が代理でアクセスできるようになるトークンの金額です。" + }, "srpInputNumberOfWords": { "message": "$1語のフレーズがあります", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "$1による提案", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "推奨通貨シンボル。" + }, "suggestedTokenName": { "message": "提案された名前:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "サポートセンターをご利用ください" }, + "supportMultiRpcInformation": { + "message": "1つのネットワークで複数のRPCがサポートされるようになりました。情報の矛盾を解決するため、最も最近のRPCがデフォルトのRPCとして選択されています。" + }, "surveyConversion": { "message": "アンケートに回答する" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "ガス代は、ネットワークトラフィックとトランザクションの複雑さに基づき推定され、変動します。" }, + "swapGasFeesExplanation": { + "message": "MetaMaskはガス代から収益を得ません。これらの手数料は見積もりであり、ネットワークの混雑状況やトランザクションの複雑さによって変わる可能性があります。詳細は$1をご覧ください。", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "こちら", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "ガス代に関する詳細" }, @@ -5186,9 +5583,19 @@ "message": "ガス代は、$1ネットワークでトランザクションを処理するクリプトマイナーに支払われます。MetaMaskはガス代から利益を得ません。", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "このクォートは、やり取りするトークンの数量を調整し、ガス代込みで提示されています。アクティビティリストの別のトランザクションでETHを受け取る場合があります。" + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "ガス代に関する詳細" + }, "swapHighSlippage": { "message": "高スリッページ" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "ガス代と$1%のMetaMask手数料が含まれています", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "$1%のMetaMask手数料が含まれています。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "利用規約が更新されました" }, + "testnets": { + "message": "テストネット" + }, "theme": { "message": "テーマ" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "トランザクション手数料" }, + "transactionFlowNetwork": { + "message": "ネットワーク" + }, "transactionHistoryBaseFee": { "message": "基本料金 (Gwei)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "送金" }, + "transferCrypto": { + "message": "仮想通貨の送金" + }, "transferFrom": { "message": "送金元" }, + "transferRequest": { + "message": "送金要求" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "更新" }, + "updateEthereumChainConfirmationDescription": { + "message": "このサイトがデフォルトのネットワークURLの更新を要求しています。デフォルトとネットワーク情報はいつでも編集できます。" + }, + "updateNetworkConfirmationTitle": { + "message": "$1を更新", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "情報を更新するか" }, "updateRequest": { "message": "更新リクエスト" }, + "updatedRpcForNetworks": { + "message": "ネットワークRPCが更新されました" + }, "uploadDropFile": { "message": "ここにファイルをドロップします" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "弊社のハードウェアウォレット接続ガイド" }, + "walletProtectedAndReadyToUse": { + "message": "ウォレットが保護され、使用する準備ができました。シークレットリカバリーフレーズは、$1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "このネットワークを追加しますか?" }, @@ -5991,6 +6424,17 @@ "message": "$1 このサードパーティは今後、通知や承諾なしにトークン残高全額を使用できます。使用上限をより低い金額にカスタマイズして、自分の身を守りましょう。", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "このオプションをオンにすると、パブリックアドレスまたはENS名でイーサリアムアカウントを監視できるようになります。ベータ機能に関するフィードバックは、こちらの$1に入力してください。", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "イーサリアムアカウントの監視 (ベータ)" + }, + "watchOutMessage": { + "message": "$1にご注意ください。", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "弱" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "これは何ですか?" }, + "withdrawing": { + "message": "引き出し中" + }, "wrongNetworkName": { "message": "弊社の記録によると、ネットワーク名がこのチェーンIDと正しく一致していない可能性があります。" }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "残高" }, + "yourBalanceIsAggregated": { + "message": "残高は集計されます" + }, "yourNFTmayBeAtRisk": { "message": "NFTが危険にさらされている可能性があります" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "ブロックチェーン上で確定しているため、トランザクションをキャンセルできませんでした。" }, + "yourWalletIsReady": { + "message": "ウォレットの準備ができました" + }, "zeroGasPriceOnSpeedUpError": { "message": "高速化用のガス価格がゼロです" } diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 86077780926a..e1eeda4487a6 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "QR 하드웨어 지갑을 연결하세요" }, + "QRHardwareWalletSteps2Description": { + "message": "N그레이브 제로" + }, "SIWEAddressInvalid": { "message": "로그인 요청 주소가 현재 로그인 계정의 주소와 일치하지 않습니다." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "알림을 수신할 계정을 선택하세요." }, + "accountBalance": { + "message": "계정 잔액" + }, "accountDetails": { "message": "계정 세부 정보" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "계정 옵션" }, + "accountPermissionToast": { + "message": "계정 권한이 업데이트됨" + }, "accountSelectionRequired": { "message": "계정을 선택해야 합니다!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "계정 연결됨" }, + "accountsPermissionsTitle": { + "message": "계정 보기 및 트랜잭션 제안" + }, + "accountsSmallCase": { + "message": "계정" + }, "active": { "message": "활성" }, @@ -180,12 +195,18 @@ "add": { "message": "추가" }, + "addACustomNetwork": { + "message": "사용자 지정 네트워크 추가" + }, "addANetwork": { "message": "네트워크 추가" }, "addANickname": { "message": "닉네임 추가" }, + "addAUrl": { + "message": "URL 추가" + }, "addAccount": { "message": "계정 추가" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "블록 탐색기 추가" }, + "addBlockExplorerUrl": { + "message": "블록 탐색기 URL 추가" + }, "addContact": { "message": "연락처 추가" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "이더리움 메인넷에 새로운 RPC 공급업체를 추가하려고 합니다" }, + "addEthereumWatchOnlyAccount": { + "message": "이더리움 계정 모니터(베타)" + }, "addFriendsAndAddresses": { "message": "신뢰하는 친구 및 주소 추가" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "네트워크 추가" }, + "addNetworkConfirmationTitle": { + "message": "$1 추가", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "새 이더리움 계정 추가" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "주소 복사 완료!" }, + "addressMismatch": { + "message": "사이트 주소 불일치" + }, + "addressMismatchOriginal": { + "message": "현재 URL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Punycode 버전: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "고급" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "우선 요금(일명 \"채굴자 팁\")이란 내 트랜잭션을 우선 거래한 것에 대한 인센티브로 채굴자에게 직접 전달되는 금액입니다." }, + "aggregatedBalancePopover": { + "message": "이는 특정 네트워크에서 소유한 모든 토큰의 가치를 반영합니다. 이 가치를 ETH 또는 다른 통화로 보고 싶다면 $1(으)로 이동하세요.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "MetaMask의 $1에 동의합니다", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "\"설정 > 경고\"에서 변경할 수 있습니다" }, + "alertMessageAddressMismatchWarning": { + "message": "공격자는 사이트 주소를 약간 변경하여 유사 사이트를 만들기도 합니다. 계속하기 전에 정상적인 사이트와 상호 작용하고 있는지 확인하세요." + }, "alertMessageGasEstimateFailed": { "message": "정확한 수수료를 제공할 수 없으며 예상 수수료가 높을 수 있습니다. 사용자 지정 가스 한도를 입력하는 것이 좋지만 트랜잭션이 여전히 실패할 위험이 있습니다." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "이 트랜잭션을 계속 진행하려면, 가스 한도를 21000 이상으로 늘려야 합니다." }, + "alertMessageInsufficientBalance2": { + "message": "계정에 네트워크 수수료로 지불할 ETH가 부족합니다." + }, "alertMessageNetworkBusy": { "message": "가스비가 높고 견적의 정확도도 떨어집니다." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "자산 옵션" }, + "assets": { + "message": "자산" + }, + "assetsDescription": { + "message": "지갑에서 토큰을 자동으로 감지하고, NFT를 표시하며, 계정 잔액을 일괄 업데이트하세요." + }, "attemptSendingAssets": { "message": "다른 네트워크로 자산을 직접 전송하면 자산이 영구적으로 손실될 수 있습니다. 브릿지를 이용하여 네트워크 간에 자금을 안전하게 전송하세요." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "전송하지 마세요, 브릿지 하세요" }, + "bridgeFrom": { + "message": "브릿지 출처" + }, + "bridgeSelectNetwork": { + "message": "네트워크 선택" + }, + "bridgeTo": { + "message": "브릿지 대상" + }, "browserNotSupported": { "message": "지원되지 않는 브라우저입니다..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "비밀복구구문 컨펌" }, + "confirmTitleApproveTransaction": { + "message": "수당 청구" + }, + "confirmTitleDeployContract": { + "message": "계약 배포" + }, + "confirmTitleDescApproveTransaction": { + "message": "이 사이트에서 NFT 인출 권한을 요청합니다" + }, + "confirmTitleDescDeployContract": { + "message": "이 사이트에서 계약을 배포하려고 합니다" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "이 사이트에서 토큰 인출 권한을 요청합니다" + }, "confirmTitleDescPermitSignature": { "message": "해당 사이트에서 토큰 사용 승인을 요청합니다." }, "confirmTitleDescSIWESignature": { "message": "회원님이 이 계정을 소유하고 있음을 확인하기 위해 로그인을 요청하는 사이트가 있습니다." }, + "confirmTitleDescSign": { + "message": "컨펌 전에 요청 세부 정보를 검토하세요." + }, "confirmTitlePermitTokens": { "message": "지출 한도 요청" }, + "confirmTitleRevokeApproveTransaction": { + "message": "권한 제거" + }, "confirmTitleSIWESignature": { "message": "로그인 요청" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "권한 제거" + }, "confirmTitleSignature": { "message": "서명 요청" }, "confirmTitleTransaction": { "message": "트랜젝션 요청" }, + "confirmationAlertModalDetails": { + "message": "자산과 로그인 정보 보호를 위해 요청을 거부하는 것이 좋습니다." + }, + "confirmationAlertModalTitle": { + "message": "의심스러운 요청입니다" + }, "confirmed": { "message": "컨펌됨" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "ENS 이름에 혼동하기 쉬운 문자가 있습니다. 잠재적 사기를 막기 위해 ENS 이름을 확인하세요." }, + "congratulations": { + "message": "축하합니다!" + }, "connect": { "message": "연결" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "연결된 사이트" }, + "connectedSitesAndSnaps": { + "message": "연결된 사이트 및 Snap" + }, "connectedSitesDescription": { "message": "$1 계정이 이 사이트에 연결되었습니다. 해당 계정 주소도 볼 수 있습니다.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask는 이 사이트와 연결되어 있지만, 아직 연결된 계정이 없습니다" }, + "connectedSnaps": { + "message": "연결된 Snap" + }, + "connectedWithAccount": { + "message": "$1 계정 연결됨", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "$1 계정과 연결됨", + "description": "$1 represents account name" + }, "connecting": { "message": "연결 중" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Sepolia 테스트 네트워크에 연결 중" }, + "connectionDescription": { + "message": "이 사이트에서 요청하는 사항:" + }, "connectionFailed": { "message": "연결 실패" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "주소를 클립보드에 복사" }, + "copyAddressShort": { + "message": "주소 복사" + }, "copyPrivateKey": { "message": "개인 키 복사" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "기본 RPC URL" }, + "defaultSettingsSubTitle": { + "message": "MetaMask의 기본 설정은 보안과 사용의 편의성 사이에서 최적의 균형을 맞추기 위한 설정입니다. 개인정보 보호 기능을 강화하려면 설정을 변경하세요." + }, + "defaultSettingsTitle": { + "message": "기본 개인정보 보호 설정" + }, "delete": { "message": "삭제" }, "deleteContact": { "message": "연락처 삭제" }, + "deleteMetaMetricsData": { + "message": "MetaMetrics 데이터 삭제" + }, + "deleteMetaMetricsDataDescription": { + "message": "이렇게 하면 이 기기에서 사용한 과거 MetaMetrics 데이터가 삭제됩니다. 이 데이터를 삭제해도 지갑과 계정에는 아무런 영향이 없습니다. 삭제는 최대 30일 정도 소요됩니다. 다음을 참고하세요: $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "분석 시스템 서버 문제로 지금 요청을 처리할 수 없습니다. 나중에 다시 시도하세요" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "지금은 데이터를 삭제할 수 없습니다" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "모든 MetaMetrics 데이터를 제거합니다. 정말 제거하시겠습니까?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "MetaMetrics 데이터를 삭제하시겠습니까?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "$1에서 이 작업을 시작했습니다. 이 과정에는 최대 30일이 소요됩니다. 다음을 참고하세요: $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "이 네트워크를 삭제하면 나중에 이 네트워크에 있는 자산을 보고 싶을 때 네트워크를 다시 추가해야 합니다" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "예치" }, + "depositCrypto": { + "message": "지갑 주소 또는 QR 코드를 사용하여 다른 계정에서 암호화폐를 입금합니다." + }, "deprecatedGoerliNtwrkMsg": { "message": "이더리움 시스템 업데이트로 인해 Goerli 테스트 네트워크는 곧 단계적으로 지원 중단될 예정입니다." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "계정" }, + "disconnectAllDescriptionText": { + "message": "이 사이트에서 연결을 해제하면, 이 사이트를 다시 사용하기 위해 계정과 네트워크를 다시 연결해야 합니다." + }, "disconnectAllSnapsText": { "message": "Snap" }, + "disconnectMessage": { + "message": "이렇게 하면 이 사이트와의 연결이 해제됩니다" + }, "disconnectPrompt": { "message": "$1 연결 해제" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "닉네임 편집" }, + "editAccounts": { + "message": "계정 편집" + }, "editAddressNickname": { "message": "주소 닉네임 편집" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "원본 네트워크 편집" }, + "editNetworksTitle": { + "message": "네트워크 편집" + }, "editNonceField": { "message": "논스 편집" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "권한 편집" }, + "editPermissions": { + "message": "권한 편집" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "가스비 가속 편집" }, + "editSpendingCap": { + "message": "지출 한도 편집" + }, + "editSpendingCapAccountBalance": { + "message": "계정 잔액: $1 $2" + }, + "editSpendingCapDesc": { + "message": "회원님을 대신하여 지출해도 부담되지 않는 금액을 입력하세요." + }, + "editSpendingCapError": { + "message": "지출 한도는 소수점 이하 $1 자리를 초과할 수 없습니다. 계속하려면 소수점 이하 숫자를 제거하세요." + }, "enableAutoDetect": { "message": " 자동 감지 활성화" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "ENS를 조회하지 못했습니다." }, + "enterANameToIdentifyTheUrl": { + "message": "URL을 식별할 이름을 입력하세요" + }, "enterANumber": { "message": "금액 입력" }, + "enterChainId": { + "message": "체인 ID 입력" + }, "enterCustodianToken": { "message": "$1 토큰을 입력하거나 새로운 토큰을 추가하세요" }, "enterMaxSpendLimit": { "message": "최대 지출 한도 입력" }, + "enterNetworkName": { + "message": "네트워크 이름 입력" + }, "enterOptionalPassword": { "message": "선택적 비밀번호를 입력하세요" }, "enterPasswordContinue": { "message": "계속하려면 비밀번호를 입력하세요" }, + "enterRpcUrl": { + "message": "RPC URL 입력" + }, + "enterSymbol": { + "message": "심볼 입력" + }, "enterTokenNameOrAddress": { "message": "토큰 이름 입력 또는 주소 붙여넣기" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "실험적" }, + "exportYourData": { + "message": "데이터 내보내기" + }, + "exportYourDataButton": { + "message": "다운로드" + }, + "exportYourDataDescription": { + "message": "계약 및 환경설정 등의 데이터를 내보낼 수 있습니다." + }, "extendWalletWithSnaps": { "message": "커뮤니티에서 만들어진 Snap을 알아보고 웹3 경험을 개인 맞춤하세요.", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "$1에서 이 가스비를 제안했습니다. 이를 무시하면 트랜잭션에 문제가 발생할 수 있습니다. 질문이 있는 경우 $1에 문의하세요.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "가스비" + }, "gasIsETH": { "message": "가스는 $1입니다 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "오류가 발생했습니다...." }, + "generalDescription": { + "message": "여러 기기의 설정을 동기화하고, 네트워크 환경을 선택하며, 토큰 데이터를 추적하세요" + }, "genericExplorerView": { "message": "$1에서 계정 보기" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "앱에 로그인하지 못하거나 새로운 기기를 사용할 경우 자금을 잃을 수 있습니다. 반드시 $1에 비밀 복구 구문을 백업하세요.", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "모두 무시" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "설정에서" }, + "included": { + "message": "포함됨" + }, "infuraBlockedNotification": { "message": "MetaMask이 블록체인 호스트에 연결할 수 없습니다. $1 오류 가능성을 검토하세요.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "JSON 파일", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "비밀복구구문을 안전한 장소에 보관하세요. 비밀복구구문을 분실하는 경우 아무도 복구를 도울 수 없으며, 영원히 지갑에 액세스하지 못할 수도 있습니다. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "계정 이름" }, @@ -2402,6 +2622,9 @@ "message": "$1 방법 알아보기", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "방법 확인하기" + }, "learnMore": { "message": "자세히 알아보기" }, @@ -2409,6 +2632,9 @@ "message": "가스에 대해 $1하시겠습니까?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "개인정보 보호 모범 사례에 대해 자세히 알아보세요." + }, "learnMoreKeystone": { "message": "자세히 알아보기" }, @@ -2497,6 +2723,9 @@ "link": { "message": "링크" }, + "linkCentralizedExchanges": { + "message": "Coinbase나 Binance 계정을 연결하여 무료로 암호화폐를 MetaMask로 전송하세요." + }, "links": { "message": "링크" }, @@ -2557,6 +2786,9 @@ "message": "다른 사람이 이 화면을 보고 있지는 않은지 확인하세요.", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "기본 개인정보 보호 설정 관리" + }, "marketCap": { "message": "시가 총액" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "방문 중인 웹사이트가 현재 선택한 계정에 연결되어 있다면 연결 상태 버튼이 표시됩니다." }, + "metaMetricsIdNotAvailableError": { + "message": "MetaMetrics에 가입한 적이 없으므로 여기에는 삭제할 데이터가 없습니다" + }, "metadataModalSourceTooltip": { "message": "$1 스냅은 npm이 호스팅합니다. 이 Snap의 고유 식별자는 $2 입니다.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "그 외" }, + "moreAccounts": { + "message": "$1개의 계정 추가", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "$1개의 네트워크 추가", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "MetaMask에 이 네트워크를 추가하고 이 사이트에 사용 권한을 허용합니다." + }, "multipleSnapConnectionWarning": { "message": "$1에서 $2 Snap 사용을 원합니다.", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "네트워크 세부 정보 편집" }, "nativeTokenScamWarningDescription": { - "message": "이 네트워크는 연결된 체인 ID 또는 이름이 일치하지 않습니다. 여러 인기 토큰이 $1(이)라는 이름을 사용하기 때문에 사기의 표적이 되고 있습니다. 사기꾼은 더 가격이 높은 암호화폐를 주겠다고 속일 수 있습니다. 계속하기 전에 모든 사항을 확인하세요.", + "message": "토큰의 네이티브 심볼이 체인 ID와 연관된 네트워크의 토큰의 네이티브 심볼과 일치하지 않습니다. $1을(를) 입력하셨지만 예상되는 토큰의 네이티브 심볼은 $2입니다. 올바른 체인에 연결되어 있는지 확인해 주세요.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "다른 선택", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "사기일 수 있습니다", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "네트워크 세부 정보" }, + "networkFee": { + "message": "네트워크 수수료" + }, "networkIsBusy": { "message": "네트워크 사용량이 많습니다. 가스비가 높고 견적의 정확도도 떨어집니다." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "네트워크 옵션" }, + "networkPermissionToast": { + "message": "네트워크 권한이 업데이트됨" + }, "networkProvider": { "message": "네트워크 공급업체" }, @@ -2865,15 +3121,26 @@ "message": "$1 연결이 불가능합니다", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "네트워크가 $1(으)로 전환됨", + "description": "$1 represents the network name" + }, "networkURL": { "message": "네트워크 URL" }, "networkURLDefinition": { "message": "이 네트워크에 액세스하는 데 사용되는 URL입니다." }, + "networkUrlErrorWarning": { + "message": "공격자는 사이트 주소를 약간 변경하여 유사 사이트를 만들기도 합니다. 계속하기 전에 정상적인 사이트와 상호 작용하고 있는지 확인하세요. Punycode 버전: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "네트워크" }, + "networksSmallCase": { + "message": "네트워크" + }, "nevermind": { "message": "괜찮습니다" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "개인정보 처리방침이 개정되었습니다" }, + "newRpcUrl": { + "message": "새 RPC URL" + }, "newTokensImportedMessage": { "message": "$1 토큰을 성공적으로 불러왔습니다.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask가 이 사이트와 연결되어 있지 않습니다" }, + "noConnectionDescription": { + "message": "사이트에 연결하려면 \"연결\" 버튼을 찾아 클릭하세요. MetaMask는 웹3의 사이트에만 연결할 수 있습니다." + }, "noConversionRateAvailable": { "message": "사용 가능한 환율 없음" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "맞춤 논스" }, + "none": { + "message": "없음" + }, "notBusy": { "message": "바쁘지 않음" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "권한 세부 정보" }, + "permissionFor": { + "message": "승인:" + }, + "permissionFrom": { + "message": "승인자:" + }, "permissionRequest": { "message": "권한 요청" }, @@ -3593,6 +3875,14 @@ "message": "MetaMask 설정에서 원하는 언어로 $1 스냅을 사용하세요. 이 기능을 사용하면 선택한 언어로 $1의 콘텐츠를 현지화하고 표시할 수 있습니다.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "선호하는 언어 및 명목화폐와 같은 정보를 확인하세요.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "MetaMask 설정에서 $1이(가) 회원님이 선호하는 언어 및 명목화폐와 같은 정보에 접근할 수 있도록 허용하세요. 이렇게 하면 $1이(가) 회원님의 선호도에 맞는 콘텐츠를 표시하는 데 도움이 됩니다. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "사용자 지정 화면 표시", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "내 계정에서 이만큼의 토큰을 사용할 수 있도록 승인합니다." }, + "permittedChainToastUpdate": { + "message": "$1은(는) $2에 액세스 할 수 있습니다." + }, "personalAddressDetected": { "message": "개인 주소가 발견되었습니다. 토큰 계약 주소를 입력하세요." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "받기" }, + "receiveCrypto": { + "message": "암호화폐 받기" + }, + "recipientAddressPlaceholderNew": { + "message": "공개 주소(0x) 또는 도메인 이름 입력" + }, "recommendedGasLabel": { "message": "권장됨" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "거부됨" }, + "rememberSRPIfYouLooseAccess": { + "message": "비밀 복구 구문을 분실하면 지갑에 액세스할 수 없습니다. 언제든지 자금에 접근할 수 있도록 $1(을)를 통해 이 단어들을 안전하게 보관하세요" + }, + "reminderSet": { + "message": "알림 설정 완료!" + }, "remove": { "message": "제거" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "오류로 인해 보안업체가 이 요청을 확인하지 못했습니다. 주의하여 진행하세요." }, + "requestingFor": { + "message": "요청 중:" + }, + "requestingForAccount": { + "message": "$1 요청 중", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "확인 대기 중인 요청" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "시드 구문 보기" }, + "review": { + "message": "검토" + }, + "reviewAlert": { + "message": "경고 검토" + }, "reviewAlerts": { "message": "경고 검토하기" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "권한 철회" }, + "revokeSimulationDetailsDesc": { + "message": "계정에서 토큰을 사용할 수 있는 다른 사람의 권한을 제거합니다." + }, "revokeSpendingCap": { "message": "$1에 대한 지출 한도 취소", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "해당 타사는 현재는 물론 미래에도 토큰을 더 이상 사용할 수 없습니다." }, + "rpcNameOptional": { + "message": "RPC 이름(선택)" + }, "rpcUrl": { "message": "새 RPC URL" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "보안 및 프라이버시" }, + "securityDescription": { + "message": "안전하지 않은 네트워크에 가입할 가능성을 줄이고 계정을 보호하세요" + }, + "securityMessageLinkForNetworks": { + "message": "네트워크 사기 및 보안 위험" + }, + "securityPrivacyPath": { + "message": "설정 > 보안 및 개인정보" + }, "securityProviderPoweredBy": { "message": "$1 제공", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "모든 권한 보기", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "세부 정보 보기" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "원하는 계정이 표시되지 않는다면 HD 경로 또는 현재 선택한 네트워크를 전환해 보세요." }, + "selectRpcUrl": { + "message": "RPC URL 선택" + }, "selectType": { "message": "유형 선택" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "모두 승인 설정" }, + "setApprovalForAllRedesignedTitle": { + "message": "인출 요청" + }, "setApprovalForAllTitle": { "message": "$1 무제한 지출 승인", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "설정" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "손쉬운 사용과 보안을 위해 설정이 최적화되었습니다. 언제든지 변경할 수 있습니다." + }, "settingsSearchMatchingNotFound": { "message": "검색 결과가 없습니다." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "더 보기" }, + "showNativeTokenAsMainBalance": { + "message": "네이티브 토큰을 기본 잔액으로 표시" + }, "showNft": { "message": "NFT 표시" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "다음으로 로그인:" }, + "simulationApproveHeading": { + "message": "인출" + }, + "simulationDetailsApproveDesc": { + "message": "다른 사람에게 내 계정에서 NFT를 인출할 수 있는 권한을 부여합니다." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "다른 사람에게 계정에서 이 금액을 사용할 수 있는 권한을 부여합니다." + }, "simulationDetailsFiatNotAvailable": { "message": "이용할 수 없음" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "보냄:" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "계정에서 NFT를 인출할 수 있는 다른 사람의 권한을 제거합니다." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "다른 사람에게 계정에서 NFT를 인출할 수 있도록 권한을 부여합니다." + }, "simulationDetailsTitle": { "message": "예상 변동 사항" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "죄송합니다! 문제가 생겼습니다." }, + "sortBy": { + "message": "정렬 기준:" + }, + "sortByAlphabetically": { + "message": "알파벳순(A-Z)" + }, + "sortByDecliningBalance": { + "message": "잔액 내림차순($1 최고-최저)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "소스" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "사용자" }, + "spenderTooltipDesc": { + "message": "NFT를 인출할 수 있는 주소입니다." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "이 주소는 회원님을 대신하여 토큰을 사용할 수 있는 주소입니다." + }, "spendingCap": { "message": "지출 한도" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "$1 관련 지출 한도 요청" }, + "spendingCapTooltipDesc": { + "message": "지출자가 회원님을 대신하여 접근할 수 있는 토큰의 양입니다." + }, "srpInputNumberOfWords": { "message": "제 구문은 $1개의 단어로 이루어져 있습니다", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "제안인: $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "제안된 통화 심볼:" + }, "suggestedTokenName": { "message": "추천 이름:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "지원 센터 방문하기" }, + "supportMultiRpcInformation": { + "message": "이제 단일 네트워크에 대해 여러 개의 RPC를 지원합니다. 충돌하는 정보를 해결하기 위해 최근의 RPC를 기본값으로 선택했습니다." + }, "surveyConversion": { "message": "설문조사에 참여하세요" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "가스비는 예상치이며 네트워크 트래픽 및 트랜잭션 복잡성에 따라 변동됩니다." }, + "swapGasFeesExplanation": { + "message": "MetaMask는 가스비로 수익을 얻지 않습니다. 이 가스비는 추정치이며 네트워크의 혼잡도와 거래의 복잡성에 따라 변할 수 있습니다. 자세히 알아보기 $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "여기", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "가스비 자세히 알아보기" }, @@ -5186,9 +5583,19 @@ "message": "가스비는 $1 네트워크에서 트랜잭션을 처리하는 암호화폐 채굴자에게 지급됩니다. MetaMask는 가스비로 수익을 창출하지 않습니다.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "이 견적에는 보내거나 받는 토큰 금액을 조정하는 방식으로 가스비가 포함됩니다. 활동 목록에서 별도의 거래로 ETH를 받을 수 있습니다." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "가스비에 대해 더 자세히 알아보기" + }, "swapHighSlippage": { "message": "높은 슬리피지" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "가스비 및 $1%의 MetaMask 수수료가 포함됩니다", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "$1%의 MetaMask 요금이 포함됩니다.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "이용약관이 개정되었습니다" }, + "testnets": { + "message": "테스트넷" + }, "theme": { "message": "테마" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "트랜잭션 수수료" }, + "transactionFlowNetwork": { + "message": "네트워크" + }, "transactionHistoryBaseFee": { "message": "기본 수수료(GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "전송" }, + "transferCrypto": { + "message": "암호화폐 전송" + }, "transferFrom": { "message": "전송 위치" }, + "transferRequest": { + "message": "전송 요청" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "업데이트" }, + "updateEthereumChainConfirmationDescription": { + "message": "이 사이트에서 기본 네트워크 URL 업데이트를 요청하고 있습니다. 기본값 및 네트워크 정보는 언제든지 수정할 수 있습니다." + }, + "updateNetworkConfirmationTitle": { + "message": "$1 업데이트", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "정보를 업데이트하거나" }, "updateRequest": { "message": "업데이트 요청" }, + "updatedRpcForNetworks": { + "message": "네트워크 RPC 업데이트 완료" + }, "uploadDropFile": { "message": "여기에 파일을 드롭" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "당사의 하드웨어 지갑 연결 가이드" }, + "walletProtectedAndReadyToUse": { + "message": "지갑이 보호되고 있으며 사용할 준비가 되었습니다. 다음에서 비밀 복구 구문을 확인할 수 있습니다: $1", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "이 네트워크를 추가하시겠습니까?" }, @@ -5991,6 +6424,17 @@ "message": "$1 타사는 추가 통보나 동의 없이도 남은 토큰 전체를 사용할 수 있습니다. 보호를 위해 지출 한도를 하향하세요.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "이 옵션을 켜면 공개 주소 또는 ENS 이름을 통해 이더리움 계정을 볼 수 있게 됩니다. 이 베타 기능에 대한 피드백을 보내주시려면 이 $1을(를) 작성해 주세요.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "이더리움 계정 모니터(베타)" + }, + "watchOutMessage": { + "message": "$1에 주의하세요.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "약함" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "이것은 무엇인가요?" }, + "withdrawing": { + "message": "인출" + }, "wrongNetworkName": { "message": "기록에 따르면 네트워크 이름이 이 체인 ID와 일치하지 않습니다." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "내 잔액" }, + "yourBalanceIsAggregated": { + "message": "잔액이 집계되었습니다." + }, "yourNFTmayBeAtRisk": { "message": "NFT가 위험할 수 있습니다" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "블록체인에서 거래가 확인되기 전에는 거래를 취소할 수 없습니다." }, + "yourWalletIsReady": { + "message": "지갑이 준비되었습니다" + }, "zeroGasPriceOnSpeedUpError": { "message": "가속화 시 가스 가격 0" } diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index e868c22a817e..1e39077afa51 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Conecte sua carteira de hardware QR" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "O endereço na solicitação de entrada não coincide com o endereço da conta que você está usando para entrar." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Selecione as contas das quais deseja receber notificações:" }, + "accountBalance": { + "message": "Saldo da conta" + }, "accountDetails": { "message": "Detalhes da conta" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Opções da conta" }, + "accountPermissionToast": { + "message": "Permissões de conta atualizadas" + }, "accountSelectionRequired": { "message": "Você precisa selecionar uma conta!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Contas conectadas" }, + "accountsPermissionsTitle": { + "message": "Ver suas contas e sugerir transações" + }, + "accountsSmallCase": { + "message": "contas" + }, "active": { "message": "Ativo" }, @@ -180,12 +195,18 @@ "add": { "message": "Adicionar" }, + "addACustomNetwork": { + "message": "Adicionar uma rede personalizada" + }, "addANetwork": { "message": "Adicionar uma rede" }, "addANickname": { "message": "Adicionar um apelido" }, + "addAUrl": { + "message": "Adicionar um URL" + }, "addAccount": { "message": "Adicionar conta" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Adicionar um explorador de blocos" }, + "addBlockExplorerUrl": { + "message": "Adicionar URL de um explorador de blocos" + }, "addContact": { "message": "Adicionar contato" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Você está adicionando um novo provedor RPC para a Mainnet da Ethereum" }, + "addEthereumWatchOnlyAccount": { + "message": "Visualize uma conta Ethereum (Beta)" + }, "addFriendsAndAddresses": { "message": "Adicionar amigos e endereços confiáveis" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Adicionar rede" }, + "addNetworkConfirmationTitle": { + "message": "Adicionar $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Adicionar uma nova conta Ethereum" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Endereço copiado!" }, + "addressMismatch": { + "message": "Divergência no endereço do site" + }, + "addressMismatchOriginal": { + "message": "URL atual: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Versão Punycode: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Avançado" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "A taxa de prioridade (ou seja, \"gorjeta do minerador\") vai diretamente para os mineradores e os incentiva a priorizar a sua transação." }, + "aggregatedBalancePopover": { + "message": "Isso reflete o valor de todos os tokens que você possui em uma dada rede. Se você preferir ver esse valor em ETH ou outras moedas, acesse a $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Concordo com os $1 da MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Isso pode ser alterado em \"Configurações > Alertas\"" }, + "alertMessageAddressMismatchWarning": { + "message": "Golpistas às vezes imitam os sites fazendo pequenas alterações em seus endereços. Certifique-se de estar interagindo com o site correto antes de continuar." + }, "alertMessageGasEstimateFailed": { "message": "Não conseguimos fornecer uma taxa precisa, e essa estimativa pode estar alta. Sugerimos que você informe um limite de gás personalizado, mas há o risco de a transação falhar mesmo assim." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Para continuar com essa transação, você precisará aumentar o limite de gás para 21000 ou mais." }, + "alertMessageInsufficientBalance2": { + "message": "Você não tem ETH suficiente em sua conta para pagar as taxas de rede." + }, "alertMessageNetworkBusy": { "message": "Os preços do gás são altos e as estimativas são menos precisas." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Opções do ativo" }, + "assets": { + "message": "Ativos" + }, + "assetsDescription": { + "message": "Detecte automaticamente os tokens em sua carteira, exiba NFTs e receba atualizações de saldo de contas em lote" + }, "attemptSendingAssets": { "message": "Você poderá perder seus ativos se tentar enviá-los a partir de outra rede. Transfira fundos entre redes com segurança usando uma ponte." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Usar uma ponte, não enviar" }, + "bridgeFrom": { + "message": "Ponte de" + }, + "bridgeSelectNetwork": { + "message": "Selecionar rede" + }, + "bridgeTo": { + "message": "Ponte para" + }, "browserNotSupported": { "message": "Seu navegador não é compatível..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Confirmar Frase de Recuperação Secreta" }, + "confirmTitleApproveTransaction": { + "message": "Solicitação de limite de gasto" + }, + "confirmTitleDeployContract": { + "message": "Implementar um contrato" + }, + "confirmTitleDescApproveTransaction": { + "message": "Este site quer permissão para sacar seus NFTs" + }, + "confirmTitleDescDeployContract": { + "message": "Este site quer que você implemente um contrato" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Este site quer permissão para sacar seus tokens" + }, "confirmTitleDescPermitSignature": { "message": "Este site quer permissão para gastar seus tokens." }, "confirmTitleDescSIWESignature": { "message": "Um site quer que você faça login para comprovar que é titular desta conta." }, + "confirmTitleDescSign": { + "message": "Revise os detalhes da solicitação antes de confirmar." + }, "confirmTitlePermitTokens": { "message": "Solicitação de limite de gastos" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Remover permissão" + }, "confirmTitleSIWESignature": { "message": "Solicitação de entrada" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Remover permissão" + }, "confirmTitleSignature": { "message": "Solicitação de assinatura" }, "confirmTitleTransaction": { "message": "Solicitação de transação" }, + "confirmationAlertModalDetails": { + "message": "Para proteger seus ativos e informações de login, é recomendável que você recuse a solicitação." + }, + "confirmationAlertModalTitle": { + "message": "Esta solicitação é suspeita" + }, "confirmed": { "message": "Confirmada" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "Detectamos um caractere ambíguo no nome ENS. Verifique o nome ENS para evitar um possível golpe." }, + "congratulations": { + "message": "Parabéns!" + }, "connect": { "message": "Conectar" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Sites conectados" }, + "connectedSitesAndSnaps": { + "message": "Sites e Snaps conectados" + }, "connectedSitesDescription": { "message": "$1 está conectada a esses sites. Eles podem visualizar o endereço da sua conta.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "A MetaMask está conectada a este site, mas nenhuma conta está conectada ainda" }, + "connectedSnaps": { + "message": "Snaps conectados" + }, + "connectedWithAccount": { + "message": "$1 contas conectadas", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Conectado com $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Conectando" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Conectando à rede de teste Sepolia" }, + "connectionDescription": { + "message": "Este site quer" + }, "connectionFailed": { "message": "Falha na conexão" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Copiar endereço para a área de transferência" }, + "copyAddressShort": { + "message": "Copiar endereço" + }, "copyPrivateKey": { "message": "Copiar chave privada" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "URL padrão da RPC" }, + "defaultSettingsSubTitle": { + "message": "A MetaMask usa as configurações padrão para melhor equilibrar a segurança e a facilidade de uso. Altere essas configurações para aumentar ainda mais sua privacidade." + }, + "defaultSettingsTitle": { + "message": "Configurações de privacidade padrão" + }, "delete": { "message": "Excluir" }, "deleteContact": { "message": "Excluir contato" }, + "deleteMetaMetricsData": { + "message": "Excluir dados do MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Isso excluirá dados históricos do MetaMetrics associados ao seu uso neste dispositivo. Sua carteira e contas continuarão exatamente como estão agora após a exclusão desses dados. Esse processo pode levar até 30 dias. Veja nossa $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Não é possível atender a essa solicitação no momento devido a um problema no servidor do sistema de análises. Tente novamente mais tarde" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Não é possível excluir estes dados no momento" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Estamos prestes a remover todos os seus dados do MetaMetrics. Tem certeza?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Excluir dados do MetaMetrics?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Você iniciou essa ação em $1. Esse processo pode levar até 30 dias. Consulte a $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Se excluir essa rede, você precisará adicioná-la novamente para ver seus ativos nela" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Depositar" }, + "depositCrypto": { + "message": "Deposite criptomoedas de outra conta com um endereço de carteira ou código QR." + }, "deprecatedGoerliNtwrkMsg": { "message": "Devido a atualizações no sistema Ethereum, a rede de teste Goerli será descontinuada em breve." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "contas" }, + "disconnectAllDescriptionText": { + "message": "Se você se desconectar deste site, precisará reconectar suas contas e redes para usar este site novamente." + }, "disconnectAllSnapsText": { "message": "Snaps" }, + "disconnectMessage": { + "message": "Isso desconectará você deste site" + }, "disconnectPrompt": { "message": "Desconectar $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Editar apelido" }, + "editAccounts": { + "message": "Editar contas" + }, "editAddressNickname": { "message": "Editar apelido do endereço" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "editar a rede original" }, + "editNetworksTitle": { + "message": "Editar redes" + }, "editNonceField": { "message": "Editar nonce" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Editar permissão" }, + "editPermissions": { + "message": "Editar permissões" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Editar taxa de gás para aceleração" }, + "editSpendingCap": { + "message": "Editar limite de gastos" + }, + "editSpendingCapAccountBalance": { + "message": "Saldo da conta: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Insira o valor que você considera adequado que seja gasto em seu nome." + }, + "editSpendingCapError": { + "message": "O limite de gastos não pode exceder $1 dígitos decimais. Remova os dígitos decimais para continuar." + }, "enableAutoDetect": { "message": " Ativar detecção automática" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "Falha na busca de ENS." }, + "enterANameToIdentifyTheUrl": { + "message": "Insira um nome para identificar o URL" + }, "enterANumber": { "message": "Insira um número" }, + "enterChainId": { + "message": "Insira a ID da cadeia" + }, "enterCustodianToken": { "message": "Insira seu token $1 ou adicione um novo" }, "enterMaxSpendLimit": { "message": "Digite um limite máximo de gastos" }, + "enterNetworkName": { + "message": "Insira o nome da rede" + }, "enterOptionalPassword": { "message": "Insira a senha opcional" }, "enterPasswordContinue": { "message": "Insira a senha para continuar" }, + "enterRpcUrl": { + "message": "Insira o URL da RPC" + }, + "enterSymbol": { + "message": "Insira o símbolo" + }, "enterTokenNameOrAddress": { "message": "Insira o nome do token ou cole o endereço" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Experimental" }, + "exportYourData": { + "message": "Exportar seus dados" + }, + "exportYourDataButton": { + "message": "Baixar" + }, + "exportYourDataDescription": { + "message": "Você pode exportar dados como seus contatos e preferências." + }, "extendWalletWithSnaps": { "message": "Explore Snaps desenvolvidos pela comunidade para personalizar sua experiência na web3", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Essa taxa de gás foi sugerida por $1. Sua substituição pode causar um problema com a sua transação. Entre em contato com $1 se tiver perguntas.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Taxa de gás" + }, "gasIsETH": { "message": "O gás é $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Algo deu errado..." }, + "generalDescription": { + "message": "Sincronize as configurações entre dispositivos, selecione as preferências de rede e rastreie dados de tokens" + }, "genericExplorerView": { "message": "Ver conta na $1" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "Se você ficar impedido de entrar no app ou se usar um novo dispositivo, perderá seus fundos. Certifique-se de fazer backup da sua Frase de Recuperação Secreta em $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Ignorar tudo" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "em suas Configurações" }, + "included": { + "message": "incluída" + }, "infuraBlockedNotification": { "message": "Não foi possível conectar a MetaMask ao servidor da blockchain. Revise possíveis motivos $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "Arquivo JSON", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Guarde um lembrete da sua Frase de Recuperação Secreta em algum lugar seguro. Se você a perder, ninguém poderá ajudar a recuperá-la. Pior ainda, você nunca mais poderá acessar sua carteira. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Nome da conta" }, @@ -2402,6 +2622,9 @@ "message": "Saiba como $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Saiba como" + }, "learnMore": { "message": "saiba mais" }, @@ -2409,6 +2632,9 @@ "message": "Quer $1 sobre gás?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Saiba mais sobre as melhores práticas de privacidade." + }, "learnMoreKeystone": { "message": "Saiba mais" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Link" }, + "linkCentralizedExchanges": { + "message": "Vincule suas contas Coinbase ou Binance para transferir gratuitamente criptomoedas para a MetaMask." + }, "links": { "message": "Links" }, @@ -2557,6 +2786,9 @@ "message": "Certifique-se de que ninguém está olhando", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Gerenciar configurações de privacidade padrão" + }, "marketCap": { "message": "Capitalização de mercado" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "O botão de status da conexão mostra se o website que você está visitando está conectado à conta selecionada no momento." }, + "metaMetricsIdNotAvailableError": { + "message": "Visto que você nunca aceitou participar do MetaMetrics, não há dados para excluir aqui." + }, "metadataModalSourceTooltip": { "message": "$1 está hospedado no npm e $2 é o identificador específico deste Snap.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "mais" }, + "moreAccounts": { + "message": "Mais $1 contas adicionais", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "Mais $1 redes adicionais", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Você está adicionando esta rede à MetaMask e dando a este site permissão para usá-la." + }, "multipleSnapConnectionWarning": { "message": "$1 quer usar Snaps de $2", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Editar detalhes da rede" }, "nativeTokenScamWarningDescription": { - "message": "Esta rede não corresponde ao seu ID da cadeia ou nome associado. Como muitos tokens populares usam o nome $1, ele é visado para golpes. Golpistas podem enganar você para que envie a eles alguma moeda mais valiosa em troca. Confirme todas as informações antes de prosseguir.", + "message": "O símbolo do token nativo é diferente do símbolo esperado do token nativo da rede com a ID da cadeia associada. Você inseriu $1, enquanto o símbolo de token esperado é $2. Verifique se você está conectado à cadeia correta.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "outra coisa", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Isto é um possível golpe", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Detalhes da rede" }, + "networkFee": { + "message": "Taxa de rede" + }, "networkIsBusy": { "message": "A rede está ocupada. Os preços de gás estão altos e as estimativas estão menos exatas." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Opções da rede" }, + "networkPermissionToast": { + "message": "Permissões de rede atualizadas" + }, "networkProvider": { "message": "Provedor de rede" }, @@ -2865,15 +3121,26 @@ "message": "Não podemos conectar a $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Rede mudou para $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "URL da rede" }, "networkURLDefinition": { "message": "O URL usado para acessar essa rede." }, + "networkUrlErrorWarning": { + "message": "Golpistas às vezes imitam os sites fazendo pequenas alterações em seus endereços. Certifique-se de estar interagindo com o site correto antes de continuar. Versão Punycode: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Redes" }, + "networksSmallCase": { + "message": "redes" + }, "nevermind": { "message": "Desistir" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Atualizamos nossa política de privacidade" }, + "newRpcUrl": { + "message": "URL da nova RPC" + }, "newTokensImportedMessage": { "message": "Você importou $1 com sucesso.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "A MetaMask não está conectada a este site" }, + "noConnectionDescription": { + "message": "Para se conectar a um site, encontre e selecione o botão \"conectar\". Lembre-se de que a MetaMask só pode se conectar a sites na web3" + }, "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Nonce personalizado" }, + "none": { + "message": "Nenhum" + }, "notBusy": { "message": "Não ocupado" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Detalhes da permissão" }, + "permissionFor": { + "message": "Permissão para" + }, + "permissionFrom": { + "message": "Permissão de" + }, "permissionRequest": { "message": "Solicitação de permissão" }, @@ -3593,6 +3875,14 @@ "message": "Permita que $1 acesse seu idioma de preferência a partir de suas configurações da MetaMask. Isso pode ser usado para traduzir e exibir o conteúdo de $1 usando seu idioma.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Ver informações como seu idioma preferencial e moeda fiduciária.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Permita que $1 acesse informações como seu idioma preferencial e moeda fiduciária em suas configurações da MetaMask. Isso ajuda $1 a exibir conteúdo ajustado às suas preferências. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Exibe uma tela personalizada", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Você está autorizando o consumidor a gastar esta quantidade de tokens de sua conta." }, + "permittedChainToastUpdate": { + "message": "$1 tem acesso a $2." + }, "personalAddressDetected": { "message": "Endereço pessoal detectado. Insira o endereço de contrato do token." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Receber" }, + "receiveCrypto": { + "message": "Receber criptomoeda" + }, + "recipientAddressPlaceholderNew": { + "message": "Insira o endereço público (0x) ou nome de domínio" + }, "recommendedGasLabel": { "message": "Recomendado" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Recusada" }, + "rememberSRPIfYouLooseAccess": { + "message": "Lembre-se, se você perder sua Frase de Recuperação Secreta, perderá acesso à sua carteira. $1 manter esse conjunto de palavras em segurança, para que possa acessar seus fundos a qualquer momento." + }, + "reminderSet": { + "message": "Lembrete definido!" + }, "remove": { "message": "Remover" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Por causa de um erro, essa solicitação não foi verificada pelo provedor de segurança. Prossiga com cautela." }, + "requestingFor": { + "message": "Solicitando para" + }, + "requestingForAccount": { + "message": "Solicitando para $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "solicitações aguardando confirmação" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Revelar a frase-semente" }, + "review": { + "message": "Revisar" + }, + "reviewAlert": { + "message": "Rever alerta" + }, "reviewAlerts": { "message": "Conferir alertas" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Revogar permissão" }, + "revokeSimulationDetailsDesc": { + "message": "Você está removendo a permissão para alguém gastar tokens da sua conta." + }, "revokeSpendingCap": { "message": "Revogar limite de gastos de seu $1", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Esse terceiro não poderá gastar mais nenhum dos seus tokens atuais ou futuros." }, + "rpcNameOptional": { + "message": "Nome da RPC (opcional)" + }, "rpcUrl": { "message": "Novo URL da RPC" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Segurança e Privacidade" }, + "securityDescription": { + "message": "Reduza suas chances de ingressar em redes perigosas e proteja suas contas" + }, + "securityMessageLinkForNetworks": { + "message": "golpes de rede e riscos de segurança" + }, + "securityPrivacyPath": { + "message": "Configurações > Segurança e Privacidade." + }, "securityProviderPoweredBy": { "message": "Com tecnologia da $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Ver todas as permissões", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Ver detalhes" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Se as contas que você esperava não forem exibidas, tente mudar o caminho do HD ou a rede atualmente selecionada." }, + "selectRpcUrl": { + "message": "Selecionar URL da RPC" + }, "selectType": { "message": "Selecione o tipo" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Definir aprovação para todos" }, + "setApprovalForAllRedesignedTitle": { + "message": "Solicitação de saque" + }, "setApprovalForAllTitle": { "message": "Aprovar $1 sem limite de gastos", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Configurações" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "As configurações são otimizadas para facilidade de uso e segurança. Altere-as quando quiser." + }, "settingsSearchMatchingNotFound": { "message": "Nenhum resultado correspondente encontrado." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Exibir mais" }, + "showNativeTokenAsMainBalance": { + "message": "Exibir tokens nativos como saldo principal" + }, "showNft": { "message": "Exibir NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Assinando com" }, + "simulationApproveHeading": { + "message": "Sacar" + }, + "simulationDetailsApproveDesc": { + "message": "Você está dando a outra pessoa permissão para sacar NFTs da sua conta." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Você está dando a alguém permissão para gastar esse valor da sua conta." + }, "simulationDetailsFiatNotAvailable": { "message": "Não disponível" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Você envia" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Você está removendo a permissão para outra pessoa sacar NFTs da sua conta." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Você está dando permissão para outra pessoa sacar NFTs de sua conta." + }, "simulationDetailsTitle": { "message": "Alterações estimadas" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Ops! Algo deu errado." }, + "sortBy": { + "message": "Classificar por" + }, + "sortByAlphabetically": { + "message": "Alfabeticamente (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Saldo decrescente ($1 alto-baixo)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Fonte" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Consumidor" }, + "spenderTooltipDesc": { + "message": "Este é o endereço que poderá sacar seus NFTs." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Este é o endereço que poderá gastar seus tokens em seu nome." + }, "spendingCap": { "message": "Limite de gastos" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Solicitação de limite de gastos para seu $1" }, + "spendingCapTooltipDesc": { + "message": "Esta é a quantidade de tokens que o consumidor poderá acessar em seu nome." + }, "srpInputNumberOfWords": { "message": "Eu tenho uma frase com $1 palavras", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Sugerido por $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Símbolo sugerido para a moeda:" + }, "suggestedTokenName": { "message": "Nome sugerido:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Visite a nossa Central de Suporte" }, + "supportMultiRpcInformation": { + "message": "Agora oferecemos suporte a várias RPCs para uma única rede. Sua RPC mais recente foi selecionada como padrão para resolver informações conflitantes." + }, "surveyConversion": { "message": "Responda à nossa pesquisa" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "As taxas de gás são estimadas e oscilam com base no tráfego da rede e na complexidade da transação." }, + "swapGasFeesExplanation": { + "message": "A MetaMask não ganha dinheiro com taxas de gás. Essas taxas são estimativas e podem mudar de acordo com a carga de atividade da rede e a complexidade da transação. Saiba mais $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "aqui", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Saiba mais sobre as taxas de gás" }, @@ -5186,9 +5583,19 @@ "message": "As taxas de gás são pagas aos mineradores de criptoativos que processam as transações na rede de $1. A MetaMask não lucra com taxas de gás.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Esta cotação incorpora taxas de gás através de ajuste do valor do token enviado ou recebido. Você pode receber ETH em uma transação separada em sua lista de atividades." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Saiba mais sobre taxas de gás" + }, "swapHighSlippage": { "message": "Slippage alto" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Inclui taxa de gás e uma taxa de $1% da MetaMask", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Inclui uma taxa de $1% da MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Nossos Termos de Uso foram atualizados" }, + "testnets": { + "message": "Testnets" + }, "theme": { "message": "Tema" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Taxa de transação" }, + "transactionFlowNetwork": { + "message": "Rede" + }, "transactionHistoryBaseFee": { "message": "Taxa-base (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Transferir" }, + "transferCrypto": { + "message": "Transferir criptomoedas" + }, "transferFrom": { "message": "Transferir de" }, + "transferRequest": { + "message": "Solicitação de transferência" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "Atualizar" }, + "updateEthereumChainConfirmationDescription": { + "message": "Este site está solicitando a atualização do URL de rede padrão. Você pode editar padrões e informações de rede quando quiser." + }, + "updateNetworkConfirmationTitle": { + "message": "Atualizar $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Atualize suas informações ou" }, "updateRequest": { "message": "Solicitação de atualização" }, + "updatedRpcForNetworks": { + "message": "RPCs de rede atualizadas" + }, "uploadDropFile": { "message": "Solte seu arquivo aqui" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "nosso guia de conexão com a carteira de hardware" }, + "walletProtectedAndReadyToUse": { + "message": "Sua carteira está protegida e pronta para ser usada. Você pode encontrar sua Frase de Recuperação Secreta em $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Desejar adicionar esta rede?" }, @@ -5991,6 +6424,17 @@ "message": "$1 O terceiro pode gastar todo o seu saldo de tokens sem aviso ou consentimento. Proteja-se personalizando um limite de gastos menor.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Ativar esta opção permitirá que você visualize contas Ethereum através de um endereço público ou nome ENS. Para dar sua opinião sobre este recurso Beta, preencha este $1.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Visualizar contas Ethereum (Beta)" + }, + "watchOutMessage": { + "message": "Tome cuidado com $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Fraca" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "O que é isso?" }, + "withdrawing": { + "message": "Sacando" + }, "wrongNetworkName": { "message": "De acordo com os nossos registros, o nome da rede pode não corresponder a esta ID de cadeia." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Seu saldo" }, + "yourBalanceIsAggregated": { + "message": "Seu saldo é agregado" + }, "yourNFTmayBeAtRisk": { "message": "Seu NFT pode estar em risco" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Não pudemos cancelar sua transação antes de ser confirmada na blockchain." }, + "yourWalletIsReady": { + "message": "Sua carteira está pronta" + }, "zeroGasPriceOnSpeedUpError": { "message": "O preço do gás está zerado na aceleração" } diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index f995ce06a662..f68ee11792a1 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Подключите свой аппаратный кошелек на основе QR-кодов" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "Адрес в запросе на вход не соответствует адресу счета, который вы используете для входа." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Выберите счета, о которых хотите получать уведомления:" }, + "accountBalance": { + "message": "Баланс счета" + }, "accountDetails": { "message": "Реквизиты счета" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Параметры счета" }, + "accountPermissionToast": { + "message": "Разрешения счета обновлены" + }, "accountSelectionRequired": { "message": "Вам необходимо выбрать счет!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Счета подключены" }, + "accountsPermissionsTitle": { + "message": "Просматривать ваши счета и предлагать транзакции" + }, + "accountsSmallCase": { + "message": "счета" + }, "active": { "message": "Активный" }, @@ -180,12 +195,18 @@ "add": { "message": "Добавить" }, + "addACustomNetwork": { + "message": "Добавить пользовательскую сеть" + }, "addANetwork": { "message": "Добавить сеть" }, "addANickname": { "message": "Добавить ник" }, + "addAUrl": { + "message": "Добавить URL" + }, "addAccount": { "message": "Добавить счет" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Добавить обозреватель блоков" }, + "addBlockExplorerUrl": { + "message": "Добавить URL обозревателя блоков" + }, "addContact": { "message": "Добавить контакт" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Вы добавляете нового поставщика RPC для Мейн-нета Ethereum" }, + "addEthereumWatchOnlyAccount": { + "message": "Следить за счетом Ethereum (бета)" + }, "addFriendsAndAddresses": { "message": "Добавьте друзей и адреса, которым доверяете" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Добавить сеть" }, + "addNetworkConfirmationTitle": { + "message": "Добавить $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Добавить новый счет Ethereum" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Адрес скопирован!" }, + "addressMismatch": { + "message": "Несоответствие адреса сайта" + }, + "addressMismatchOriginal": { + "message": "Текущий URL-адрес: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Версия пьюникода: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Дополнительно" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Плата за приоритет (также известная как «чаевые» майнеру) направляется непосредственно майнерам, чтобы они уделили приоритетное внимание вашей транзакции." }, + "aggregatedBalancePopover": { + "message": "Это значение отражает стоимость всех токенов, которыми вы владеете в данной сети. Если вы предпочитаете видеть это значение в ETH или других валютах, перейдите к $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Я соглашаюсь с $1 MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Это можно изменить в разделе «Настройки» > «Оповещения»" }, + "alertMessageAddressMismatchWarning": { + "message": "Злоумышленники иногда имитируют сайты, внося небольшие изменения в адрес сайта. Прежде чем продолжить, убедитесь, что вы взаимодействуете с нужным сайтом." + }, "alertMessageGasEstimateFailed": { "message": "Мы не можем указать точную сумму комиссии, и эта оценка может быть высокой. Мы предлагаем вам ввести индивидуальный лимит газа, но существует риск, что транзакция все равно не удастся." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Чтобы продолжить эту транзакцию, вам необходимо увеличить лимит газа до 21 000 или выше." }, + "alertMessageInsufficientBalance2": { + "message": "У вас на счету недостаточно ETH для оплаты комиссий сети." + }, "alertMessageNetworkBusy": { "message": "Цены газа высоки, а оценки менее точны." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Параметры актива" }, + "assets": { + "message": "Активы" + }, + "assetsDescription": { + "message": "Автоматически обнаруживайте токены в своем кошельке, отображайте NFT и получайте пакетные обновления баланса счета" + }, "attemptSendingAssets": { "message": "Вы можете потерять свои активы, если попытаетесь отправить их из другой сети. Безопасно переводите средства между сетями с помощью моста." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Мост, не отправлять" }, + "bridgeFrom": { + "message": "Мост из" + }, + "bridgeSelectNetwork": { + "message": "Выбор сети" + }, + "bridgeTo": { + "message": "Мост в" + }, "browserNotSupported": { "message": "Ваш браузер не поддерживается..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Подтвердите секретную фразу для восстановления" }, + "confirmTitleApproveTransaction": { + "message": "Запрос квоты" + }, + "confirmTitleDeployContract": { + "message": "Развернуть контракт" + }, + "confirmTitleDescApproveTransaction": { + "message": "Этот сайт хочет получить разрешение на вывод ваших NFT" + }, + "confirmTitleDescDeployContract": { + "message": "Этот сайт хочет, чтобы вы развернули контракт" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Этот сайт хочет получить разрешение на вывод ваших токенов" + }, "confirmTitleDescPermitSignature": { "message": "Этот сайт хочет получить разрешение на расходование ваших токенов." }, "confirmTitleDescSIWESignature": { "message": "Сайт требует, чтобы вы вошли в систему и доказали, что вы являетесь владельцем этого счета." }, + "confirmTitleDescSign": { + "message": "Прежде чем подтвердить запрос, проверьте его реквизиты." + }, "confirmTitlePermitTokens": { "message": "Запрос лимита расходов" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Удалить разрешение" + }, "confirmTitleSIWESignature": { "message": "Запрос на вход" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Удалить разрешение" + }, "confirmTitleSignature": { "message": "Запрос подписи" }, "confirmTitleTransaction": { "message": "Запрос транзакции" }, + "confirmationAlertModalDetails": { + "message": "Чтобы защитить ваши активы и данные для входа, рекомендуем отклонить запрос." + }, + "confirmationAlertModalTitle": { + "message": "Этот запрос подозрительный" + }, "confirmed": { "message": "Подтвержден(-а/о)" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "В имени ENS обнаружен непонятный символ. Проверьте это имя, что исключить возможное мошенничество." }, + "congratulations": { + "message": "Поздравляем!" + }, "connect": { "message": "Подключиться" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Подключенные сайты" }, + "connectedSitesAndSnaps": { + "message": "Подключенные сайты и Snaps" + }, "connectedSitesDescription": { "message": "$1 подключен к этим сайтам. Они могут увидеть адрес вашего счета.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask подключен к этому сайту, но счета пока не подключены" }, + "connectedSnaps": { + "message": "Подключенные Snaps" + }, + "connectedWithAccount": { + "message": "Счета ($1) не подключены", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Подключен к $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Подключение..." }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Подключение к тестовой сети Sepolia..." }, + "connectionDescription": { + "message": "Этот сайт хочет" + }, "connectionFailed": { "message": "Не удалось установить подключение" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Скопировать адрес в буфер обмена" }, + "copyAddressShort": { + "message": "Копировать адрес" + }, "copyPrivateKey": { "message": "Копировать закрытый ключ" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "URL-адрес RPC по умолчанию" }, + "defaultSettingsSubTitle": { + "message": "MetaMask использует настройки по умолчанию, чтобы наилучшим образом сбалансировать безопасность и простоту использования. Измените эти настройки, чтобы еще больше повысить свою конфиденциальность." + }, + "defaultSettingsTitle": { + "message": "Настройки конфиденциальности по умолчанию" + }, "delete": { "message": "Удалить" }, "deleteContact": { "message": "Удалить контакт" }, + "deleteMetaMetricsData": { + "message": "Удалить данные MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Это приведет к удалению исторических данных MetaMetrics, связанных с использованием вами этого устройства. Ваш кошелек и счета останутся такими же, как сейчас, после удаления этих данных. Этот процесс может занять до 30 дней. Наша $1 содержит подробные сведения по этому вопросу.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Этот запрос нельзя выполнить сейчас из-за проблемы с сервером аналитической системы. Повторите попытку позже" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Мы не можем удалить эти данные прямо сейчас" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Мы собираемся удалить все ваши данные MetaMetrics. Вы уверены?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Удалить данные MetaMetrics?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Вы инициировали это действие $1. Этот процесс может занять до 30 дней. $2 содержит подробности по этому вопросу", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Если вы удалите эту сеть, вам нужно будет добавить ее снова, чтобы просмотреть свои активы в этой сети" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Депозит" }, + "depositCrypto": { + "message": "Внесите криптовалюту с другого счета, используя адрес кошелька или QR-код." + }, "deprecatedGoerliNtwrkMsg": { "message": "Из-за обновлений системы Ethereum тестовая сеть Goerli скоро будет закрыта." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "счета" }, + "disconnectAllDescriptionText": { + "message": "Если вы отключитесь от этого сайта, вам придется повторно подключить свои счета и сети, чтобы снова использовать этот сайт." + }, "disconnectAllSnapsText": { "message": "Snaps" }, + "disconnectMessage": { + "message": "Это отключит вас от этого сайта" + }, "disconnectPrompt": { "message": "Отключить $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Изменить ник" }, + "editAccounts": { + "message": "Изменить аккаунты" + }, "editAddressNickname": { "message": "Изменить ник адреса" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "изменить исходную сеть" }, + "editNetworksTitle": { + "message": "Изменить сети" + }, "editNonceField": { "message": "Изменить одноразовый номер" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Изменить разрешение" }, + "editPermissions": { + "message": "Изменить разрешения" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Изменить плату за газ за ускорение" }, + "editSpendingCap": { + "message": "Изменить лимит расходов" + }, + "editSpendingCapAccountBalance": { + "message": "Баланс счета: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Введите сумму, которая может быть потрачена от вашего имени." + }, + "editSpendingCapError": { + "message": "Лимит расходов не может иметь более $1 десятичных знаков. Удалите лишние десятичные знаки, чтобы продолжить." + }, "enableAutoDetect": { "message": " Включить автоопределение" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "Ошибка поиска ENS." }, + "enterANameToIdentifyTheUrl": { + "message": "Введите имя, чтобы идентифицировать URL" + }, "enterANumber": { "message": "Введите цифру" }, + "enterChainId": { + "message": "Введите ID блокчейна" + }, "enterCustodianToken": { "message": "Введите свой токен $1 или добавьте новый токен" }, "enterMaxSpendLimit": { "message": "Введите максимальный лимит расходов" }, + "enterNetworkName": { + "message": "Введите имя сети" + }, "enterOptionalPassword": { "message": "Введите необязательный пароль" }, "enterPasswordContinue": { "message": "Введите пароль, чтобы продолжить" }, + "enterRpcUrl": { + "message": "Введите URL RPC" + }, + "enterSymbol": { + "message": "Введите символ" + }, "enterTokenNameOrAddress": { "message": "Введите имя токена или вставьте адрес" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Экспериментальные" }, + "exportYourData": { + "message": "Экспортируйте свои данные" + }, + "exportYourDataButton": { + "message": "Скачать" + }, + "exportYourDataDescription": { + "message": "Вы можете экспортировать такие данные, как ваши контакты и предпочтения." + }, "extendWalletWithSnaps": { "message": "Изучите Snaps, созданные сообществом, чтобы персонализировать работу с web3", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Эта плата за газ была предложена $1. Ее переопредление может вызвать проблемы с вашей транзакцией. При наличии вопросов обратитесь к $1.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Плата за газ" + }, "gasIsETH": { "message": "Газ стоит $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Что-то пошло не так...." }, + "generalDescription": { + "message": "Синхронизируйте настройки между устройствами, выбирайте настройки сети и отслеживайте данные токенов" + }, "genericExplorerView": { "message": "Посмотреть счет на $1" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "Если вы не сможете войти в приложение или приобретете новое устройство, вы потеряете свои средства. Обязательно сохраните резервную копию секретной фразы для восстановления в $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Игнорировать все" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "в ваших Настройках" }, + "included": { + "message": "включено" + }, "infuraBlockedNotification": { "message": "MetaMask не удалось подключиться к хосту блокчейна. Узнать возможные причины можно $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "JSON-файл", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Сохраните напоминание о своей секретной фразе для восстановления в безопасном месте. Если вы потеряете ее, никто не сможет помочь вам вернуть ее. Хуже того, вы больше никогда не сможете получить доступ к своему кошельку. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Название счета" }, @@ -2402,6 +2622,9 @@ "message": "Узнайте подробнее, как $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Узнайте как" + }, "learnMore": { "message": "подробнее" }, @@ -2409,6 +2632,9 @@ "message": "Хотите $1 о газе?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Узнайте больше о лучших методах обеспечения конфиденциальности." + }, "learnMoreKeystone": { "message": "Подробнее" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Ссылка" }, + "linkCentralizedExchanges": { + "message": "Привяжите счета Coinbase или Binance, чтобы бесплатно переводить криптовалюту в MetaMask." + }, "links": { "message": "Ссылки" }, @@ -2557,6 +2786,9 @@ "message": "Убедитесь, что никто не смотрит", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Управление настройками конфиденциальности по умолчанию" + }, "marketCap": { "message": "Рыночная капитализация" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Кнопка статуса подключения показывает, подключен ли посещаемый вами веб-сайт, к выбранному вами в настоящее время счету." }, + "metaMetricsIdNotAvailableError": { + "message": "Поскольку вы никогда не включали MetaMetrics, здесь нет данных для удаления." + }, "metadataModalSourceTooltip": { "message": "$1 размещается на npm, а $2 — это уникальный идентификатор Snap.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "больше" }, + "moreAccounts": { + "message": "+ еще $1 счета(-ов)", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ еще $1 сети(-ей)", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Вы добавляете эту сеть в MetaMask и разрешаете этому сайту использовать ее." + }, "multipleSnapConnectionWarning": { "message": "$1 хочет использовать $2 Snaps", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Изменить сведения о сети" }, "nativeTokenScamWarningDescription": { - "message": "Эта сеть не соответствует ID или имени связанного с ней блокчейна. Многие популярные токены используют название $1, что делает его объектом мошенничества. Мошенники могут обманом заставить вас отправить им взамен более ценную валюту. Проверьте все, прежде чем продолжить.", + "message": "Символ нативного токена не соответствует ожидаемому символу нативного токена для сети со связанным ID блокчейна. Вы ввели $1, а ожидаемый символ токена — $2. Убедитесь, что вы подключены к правильному блокчейну.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "что-то еще", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Это потенциальное мошенничество", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Сведения о сети" }, + "networkFee": { + "message": "Комиссия сети" + }, "networkIsBusy": { "message": "Сеть занята. Цены газа высоки, а оценки менее точны." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Параметры сети" }, + "networkPermissionToast": { + "message": "Обновлены сетевые разрешения" + }, "networkProvider": { "message": "Поставщик услуг сети" }, @@ -2865,15 +3121,26 @@ "message": "Нам не удается подключиться к $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Сеть изменена на $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "URL сети" }, "networkURLDefinition": { "message": "URL, используемый для доступа к этой сети." }, + "networkUrlErrorWarning": { + "message": "Злоумышленники иногда имитируют сайты, внося небольшие изменения в адрес сайта. Прежде чем продолжить, убедитесь, что вы взаимодействуете с нужным сайтом. Версия пьюникода: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Сети" }, + "networksSmallCase": { + "message": "сети" + }, "nevermind": { "message": "Неважно" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Мы обновили нашу политику конфиденциальности" }, + "newRpcUrl": { + "message": "Новый URL RPC" + }, "newTokensImportedMessage": { "message": "Вы успешно импортировали $1.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask не подключен к этому сайту" }, + "noConnectionDescription": { + "message": "Для подключения к сайту найдите и нажмите кнопку «подключиться». Помните, что MetaMask может подключаться только к сайтам в web3" + }, "noConversionRateAvailable": { "message": "Нет доступного обменного курса" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Пользовательский одноразовый номер" }, + "none": { + "message": "Нет" + }, "notBusy": { "message": "Не занят" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Сведения о разрешении" }, + "permissionFor": { + "message": "Разрешение для" + }, + "permissionFrom": { + "message": "Разрешение от" + }, "permissionRequest": { "message": "Запрос разрешения" }, @@ -3593,6 +3875,14 @@ "message": "Разрешите $1 получить доступ к предпочитаемому вами языку в настройках MetaMask. Его можно использовать для локализации и отображения содержимого $1 на вашем языке.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Просматривайте такую ​​информацию, как ваш предпочитаемый язык и фиатная валюта.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Предоставьте $1 доступ к такой информации, как ваш предпочитаемый язык и фиатная валюта, в настройках MetaMask. Это помогает $1 отображать контент с учетом ваших предпочтений. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Показать пользовательский экран", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Вы даёте расходующему лицу разрешение потратить именно столько токенов из вашего аккаунта" }, + "permittedChainToastUpdate": { + "message": "У $1 есть доступ к $2." + }, "personalAddressDetected": { "message": "Обнаружен личный адрес. Введите адрес контракта токена." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Получить" }, + "receiveCrypto": { + "message": "Получить криптовалюту" + }, + "recipientAddressPlaceholderNew": { + "message": "Введите публичный адрес (0x) или имя домена" + }, "recommendedGasLabel": { "message": "Рекомендовано" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Отклонено" }, + "rememberSRPIfYouLooseAccess": { + "message": "Помните: если вы потеряете секретную фразу для восстановления, вы утратите доступ к своему кошельку. $1, чтобы сохранить этот набор слов в безопасности и всегда иметь возможность доступа к своим средствам." + }, + "reminderSet": { + "message": "Напоминание установлено!" + }, "remove": { "message": "Удалить" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Из-за ошибки этот запрос не был подтвержден поставщиком услуг безопасности. Действуйте осторожно." }, + "requestingFor": { + "message": "Запрашивается для" + }, + "requestingForAccount": { + "message": "Запрашивается для $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "запросы, ожидающие подтверждения" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Показать сид-фразу" }, + "review": { + "message": "Проверить" + }, + "reviewAlert": { + "message": "Оповещение о проверке" + }, "reviewAlerts": { "message": "Просмотреть оповещения" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Отозвать разрешение" }, + "revokeSimulationDetailsDesc": { + "message": "Вы удаляете чье-либо разрешение на трату токенов с вашего счета." + }, "revokeSpendingCap": { "message": "Отменить верхний лимит расходов для вашего $1", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Эта третья сторона больше не сможет тратить ваши текущие или будущие токены." }, + "rpcNameOptional": { + "message": "Имя RPC (необязательно)" + }, "rpcUrl": { "message": "Новый URL RPC" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Безопасность и конфиденциальность" }, + "securityDescription": { + "message": "Уменьшите свои шансы присоединиться к небезопасным сетям и защитите свои счета" + }, + "securityMessageLinkForNetworks": { + "message": "мошенничества с сетью и угроз безопасности" + }, + "securityPrivacyPath": { + "message": "«Настройки» > «Безопасность и конфиденциальность»." + }, "securityProviderPoweredBy": { "message": "На основе $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Смотреть все разрешения", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "См. подробности" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Если вы не видите ожидаемые счета, попробуйте переключиться на путь HD или текущую выбранную сеть." }, + "selectRpcUrl": { + "message": "Выберите URL RPC" + }, "selectType": { "message": "Выбрать тип" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Установить одобрение для всех" }, + "setApprovalForAllRedesignedTitle": { + "message": "Запрос вывода средств" + }, "setApprovalForAllTitle": { "message": "Одобрить $1 без ограничений по расходам", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Настройки" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Настройки оптимизированы для простоты использования и безопасности. Изменяйте их в любое время." + }, "settingsSearchMatchingNotFound": { "message": "Совпадений не найдено." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Показать больше" }, + "showNativeTokenAsMainBalance": { + "message": "Показывать нативный токен в качестве основного баланса" + }, "showNft": { "message": "Показать NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Вход с помощью" }, + "simulationApproveHeading": { + "message": "Отозвать" + }, + "simulationDetailsApproveDesc": { + "message": "Вы даете кому-то другому разрешение на вывод NFT с вашего счета." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Вы даете кому-то разрешение на трату этой суммы с вашего счета." + }, "simulationDetailsFiatNotAvailable": { "message": "Недоступно" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Вы отправляете" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Вы отзываете чье-то разрешение на вывод NFT с вашего счета." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Вы даете кому-то разрешение на вывод NFT с вашего счета." + }, "simulationDetailsTitle": { "message": "Прогнозируемые изменения" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Ой! Что-то пошло не так." }, + "sortBy": { + "message": "Сортировать по" + }, + "sortByAlphabetically": { + "message": "По алфавиту (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Уменьшающийся баланс (от высокого к низкому в $1)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Источник" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Расходующее лицо" }, + "spenderTooltipDesc": { + "message": "Это адрес, по которому можно будет вывести ваши NFT." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Это адрес, который сможет потратить ваши токены от вашего имени." + }, "spendingCap": { "message": "Лимит расходов" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Запрос лимита расходов для вашего $1" }, + "spendingCapTooltipDesc": { + "message": "Это количество токенов, к которым покупатель сможет получить доступ от вашего имени." + }, "srpInputNumberOfWords": { "message": "У меня есть фраза из $1 слов(-а)", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Рекомендовано $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Рекомендуемый символ валюты:" + }, "suggestedTokenName": { "message": "Предлагаемое имя:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Посетите наш центр поддержки" }, + "supportMultiRpcInformation": { + "message": "Теперь мы поддерживаем несколько RPC для одной сети. Ваш последний RPC был выбран в качестве RPC по умолчанию для разрешения проблемы с противоречивой информацией." + }, "surveyConversion": { "message": "Пройдите наш опрос" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Плата за газ является примерной и будет колебаться в зависимости от сетевого трафика и сложности транзакции." }, + "swapGasFeesExplanation": { + "message": "MetaMask не получает плату за газ. Сумма этой платы являются приблизительной и может меняться в зависимости от загруженности сети и сложности транзакции. Узнайте подробнее $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "здесь", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Узнать больше о плате за газ" }, @@ -5186,9 +5583,19 @@ "message": "Плата за газ переводится криптомайнерам, которые обрабатывают транзакции в сети $1. MetaMask не получает прибыли от платы за газ.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Плата за газ включена в эту котировку путем корректировки суммы отправленных или полученных токенов. Вы можете получить ETH отдельной транзакцией в своем списке действий." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Подробнее о плате за газ" + }, "swapHighSlippage": { "message": "Высокое проскальзывание" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Включает плату за газ и комиссию MetaMask в размере $1%", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Включает комиссию MetaMask в размере $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Наши Условия использования обновлены" }, + "testnets": { + "message": "Тестнеты" + }, "theme": { "message": "Тема" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Комиссия за транзакцию" }, + "transactionFlowNetwork": { + "message": "Сеть" + }, "transactionHistoryBaseFee": { "message": "Базовая комиссия (Гвей)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Перевести" }, + "transferCrypto": { + "message": "Перевести криптовалюту" + }, "transferFrom": { "message": "Перевести из" }, + "transferRequest": { + "message": "Запрос на перевод" + }, "trillionAbbreviation": { "message": "трлн", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "Обновить" }, + "updateEthereumChainConfirmationDescription": { + "message": "Этот сайт запрашивает обновление URL вашей сети по умолчанию. Вы можете редактировать настройки по умолчанию и информацию о сети в любое время." + }, + "updateNetworkConfirmationTitle": { + "message": "Обновить $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Обновите свою информацию или" }, "updateRequest": { "message": "Запрос обновления" }, + "updatedRpcForNetworks": { + "message": "Обновлены сетевые RPC" + }, "uploadDropFile": { "message": "Переместите свой файл сюда" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "наше руководство по подключению аппаратного кошелька" }, + "walletProtectedAndReadyToUse": { + "message": "Ваш кошелек защищен и готов к использованию. Вы можете найти свою секретную фразу для восстановления в $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Хотите добавить эту сеть?" }, @@ -5991,6 +6424,17 @@ "message": "$1 Третья сторона может потратить весь ваш баланс токенов без дополнительного уведомления или согласия. Защитите себя, установив более низкий лимит расходов.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Включение этой опции даст вам возможность отслеживать счета Ethereum через публичный адрес или имя ENS. Чтобы оставить отзыв об этой бета-функции, заполните этот $1.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Следить за счетами Ethereum (бета)" + }, + "watchOutMessage": { + "message": "Остерегайтесь $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Слабый" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Что это?" }, + "withdrawing": { + "message": "Выполняется вывод" + }, "wrongNetworkName": { "message": "Согласно нашим записям, имя сети может не соответствовать этому ID блокчейна." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Ваш баланс" }, + "yourBalanceIsAggregated": { + "message": "Ваш баланс суммируется" + }, "yourNFTmayBeAtRisk": { "message": "Ваш NFT могут быть в опасности" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Нам не удалось отменить вашу транзакцию до ее подтверждения в блокчейне." }, + "yourWalletIsReady": { + "message": "Ваш кошелек готов" + }, "zeroGasPriceOnSpeedUpError": { "message": "Нулевая цена газа при ускорении" } diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index f49c14518dc3..4f6e3098171d 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Ikonekta ang iyong QR na wallet na hardware" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "Ang address sa kahilingan sa pag-sign in ay hindi tugma sa address ng account na ginagamit mo sa pag-sign in." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Piliin ang mga account na nais mong makakuha ng abiso:" }, + "accountBalance": { + "message": "Balanse ng account" + }, "accountDetails": { "message": "Mga detalye ng account" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Mga Opsyon sa Account" }, + "accountPermissionToast": { + "message": "In-update ang mga pahintulot ng account" + }, "accountSelectionRequired": { "message": "Kailangan mong pumili ng account!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Mga konektadong account" }, + "accountsPermissionsTitle": { + "message": "Tingnan ang mga account mo at magmungkahi ng mga transaksyon" + }, + "accountsSmallCase": { + "message": "mga account" + }, "active": { "message": "Aktibo" }, @@ -180,12 +195,18 @@ "add": { "message": "Magdagdag" }, + "addACustomNetwork": { + "message": "Magdagdag ng custom na network" + }, "addANetwork": { "message": "Magdagdag ng network" }, "addANickname": { "message": "Magdagdag ng palayaw" }, + "addAUrl": { + "message": "Magdagdag ng URL" + }, "addAccount": { "message": "Magdagdag ng account" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Magdagdag ng block explorer" }, + "addBlockExplorerUrl": { + "message": "Magdagdag ng block explorer URL" + }, "addContact": { "message": "Magdagdag ng contact" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Nagdaragdag ka ng bagong RPC provider para sa Mainnet ng Ethereum" }, + "addEthereumWatchOnlyAccount": { + "message": "Panoorin ang isang account sa Ethereum (Beta)" + }, "addFriendsAndAddresses": { "message": "Magdagdag ng mga kaibigan at address na pinagkakatiwalaan mo" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Magdagdag ng Network" }, + "addNetworkConfirmationTitle": { + "message": "Magdagdag ng $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Magdagdag ng bagong account sa Ethereum" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Nakopya ang address!" }, + "addressMismatch": { + "message": "Hindi tugma ang address ng site" + }, + "addressMismatchOriginal": { + "message": "Kasalukuyang URL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Bersyon ng Punycode: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Makabago" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Ang bayad sa priyoridad (kilala rin bilang “tip ng minero”) ay direktang napupunta sa mga minero at ginagawang insentibo ang mga ito upang unahin ang iyong mga transaksyon." }, + "aggregatedBalancePopover": { + "message": "Ipinapakita nito ang halaga ng lahat ng token na pagmamay-ari mo sa ibinigay na network. Kung mas gusto mong makita ang halagang ito sa ETH o sa ibang currency, pumunta sa $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Sumasang-ayon ako sa $1 ng MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Puwede itong baguhin sa \"Mga Setting > Mga Alerto\"" }, + "alertMessageAddressMismatchWarning": { + "message": "Minsan ginagaya ng mga umaatake ang mga site sa pamamagitan ng paggawa ng maliliit na pagbabago sa address ng site. Tiyaking nakikipag-ugnayan ka sa nilalayong site bago ka magpatuloy." + }, "alertMessageGasEstimateFailed": { "message": "Hindi kami makapagbigay ng tumpak na bayad at ang pagtantiya na ito ay maaaring mataas. Iminumungkahi namin na maglagay ka ng naka-custom na gas limit, ngunit may panganib na mabigo pa rin ang transaksyon." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Para magpatuloy sa transaksyong ito, kakailanganin mong dagdagan ang gas limit sa 21000 o mas mataas." }, + "alertMessageInsufficientBalance2": { + "message": "Wala kang sapat na ETH sa iyong account para bayaran ang mga bayarin sa network." + }, "alertMessageNetworkBusy": { "message": "Ang mga presyo ng gas ay mataas at ang pagtantiya ay hindi gaanong tumpak." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Mga opsyon ng asset" }, + "assets": { + "message": "Mga asset" + }, + "assetsDescription": { + "message": "I-autodetect ang mga token sa wallet mo, ipakita ang mga NFT, at kumuha ng naka-batch na mga update sa balanse ng account" + }, "attemptSendingAssets": { "message": "Maaari mong mawala ang mga asset kung susubukan mo itong ipadala mula sa isang network papunta sa isa pa. Ligtas ng maglipat ng pondo sa pagitan ng mga network sa pamamagitan ng paggamit ng bridge." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Bridge, huwag ipadala" }, + "bridgeFrom": { + "message": "I-bridge mula sa" + }, + "bridgeSelectNetwork": { + "message": "Pumili ng network" + }, + "bridgeTo": { + "message": "I-bridge papunta sa" + }, "browserNotSupported": { "message": "Hindi sinusuportahan ang iyong browser..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Kumpirmahin ang Lihim na Parirala sa Pagbawi" }, + "confirmTitleApproveTransaction": { + "message": "Hiling sa allowance" + }, + "confirmTitleDeployContract": { + "message": "Mag-deploy ng kontrata" + }, + "confirmTitleDescApproveTransaction": { + "message": "Gusto ng site na ito ng pahintulot na i-withdraw ang iyong mga NFT" + }, + "confirmTitleDescDeployContract": { + "message": "Gusto ng site na ito na mag-deploy ka ng kontrata" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Gusto ng site na ito ng pahintulot na i-withdraw ang iyong mga token" + }, "confirmTitleDescPermitSignature": { "message": "Ang site na ito ay nais ng permiso para gamitin ang iyong mga token." }, "confirmTitleDescSIWESignature": { "message": "Nais ng site na mag-sign in ka upang patunayan na pag-aari mo ang account na ito." }, + "confirmTitleDescSign": { + "message": "Suriin ang mga detalye ng kahilingan bago mo kumpirmahin." + }, "confirmTitlePermitTokens": { "message": "Hiling sa limitasyon ng paggastos" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Alisin ang pahintulot" + }, "confirmTitleSIWESignature": { "message": "Kahilingan sa pag-sign in" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Alisin ang pahintulot" + }, "confirmTitleSignature": { "message": "Kahilingan sa paglagda" }, "confirmTitleTransaction": { "message": "Hiling na Transaksyon" }, + "confirmationAlertModalDetails": { + "message": "Para protektahan ang iyong mga asset at impormasyon sa pag-login, iminumungkahi namin na tanggihan mo ang kahilingan." + }, + "confirmationAlertModalTitle": { + "message": "Kahina-hinala ang kahilingang ito" + }, "confirmed": { "message": "Nakumpirma" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "May na-detect kami na nakakalitong character sa pangalan ng ENS. Suriin ang pangalan ng ENS para maiwasan ang potensyal na panloloko." }, + "congratulations": { + "message": "Congratulations!" + }, "connect": { "message": "Kumonekta" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Mga konektadong site" }, + "connectedSitesAndSnaps": { + "message": "Mga konektadong site at Snap" + }, "connectedSitesDescription": { "message": "Ang $1 ay nakakonekta sa mga site na ito. Matitingnan nila ang address ng iyong account.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "Konektado ang MetaMask sa site na ito, ngunit wala pang mga account ang konektado" }, + "connectedSnaps": { + "message": "Mga konektadong Snap" + }, + "connectedWithAccount": { + "message": "$1 (na) account ang nakakonekta", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Nakakonekta sa $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Kumokonekta" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Kumokonekta sa Sepolia test network" }, + "connectionDescription": { + "message": "Gusto ng site na ito na" + }, "connectionFailed": { "message": "Nabigo ang koneksyon" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Kopyahin ang address sa clipboard" }, + "copyAddressShort": { + "message": "Kopyahin ang address" + }, "copyPrivateKey": { "message": "Kopyahin ang pribadong key" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "Default na RPC URL" }, + "defaultSettingsSubTitle": { + "message": "Ginagamit ng MetaMask ang mga default na setting upang pinakamahusay na mabalanse ang kaligtasan at dali ng paggamit. Baguhin ang mga setting na ito para maging mahigpit ang iyong pagkapribado." + }, + "defaultSettingsTitle": { + "message": "Mga default na setting sa pagkapribado" + }, "delete": { "message": "Tanggalin" }, "deleteContact": { "message": "Tanggalin ang kontak" }, + "deleteMetaMetricsData": { + "message": "Tanggalin ang data ng MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Tatanggalin nito ang kasaysayan ng data ng MetaMetrics na may kaugnayan sa paggamit mo sa device na ito. Hindi magbabago ang wallet at mga account mo sa kung ano ito ngayon pagkatapos mabura ang data na ito. Ang prosesong ito ay maaaring tumagal nang hanggang 30 araw. Tingnan ang aming $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Hindi makumpleto ang kahilingang ito sa ngayon dahil sa isang isyu sa server ng analytics system, pakisubukang muli mamaya" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Hindi namin matanggal ang data na ito sa ngayon" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Aalisin na namin ang lahat ng iyong data ng MetaMetrics. Sigurado ka ba?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Tanggalin ang data ng MetaMetrics?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Pinasimulan mo ang aksyon na ito sa $1. Ang prosesong ito ay maaaring tumagal nang hanggang 30 araw. Tingnan ang $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Kapag tinanggal mo ang network na ito, kakailanganin mo itong idagdag muli para makita ang mga asset mo sa network na ito" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Deposito" }, + "depositCrypto": { + "message": "Magdeposito ng crypto mula sa ibang account gamit ang wallet address o QR code." + }, "deprecatedGoerliNtwrkMsg": { "message": "Dahil sa mga update sa sistema ng Ethereum, ang Goerli test network malapit nang itigil." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "mga account" }, + "disconnectAllDescriptionText": { + "message": "Kapag idiniskonekta mo sa site na ito, kakailanganin mong muling ikonekta ang mga account at network mo para magamit muli ang site na ito." + }, "disconnectAllSnapsText": { "message": "Mga Snap" }, + "disconnectMessage": { + "message": "Ididiskonekta ka nito mula sa site na ito" + }, "disconnectPrompt": { "message": "Idiskonekta $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "I-edit ang palayaw" }, + "editAccounts": { + "message": "I-edit ang mga account" + }, "editAddressNickname": { "message": "I-edit ang address ng palayaw" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "i-edit ang orihinal na network" }, + "editNetworksTitle": { + "message": "I-edit ang mga network" + }, "editNonceField": { "message": "I-edit sa Nonce" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Pahintulot sa pag-edit" }, + "editPermissions": { + "message": "Mga pahintulot sa pag-edit" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "I-edit ang pagpapabilis ng bayad sa gas" }, + "editSpendingCap": { + "message": "I-edit ang limitasyon sa paggastos" + }, + "editSpendingCapAccountBalance": { + "message": "Balanse ng account: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Ilagay ang halaga na sa tingin mo ay komportable kang gastusin sa ngalan mo." + }, + "editSpendingCapError": { + "message": "Ang cap sa paggastos ay hindi maaaring lumampas sa $1 (na) decimal digit. Alisin ang mga decimal digit para magpatuloy." + }, "enableAutoDetect": { "message": " Paganahin ang autodetect" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "Nabigong makita ang ENS." }, + "enterANameToIdentifyTheUrl": { + "message": "Lagyan ng pangalan para matukoy ang URL" + }, "enterANumber": { "message": "Maglagay ng numero" }, + "enterChainId": { + "message": "Ilagay ang ID ng Chain" + }, "enterCustodianToken": { "message": "Ilagay ang iyong $1 na token o magdagdag ng bagong token" }, "enterMaxSpendLimit": { "message": "Ilagay ang max na limitasyon sa paggastos" }, + "enterNetworkName": { + "message": "Ilagay ang pangalan ng network" + }, "enterOptionalPassword": { "message": "Ilagay ang opsyonal na password" }, "enterPasswordContinue": { "message": "Ilagay ang password para magpatuloy" }, + "enterRpcUrl": { + "message": "Ilagay ang RPC URL" + }, + "enterSymbol": { + "message": "Ilagay ang simbolo" + }, "enterTokenNameOrAddress": { "message": "Ilagay ang pangalan ng token o i-paste ang address" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Eksperimental" }, + "exportYourData": { + "message": "I-export ang data mo" + }, + "exportYourDataButton": { + "message": "I-download" + }, + "exportYourDataDescription": { + "message": "Maaari mong i-export ang mga data mo gaya ng mga kontak at kagustuhan." + }, "extendWalletWithSnaps": { "message": "Tuklasin ang mga Snap na binuo ng komunidad para i-customize ang iyong karanasan sa web3", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Ang bayad sa gas na ito ay iminungkahi ng $1. Ang pag-override dito ay maaaring magdulot ng problema sa iyong transaksyon. Mangyaring makipag-ugnayan sa $1 kung mayroon kang mga tanong.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Bayad sa gas" + }, "gasIsETH": { "message": "Ang gas ay $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Nagkaproblema...." }, + "generalDescription": { + "message": "I-sync ang mga setting sa lahat ng device, pumili ng mga kagustuhan sa network, at subaybayan ang data ng token" + }, "genericExplorerView": { "message": "Tingnan ang account sa $1" }, @@ -2093,6 +2302,10 @@ "id": { "message": "Id" }, + "ifYouGetLockedOut": { + "message": "Kung ikaw ay na-lock out sa app o kumuha ng bagong device, mawawala ang iyong mga pondo. Siguraduhing i-back up ang iyong Lihim na Parirala sa Pagbawi sa $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Huwag pansinin ang lahat" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "sa iyong Mga Setting" }, + "included": { + "message": "kalakip" + }, "infuraBlockedNotification": { "message": "Hindi makakonekta ang MetaMask sa blockchain host. I-review ang posibleng mga dahilan $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "File na JSON", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Gumawa ng paalala sa iyong Lihim na Parirala sa Pagbawi sa isang ligtas na lugar. Kung mawala mo ito, walang makakatulong sa iyo na mabawi ito. Ang mas masama pa, hindi mo na maaaring ma-access ang iyong wallet kahit kailan. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Pangalan ng Account" }, @@ -2402,6 +2622,9 @@ "message": "Alamin kung paano sa $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Alamin kung paano" + }, "learnMore": { "message": "matuto pa" }, @@ -2409,6 +2632,9 @@ "message": "Gusto mo bang $1 tungkol sa gas?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Matuto pa tungkol sa pinakamahusay na mga kasanayan sa pagkapribado." + }, "learnMoreKeystone": { "message": "Matuto Pa" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Link" }, + "linkCentralizedExchanges": { + "message": "I-link ang iyong Coinbase o mga account sa Binance para maglipat ng crypto sa MetaMask nang libre." + }, "links": { "message": "Mga Link" }, @@ -2557,6 +2786,9 @@ "message": "Siguraduhing walang tumitingin", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Pamahalaan ang mga default na setting sa pagkapribado" + }, "marketCap": { "message": "Market cap" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Makikita sa button ng katayuan ng koneksyon kung nakakonekta ang website na binibisita mo sa kasalukuyan mong napiling account." }, + "metaMetricsIdNotAvailableError": { + "message": "Dahil hindi mo kailanman sinubukan ang MetaMetrics, walang data na tatanggalin dito." + }, "metadataModalSourceTooltip": { "message": "Ang $1 ay naka-host sa npm at $2 ang natatanging pagkakailanlan ng Snap.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "higit pa" }, + "moreAccounts": { + "message": "+ $1 pang mga account", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 pa na mga network", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Idinadagdag mo ang network na ito sa MetaMask at binibigyan ng pahintulot ang site na ito na gamitin ito." + }, "multipleSnapConnectionWarning": { "message": "Si $1 ay gustong gumamit ng $2 Snaps", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "I-edit ang mga detalye ng network" }, "nativeTokenScamWarningDescription": { - "message": "Ang network na ito ay hindi tugma sa kaugnay na chain ID o pangalan nito. Maraming popular na token ang gumagamit ng pangalang $1, kaya nagiging puntirya ito ng mga scam. Maaari kang linlangin ng mga scammer na magpadala sa kanila ng mas mahal na currency bilang kapalit. I-verify ang lahat bago magpatuloy.", + "message": "Ang simbolo ng native token ay hindi tumutugma sa inaasahang simbolo ng native token para sa network na may nauugnay na ID ng chain. Ang inilagay mo ay $1 samantalang ang inaasahang simbolo ng token ay $2. Pakiberipika na konektado ka sa tamang chain.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "iba pa", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Isa itong potensyal na scam", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Mga Detalye ng Network" }, + "networkFee": { + "message": "Bayad sa network" + }, "networkIsBusy": { "message": "Busy ang network. Ang presyo ng gas ay mataas at ang pagtantya ay hindi gaanong tumpak." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Mga pagpipilian na network" }, + "networkPermissionToast": { + "message": "In-update ang mga pahintulot ng network" + }, "networkProvider": { "message": "Network provider" }, @@ -2865,15 +3121,26 @@ "message": "Hindi kami makakonekta sa $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Ang network ay lumipat sa $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "URL Network" }, "networkURLDefinition": { "message": "Ang URL ay ginamit upang ma-access ang network na ito." }, + "networkUrlErrorWarning": { + "message": "Minsan ginagaya ng mga umaatake ang mga site sa pamamagitan ng paggawa ng maliliit na pagbabago sa address ng site. Tiyaking nakikipag-ugnayan ka sa nilalayong site bago ka magpatuloy. Bersyon ng Punycode: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Mga Network" }, + "networksSmallCase": { + "message": "mga network" + }, "nevermind": { "message": "Huwag na" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Binago namin ang aming patakaran sa pagkapribado" }, + "newRpcUrl": { + "message": "Bagong RPC URL" + }, "newTokensImportedMessage": { "message": "Matagumpay mong na-import ang $1.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "Ang MetaMask ay hindi nakakonekta sa site na ito" }, + "noConnectionDescription": { + "message": "Para kumonekta sa site, hanapin at piliin ang \"connect\" button. Tandaan na ang MetaMask ay maaari lamang kumonekta sa mga site sa web3" + }, "noConversionRateAvailable": { "message": "Hindi available ang rate ng palitan" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "I-custom ang nonce" }, + "none": { + "message": "Wala" + }, "notBusy": { "message": "Hindi busy" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Mga detalye ng permiso" }, + "permissionFor": { + "message": "Pahintulot para sa" + }, + "permissionFrom": { + "message": "Pahintulot mula sa" + }, "permissionRequest": { "message": "Kahilingan sa pahintulot" }, @@ -3593,6 +3875,14 @@ "message": "Payagan ang $1 na i-access ang iyong mas gustong wika mula sa iyong mga setting sa MetaMask. Maaari itong gamitin para i-localize at ipakita ang content ng $1 gamit ang iyong wika.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Tingnan ang impormasyon tulad ng gusto mong wika at fiat na salapi.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Pahintulutan ang $1 na ma-access ang impormasyon tulad ng gusto mong wika at fiat na salapi sa iyong mga setting ng MetaMask. Tumutulong ito sa $1 na magpakita ng content na nakaayon sa iyong mga kagustuhan. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Magpakita ng custom na screen", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Binibigyan mo ang gumagastos ng permiso upang gumastos ng ganito karaming token mula sa iyong account." }, + "permittedChainToastUpdate": { + "message": "Ang $1 ay may access sa $2." + }, "personalAddressDetected": { "message": "Natukoy ang personal na address. Ilagay ang address ng kontrata ng token." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Tumanggap" }, + "receiveCrypto": { + "message": "Tumanggap ng crypto" + }, + "recipientAddressPlaceholderNew": { + "message": "Ilagay ang pampublikong address (0x) o pangalan ng domain" + }, "recommendedGasLabel": { "message": "Nirekomenda" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Tinanggihan" }, + "rememberSRPIfYouLooseAccess": { + "message": "Tandaan, kapag nawala mo ang iyong Lihim na Parirala sa Pagbawi, mawawala ang access mo sa iyong wallet. $1 upang mapanatiling ligtas ang pangkat ng mga salitang ito nang sa gayon lagi kang may access sa mga pondo mo." + }, + "reminderSet": { + "message": "Naitakda ang Paalala!" + }, "remove": { "message": "Alisin" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Dahil sa isang error, ang kahilingang ito ay hindi na-verify ng tagapagbigay ng seguridad. Magpatuloy nang may pag-iingat." }, + "requestingFor": { + "message": "Hinihiling para sa" + }, + "requestingForAccount": { + "message": "Hinihiling para sa $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "mga kahilingang hinihintay na tanggapin" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Ibunyag ang pariralang binhi" }, + "review": { + "message": "Suriin" + }, + "reviewAlert": { + "message": "Suriin ang alerto" + }, "reviewAlerts": { "message": "Suriin ang mga alerto" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Bawiin ang pahintulot" }, + "revokeSimulationDetailsDesc": { + "message": "Aalisin mo ang pahintulot sa ibang tao na gumastos ng mga token mula sa account mo." + }, "revokeSpendingCap": { "message": "Bawiin ang limitasyon sa paggastos para sa iyong $1", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Ang third party na ito ay hindi na makakagastos pa sa iyong kasalukuyan o hinaharap na mga token." }, + "rpcNameOptional": { + "message": "Pangalan ng RPC (Opsyonal)" + }, "rpcUrl": { "message": "Bagong RPC URL" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Seguridad at pagkapribado" }, + "securityDescription": { + "message": "Bawasan ang iyong mga tsansa ng pagsali sa mga hindi ligtas na network at protektahan ang iyong mga account" + }, + "securityMessageLinkForNetworks": { + "message": "mga panloloko sa network at panganib sa seguridad" + }, + "securityPrivacyPath": { + "message": "Mga Setting > Seguridad at Pagkapribado." + }, "securityProviderPoweredBy": { "message": "Pinapagana ng $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Tingnan ang lahat ng pahintulot", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Tingnan ang mga detalye" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Kung hindi mo makita ang mga account na inaasahan mo, subukang lumipat ng HD path o kasalukuyang piniling network." }, + "selectRpcUrl": { + "message": "Pumili ng RPC URL" + }, "selectType": { "message": "Pumili ng Uri" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Itakda ang Pag-apruba para sa Lahat" }, + "setApprovalForAllRedesignedTitle": { + "message": "Kahilingan sa pag-withdraw" + }, "setApprovalForAllTitle": { "message": "Aprubahan ang $1 nang walang limitasyon sa paggastos", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Mga Setting" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Ang mga setting ay nakaayos para sa dali ng paggamit at seguridad.\nBaguhin ang mga ito anumang oras." + }, "settingsSearchMatchingNotFound": { "message": "Walang nakitang katugmang resulta." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Ipakita ang iba pa" }, + "showNativeTokenAsMainBalance": { + "message": "Ipakita ang native token bilang pangunahing balanse" + }, "showNft": { "message": "Ipakita ang NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Nagsa-sign in gamit ang" }, + "simulationApproveHeading": { + "message": "I-withdraw" + }, + "simulationDetailsApproveDesc": { + "message": "Binibigyan mo ng pahintulot ang ibang tao na i-withdraw ang mga NFT mula sa iyong account." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Binibigyan mo ng pahintulot ang ibang tao na gumastos ng ganitong halaga mula sa account mo." + }, "simulationDetailsFiatNotAvailable": { "message": "Hindi Available" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Nagpadala ka" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Aalisin mo ang pahintulot sa ibang tao na i-withdraw ang mga NFT mula sa account mo." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Binibigyan mo ng pahintulot ang ibang tao na i-withdraw ang mga NFT mula sa account mo." + }, "simulationDetailsTitle": { "message": "Tinatayang mga pagbabago" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Oops! Nagkaproblema." }, + "sortBy": { + "message": "Isaayos ayon sa" + }, + "sortByAlphabetically": { + "message": "Ayon sa alpabeto (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Bumababa ang balanse ($1 high-low)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Pinagmulan" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Gumagastos" }, + "spenderTooltipDesc": { + "message": "Ito ang address na makakapag-withdraw ng iyong mga NFT." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Ito ang address na maaaring gumastos ng mga token mo sa ngalan mo." + }, "spendingCap": { "message": "Limitasyon sa paggastos" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Kahilingan na limitasyon sa paggastos para sa iyong $1" }, + "spendingCapTooltipDesc": { + "message": "Ito ang halaga ng mga token na maa-access ng gumagastos sa ngalan mo." + }, "srpInputNumberOfWords": { "message": "Mayroon akong word phrase ng $1", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Iminumungkahi ng $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Iminumungkahing simbolo ng salapi:" + }, "suggestedTokenName": { "message": "Iminumungkahing pangalan:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Bisitahin ang aming Sentro ng Suporta" }, + "supportMultiRpcInformation": { + "message": "Sinusuportahan na namin ngayon ang maramihang RPC para sa isang network. Pinili ang pinakabago mong RPC bilang ang default para lutasin ang magkakasalungat na impormasyon." + }, "surveyConversion": { "message": "Sagutan ang aming survey" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Ang mga bayad sa gas ay tinatantya at magbabago batay sa trapiko sa network at pagiging kumplikado ng transaksyon." }, + "swapGasFeesExplanation": { + "message": "Hindi kumikita ang MetaMask mula sa mga bayad sa gas. Ang mga bayarin na ito ay pagtatantiya at maaaring mabago batay sa kung gaano kaabala ang network at kung gaano kakumplikado ang transaksyon. Alamin pa ang $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "dito", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Alamin pa ang tungkol sa mga bayad sa gas" }, @@ -5186,9 +5583,19 @@ "message": "Ang mga bayad sa gas ay binabayaran sa mga minero ng crypto na nagpoproseso ng mga transaksyon sa $1 na network. Ang MetaMask ay hindi kumikita mula sa mga bayad sa gas.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Isinasama sa presyo na ito ang mga bayad sa gas sa pamamagitan ng pagsasaayos sa halaga ng ipinadala o natanggap na token. Maaari kang tumanggap ng ETH sa hiwalay na transaksyon sa iyong listahan ng aktibidad." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Alamin pa ang tungkol sa mga bayad sa gas" + }, "swapHighSlippage": { "message": "Mataas na slippage" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Kasama ang gas at $1% bayad sa MetaMask", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Kasama ang $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Na-update ang aming Mga Tuntunin sa Paggamit" }, + "testnets": { + "message": "Mga Testnet" + }, "theme": { "message": "Tema" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Bayad sa transaksyon" }, + "transactionFlowNetwork": { + "message": "Network" + }, "transactionHistoryBaseFee": { "message": "Batayang bayad (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Maglipat" }, + "transferCrypto": { + "message": "Ilipat ang crypto" + }, "transferFrom": { "message": "Maglipat mula kay/sa" }, + "transferRequest": { + "message": "Kahilingan na paglilipat" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "I-update" }, + "updateEthereumChainConfirmationDescription": { + "message": "Hinihiling ng site na ito na i-update ang iyong default na URL ng network. Maaari mong i-edit ang mga default at impormasyon ng network anumang oras." + }, + "updateNetworkConfirmationTitle": { + "message": "I-update ang $1 ", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "I-update ang iyong impormasyon o" }, "updateRequest": { "message": "Hiling sa pag-update" }, + "updatedRpcForNetworks": { + "message": "Na-update ang mga RPC ng Network" + }, "uploadDropFile": { "message": "I-drop ang file mo rito" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "ang aming gabay sa pagkonekta ng wallet na hardware" }, + "walletProtectedAndReadyToUse": { + "message": "Ang iyong wallet ay protektado at handa ng gamitin. Matatagpuan mo ang iyong Lihim na Parirala sa Pagbawi sa $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Gusto mo bang idagdag ang network na ito?" }, @@ -5991,6 +6424,17 @@ "message": "$1 Maaaring gastusin ng third party ang iyong buong balanse ng token nang walang karagdagang abiso o pahintulot. Protektahan ang iyong sarili sa pamamagitan ng pag-customize ng mas mababang limitasyon sa paggastos.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Ang pag-on sa opsyon na ito ay magbibigay sa iyo ng kakayahan na panoorin ang mga account ng Ethereum sa pamamagitan ng isang pampublikong address o pangalan ng ENS. Para sa feedback sa Beta feature na ito, mangyaring sagutan ito $1.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Panoorin ang mga account sa Ethereum (Beta)" + }, + "watchOutMessage": { + "message": "Mag-ingat sa $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Madali" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Ano ito?" }, + "withdrawing": { + "message": "Wini-withdraw" + }, "wrongNetworkName": { "message": "Ayon sa aming mga talaan, ang pangalan ng network ay maaaring hindi tumugma nang tama sa ID ng chain na ito." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Iyong balanse" }, + "yourBalanceIsAggregated": { + "message": "Ang iyong balanse ay pinagsama-sama" + }, "yourNFTmayBeAtRisk": { "message": "Maaaring nasa panganib ang iyong NFT" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Hindi namin makansela ang iyong transaksyon bago ito nakumpirma sa blockchain." }, + "yourWalletIsReady": { + "message": "Handa na ang iyong wallet" + }, "zeroGasPriceOnSpeedUpError": { "message": "Walang presyo ng gas sa pagpapabilis" } diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 7af54fd3eedd..771089050317 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "QR donanım cüzdanınızı bağlayın" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "Giriş talebindeki adres, giriş yapmak için kullandığınız hesapla uyumlu değil." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Hakkında bildirim almak istediğiniz hesapları seçin:" }, + "accountBalance": { + "message": "Hesap bakiyesi" + }, "accountDetails": { "message": "Hesap bilgileri" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Hesap Seçenekleri" }, + "accountPermissionToast": { + "message": "Hesap izinleri güncellendi" + }, "accountSelectionRequired": { "message": "Bir hesap seçmeniz gerekiyor!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Hesaplar bağlandı" }, + "accountsPermissionsTitle": { + "message": "Hesaplarınızı görmek ve işlem önermek" + }, + "accountsSmallCase": { + "message": "hesaplar" + }, "active": { "message": "Aktif" }, @@ -180,12 +195,18 @@ "add": { "message": "Ekle" }, + "addACustomNetwork": { + "message": "Özel bir ağ ekle" + }, "addANetwork": { "message": "Ağ ekle" }, "addANickname": { "message": "Takma ad ekle" }, + "addAUrl": { + "message": "URL ekle" + }, "addAccount": { "message": "Hesap ekleyin" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Bir blok gezgini ekle" }, + "addBlockExplorerUrl": { + "message": "Bir blok gezgini URL adresi ekle" + }, "addContact": { "message": "Kişi ekle" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Ethereum Ana Ağı için yeni bir RPC sağlayıcısı ekliyorsunuz" }, + "addEthereumWatchOnlyAccount": { + "message": "Bir Ethereum hesabını izle (Beta)" + }, "addFriendsAndAddresses": { "message": "Güvendiğiniz arkadaşlarınızı ve adresleri ekleyin" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Ağ ekle" }, + "addNetworkConfirmationTitle": { + "message": "$1 ekle", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Yeni bir Ethereum hesabı ekle" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Adres kopyalandı!" }, + "addressMismatch": { + "message": "Site adresi uyumsuzluğu" + }, + "addressMismatchOriginal": { + "message": "Mevcut URL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Punycode sürümü: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Gelişmiş" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Maks. öncelik ücreti (başka bir deyişle \"madenci bahşişi\") doğrudan madencilere gider ve işleminizin öncelikli olarak gerçekleştirilmesini teşvik eder." }, + "aggregatedBalancePopover": { + "message": "Bu, belirli bir ağda sahip olduğunuz tüm token'lerin değerini yansıtır. Bu değeri ETH olarak veya diğer para birimlerinde görmeyi tercih ediyorsanız $1 alanına gidin.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "MetaMask'in $1 bölümünü kabul ediyorum", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "\"Ayarlar > Uyarılar\" kısmında değiştirilebilir" }, + "alertMessageAddressMismatchWarning": { + "message": "Saldırganlar bazen site adresinde küçük değişiklikler yaparak siteleri taklit edebilir. Devam etmeden önce planladığınız site ile etkileşim kurduğunuzdan emin olun." + }, "alertMessageGasEstimateFailed": { "message": "Kesin bir ücret sunamıyoruz ve bu tahmin yüksek olabilir. Özel bir gaz limiti girmenizi öneririz ancak işlemin yine de başarısız olma riski vardır." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Bu işlemle devam etmek için gaz limitini 21000 veya üzeri olacak şekilde artırmanız gerekecek." }, + "alertMessageInsufficientBalance2": { + "message": "Hesabınızda ağ ücretlerini ödemek için yeterli ETH yok." + }, "alertMessageNetworkBusy": { "message": "Gaz fiyatları yüksektir ve tahmin daha az kesindir." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Varlık seçenekleri" }, + "assets": { + "message": "Varlıklar" + }, + "assetsDescription": { + "message": "Cüzdanınızdaki token'ler otomatik algılansın, NFT'ler gösterilsin ve toplu hesap bakiye güncellemeleri alınsın" + }, "attemptSendingAssets": { "message": "Varlıklarınızı doğrudan bir ağdan diğerine göndermeye çalışırsanız onları kaybedebilirsiniz. Fonları köprü kullanarak ağlar arasında güvenli bir şekilde transfer edin." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Köprü yap, gönderme" }, + "bridgeFrom": { + "message": "Şuradan köprü:" + }, + "bridgeSelectNetwork": { + "message": "Ağ seç" + }, + "bridgeTo": { + "message": "Şuraya köprü:" + }, "browserNotSupported": { "message": "Tarayıcınız desteklenmiyor..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Gizli Kurtarma İfadesini Onayla" }, + "confirmTitleApproveTransaction": { + "message": "Ödenek talebi" + }, + "confirmTitleDeployContract": { + "message": "Bir sözleşme kurulumu gerçekleştirin" + }, + "confirmTitleDescApproveTransaction": { + "message": "Bu site NFT'lerinizi çekmek için izin istiyor" + }, + "confirmTitleDescDeployContract": { + "message": "Bu site sözleşme kurulumu gerçekleştirmenizi istiyor" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Bu site tokenlerinizi çekmek için izin istiyor" + }, "confirmTitleDescPermitSignature": { "message": "Bu site token'lerinizi harcamak için izin istiyor." }, "confirmTitleDescSIWESignature": { "message": "Bir site bu hesabın sahibi olduğunuzu kanıtlamak için giriş yapmanızı istiyor." }, + "confirmTitleDescSign": { + "message": "Onaylamadan önce talep bilgilerini inceleyin." + }, "confirmTitlePermitTokens": { "message": "Harcama üst limiti talebi" }, + "confirmTitleRevokeApproveTransaction": { + "message": "İzni kaldır" + }, "confirmTitleSIWESignature": { "message": "Giriş talebi" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "İzni kaldır" + }, "confirmTitleSignature": { "message": "İmza talebi" }, "confirmTitleTransaction": { "message": "İşlem talebi" }, + "confirmationAlertModalDetails": { + "message": "Varlıklarınızı ve oturum açma bilgilerinizi korumak için talebi reddetmenizi öneririz." + }, + "confirmationAlertModalTitle": { + "message": "Bu talep şüphelidir" + }, "confirmed": { "message": "Onaylandı" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "ENS adında karıştırılabilir bir karakter tespit ettik. Olası bir dolandırıcılığı önlemek için ENS adını kontrol edin." }, + "congratulations": { + "message": "Tebrikler!" + }, "connect": { "message": "Bağla" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Bağlı siteler" }, + "connectedSitesAndSnaps": { + "message": "Bağlı siteler ve Snap'ler" + }, "connectedSitesDescription": { "message": "$1 bu sitelere bağlanmış. Bu siteler hesap adresinizi görüntüleyebilir.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask bu siteye bağlı ancak henüz bağlı hesap yok" }, + "connectedSnaps": { + "message": "Bağlı Snap'ler" + }, + "connectedWithAccount": { + "message": "$1 hesap bağlandı", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "$1 ile bağlandı", + "description": "$1 represents account name" + }, "connecting": { "message": "Bağlanıyor" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Sepolia test ağına bağlanılıyor" }, + "connectionDescription": { + "message": "Bu site şunları yapmak istiyor:" + }, "connectionFailed": { "message": "Bağlantı başarısız oldu" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Adresi panoya kopyala" }, + "copyAddressShort": { + "message": "Adresi kopyala" + }, "copyPrivateKey": { "message": "Özel anahtarı kopyala" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "Varsayılan RPC URL adresi" }, + "defaultSettingsSubTitle": { + "message": "MetaMask, güvenlik ve kullanım kolaylığını en iyi şekilde dengelemek için varsayılan ayarları kullanır. Gizliliğinizi daha fazla artırmak için bu ayarları değiştirin." + }, + "defaultSettingsTitle": { + "message": "Varsayılan gizlilik ayarları" + }, "delete": { "message": "Sil" }, "deleteContact": { "message": "Kişiyi sil" }, + "deleteMetaMetricsData": { + "message": "MetaMetrics verilerini sil" + }, + "deleteMetaMetricsDataDescription": { + "message": "Bu işlem, bu cihazdaki kullanımınızla ilişkili geçmiş MetaMetrics verilerini silecektir. Bu veriler silindikten sonra cüzdanınız ve hesaplarınız tam olarak şu anda oldukları gibi kalacaktır. Bu süreç 30 güne kadar sürebilir. $1 bölümümüzü görüntüleyin.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Bu talep analitik bir sistem sunucusu sorunundan dolayı şu anda tamamlanamıyor, lütfen daha sonra tekrar deneyin" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Şu anda bu verileri silemiyoruz" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Tüm MetaMetrics verilerini kaldırmak üzeresiniz. Emin misiniz?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "MetaMetrics verilerini sil?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "$1 tarihinde bir eylem başlattınız. Bu süreç 30 güne kadar sürebilir. $2 bölümünü görüntüleyin", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Bu ağı silerseniz bu ağdaki varlıklarınızı görüntülemek için bu ağı tekrar eklemeniz gerekecek" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Para Yatır" }, + "depositCrypto": { + "message": "Cüzdan adresi veya QR kodu ile başka bir hesaptan kripto yatırın." + }, "deprecatedGoerliNtwrkMsg": { "message": "Ethereum sistemindeki güncellemelerden dolayı Goerli test ağı yakında aşamalı olarak devre dışı bırakılacaktır." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "hesaplar" }, + "disconnectAllDescriptionText": { + "message": "Bu site ile bağlantınızı keserseniz bu siteyi tekrar kullanabilmek için hesaplarınızı ve ağlarınızı tekrar bağlamanız gerekecek." + }, "disconnectAllSnapsText": { "message": "Snap'ler" }, + "disconnectMessage": { + "message": "Bu işlem bu siteyle bağlantınızı kesecektir" + }, "disconnectPrompt": { "message": "$1 bağlantısını kes" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Takma adı düzenle" }, + "editAccounts": { + "message": "Hesapları düzenle" + }, "editAddressNickname": { "message": "Adres takma adını düzenle" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "orijinal ağı düzenle" }, + "editNetworksTitle": { + "message": "Ağları düzenle" + }, "editNonceField": { "message": "Nonce'u düzenle" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "İzni düzenle" }, + "editPermissions": { + "message": "İzinleri düzenle" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Hızlandırma gaz ücretini düzenle" }, + "editSpendingCap": { + "message": "Harcama üst limitini düzenle" + }, + "editSpendingCapAccountBalance": { + "message": "Hesap bakiyesi: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Sizin adınıza harcanması konusunda rahat hissedeceğiniz tutarı girin." + }, + "editSpendingCapError": { + "message": "Harcama üst limiti $1 ondalık haneyi geçemez. Devam etmek için ondalık haneleri kaldırın." + }, "enableAutoDetect": { "message": " Otomatik algılamayı etkinleştir" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "ENS arama başarısız oldu." }, + "enterANameToIdentifyTheUrl": { + "message": "URL adresini tanımlamak için bir ad girin" + }, "enterANumber": { "message": "Bir sayı girin" }, + "enterChainId": { + "message": "Zincir kimliği girin" + }, "enterCustodianToken": { "message": "$1 tokeninizi girin veya yeni bir token ekleyin" }, "enterMaxSpendLimit": { "message": "Maks. harcama limiti gir" }, + "enterNetworkName": { + "message": "Ağ adını girin" + }, "enterOptionalPassword": { "message": "İsteğe bağlı şifreyi girin" }, "enterPasswordContinue": { "message": "Devam etmek için şifre girin" }, + "enterRpcUrl": { + "message": "RPC URL adresi girin" + }, + "enterSymbol": { + "message": "Sembol girin" + }, "enterTokenNameOrAddress": { "message": "Token adı girin veya adresi yapıştırın" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Deneysel" }, + "exportYourData": { + "message": "Verilerinizi dışa aktarın" + }, + "exportYourDataButton": { + "message": "İndir" + }, + "exportYourDataDescription": { + "message": "Kişileriniz ve tercihleriniz gibi verileri dışa aktarabilirsiniz." + }, "extendWalletWithSnaps": { "message": "web3 deneyiminizi kişiselleştirmek için topluluk tarafından oluşturulmuş Snapleri keşfedin", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Bu gaz ücreti $1 tarafından önerilmiştir. Bu değerin başka bir değerle değiştirilmesi işleminizle ilgili bir soruna neden olabilir. Sorularınız olursa lütfen $1 ile iletişime geçin.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Gaz ücreti" + }, "gasIsETH": { "message": "Gaz $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Bir şeyler ters gitti...." }, + "generalDescription": { + "message": "Cihazlar genelindeki ayarları senkronize edin, ağ tercihlerini seçin ve token verilerini takip edin" + }, "genericExplorerView": { "message": "Hesabı $1 üzerinde görüntüleyin" }, @@ -2093,6 +2302,10 @@ "id": { "message": "Kimlik" }, + "ifYouGetLockedOut": { + "message": "Uygulamaya giriş yapamazsanız veya yeni bir cihaz edinirseniz paranızı kaybedersiniz. Gizli Kurtarma İfadenizi şurada yedeklediğinizden emin olun: $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Tümünü yoksay" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "Ayarlar kısmında" }, + "included": { + "message": "dahil" + }, "infuraBlockedNotification": { "message": "MetaMask blokzinciri ana bilgisayarına bağlanamıyor. $1 olası nedenleri inceleyin.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "JSON Dosyası", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Gizli Kurtarma İfadenizin hatırlatıcısını güvenli bir yerde tutun. Güvenli kurtarma ifadenizi kaybederseniz hiç kimse geri almanıza yardımcı olamaz. Daha da kötüsü, cüzdanınıza bir daha asla erişemezsiniz. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Hesap adı" }, @@ -2402,6 +2622,9 @@ "message": "Nasıl $1 yapacağınızı öğrenin", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Nasıl olduğunu öğrenin" + }, "learnMore": { "message": "daha fazla bilgi" }, @@ -2409,6 +2632,9 @@ "message": "Gaz hakkında $1 istiyor musun?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "En iyi gizlilik uygulamaları hakkında daha fazla bilgi edinin." + }, "learnMoreKeystone": { "message": "Daha Fazla Bilgi" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Bağlantı" }, + "linkCentralizedExchanges": { + "message": "MetaMask'e ücretsiz kripto transferi yapmak için Coinbase veya Binance hesaplarınızı bağlayın." + }, "links": { "message": "Bağlantılar" }, @@ -2557,6 +2786,9 @@ "message": "Hiç kimsenin bakmadığından emin olun", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Varsayılan gizlilik ayarlarını yönet" + }, "marketCap": { "message": "Piyasa değeri" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Bağlantı durumu düğmesi ziyaret ettiğiniz web sitesinin şu anda seçilen hesabınıza bağlı olup olmadığını gösterir." }, + "metaMetricsIdNotAvailableError": { + "message": "Hiçbir zaman MetaMetrics'e dahil olmadığınız için burada silinecek veri yok." + }, "metadataModalSourceTooltip": { "message": "$1 npm'de barındırılmaktadır ve $2 bu Snap'in eşsiz tanımlayıcısıdır.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "daha fazla" }, + "moreAccounts": { + "message": "+ $1 hesap daha", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 ağ daha", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Bu ağı MetaMask'e ekliyor ve bu siteye ağı kullanma izni veriyorsunuz." + }, "multipleSnapConnectionWarning": { "message": "$1 $2 Snap kullanmak istiyor", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Ağ bilgilerini düzenle" }, "nativeTokenScamWarningDescription": { - "message": "Bu ağ, ilişkili zincir kimliği veya adı ile uyumlu değil. Pek çok popüler token $1 adını kullanarak bunu dolandırıcılar için hedef haline getirir. Dolandırıcılar, karşılığında kendilerine daha değerli para birimi göndermek üzere sizi kandırabilir. Devam etmeden önce her şeyi doğrulayın.", + "message": "Yerel token sembolü, zincir kimliği ile ilişkili ağın yerel token'inin beklenen sembolü ile uyumlu değil. Sizin girdiğiniz $1 iken beklenen sembol $2 idi. Lütfen doğru zincire bağlandığınızdan emin olun.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "başka bir şey", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Bu potansiyel bir dolandırıcılıktır", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Ağ bilgileri" }, + "networkFee": { + "message": "Ağ ücreti" + }, "networkIsBusy": { "message": "Ağ meşgul. Gaz fiyatları yüksektir ve tahminler daha az doğrudur." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Ağ seçenekleri" }, + "networkPermissionToast": { + "message": "Ağ izinleri güncellendi" + }, "networkProvider": { "message": "Ağ sağlayıcısı" }, @@ -2865,15 +3121,26 @@ "message": "$1 ağına bağlanamıyoruz", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Ağ şuna geçti: $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "Ağ URL adresi" }, "networkURLDefinition": { "message": "Bu ağa erişim sağlamak için kullanılan URL adresi." }, + "networkUrlErrorWarning": { + "message": "Saldırganlar bazen site adresinde küçük değişiklikler yaparak siteleri taklit edebilir. Devam etmeden önce planladığınız site ile etkileşim kurduğunuzdan emin olun. Punycode sürümü: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Ağlar" }, + "networksSmallCase": { + "message": "ağ" + }, "nevermind": { "message": "Boşver" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Gizlilik politikamızı güncelledik" }, + "newRpcUrl": { + "message": "Yeni RPC URL adresi" + }, "newTokensImportedMessage": { "message": "$1 tokeni başarılı bir şekilde içe aktardın.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask bu siteye bağlı değil" }, + "noConnectionDescription": { + "message": "Bir siteye bağlanmak için \"bağlan\" düğmesini bularak seçin. MetaMask'in sadece web3 üzerindeki sitelere bağlanabildiğini unutmayın" + }, "noConversionRateAvailable": { "message": "Dönüşüm oranı mevcut değil" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Özel nonce" }, + "none": { + "message": "Yok" + }, "notBusy": { "message": "Meşgul değil" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "İzin ayrıntıları" }, + "permissionFor": { + "message": "Şunun için izin:" + }, + "permissionFrom": { + "message": "Şuradan izin:" + }, "permissionRequest": { "message": "İzin talebi" }, @@ -3593,6 +3875,14 @@ "message": "$1 adlı bu snap'in MetaMask ayarlarınızdan tercih ettiğiniz dile erişim sağlamasına izin verin. $1 içeriğini dilinizi kullanarak yerelleştirmek ve görüntülemek için bu özellik kullanılabilir.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Tercih ettiğiniz dil ve itibari para gibi bilgileri görün.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "$1 adlı snap'in MetaMask ayarlarınızda tercih ettiğiniz dil ve fiat para gibi bilgilere erişim sağlamasına izin verin. Bu durum $1 adlı snap'in sizin tercihlerinize uygun içerikler göstermesine yardımcı olur. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Özel bir ekran göster", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Harcama yapan tarafa hesabınızdan bu kadar çok token'i harcama izni veriyorsunuz." }, + "permittedChainToastUpdate": { + "message": "$1 için $2 erişimi var." + }, "personalAddressDetected": { "message": "Kişisel adres algılandı. Token sözleşme adresini girin." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Al" }, + "receiveCrypto": { + "message": "Kripto al" + }, + "recipientAddressPlaceholderNew": { + "message": "Genel adres (0x) veya alan adı gir" + }, "recommendedGasLabel": { "message": "Önerilen" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Reddedildi" }, + "rememberSRPIfYouLooseAccess": { + "message": "Unutmayın, Gizli Kurtarma İfadenizi kaybederseniz cüzdanınıza erişiminizi kaybedersiniz. Paranıza her zaman erişebilmeniz için bu sözcük dizisini saklamak amacıyla şunu yapın: $1." + }, + "reminderSet": { + "message": "Hatırlatıcı ayarlandı!" + }, "remove": { "message": "Kaldır" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Bu talep bir hatadan dolayı güvenlik sağlayıcısı tarafından doğrulanmadı. Dikkatli bir şekilde ilerleyin." }, + "requestingFor": { + "message": "Talep:" + }, + "requestingForAccount": { + "message": "$1 için talep ediliyor", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "onaylanmayı bekleyen talepler" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Anahtar cümleyi göster" }, + "review": { + "message": "İncele" + }, + "reviewAlert": { + "message": "Uyarıyı incele" + }, "reviewAlerts": { "message": "Uyarıları incele" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "İzni geri al" }, + "revokeSimulationDetailsDesc": { + "message": "Birisinin sizin hesabınızdan token harcama iznini kaldırıyorsunuz." + }, "revokeSpendingCap": { "message": "$1 için harcama üst limitini iptal et", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Bu üçüncü taraf şimdiki ya da gelecekteki tokenlerinizi artık kullanamayacak." }, + "rpcNameOptional": { + "message": "RPC Adı (İsteğe Bağlı)" + }, "rpcUrl": { "message": "Yeni RPC URL adresi" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Güvenlik ve gizlilik" }, + "securityDescription": { + "message": "Güvensiz ağlara katılma şansınızı azaltın ve hesaplarınızı koruyun" + }, + "securityMessageLinkForNetworks": { + "message": "ağ dolandırıcılıkları ve güvenlik riskleri" + }, + "securityPrivacyPath": { + "message": "Ayarlar > Güvenlik ve Gizlilik." + }, "securityProviderPoweredBy": { "message": "$1 hizmetidir", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Tüm izinleri gör", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Ayrıntılara bakın" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Beklediğiniz hesapları görmüyorsanız HD yoluna veya geçerli seçilen ağa geçmeyi deneyin." }, + "selectRpcUrl": { + "message": "RPC URL adresini seçin" + }, "selectType": { "message": "Tür Seç" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Tümüne onay ver" }, + "setApprovalForAllRedesignedTitle": { + "message": "Para çekme talebi" + }, "setApprovalForAllTitle": { "message": "$1 için harcama limiti olmadan onay ver", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Ayarlar" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Ayarlar, kullanım kolaylığı ve güvenlik için optimize edildi. Bunları dilediğiniz zaman değiştirin." + }, "settingsSearchMatchingNotFound": { "message": "Eşleşen sonuç bulunamadı." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Daha fazlasını göster" }, + "showNativeTokenAsMainBalance": { + "message": "Yerli token'i ana bakiye olarak göster" + }, "showNft": { "message": "NFT'yi göster" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Şununla giriş yap:" }, + "simulationApproveHeading": { + "message": "Çek" + }, + "simulationDetailsApproveDesc": { + "message": "Bir başkasına hesabınızdan NFT çekme izni veriyorsunuz." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Bir başkasına sizin hesabınızdan bu tutarı harcama izni veriyorsunuz." + }, "simulationDetailsFiatNotAvailable": { "message": "Mevcut Değil" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Gönderdiğiniz" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Bir başkasının sizin hesabınızdan NFT çekme iznini kaldırıyorsunuz." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Bir başkasına sizin hesabınızdan NFT çekme izni veriyorsunuz." + }, "simulationDetailsTitle": { "message": "Tahmini değişiklikler" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Eyvah! Bir şeyler ters gitti." }, + "sortBy": { + "message": "Sıralama ölçütü" + }, + "sortByAlphabetically": { + "message": "Alfabetik (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Azalan bakiye ($1 yüksek-düşük)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Kaynak" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Harcama Yapan Taraf" }, + "spenderTooltipDesc": { + "message": "Bu, NFT'lerinizi çekebilecek adrestir." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Bu, token'lerinizi sizin adınıza harcayabilecek adrestir." + }, "spendingCap": { "message": "Harcama üst limiti" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "$1 için harcama üst limiti talebi" }, + "spendingCapTooltipDesc": { + "message": "Bu, harcama yapan tarafın sizin adınıza erişim sağlayabileceği token miktarıdır." + }, "srpInputNumberOfWords": { "message": "$1 sözcükten oluşan bir ifadem var", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "$1 tarafından öneriliyor", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Önerilen para birimi sembolü:" + }, "suggestedTokenName": { "message": "Önerilen isim:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Destek Merkezi bölümümüzü ziyaret et" }, + "supportMultiRpcInformation": { + "message": "Şu anda tek bir ağ için birden fazla RPC destekliyoruz. Çelişkili bilgileri çözmek amacıyla en son RPC'niz varsayılan olarak seçilmiştir." + }, "surveyConversion": { "message": "Anketimize katılın" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Gaz ücretleri tahmini olup ağ trafiği ve işlem karmaşıklığına göre dalgalanır." }, + "swapGasFeesExplanation": { + "message": "MetaMask, gaz ücretlerinden para kazanmaz. Bu ücretler tahminlerdir ve ağın yoğunluğuna ve işlemin karmaşıklığına göre değişebilir. $1 daha fazla bilgi edinin.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "Buradan", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Gaz ücretleri hakkında daha fazla bilgi edinin" }, @@ -5186,9 +5583,19 @@ "message": "Gaz ücretleri, $1 ağında işlemleri gerçekleştiren kripto madencilerine ödenir. MetaMask gaz ücretlerinden herhangi bir kazanç elde etmemektedir.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Bu teklif, gönderilen veya alınan token tutarını ayarlayarak gaz ücretlerini dahil eder. Aktivite listenizde ayrı bir işlemde ETH alabilirsiniz." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Gaz ücretleri hakkında daha fazla bilgi edinin" + }, "swapHighSlippage": { "message": "Yüksek kayma" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Gaz ve %$1 MetaMask ücreti dahildir", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "%$1 MetaMask ücreti dahildir.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Kullanım Şartları bölümümüz güncellendi" }, + "testnets": { + "message": "Testnet'ler" + }, "theme": { "message": "Tema" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "İşlem ücreti" }, + "transactionFlowNetwork": { + "message": "Ağ" + }, "transactionHistoryBaseFee": { "message": "Baz ücret (GEWI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Transfer et" }, + "transferCrypto": { + "message": "Kripto transfer et" + }, "transferFrom": { "message": "Transfer kaynağı:" }, + "transferRequest": { + "message": "Transfer talebi" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "Güncelle" }, + "updateEthereumChainConfirmationDescription": { + "message": "Bu site, varsayılan ağ URL adresinizi güncellemeyi talep ediyor. Varsayılanları ve ağ bilgilerini dilediğiniz zaman düzenleyebilirsiniz." + }, + "updateNetworkConfirmationTitle": { + "message": "Güncelle: $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Bilgilerinizi güncelleyin veya" }, "updateRequest": { "message": "Talebi güncelle" }, + "updatedRpcForNetworks": { + "message": "Ağ RPC'leri Güncellendi" + }, "uploadDropFile": { "message": "Dosyanızı buraya sürükleyin" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "donanım cüzdanı bağlantı kılavuzumuz" }, + "walletProtectedAndReadyToUse": { + "message": "Cüzdanınız korunuyor ve kullanıma hazır. Gizli Kurtarma İfadenizi $1 bölümünde bulabilirsiniz ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Bu ağı eklemek istiyor musunuz?" }, @@ -5991,6 +6424,17 @@ "message": "$1 Üçüncü taraf, başkaca bildiri ya da rıza olmaksızın tüm token bakiyenizi harcayabilir. Düşük bir harcama limitini özelleştirerek kendinizi koruyun.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Bu seçeneği açtığınızda size genel adres veya ENS adı ile Ethereum hesaplarını izleme yeteneği verilir. Bu Beta özellik hakkında geri bildirim için lütfen bu $1 formunu doldurun.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Ethereum Hesaplarını İzle (Beta)" + }, + "watchOutMessage": { + "message": "Şuraya dikkat edin: $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Zayıf" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Bu nedir?" }, + "withdrawing": { + "message": "Çekiliyor" + }, "wrongNetworkName": { "message": "Kayıtlarımıza göre ağ adı bu zincir kimliği ile doğru şekilde eşleşmiyor olabilir." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Bakiyeniz" }, + "yourBalanceIsAggregated": { + "message": "Bakiyeniz toplanıyor" + }, "yourNFTmayBeAtRisk": { "message": "NFT'niz tehlikede olabilir" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Blok zinciri üzerinde onaylanmadan işleminizi iptal edemedik." }, + "yourWalletIsReady": { + "message": "Cüzdanınız hazır" + }, "zeroGasPriceOnSpeedUpError": { "message": "Sıfır gaz fiyatı hızlandırmada" } diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 700d73f11649..1983185816c0 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "Kết nối với ví cứng QR của bạn" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "Địa chỉ trong yêu cầu đăng nhập không trùng khớp với địa chỉ của tài khoản bạn đang sử dụng để đăng nhập." }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "Chọn tài khoản mà bạn muốn nhận thông báo:" }, + "accountBalance": { + "message": "Số dư tài khoản" + }, "accountDetails": { "message": "Chi tiết tài khoản" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "Tùy chọn tài khoản" }, + "accountPermissionToast": { + "message": "Đã cập nhật quyền đối với tài khoản" + }, "accountSelectionRequired": { "message": "Bạn cần chọn một tài khoản!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "Đã kết nối tài khoản" }, + "accountsPermissionsTitle": { + "message": "Xem tài khoản của bạn và đề xuất giao dịch" + }, + "accountsSmallCase": { + "message": "tài khoản" + }, "active": { "message": "Đang hoạt động" }, @@ -180,12 +195,18 @@ "add": { "message": "Thêm" }, + "addACustomNetwork": { + "message": "Thêm mạng tùy chỉnh" + }, "addANetwork": { "message": "Thêm mạng" }, "addANickname": { "message": "Thêm biệt danh" }, + "addAUrl": { + "message": "Thêm URL" + }, "addAccount": { "message": "Thêm tài khoản" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "Thêm một trình khám phá khối" }, + "addBlockExplorerUrl": { + "message": "Thêm URL trình khám phá khối" + }, "addContact": { "message": "Thêm liên hệ" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "Bạn đang thêm một nhà cung cấp RPC mới cho Ethereum Mainnet" }, + "addEthereumWatchOnlyAccount": { + "message": "Theo dõi tài khoản Ethereum (Beta)" + }, "addFriendsAndAddresses": { "message": "Thêm bạn bè và địa chỉ mà bạn tin tưởng" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "Thêm mạng" }, + "addNetworkConfirmationTitle": { + "message": "Thêm $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "Thêm tài khoản Ethereum mới" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "Đã sao chép địa chỉ!" }, + "addressMismatch": { + "message": "Địa chỉ trang web không khớp" + }, + "addressMismatchOriginal": { + "message": "URL hiện tại: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Phiên bản Punycode: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Nâng cao" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "Phí ưu tiên (hay còn được gọi là \"tiền thưởng cho thợ đào\") được chuyển trực tiếp cho các thợ đào và khuyến khích họ ưu tiên giao dịch của bạn." }, + "aggregatedBalancePopover": { + "message": "Điều này phản ánh giá trị của tất cả các token mà bạn sở hữu trên một mạng nhất định. Nếu bạn muốn xem giá trị này bằng ETH hoặc các loại tiền tệ khác, hãy truy cập $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "Tôi đồng ý với $1 của MetaMask", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "Bạn có thể thay đổi trong phần \"Cài đặt > Cảnh báo\"" }, + "alertMessageAddressMismatchWarning": { + "message": "Kẻ tấn công đôi khi bắt chước các trang web bằng cách thực hiện những thay đổi nhỏ trong địa chỉ trang web. Đảm bảo bạn đang tương tác với trang web dự định trước khi tiếp tục." + }, "alertMessageGasEstimateFailed": { "message": "Chúng tôi không thể cung cấp phí chính xác và ước tính này có thể cao. Chúng tôi khuyên bạn nên nhập hạn mức phí gas tùy chỉnh, nhưng vẫn có rủi ro giao dịch sẽ thất bại." }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "Để tiếp tục giao dịch này, bạn cần tăng giới hạn phí gas lên 21000 hoặc cao hơn." }, + "alertMessageInsufficientBalance2": { + "message": "Bạn không có đủ ETH trong tài khoản để thanh toán phí mạng." + }, "alertMessageNetworkBusy": { "message": "Phí gas cao và ước tính kém chính xác hơn." }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "Tùy chọn tài sản" }, + "assets": { + "message": "Tài sản" + }, + "assetsDescription": { + "message": "Tự động phát hiện token trong ví của bạn, hiển thị NFT và nhận hàng loạt thông tin cập nhật về số dư tài khoản" + }, "attemptSendingAssets": { "message": "Bạn có thể bị mất tài sản nếu cố gắng gửi tài sản từ một mạng khác. Chuyển tiền an toàn giữa các mạng bằng cách sử dụng cầu nối." }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "Cầu nối, không gửi" }, + "bridgeFrom": { + "message": "Cầu nối từ" + }, + "bridgeSelectNetwork": { + "message": "Chọn mạng" + }, + "bridgeTo": { + "message": "Cầu nối đến" + }, "browserNotSupported": { "message": "Trình duyệt của bạn không được hỗ trợ..." }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "Xác nhận Cụm từ khôi phục bí mật" }, + "confirmTitleApproveTransaction": { + "message": "Yêu cầu cho phép" + }, + "confirmTitleDeployContract": { + "message": "Triển khai hợp đồng" + }, + "confirmTitleDescApproveTransaction": { + "message": "Trang web này muốn được cấp quyền để rút NFT của bạn" + }, + "confirmTitleDescDeployContract": { + "message": "Trang web này muốn bạn triển khai hợp đồng" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "Trang web này muốn được cấp quyền để rút token của bạn" + }, "confirmTitleDescPermitSignature": { "message": "Trang web này muốn được cấp quyền để chi tiêu số token của bạn." }, "confirmTitleDescSIWESignature": { "message": "Một trang web yêu cầu bạn đăng nhập để chứng minh quyền sở hữu tài khoản này." }, + "confirmTitleDescSign": { + "message": "Xem lại thông tin yêu cầu trước khi xác nhận." + }, "confirmTitlePermitTokens": { "message": "Yêu cầu hạn mức chi tiêu" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Xóa quyền" + }, "confirmTitleSIWESignature": { "message": "Yêu cầu đăng nhập" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "Xóa quyền" + }, "confirmTitleSignature": { "message": "Yêu cầu chữ ký" }, "confirmTitleTransaction": { "message": "Yêu cầu giao dịch" }, + "confirmationAlertModalDetails": { + "message": "Để bảo vệ tài sản và thông tin đăng nhập của bạn, chúng tôi đề nghị bạn từ chối yêu cầu này." + }, + "confirmationAlertModalTitle": { + "message": "Yêu cầu này có dấu hiệu đáng ngờ" + }, "confirmed": { "message": "Đã xác nhận" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "Chúng tôi đã phát hiện thấy một ký tự có thể gây nhầm lẫn trong tên ENS. Hãy kiểm tra tên ENS để tránh nguy cơ bị lừa đảo." }, + "congratulations": { + "message": "Chúc mừng!" + }, "connect": { "message": "Kết nối" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "Trang web đã kết nối" }, + "connectedSitesAndSnaps": { + "message": "Trang web và Snap đã kết nối" + }, "connectedSitesDescription": { "message": "$1 đã được kết nối với các trang web này. Các trang web này có thể xem địa chỉ tài khoản của bạn.", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask được kết nối với trang web này, nhưng chưa có tài khoản nào được kết nối" }, + "connectedSnaps": { + "message": "Snap đã kết nối" + }, + "connectedWithAccount": { + "message": "Đã kết nối $1 tài khoản", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Đã kết nối với $1", + "description": "$1 represents account name" + }, "connecting": { "message": "Đang kết nối" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "Đang kết nối với mạng thử nghiệm Sepolia" }, + "connectionDescription": { + "message": "Trang web này muốn" + }, "connectionFailed": { "message": "Kết nối thất bại" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "Sao chép địa chỉ vào bộ nhớ đệm" }, + "copyAddressShort": { + "message": "Sao chép địa chỉ" + }, "copyPrivateKey": { "message": "Sao chép khóa riêng tư" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "URL RPC mặc định" }, + "defaultSettingsSubTitle": { + "message": "MetaMask sử dụng chế độ cài đặt mặc định để cân bằng tốt nhất giữa tính an toàn và dễ sử dụng. Thay đổi các chế độ cài đặt này để nâng cao quyền riêng tư của bạn hơn nữa." + }, + "defaultSettingsTitle": { + "message": "Cài đặt quyền riêng tư mặc định" + }, "delete": { "message": "Xóa" }, "deleteContact": { "message": "Xóa địa chỉ liên hệ" }, + "deleteMetaMetricsData": { + "message": "Xóa dữ liệu MetaMetrics" + }, + "deleteMetaMetricsDataDescription": { + "message": "Thao tác này sẽ xóa dữ liệu MetaMetrics trước đây liên quan đến quá trình sử dụng của bạn trên thiết bị này. Ví và tài khoản của bạn sẽ vẫn như hiện tại sau khi dữ liệu này bị xóa. Quá trình này có thể mất đến 30 ngày. Xem $1 của chúng tôi.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "Không thể hoàn tất yêu cầu này ngay bây giờ do sự cố máy chủ hệ thống phân tích, vui lòng thử lại sau" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "Chúng tôi không thể xóa dữ liệu này bây giờ" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "Chúng tôi sắp xóa tất cả dữ liệu MetaMetrics của bạn. Bạn có chắc không?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Xóa dữ liệu MetaMetrics?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "Bạn đã khởi tạo hành động này trên $1. Quá trình này có thể mất đến 30 ngày. Xem $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "Nếu xóa mạng này, bạn sẽ cần thêm lại mạng này để xem các tài sản của mình trong mạng này" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "Nạp" }, + "depositCrypto": { + "message": "Nạp tiền mã hóa từ tài khoản khác bằng địa chỉ ví hoặc mã QR." + }, "deprecatedGoerliNtwrkMsg": { "message": "Do các cập nhật của hệ thống Ethereum, mạng thử nghiệm Goerli sẽ sớm được loại bỏ dần." }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "tài khoản" }, + "disconnectAllDescriptionText": { + "message": "Nếu bạn ngắt kết nối khỏi trang web này, bạn sẽ cần kết nối lại tài khoản và mạng của bạn để sử dụng lại trang web này." + }, "disconnectAllSnapsText": { "message": "Snap" }, + "disconnectMessage": { + "message": "Hành động này sẽ ngắt kết nối bạn khỏi trang web này" + }, "disconnectPrompt": { "message": "Ngắt kết nối $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "Chỉnh sửa biệt danh" }, + "editAccounts": { + "message": "Chỉnh sửa tài khoản" + }, "editAddressNickname": { "message": "Chỉnh sửa biệt danh địa chỉ" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "chỉnh sửa mạng gốc" }, + "editNetworksTitle": { + "message": "Chỉnh sửa mạng" + }, "editNonceField": { "message": "Chỉnh sửa số nonce" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "Chỉnh sửa quyền" }, + "editPermissions": { + "message": "Chỉnh sửa quyền" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Chỉnh sửa phí gas tăng tốc" }, + "editSpendingCap": { + "message": "Chỉnh sửa hạn mức chi tiêu" + }, + "editSpendingCapAccountBalance": { + "message": "Số dư tài khoản: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Nhập số tiền mà bạn cảm thấy thoải mái khi được chi tiêu thay mặt cho bạn." + }, + "editSpendingCapError": { + "message": "Hạn mức chi tiêu không được vượt quá $1 chữ số thập phân. Xóa bớt chữ số thập phân để tiếp tục." + }, "enableAutoDetect": { "message": " Bật tự động phát hiện" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "Tra cứu ENS thất bại." }, + "enterANameToIdentifyTheUrl": { + "message": "Nhập tên để xác định URL" + }, "enterANumber": { "message": "Nhập số" }, + "enterChainId": { + "message": "Nhập ID chuỗi" + }, "enterCustodianToken": { "message": "Nhập token $1 của bạn hoặc thêm token mới" }, "enterMaxSpendLimit": { "message": "Nhập hạn mức chi tiêu tối đa" }, + "enterNetworkName": { + "message": "Nhập tên mạng" + }, "enterOptionalPassword": { "message": "Nhập mật khẩu tùy chọn" }, "enterPasswordContinue": { "message": "Nhập mật khẩu để tiếp tục" }, + "enterRpcUrl": { + "message": "Nhập URL RPC" + }, + "enterSymbol": { + "message": "Nhập ký hiệu" + }, "enterTokenNameOrAddress": { "message": "Nhập tên token hoặc dán địa chỉ" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "Thử nghiệm" }, + "exportYourData": { + "message": "Xuất dữ liệu của bạn" + }, + "exportYourDataButton": { + "message": "Tải xuống" + }, + "exportYourDataDescription": { + "message": "Bạn có thể xuất các dữ liệu như địa chỉ liên hệ và tùy chọn của bạn." + }, "extendWalletWithSnaps": { "message": "Khám phá các Snap do cộng đồng xây dựng để tùy chỉnh trải nghiệm web3 của bạn", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "Phí gas này đã được gợi ý bởi $1. Việc sửa đổi có thể khiến giao dịch của bạn gặp sự cố. Vui lòng liên hệ với $1 nếu bạn có câu hỏi.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Phí gas" + }, "gasIsETH": { "message": "Gas là $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "Đã xảy ra sự cố..." }, + "generalDescription": { + "message": "Đồng bộ chế độ cài đặt trên các thiết bị, chọn mạng ưa thích và theo dõi dữ liệu token" + }, "genericExplorerView": { "message": "Xem tài khoản trên $1" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "Nếu bạn bị khóa ứng dụng hoặc sử dụng thiết bị mới, bạn sẽ mất tiền. Nhớ sao lưu Cụm từ khôi phục bí mật của bạn trong $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Bỏ qua tất cả" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "trong phần Cài đặt" }, + "included": { + "message": "đã bao gồm" + }, "infuraBlockedNotification": { "message": "MetaMask không thể kết nối với máy chủ chuỗi khối. Hãy xem xét các lý do tiềm ẩn $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "Tập tin JSON", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Cất giữ lời nhắc về Cụm từ khôi phục bí mật ở một nơi an toàn. Nếu bạn làm mất, không ai có thể giúp bạn lấy lại. Tệ hơn nữa, bạn sẽ không bao giờ truy cập được vào ví của bạn nữa. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Tên tài khoản" }, @@ -2402,6 +2622,9 @@ "message": "Tìm hiểu cách $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Tìm hiểu cách thức" + }, "learnMore": { "message": "tìm hiểu thêm" }, @@ -2409,6 +2632,9 @@ "message": "Muốn $1 về gas?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Tìm hiểu thêm về các cách thức bảo mật tốt nhất." + }, "learnMoreKeystone": { "message": "Tìm hiểu thêm" }, @@ -2497,6 +2723,9 @@ "link": { "message": "Liên kết" }, + "linkCentralizedExchanges": { + "message": "Liên kết tài khoản Coinbase hoặc Binance của bạn để chuyển tiền mã hóa đến MetaMask miễn phí." + }, "links": { "message": "Liên kết" }, @@ -2557,6 +2786,9 @@ "message": "Đảm bảo không có ai đang nhìn", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Quản lý chế độ cài đặt quyền riêng tư mặc định" + }, "marketCap": { "message": "Vốn hóa thị trường" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "Nút trạng thái kết nối sẽ hiển thị nếu trang web mà bạn đang truy cập được kết nối với tài khoản bạn đang chọn." }, + "metaMetricsIdNotAvailableError": { + "message": "Vì bạn chưa bao giờ chọn tham gia MetaMetrics, nên ở đây không có dữ liệu nào để xóa." + }, "metadataModalSourceTooltip": { "message": "$1 được lưu trữ trên npm và $2 là mã định danh duy nhất của Snap này.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "thêm" }, + "moreAccounts": { + "message": "+ $1 tài khoản nữa", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 mạng nữa", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "Bạn đang thêm mạng này vào MetaMask và cấp cho trang web này quyền sử dụng mạng này." + }, "multipleSnapConnectionWarning": { "message": "$1 muốn sử dụng $2 Snap", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "Chỉnh sửa thông tin mạng" }, "nativeTokenScamWarningDescription": { - "message": "Mạng này không trùng khớp với tên hoặc ID chuỗi liên quan của nó. Nhiều token phổ biến sử dụng tên $1, khiến nó trở thành mục tiêu của các hành vi lừa đảo. Kẻ lừa đảo có thể lừa bạn gửi lại cho họ loại tiền tệ có giá trị hơn. Hãy xác minh mọi thứ trước khi tiếp tục.", + "message": "Ký hiệu token gốc không khớp với ký hiệu dự kiến của token gốc dành cho mạng với ID chuỗi liên quan. Bạn đã nhập $1 trong khi ký hiệu token dự kiến là $2. Vui lòng xác minh rằng bạn đã kết nối với đúng chuỗi.", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "cảnh báo khác", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "Đây có khả năng là một hành vi lừa đảo", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "Thông tin về mạng" }, + "networkFee": { + "message": "Phí mạng" + }, "networkIsBusy": { "message": "Mạng đang bận. Giá gas cao và ước tính kém chính xác hơn." }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "Tùy chọn mạng" }, + "networkPermissionToast": { + "message": "Đã cập nhật quyền đối với mạng" + }, "networkProvider": { "message": "Nhà cung cấp mạng" }, @@ -2865,15 +3121,26 @@ "message": "Chúng tôi không thể kết nối với $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Mạng được chuyển thành $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "URL mạng" }, "networkURLDefinition": { "message": "URL dùng để truy cập vào mạng này." }, + "networkUrlErrorWarning": { + "message": "Kẻ tấn công đôi khi bắt chước các trang web bằng cách thực hiện những thay đổi nhỏ trong địa chỉ trang web. Đảm bảo bạn đang tương tác với trang web dự định trước khi tiếp tục. Phiên bản Punycode: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Mạng" }, + "networksSmallCase": { + "message": "mạng" + }, "nevermind": { "message": "Bỏ qua" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "Chúng tôi đã cập nhật chính sách quyền riêng tư" }, + "newRpcUrl": { + "message": "URL RPC mới" + }, "newTokensImportedMessage": { "message": "Bạn đã nhập thành công $1.", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask không được kết nối với trang web này" }, + "noConnectionDescription": { + "message": "Để kết nối với trang web, hãy tìm và chọn nút \"kết nối\". Hãy nhớ rằng, MetaMask chỉ có thể kết nối với các trang web trên web3" + }, "noConversionRateAvailable": { "message": "Không có sẵn tỷ lệ quy đổi nào" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "Số nonce tùy chỉnh" }, + "none": { + "message": "Không có" + }, "notBusy": { "message": "Không bận" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "Chi tiết Quyền" }, + "permissionFor": { + "message": "Quyền cho" + }, + "permissionFrom": { + "message": "Quyền từ" + }, "permissionRequest": { "message": "Yêu cầu cấp quyền" }, @@ -3593,6 +3875,14 @@ "message": "Cho phép $1 truy cập ngôn ngữ ưa thích của bạn từ cài đặt MetaMask. Điều này có thể được sử dụng để dịch và hiển thị nội dung của $1 theo ngôn ngữ của bạn.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "Xem các thông tin như ngôn ngữ ưu tiên và loại tiền pháp định của bạn.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Cho phép $1 truy cập các thông tin như ngôn ngữ ưu tiên và loại tiền pháp định trong cài đặt MetaMask của bạn. Điều này giúp $1 hiển thị nội dung phù hợp với sở thích của bạn. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Hiển thị màn hình tùy chỉnh", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "Bạn đang cấp cho người chi tiêu quyền chi tiêu số lượng token này từ tài khoản của bạn." }, + "permittedChainToastUpdate": { + "message": "$1 có quyền truy cập vào $2." + }, "personalAddressDetected": { "message": "Đã tìm thấy địa chỉ cá nhân. Nhập địa chỉ hợp đồng token." }, @@ -3963,6 +4256,12 @@ "receive": { "message": "Nhận" }, + "receiveCrypto": { + "message": "Nhận tiền mã hóa" + }, + "recipientAddressPlaceholderNew": { + "message": "Nhập địa chỉ công khai (0x) hoặc tên miền" + }, "recommendedGasLabel": { "message": "Được đề xuất" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "Đã từ chối" }, + "rememberSRPIfYouLooseAccess": { + "message": "Hãy nhớ rằng, nếu bạn làm mất Cụm từ khôi phục bí mật, bạn sẽ mất quyền truy cập vào ví. $1 để cất giữ cụm từ này ở nơi an toàn, nhờ đó bạn luôn có thể truy cập vào tiền của bạn." + }, + "reminderSet": { + "message": "Đã thiết lập lời nhắc!" + }, "remove": { "message": "Xóa" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "Do có lỗi, yêu cầu này đã không được nhà cung cấp bảo mật xác minh. Hãy thực hiện cẩn thận." }, + "requestingFor": { + "message": "Yêu cầu cho" + }, + "requestingForAccount": { + "message": "Yêu cầu cho $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "yêu cầu đang chờ xác nhận" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "Hiển thị cụm từ khôi phục bí mật" }, + "review": { + "message": "Xem lại" + }, + "reviewAlert": { + "message": "Xem lại cảnh báo" + }, "reviewAlerts": { "message": "Xem lại cảnh báo" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "Thu hồi quyền" }, + "revokeSimulationDetailsDesc": { + "message": "Bạn đang xóa quyền chi tiêu token của người khác khỏi tài khoản của bạn." + }, "revokeSpendingCap": { "message": "Thu hồi hạn mức chi tiêu cho $1 của bạn", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "Bên thứ ba này sẽ không thể chi tiêu thêm bất kỳ token hiện tại hoặc tương lai nào của bạn." }, + "rpcNameOptional": { + "message": "Tên RPC (Không bắt buộc)" + }, "rpcUrl": { "message": "URL RPC mới" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "Bảo mật và quyền riêng tư" }, + "securityDescription": { + "message": "Giảm khả năng tham gia các mạng không an toàn và bảo vệ tài khoản của bạn" + }, + "securityMessageLinkForNetworks": { + "message": "lừa đảo mạng và rủi ro bảo mật" + }, + "securityPrivacyPath": { + "message": "Cài đặt > Bảo mật & Quyền riêng tư." + }, "securityProviderPoweredBy": { "message": "Được cung cấp bởi $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "Xem tất cả quyền", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "Xem chi tiết" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "Nếu bạn không thấy các tài khoản như mong đợi, hãy chuyển sang đường dẫn HD hoặc mạng đã chọn hiện tại." }, + "selectRpcUrl": { + "message": "Chọn URL RPC" + }, "selectType": { "message": "Chọn loại" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "Thiết lập chấp thuận tất cả" }, + "setApprovalForAllRedesignedTitle": { + "message": "Yêu cầu rút tiền" + }, "setApprovalForAllTitle": { "message": "Chấp thuận $1 không có hạn mức chi tiêu", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "Cài đặt" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Chế độ cài đặt được tối ưu hóa để đảm bảo an toàn và dễ sử dụng. Thay đổi các chế độ cài đặt này bất cứ lúc nào." + }, "settingsSearchMatchingNotFound": { "message": "Không tìm thấy kết quả trùng khớp." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "Hiển thị thêm" }, + "showNativeTokenAsMainBalance": { + "message": "Hiển thị token gốc làm số dư chính" + }, "showNft": { "message": "Hiển thị NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "Đăng nhập bằng" }, + "simulationApproveHeading": { + "message": "Rút" + }, + "simulationDetailsApproveDesc": { + "message": "Bạn đang cấp cho người khác quyền rút NFT khỏi tài khoản của bạn." + }, + "simulationDetailsERC20ApproveDesc": { + "message": "Bạn đang cấp cho người khác quyền chi tiêu số tiền này từ tài khoản của bạn." + }, "simulationDetailsFiatNotAvailable": { "message": "Không có sẵn" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "Bạn gửi" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "Bạn đang xóa quyền rút NFT của người khác khỏi tài khoản của bạn." + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "Bạn đang cấp quyền cho người khác rút NFT khỏi tài khoản của bạn." + }, "simulationDetailsTitle": { "message": "Thay đổi ước tính" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "Rất tiếc! Đã xảy ra sự cố." }, + "sortBy": { + "message": "Sắp xếp theo" + }, + "sortByAlphabetically": { + "message": "Bảng chữ cái (A - Z)" + }, + "sortByDecliningBalance": { + "message": "Số dư giảm dần ($1 cao - thấp)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Nguồn" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "Người chi tiêu" }, + "spenderTooltipDesc": { + "message": "Đây là địa chỉ có thể rút NFT của bạn." + }, + "spenderTooltipERC20ApproveDesc": { + "message": "Đây là địa chỉ sẽ có thể chi tiêu token thay mặt cho bạn." + }, "spendingCap": { "message": "Hạn mức chi tiêu" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "Yêu cầu hạn mức chi tiêu cho $1 của bạn" }, + "spendingCapTooltipDesc": { + "message": "Đây là số lượng token mà người chi tiêu có thể truy cập thay mặt cho bạn." + }, "srpInputNumberOfWords": { "message": "Tôi có một cụm từ gồm $1 từ", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "Được đề xuất bởi $1", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "Ký hiệu tiền tệ đề xuất:" + }, "suggestedTokenName": { "message": "Tên đề xuất:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "Truy cập trung tâm hỗ trợ của chúng tôi" }, + "supportMultiRpcInformation": { + "message": "Chúng tôi hiện hỗ trợ nhiều RPC cho một mạng duy nhất. RPC gần đây nhất của bạn đã được chọn làm RPC mặc định để giải quyết thông tin xung đột." + }, "surveyConversion": { "message": "Tham gia khảo sát của chúng tôi" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "Phí gas được ước tính và sẽ dao động dựa trên lưu lượng mạng và độ phức tạp của giao dịch." }, + "swapGasFeesExplanation": { + "message": "MetaMask không kiếm tiền từ phí gas. Các khoản phí này là ước tính và có thể thay đổi dựa trên lưu lượng của mạng và độ phức tạp của giao dịch. Tìm hiểu thêm $1.", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "tại đây", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "Tìm hiểu thêm về phí gas" }, @@ -5186,9 +5583,19 @@ "message": "Phí gas được trả cho thợ đào tiền điện tử, họ là những người xử lý các giao dịch trên mạng $1. MetaMask không thu lợi nhuận từ phí gas.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "Báo giá này bao gồm phí gas bằng cách điều chỉnh số lượng token được gửi hoặc nhận. Bạn có thể nhận được ETH trong một giao dịch riêng biệt trên danh sách hoạt động của bạn." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Tìm hiểu thêm về phí gas" + }, "swapHighSlippage": { "message": "Mức trượt giá cao" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Bao gồm phí gas và $1% phí của MetaMask", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Bao gồm $1% phí của MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "Điều khoản sử dụng của chúng tôi đã được cập nhật" }, + "testnets": { + "message": "Mạng thử nghiệm" + }, "theme": { "message": "Chủ đề" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "Phí giao dịch" }, + "transactionFlowNetwork": { + "message": "Mạng" + }, "transactionHistoryBaseFee": { "message": "Phí cơ sở (GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "Chuyển" }, + "transferCrypto": { + "message": "Chuyển tiền mã hóa" + }, "transferFrom": { "message": "Chuyển từ" }, + "transferRequest": { + "message": "Yêu cầu chuyển tiền" + }, "trillionAbbreviation": { "message": "Nghìn Tỷ", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "Cập nhật" }, + "updateEthereumChainConfirmationDescription": { + "message": "Trang web này đang yêu cầu cập nhật URL mạng mặc định của bạn. Bạn có thể chỉnh sửa thông tin mặc định và mạng bất cứ lúc nào." + }, + "updateNetworkConfirmationTitle": { + "message": "Cập nhật $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "Cập nhật thông tin của bạn hoặc" }, "updateRequest": { "message": "Yêu cầu cập nhật" }, + "updatedRpcForNetworks": { + "message": "Đã cập nhật RPC mạng" + }, "uploadDropFile": { "message": "Thả tập tin của bạn vào đây" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "hướng dẫn của chúng tôi về cách kết nối ví cứng" }, + "walletProtectedAndReadyToUse": { + "message": "Ví của bạn đã được bảo vệ và sẵn sàng để sử dụng. Bạn có thể xem Cụm từ khôi phục bí mật trong $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "Bạn muốn thêm mạng này?" }, @@ -5991,6 +6424,17 @@ "message": "$1 Bên thứ ba có thể chi tiêu toàn bộ số dư token của bạn mà không cần thông báo hoặc đồng ý thêm. Hãy tự bảo vệ chính mình bằng cách điều chỉnh hạn mức chi tiêu thấp hơn.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Bật tùy chọn này sẽ cho phép bạn theo dõi tài khoản Ethereum thông qua địa chỉ công khai hoặc tên ENS. Để phản hồi về tính năng Beta này, vui lòng hoàn thành $1 này.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Theo dõi tài khoản Ethereum (Beta)" + }, + "watchOutMessage": { + "message": "Hãy cẩn thận với $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Yếu" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "Đây là gì?" }, + "withdrawing": { + "message": "Đang rút tiền" + }, "wrongNetworkName": { "message": "Theo hồ sơ của chúng tôi, tên mạng có thể không khớp chính xác với ID chuỗi này." }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "Số dư của bạn" }, + "yourBalanceIsAggregated": { + "message": "Số dư của bạn đã được tổng hợp" + }, "yourNFTmayBeAtRisk": { "message": "NFT của bạn có thể gặp rủi ro" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "Chúng tôi không thể hủy giao dịch của bạn trước khi nó được xác nhận trên chuỗi khối." }, + "yourWalletIsReady": { + "message": "Ví của bạn đã sẵn sàng" + }, "zeroGasPriceOnSpeedUpError": { "message": "Giá gas bằng 0 khi tăng tốc" } diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index a24b993874a0..b6c050f6b264 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -41,6 +41,9 @@ "QRHardwareWalletSteps1Title": { "message": "关联您的二维码硬件钱包" }, + "QRHardwareWalletSteps2Description": { + "message": "Ngrave Zero" + }, "SIWEAddressInvalid": { "message": "登录请求中的地址与您用于登录的账户地址不匹配。" }, @@ -133,6 +136,9 @@ "accountActivityText": { "message": "选择您想要接收通知的账户:" }, + "accountBalance": { + "message": "账户余额" + }, "accountDetails": { "message": "账户详情" }, @@ -156,6 +162,9 @@ "accountOptions": { "message": "账户选项" }, + "accountPermissionToast": { + "message": "账户许可已更新" + }, "accountSelectionRequired": { "message": "您需要选择一个账户!" }, @@ -168,6 +177,12 @@ "accountsConnected": { "message": "账户已连接" }, + "accountsPermissionsTitle": { + "message": "查看您的账户并建议交易" + }, + "accountsSmallCase": { + "message": "账户" + }, "active": { "message": "活跃" }, @@ -180,12 +195,18 @@ "add": { "message": "添加" }, + "addACustomNetwork": { + "message": "添加自定义网络" + }, "addANetwork": { "message": "添加网络" }, "addANickname": { "message": "添加昵称" }, + "addAUrl": { + "message": "添加 URL" + }, "addAccount": { "message": "添加账户" }, @@ -201,6 +222,9 @@ "addBlockExplorer": { "message": "添加区块浏览器" }, + "addBlockExplorerUrl": { + "message": "添加区块浏览器 URL" + }, "addContact": { "message": "添加联系信息" }, @@ -229,6 +253,9 @@ "addEthereumChainWarningModalTitle": { "message": "您正在为以太坊主网添加新的 RPC 提供商" }, + "addEthereumWatchOnlyAccount": { + "message": "查看以太坊账户(测试版)" + }, "addFriendsAndAddresses": { "message": "添加您信任的朋友和地址" }, @@ -247,6 +274,10 @@ "addNetwork": { "message": "添加网络" }, + "addNetworkConfirmationTitle": { + "message": "添加 $1", + "description": "$1 represents network name" + }, "addNewAccount": { "message": "添加新账户" }, @@ -311,6 +342,17 @@ "addressCopied": { "message": "地址已复制!" }, + "addressMismatch": { + "message": "网站地址不匹配" + }, + "addressMismatchOriginal": { + "message": "当前 URL:$1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Punycode 版本:$1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "高级" }, @@ -342,6 +384,10 @@ "advancedPriorityFeeToolTip": { "message": "优先费用(又称“矿工费”)直接向矿工支付,激励他们优先处理您的交易。" }, + "aggregatedBalancePopover": { + "message": "这反映了您在给定网络上拥有的所有代币的价值。如果您更愿意看到以 ETH 或其他货币反映代币价值,请转到 $1。", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "我同意MetaMask的$1", "description": "$1 is the `terms` link" @@ -367,6 +413,9 @@ "alertDisableTooltip": { "message": "这可以在“设置 > 提醒”中进行更改" }, + "alertMessageAddressMismatchWarning": { + "message": "攻击者有时会通过对网站地址进行细微更改来模仿网站。在继续操作之前,请确保您是与目标网站进行交互。" + }, "alertMessageGasEstimateFailed": { "message": "我们无法提供准确的费用,估算可能较高。我们建议您输入自定义的燃料限制,但交易仍存在失败风险。" }, @@ -376,6 +425,9 @@ "alertMessageGasTooLow": { "message": "要继续此交易,您需要将燃料限制提高到 21000 或更高。" }, + "alertMessageInsufficientBalance2": { + "message": "您的账户中没有足够的 ETH 来支付网络费用。" + }, "alertMessageNetworkBusy": { "message": "燃料价格很高,估算不太准确。" }, @@ -569,6 +621,12 @@ "assetOptions": { "message": "资产选项" }, + "assets": { + "message": "资产" + }, + "assetsDescription": { + "message": "自动检测钱包中的代币,显示 NFT,并获取批量账户余额更新" + }, "attemptSendingAssets": { "message": "如果您试图将资产从一个网络直接发送到另一个网络,这可能会导致永久的资产损失。请务必使用跨链桥进行操作。" }, @@ -782,6 +840,15 @@ "bridgeDontSend": { "message": "跨链桥,不要发送" }, + "bridgeFrom": { + "message": "桥接自" + }, + "bridgeSelectNetwork": { + "message": "选择网络" + }, + "bridgeTo": { + "message": "桥接至" + }, "browserNotSupported": { "message": "您的浏览器不受支持……" }, @@ -945,24 +1012,54 @@ "confirmRecoveryPhrase": { "message": "确认私钥助记词" }, + "confirmTitleApproveTransaction": { + "message": "许可请求" + }, + "confirmTitleDeployContract": { + "message": "部署合约" + }, + "confirmTitleDescApproveTransaction": { + "message": "此网站需要获得许可来提取您的 NFT。" + }, + "confirmTitleDescDeployContract": { + "message": "该网站希望您部署一份合约" + }, + "confirmTitleDescERC20ApproveTransaction": { + "message": "此网站需要获得许可来提取您的代币" + }, "confirmTitleDescPermitSignature": { "message": "此网站需要获得许可来使用您的代币。" }, "confirmTitleDescSIWESignature": { "message": "某个网站要求您登录以证明您拥有该账户。" }, + "confirmTitleDescSign": { + "message": "在确认之前,请查看请求详细信息。" + }, "confirmTitlePermitTokens": { "message": "支出上限请求" }, + "confirmTitleRevokeApproveTransaction": { + "message": "撤销权限" + }, "confirmTitleSIWESignature": { "message": "登录请求" }, + "confirmTitleSetApprovalForAllRevokeTransaction": { + "message": "撤销权限" + }, "confirmTitleSignature": { "message": "签名请求" }, "confirmTitleTransaction": { "message": "交易请求" }, + "confirmationAlertModalDetails": { + "message": "为了保护您的资产和登录信息,我们建议您拒绝该请求。" + }, + "confirmationAlertModalTitle": { + "message": "此请求可疑" + }, "confirmed": { "message": "已确认" }, @@ -975,6 +1072,9 @@ "confusingEnsDomain": { "message": "我们在 ENS 名称中检测到一个可令人混淆的字符。检查 ENS 名称以避免潜在的欺诈。" }, + "congratulations": { + "message": "恭喜!" + }, "connect": { "message": "连接" }, @@ -1035,6 +1135,9 @@ "connectedSites": { "message": "已连接的网站" }, + "connectedSitesAndSnaps": { + "message": "连接的网站和 Snap" + }, "connectedSitesDescription": { "message": "$1 已连接到这些网站。他们可以查看您的账户地址。", "description": "$1 is the account name" @@ -1046,6 +1149,17 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask 已连接到此网站,但尚未连接任何账户" }, + "connectedSnaps": { + "message": "连接的 Snap" + }, + "connectedWithAccount": { + "message": "已连接 $1 账户", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "与 $1 连接", + "description": "$1 represents account name" + }, "connecting": { "message": "连接中……" }, @@ -1073,6 +1187,9 @@ "connectingToSepolia": { "message": "正在连接Sepolia测试网络" }, + "connectionDescription": { + "message": "此网站想要:" + }, "connectionFailed": { "message": "连接失败" }, @@ -1153,6 +1270,9 @@ "copyAddress": { "message": "复制地址到剪贴板" }, + "copyAddressShort": { + "message": "复制地址" + }, "copyPrivateKey": { "message": "复制私钥" }, @@ -1402,12 +1522,41 @@ "defaultRpcUrl": { "message": "默认 RPC(远程过程调用)URL" }, + "defaultSettingsSubTitle": { + "message": "MetaMask 使用默认设置达到安全性和易用性的最佳平衡。更改这些设置以进一步保护您的隐私。" + }, + "defaultSettingsTitle": { + "message": "默认隐私设置" + }, "delete": { "message": "删除" }, "deleteContact": { "message": "删除联系人" }, + "deleteMetaMetricsData": { + "message": "删除 MetaMetrics 数据" + }, + "deleteMetaMetricsDataDescription": { + "message": "这将删除与您在此设备上的使用相关的 MetaMetrics 历史数据。删除此数据后,您的钱包和账户将保持原样。此过程可能需要最多 30 天。查看我们的 $1。", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "由于分析系统服务器问题,现在无法完成此请求,请稍后再试" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "我们现在无法删除此数据" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "我们即将删除您所有的 MetaMetrics 数据。您确定吗?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "要删除 MetaMetrics 数据吗?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "您在 $1 启动了此操作。此过程可能需要最多 30 天。查看 $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "如果您删除此网络,则需要再次添加此网络才能查看您在其中的资产" }, @@ -1418,6 +1567,9 @@ "deposit": { "message": "保证金" }, + "depositCrypto": { + "message": "使用钱包地址或二维码从另一账户存入加密货币。" + }, "deprecatedGoerliNtwrkMsg": { "message": "由于以太坊系统的升级,Goerli 测试网络将很快淘汰。" }, @@ -1459,9 +1611,15 @@ "disconnectAllAccountsText": { "message": "账户" }, + "disconnectAllDescriptionText": { + "message": "如果您断开与此网站的连接,则需要重新连接您的账户和网络才能再次使用此网站。" + }, "disconnectAllSnapsText": { "message": "Snap" }, + "disconnectMessage": { + "message": "这将断开您与此站点的连接" + }, "disconnectPrompt": { "message": "断开连接 $1" }, @@ -1531,6 +1689,9 @@ "editANickname": { "message": "编辑昵称" }, + "editAccounts": { + "message": "编辑账户" + }, "editAddressNickname": { "message": "编辑地址昵称" }, @@ -1611,6 +1772,9 @@ "editNetworkLink": { "message": "编辑原始网络" }, + "editNetworksTitle": { + "message": "编辑网络" + }, "editNonceField": { "message": "编辑 nonce" }, @@ -1620,9 +1784,24 @@ "editPermission": { "message": "编辑权限" }, + "editPermissions": { + "message": "编辑权限" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "编辑加速燃料费用" }, + "editSpendingCap": { + "message": "编辑支出上限" + }, + "editSpendingCapAccountBalance": { + "message": "账户余额:$1 $2" + }, + "editSpendingCapDesc": { + "message": "输入您觉得可以代您花费的适当金额。" + }, + "editSpendingCapError": { + "message": "支出上限不能超过 $1 小数位数。删除小数位数以继续。" + }, "enableAutoDetect": { "message": " 启用自动检测" }, @@ -1674,21 +1853,36 @@ "ensUnknownError": { "message": "ENS 查找失败。" }, + "enterANameToIdentifyTheUrl": { + "message": "输入名称以标识 URL" + }, "enterANumber": { "message": "输入一个数字" }, + "enterChainId": { + "message": "输入链 ID" + }, "enterCustodianToken": { "message": "输入您的 $1 代币或添加新代币" }, "enterMaxSpendLimit": { "message": "输入最大消费限额" }, + "enterNetworkName": { + "message": "输入网络名称" + }, "enterOptionalPassword": { "message": "输入可选密码" }, "enterPasswordContinue": { "message": "输入密码继续" }, + "enterRpcUrl": { + "message": "输入 RPC(远程过程调用)URL" + }, + "enterSymbol": { + "message": "输入符号" + }, "enterTokenNameOrAddress": { "message": "输入代币名称或粘贴地址" }, @@ -1762,6 +1956,15 @@ "experimental": { "message": "实验性" }, + "exportYourData": { + "message": "导出您的数据" + }, + "exportYourDataButton": { + "message": "下载" + }, + "exportYourDataDescription": { + "message": "您可以导出联系人和偏好等数据。" + }, "extendWalletWithSnaps": { "message": "探索社区构建的 Snap,定制您的 Web3 体验", "description": "Banner description displayed on Snaps list page in Settings when less than 6 Snaps is installed." @@ -1877,6 +2080,9 @@ "message": "这笔燃料费是由 $1 建议的。忽略它可能会导致您的交易出现问题。如果您有疑问,请联系 $1。", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "燃料费" + }, "gasIsETH": { "message": "燃料是 $1 " }, @@ -1947,6 +2153,9 @@ "generalCameraErrorTitle": { "message": "出错了..." }, + "generalDescription": { + "message": "跨设备同步设置、选择网络首选项和跟踪代币数据" + }, "genericExplorerView": { "message": "在$1查看账户" }, @@ -2093,6 +2302,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "如果应用程序锁定导致无法登录或使用新设备,则会失去资金。请务必在 $1 备份您的私钥助记词 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "忽略所有" }, @@ -2174,6 +2387,9 @@ "inYourSettings": { "message": "在设置中" }, + "included": { + "message": "包括" + }, "infuraBlockedNotification": { "message": "MetaMask 无法连接到区块链主机。请检查可能的原因 $1。", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2344,6 +2560,10 @@ "message": "JSON 文件", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "将您的私钥助记词提醒存放在安全的地方。如果遗失,没有人能帮您找回。更糟糕的是,您将无法再访问您的钱包。$1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "账户名称" }, @@ -2402,6 +2622,9 @@ "message": "学习如何 $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "了解操作方法" + }, "learnMore": { "message": "了解更多" }, @@ -2409,6 +2632,9 @@ "message": "想了解有关燃料费的 $1?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "了解有关隐私最佳实践的更多信息。" + }, "learnMoreKeystone": { "message": "了解更多" }, @@ -2497,6 +2723,9 @@ "link": { "message": "链接" }, + "linkCentralizedExchanges": { + "message": "链接您的 Coinbase 或 Binance 账户,将加密货币免费转移到 MetaMask。" + }, "links": { "message": "链接" }, @@ -2557,6 +2786,9 @@ "message": "请确保没有人在看您的屏幕", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "管理默认隐私设置" + }, "marketCap": { "message": "市值" }, @@ -2600,6 +2832,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "连接状态按钮显示所访问的网站是否与您当前选择的账户连接。" }, + "metaMetricsIdNotAvailableError": { + "message": "由于您从未选择过 MetaMetrics,因此此处没有要删除的数据。" + }, "metadataModalSourceTooltip": { "message": "$1 托管于 npm 上,$2 是此 Snap 的唯一标识符。", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2676,6 +2911,17 @@ "more": { "message": "更多" }, + "moreAccounts": { + "message": "+ 另外 $1 个账户", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ 另外 $1 个网络", + "description": "$1 is the number of networks" + }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "您正在将此网络添加到 MetaMask 并授予该网站其使用许可。" + }, "multipleSnapConnectionWarning": { "message": "$1 想连接 $2 个 snap。", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2754,9 +3000,13 @@ "message": "编辑网络详情" }, "nativeTokenScamWarningDescription": { - "message": "此网络与其关联的链 ID 或名称不匹配。多种常用代币均使用名称 $1,使其成为欺诈目标。欺诈方可能会诱骗您向其发送更有价值的货币作为回报。在继续之前,请验证所有内容。", + "message": "原生代币符号与具有关联链 ID 网络的原生代币的预期符号不匹配。您输入了 $1,而预期的代币符号为 $2。请验证您是否连接到正确的链。", "description": "$1 represents the currency name, $2 represents the expected currency symbol" }, + "nativeTokenScamWarningDescriptionExpectedTokenFallback": { + "message": "其他", + "description": "graceful fallback for when token symbol isn't found" + }, "nativeTokenScamWarningTitle": { "message": "这可能是欺诈", "description": "Title for nativeTokenScamWarningDescription" @@ -2790,6 +3040,9 @@ "networkDetails": { "message": "网络详情" }, + "networkFee": { + "message": "网络费" + }, "networkIsBusy": { "message": "网络繁忙。燃料价格较高,估值较不准确。" }, @@ -2844,6 +3097,9 @@ "networkOptions": { "message": "网络选项" }, + "networkPermissionToast": { + "message": "网络许可已更新" + }, "networkProvider": { "message": "网络提供商" }, @@ -2865,15 +3121,26 @@ "message": "我们无法连接到 $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "网络已切换至 $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "网络 URL" }, "networkURLDefinition": { "message": "用于访问此网络的 URL。" }, + "networkUrlErrorWarning": { + "message": "攻击者有时会通过对网站地址进行细微更改来模仿网站。在继续操作之前,请确保您是与目标网站进行交互。Punycode 版本:$1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "网络" }, + "networksSmallCase": { + "message": "网络" + }, "nevermind": { "message": "没关系" }, @@ -2924,6 +3191,9 @@ "newPrivacyPolicyTitle": { "message": "我们已经更新了隐私政策" }, + "newRpcUrl": { + "message": "新的 RPC(远程过程调用) URL" + }, "newTokensImportedMessage": { "message": "您已成功导入$1。", "description": "$1 is the string of symbols of all the tokens imported" @@ -2986,6 +3256,9 @@ "noConnectedAccountTitle": { "message": "MetaMask 未连接到此站点" }, + "noConnectionDescription": { + "message": "要连接到网站,请找到并选择“连接”按钮。记住,MetaMask 仅限连接到 Web3 上的网站" + }, "noConversionRateAvailable": { "message": "无可用汇率" }, @@ -3031,6 +3304,9 @@ "nonceFieldHeading": { "message": "自定义 nonce" }, + "none": { + "message": "无" + }, "notBusy": { "message": "非忙碌中" }, @@ -3512,6 +3788,12 @@ "permissionDetails": { "message": "许可详情" }, + "permissionFor": { + "message": "权限用于" + }, + "permissionFrom": { + "message": "权限来自" + }, "permissionRequest": { "message": "权限请求" }, @@ -3593,6 +3875,14 @@ "message": "允许 $1 访问您的 MetaMask 首选语言设置。这可用于进行本地化,以及使用您的语言显示$1的内容。", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "查看您的首选语言和法币等信息。", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "让 $1 访问 MetaMask 设置中的首选语言和法币等信息。这有助于 $1 显示符合您偏好的内容。 ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "显示自定义屏幕", "description": "The description for the `endowment:page-home` permission" @@ -3751,6 +4041,9 @@ "permitSimulationDetailInfo": { "message": "您将授予该消费者许可从您的账户中支出这些代币。" }, + "permittedChainToastUpdate": { + "message": "$1 可以访问 $2。" + }, "personalAddressDetected": { "message": "检测到个人地址。请输入代币合约地址。" }, @@ -3963,6 +4256,12 @@ "receive": { "message": "收款" }, + "receiveCrypto": { + "message": "接收加密货币" + }, + "recipientAddressPlaceholderNew": { + "message": "输入公钥 (0x) 或域名" + }, "recommendedGasLabel": { "message": "建议" }, @@ -4026,6 +4325,12 @@ "rejected": { "message": "已拒绝" }, + "rememberSRPIfYouLooseAccess": { + "message": "请记住,如果私钥助记词遗失,您将无法访问您的钱包。$1 以保护这组助记词的安全,这样您可以随时访问您的资金。" + }, + "reminderSet": { + "message": "提醒设置完毕!" + }, "remove": { "message": "删除" }, @@ -4105,6 +4410,13 @@ "requestNotVerifiedError": { "message": "由于出现错误,安全提供商没有验证此请求。请谨慎操作。" }, + "requestingFor": { + "message": "请求" + }, + "requestingForAccount": { + "message": "请求 $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "待确认的请求" }, @@ -4193,6 +4505,12 @@ "revealTheSeedPhrase": { "message": "显示助记词" }, + "review": { + "message": "查看" + }, + "reviewAlert": { + "message": "查看提醒" + }, "reviewAlerts": { "message": "查看提醒" }, @@ -4218,6 +4536,9 @@ "revokePermission": { "message": "撤销权限" }, + "revokeSimulationDetailsDesc": { + "message": "您正在从您的账户中删除某人使用代币的权限。" + }, "revokeSpendingCap": { "message": "撤销 $1 的支出上限", "description": "$1 is a token symbol" @@ -4225,6 +4546,9 @@ "revokeSpendingCapTooltipText": { "message": "第三方将无法再使用您当前或未来的任何代币。" }, + "rpcNameOptional": { + "message": "RPC(远程过程调用)名称(可选)" + }, "rpcUrl": { "message": "新的 RPC URL" }, @@ -4277,10 +4601,23 @@ "securityAndPrivacy": { "message": "安全和隐私" }, + "securityDescription": { + "message": "减少您加入不安全网络的机会并保护您的账户" + }, + "securityMessageLinkForNetworks": { + "message": "网络欺诈和安全风险" + }, + "securityPrivacyPath": { + "message": "设置 > 安全与隐私。" + }, "securityProviderPoweredBy": { "message": "由 $1 提供支持", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "查看所有许可", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "查看详情" }, @@ -4374,6 +4711,9 @@ "selectPathHelp": { "message": "如果您没有看到您期望的账户,请尝试切换 HD 路径。" }, + "selectRpcUrl": { + "message": "选择 RPC(远程过程调用)URL" + }, "selectType": { "message": "选择类型" }, @@ -4436,6 +4776,9 @@ "setApprovalForAll": { "message": "设置批准所有" }, + "setApprovalForAllRedesignedTitle": { + "message": "提取请求" + }, "setApprovalForAllTitle": { "message": "批准$1,且无消费限制", "description": "The token symbol that is being approved" @@ -4446,6 +4789,9 @@ "settings": { "message": "设置" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "设置经过优化,更具易用性和安全性。可随时对此进行更改。" + }, "settingsSearchMatchingNotFound": { "message": "没有找到匹配的结果." }, @@ -4492,6 +4838,9 @@ "showMore": { "message": "展开" }, + "showNativeTokenAsMainBalance": { + "message": "将原生代币显示为主余额" + }, "showNft": { "message": "显示 NFT" }, @@ -4531,6 +4880,15 @@ "signingInWith": { "message": "使用以下登录方式" }, + "simulationApproveHeading": { + "message": "提取" + }, + "simulationDetailsApproveDesc": { + "message": "您正在授予其他人从您的账户中提取 NFT 的许可。" + }, + "simulationDetailsERC20ApproveDesc": { + "message": "您正在授予其他人从您的账户中花费该金额的权限。" + }, "simulationDetailsFiatNotAvailable": { "message": "不可用" }, @@ -4540,6 +4898,12 @@ "simulationDetailsOutgoingHeading": { "message": "您发送" }, + "simulationDetailsRevokeSetApprovalForAllDesc": { + "message": "您正在删除其他人从您的账户中提取 NFT 的权限。" + }, + "simulationDetailsSetApprovalForAllDesc": { + "message": "您正在授予其他人从您的账户中提取 NFT 的权限。" + }, "simulationDetailsTitle": { "message": "预计变化" }, @@ -4792,6 +5156,16 @@ "somethingWentWrong": { "message": "哎呀!出了点问题。" }, + "sortBy": { + "message": "排序方式" + }, + "sortByAlphabetically": { + "message": "按字母顺序排列(A-Z)" + }, + "sortByDecliningBalance": { + "message": "余额降序($1 高-低)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "来源" }, @@ -4835,6 +5209,12 @@ "spender": { "message": "消费者" }, + "spenderTooltipDesc": { + "message": "这是可以提取您的 NFT 的地址。" + }, + "spenderTooltipERC20ApproveDesc": { + "message": "这是能够代表您花费您的代币的地址。" + }, "spendingCap": { "message": "支出上限" }, @@ -4848,6 +5228,9 @@ "spendingCapRequest": { "message": "$1的支出上限请求" }, + "spendingCapTooltipDesc": { + "message": "他人可代表您访问该代币金额并进行消费。" + }, "srpInputNumberOfWords": { "message": "我有一个包含$1个单词的私钥助记词", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5051,6 +5434,9 @@ "message": "由 $1 建议", "description": "$1 is the snap name" }, + "suggestedCurrencySymbol": { + "message": "建议的货币符号:" + }, "suggestedTokenName": { "message": "建议名称:" }, @@ -5060,6 +5446,9 @@ "supportCenter": { "message": "访问我们的支持中心" }, + "supportMultiRpcInformation": { + "message": "我们现在支持单个网络的多个 RPC(远程过程调用)。您最近的 RPC 已被选为默认 RPC,以解决冲突信息。" + }, "surveyConversion": { "message": "参与我们的调查" }, @@ -5176,6 +5565,14 @@ "swapGasFeesDetails": { "message": "燃料费用是估算的,并将根据网络流量和交易复杂性而波动。" }, + "swapGasFeesExplanation": { + "message": "MetaMask 不从燃料费中赚钱。这些费用是估计值,可能会根据网络的繁忙程度和交易的复杂程度而变化。了解更多信息 $1。", + "description": "$1 is a link (text in link can be found at 'swapGasFeesSummaryLinkText')" + }, + "swapGasFeesExplanationLinkText": { + "message": "参见此处", + "description": "Text for link in swapGasFeesExplanation" + }, "swapGasFeesLearnMore": { "message": "了解更多关于燃料费的信息" }, @@ -5186,9 +5583,19 @@ "message": "燃料费用支付给在 $1 网络上处理交易的加密矿工。MetaMask 不会从燃料费用中获利。", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "此报价通过调整发送或接收的代币金额将燃料费包含在内。您可能会在活动列表上的单独交易中收到 ETH。" + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "了解更多有关燃料费的信息" + }, "swapHighSlippage": { "message": "高滑点" }, + "swapIncludesGasAndMetaMaskFee": { + "message": "包括燃料和 $1% 的 MetaMask 费用", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "包括 $1% 的 MetaMask 费用。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5485,6 +5892,9 @@ "termsOfUseTitle": { "message": "我们的使用条款已更新" }, + "testnets": { + "message": "测试网" + }, "theme": { "message": "主题" }, @@ -5666,6 +6076,9 @@ "transactionFee": { "message": "交易费用" }, + "transactionFlowNetwork": { + "message": "网络" + }, "transactionHistoryBaseFee": { "message": "基础费用(GWEI)" }, @@ -5708,9 +6121,15 @@ "transfer": { "message": "转账" }, + "transferCrypto": { + "message": "转移加密货币" + }, "transferFrom": { "message": "转账自" }, + "transferRequest": { + "message": "转账请求" + }, "trillionAbbreviation": { "message": "万亿", "description": "Shortened form of 'trillion'" @@ -5844,12 +6263,22 @@ "update": { "message": "更新" }, + "updateEthereumChainConfirmationDescription": { + "message": "此网站正在请求更新您的默认网络 URL。您可以随时编辑默认值和网络信息。" + }, + "updateNetworkConfirmationTitle": { + "message": "更新 $1", + "description": "$1 represents network name" + }, "updateOrEditNetworkInformations": { "message": "更新您的信息或者" }, "updateRequest": { "message": "更新请求" }, + "updatedRpcForNetworks": { + "message": "网络 RPC(远程过程调用)已更新" + }, "uploadDropFile": { "message": "将您的文件放在此处" }, @@ -5974,6 +6403,10 @@ "walletConnectionGuide": { "message": "我们的硬件钱包连接指南" }, + "walletProtectedAndReadyToUse": { + "message": "您的钱包受到保护,可随时使用。您可在 $1 找到私钥助记词 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "wantToAddThisNetwork": { "message": "想要添加此网络吗?" }, @@ -5991,6 +6424,17 @@ "message": "$1 第三方可能会支出您的全部代币余额,无需进一步通知或同意。请自定义较低的支出上限以保护自己。", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "启用此选项后,您将能够通过公共地址或 ENS(Ethereum 域名服务)名称查看以太坊账户。如需有关此测试功能的反馈,请完成此 $1。", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "查看以太坊账户(测试版)" + }, + "watchOutMessage": { + "message": "小心 $1。", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "弱" }, @@ -6037,6 +6481,9 @@ "whatsThis": { "message": "这是什么?" }, + "withdrawing": { + "message": "提取" + }, "wrongNetworkName": { "message": "根据我们的记录,该网络名称可能与此链 ID 不匹配。" }, @@ -6065,6 +6512,9 @@ "yourBalance": { "message": "您的余额" }, + "yourBalanceIsAggregated": { + "message": "您的余额已汇总" + }, "yourNFTmayBeAtRisk": { "message": "您的 NFT 可能面临风险" }, @@ -6077,6 +6527,9 @@ "yourTransactionJustConfirmed": { "message": "在区块链上确认之前,我们无法取消您的交易。" }, + "yourWalletIsReady": { + "message": "您的钱包已准备就绪" + }, "zeroGasPriceOnSpeedUpError": { "message": "加速时零燃料价格" }