diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index dd34a61bf4e4..750420f06675 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Zusätzliche Netzwerke" }, - "additionalRpcUrl": { - "message": "Weitere RPC-URL" - }, "address": { "message": "Adresse" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 45be1ea60597..65b4f9f07bec 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Επιπλέον δίκτυα" }, - "additionalRpcUrl": { - "message": "Επιπλέον διεύθυνση URL RPC" - }, "address": { "message": "Διεύθυνση" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 088c48829273..25ac243158c2 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -345,9 +345,6 @@ "additionalNetworks": { "message": "Additional networks" }, - "additionalRpcUrl": { - "message": "Additional RPC URL" - }, "address": { "message": "Address" }, @@ -888,6 +885,12 @@ "message": "Buy $1", "description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase" }, + "buyCrypto": { + "message": "Buy crypto" + }, + "buyFirstCrypto": { + "message": "Buy your first crypto with a debit or credit card." + }, "buyMoreAsset": { "message": "Buy more $1", "description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase" @@ -1560,6 +1563,9 @@ "deposit": { "message": "Deposit" }, + "depositCrypto": { + "message": "Deposit crypto from another account with a wallet address or QR code." + }, "deprecatedGoerliNtwrkMsg": { "message": "Because of updates to the Ethereum system, the Goerli test network will be phased out soon." }, @@ -1842,6 +1848,9 @@ "ensUnknownError": { "message": "ENS lookup failed." }, + "enterANameToIdentifyTheUrl": { + "message": "Enter a name to identify the URL" + }, "enterANumber": { "message": "Enter a number" }, @@ -1857,6 +1866,9 @@ "enterPasswordContinue": { "message": "Enter password to continue" }, + "enterRpcUrl": { + "message": "Enter RPC URL" + }, "enterTokenNameOrAddress": { "message": "Enter token name or paste address" }, @@ -2159,6 +2171,12 @@ "genericExplorerView": { "message": "View account on $1" }, + "getStarted": { + "message": "Get Started" + }, + "getStartedByFundingWallet": { + "message": "Get started by adding some crypto to your wallet." + }, "getStartedWithNFTs": { "message": "Get $1 to buy NFTs", "description": "$1 is the token symbol" @@ -2726,6 +2744,9 @@ "link": { "message": "Link" }, + "linkCentralizedExchanges": { + "message": "Link your Coinbase or Binance accounts to transfer crypto to MetaMask for free." + }, "links": { "message": "Links" }, @@ -4257,6 +4278,9 @@ "receive": { "message": "Receive" }, + "receiveCrypto": { + "message": "Receive crypto" + }, "recipientAddressPlaceholder": { "message": "Enter public address (0x) or ENS name" }, @@ -4528,6 +4552,9 @@ "revokeSpendingCapTooltipText": { "message": "This third party will be unable to spend any more of your current or future tokens." }, + "rpcNameOptional": { + "message": "RPC Name (Optional)" + }, "rpcUrl": { "message": "New RPC URL" }, @@ -4669,6 +4696,9 @@ "selectEnableDisplayMediaPrivacyPreference": { "message": "Turn on Display NFT Media" }, + "selectFundingMethod": { + "message": "Select a funding method" + }, "selectHdPath": { "message": "Select HD path" }, @@ -5435,6 +5465,9 @@ "supportCenter": { "message": "Visit our support center" }, + "supportMultiRpcInformation": { + "message": "We now support multiple RPCs for a single network. Your most recent RPC has been selected as the default one to resolve conflicting information." + }, "surveyConversion": { "message": "Take our survey" }, @@ -6149,6 +6182,9 @@ "transfer": { "message": "Transfer" }, + "transferCrypto": { + "message": "Transfer crypto" + }, "transferFrom": { "message": "Transfer from" }, @@ -6297,6 +6333,9 @@ "updateRequest": { "message": "Update request" }, + "updatedRpcForNetworks": { + "message": "Network RPCs Updated" + }, "updatedWithDate": { "message": "Updated $1" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 4bf761eef0ba..2cfa0ad872c3 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Redes adicionales" }, - "additionalRpcUrl": { - "message": "URL RPC adicional" - }, "address": { "message": "Dirección" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 8980dfb0fd48..1027e859fbf6 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Réseaux supplémentaires" }, - "additionalRpcUrl": { - "message": "URL supplémentaire de RPC" - }, "address": { "message": "Adresse" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 2d0caae66f01..57ee2cf4cccb 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "अतिरिक्त नेटवर्क" }, - "additionalRpcUrl": { - "message": "अतिरिक्त RPC URL" - }, "address": { "message": "एड्रेस" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 4047a026b379..8a4198e34b9b 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Jaringan tambahan" }, - "additionalRpcUrl": { - "message": "URL RPC Tambahan" - }, "address": { "message": "Alamat" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 46f0ef9d1d95..39e329fd2809 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "他のネットワーク" }, - "additionalRpcUrl": { - "message": "他のRPC URL" - }, "address": { "message": "アドレス" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 904cd16deb49..b5c85683bc55 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "추가 네트워크" }, - "additionalRpcUrl": { - "message": "추가 RPC URL" - }, "address": { "message": "주소" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index a1340fd2568a..3e38602dbf97 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Redes adicionais" }, - "additionalRpcUrl": { - "message": "URL da RPC adicional" - }, "address": { "message": "Endereço" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 8c94d3ad9074..c16fbf8b770f 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Дополнительные сети" }, - "additionalRpcUrl": { - "message": "Дополнительный URL-адрес RPC" - }, "address": { "message": "Адрес" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 1b86e07a8628..5f5f3d73d6c4 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Mga karagdagang network" }, - "additionalRpcUrl": { - "message": "Karagdagang RPC URL" - }, "address": { "message": "Address" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index f1833f56875d..88709f34eea8 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "İlave ağlar" }, - "additionalRpcUrl": { - "message": "Diğer RPC URL" - }, "address": { "message": "Adres" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 0f1d335eb92b..09720f543a32 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "Mạng bổ sung" }, - "additionalRpcUrl": { - "message": "URL RPC Bổ sung" - }, "address": { "message": "Địa chỉ" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 584299b4c155..ea37ffaddd49 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -332,9 +332,6 @@ "additionalNetworks": { "message": "其他网络" }, - "additionalRpcUrl": { - "message": "其他 RPC(远程过程调用)URL" - }, "address": { "message": "地址" }, diff --git a/app/scripts/migrations/125.1.test.ts b/app/scripts/migrations/125.1.test.ts new file mode 100644 index 000000000000..eb00db9d1e07 --- /dev/null +++ b/app/scripts/migrations/125.1.test.ts @@ -0,0 +1,107 @@ +import { migrate, version } from './125.1'; + +const oldVersion = 125; + +describe(`migration #${version}`, () => { + afterEach(() => jest.resetAllMocks()); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('Gracefully handles empty/undefined PreferencesController', async () => { + for (const PreferencesController of [{}, undefined, null, 1, '', []]) { + const oldStorage = { + meta: { version: oldVersion }, + data: { PreferencesController }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data.TxController).toStrictEqual(undefined); + } + }); + + it('Enables token autodetection when basic functionality is on', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + useExternalServices: true, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toEqual({ + PreferencesController: { + useExternalServices: true, + useTokenDetection: true, + }, + }); + }); + + it('Does not enable token autodetection when basic functionality is off', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + useExternalServices: false, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toEqual({ + PreferencesController: { + useExternalServices: false, + }, + }); + }); + + it('Removes showTokenAutodetectModalOnUpgrade from the app metadata controller', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AppMetadataController: { + previousMigrationVersion: oldVersion, + currentMigrationVersion: version, + showTokenAutodetectModalOnUpgrade: null, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toEqual({ + AppMetadataController: { + previousMigrationVersion: oldVersion, + currentMigrationVersion: version, + }, + }); + }); + + it('Does nothing if showTokenAutodetectModalOnUpgrade is not in the app metadata controller', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AppMetadataController: { + previousMigrationVersion: oldVersion, + currentMigrationVersion: version, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toEqual({ + AppMetadataController: { + previousMigrationVersion: oldVersion, + currentMigrationVersion: version, + }, + }); + }); +}); diff --git a/app/scripts/migrations/125.1.ts b/app/scripts/migrations/125.1.ts new file mode 100644 index 000000000000..d3c975a78a11 --- /dev/null +++ b/app/scripts/migrations/125.1.ts @@ -0,0 +1,50 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 125.1; + +/** + * This migration enables token auto-detection if the basic functionality toggle is on. + * + * It also removes an unused property `showTokenAutodetectModalOnUpgrade` from the app metadata controller. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by + * controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if ( + hasProperty(state, 'PreferencesController') && + isObject(state.PreferencesController) && + state.PreferencesController.useExternalServices === true + ) { + state.PreferencesController.useTokenDetection = true; + } + + if ( + hasProperty(state, 'AppMetadataController') && + isObject(state.AppMetadataController) + ) { + delete state.AppMetadataController.showTokenAutodetectModalOnUpgrade; + } + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 7e800337da3b..e5cfb6218019 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -144,6 +144,7 @@ const migrations = [ require('./123'), require('./124'), require('./125'), + require('./125.1'), require('./126'), ]; diff --git a/test/e2e/accounts/snap-account-eth-swap.spec.ts b/test/e2e/accounts/snap-account-eth-swap.spec.ts index bb4c3c7a5770..2a99287e5db1 100644 --- a/test/e2e/accounts/snap-account-eth-swap.spec.ts +++ b/test/e2e/accounts/snap-account-eth-swap.spec.ts @@ -7,11 +7,24 @@ import { waitForTransactionToComplete, checkActivityTransaction, } from '../tests/swaps/shared'; +import { TRADES_API_MOCK_RESULT } from '../../data/mock-data'; +import { Mockttp } from '../mock-e2e'; import { installSnapSimpleKeyring } from './common'; const DAI = 'DAI'; const TEST_ETH = 'TESTETH'; +async function mockSwapsTransactionQuote(mockServer: Mockttp) { + return [ + await mockServer + .forGet('https://swap.api.cx.metamask.io/networks/1/trades') + .thenCallback(() => ({ + statusCode: 200, + json: TRADES_API_MOCK_RESULT, + })), + ]; +} + describe('Snap Account - Swap', function () { it('swaps ETH for DAI using a snap account', async function () { await withFixtures( @@ -19,6 +32,7 @@ describe('Snap Account - Swap', function () { fixtures: new FixtureBuilder().build(), ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), + testSpecificMock: mockSwapsTransactionQuote, }, async ({ driver }: { driver: Driver }) => { await installSnapSimpleKeyring(driver, false); @@ -26,11 +40,11 @@ describe('Snap Account - Swap', function () { WINDOW_TITLES.ExtensionInFullScreenView, ); await buildQuote(driver, { - amount: 0.001, + amount: 2, swapTo: DAI, }); await reviewQuote(driver, { - amount: 0.001, + amount: 2, swapFrom: TEST_ETH, swapTo: DAI, }); @@ -38,7 +52,7 @@ describe('Snap Account - Swap', function () { await waitForTransactionToComplete(driver, { tokenName: 'DAI' }); await checkActivityTransaction(driver, { index: 0, - amount: '0.001', + amount: '2', swapFrom: TEST_ETH, swapTo: DAI, }); 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 38473a52b36c..7691ef634b5e 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 @@ -181,7 +181,7 @@ "dismissSeedBackUpReminder": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": "boolean", - "useTokenDetection": false, + "useTokenDetection": true, "useNftDetection": false, "use4ByteResolution": true, "useCurrencyRateCheck": true, 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 3e81087c44c2..14c0b1712b19 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 @@ -99,7 +99,7 @@ "dismissSeedBackUpReminder": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": true, - "useTokenDetection": false, + "useTokenDetection": true, "useNftDetection": false, "useCurrencyRateCheck": true, "useRequestQueue": true, diff --git a/test/e2e/tests/multichain/asset-picker-send.spec.ts b/test/e2e/tests/multichain/asset-picker-send.spec.ts index 8358e8242f69..5accb14c6074 100644 --- a/test/e2e/tests/multichain/asset-picker-send.spec.ts +++ b/test/e2e/tests/multichain/asset-picker-send.spec.ts @@ -34,6 +34,15 @@ describe('AssetPickerSendFlow @no-mmi', function () { async ({ driver }: { driver: Driver }) => { await unlockWallet(driver); + // Disable token auto detection + await driver.openNewURL( + `${driver.extensionUrl}/home.html#settings/security`, + ); + await driver.clickElement( + '[data-testid="autoDetectTokens"] .toggle-button', + ); + await driver.navigate(); + // Open the send flow openActionMenuAndStartSendFlow(driver); @@ -72,13 +81,13 @@ describe('AssetPickerSendFlow @no-mmi', function () { assert.equal(tokenListSecondaryValue, '$250,000.00'); - // Search for BNB + // Search for CHZ const searchInputField = await driver.waitForSelector( '[data-testid="asset-picker-modal-search-input"]', ); await searchInputField.sendKeys('CHZ'); - // check that BNB is disabled + // check that CHZ is disabled const [, tkn] = await driver.findElements( '[data-testid="multichain-token-list-button"]', ); diff --git a/ui/components/app/assets/asset-list/asset-list.js b/ui/components/app/assets/asset-list/asset-list.js index 81c70d433dc9..916d86a07c26 100644 --- a/ui/components/app/assets/asset-list/asset-list.js +++ b/ui/components/app/assets/asset-list/asset-list.js @@ -35,14 +35,17 @@ import { DetectedTokensBanner, TokenListItem, ImportTokenLink, + ReceiveModal, } from '../../../multichain'; import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; import { showPrimaryCurrency, showSecondaryCurrency, } from '../../../../../shared/modules/currency-display.utils'; import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; +import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { RAMPS_CARD_VARIANT_TYPES, @@ -66,6 +69,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => { type, rpcUrl, ); + const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const balance = useSelector(getMultichainSelectedAccountCachedBalance); const balanceIsLoading = !balance; @@ -101,6 +105,14 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => { getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, ); + const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); + const [showReceiveModal, setShowReceiveModal] = useState(false); + + const onClickReceive = () => { + setShowFundingMethodModal(false); + setShowReceiveModal(true); + }; + const { tokensWithBalances, loading } = useAccountTotalFiatBalance( selectedAccount, shouldHideZeroBalanceTokens, @@ -151,6 +163,9 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => { ? RAMPS_CARD_VARIANT_TYPES.BTC : RAMPS_CARD_VARIANT_TYPES.TOKEN } + handleOnClick={ + isBtc ? undefined : () => setShowFundingMethodModal(true) + } /> ) : null ///: END:ONLY_INCLUDE_IF @@ -213,6 +228,20 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => { {showDetectedTokens && ( )} + {showReceiveModal && selectedAccount?.address && ( + setShowReceiveModal(false)} + /> + )} + {showFundingMethodModal && ( + setShowFundingMethodModal(false)} + title={t('selectFundingMethod')} + onClickReceive={onClickReceive} + /> + )} ); }; diff --git a/ui/components/app/currency-input/currency-input.js b/ui/components/app/currency-input/currency-input.js index 71425f797e9f..43da00ad3ab0 100644 --- a/ui/components/app/currency-input/currency-input.js +++ b/ui/components/app/currency-input/currency-input.js @@ -53,7 +53,9 @@ export default function CurrencyInput({ isSkeleton, isMatchingUpstream, }) { - const assetDecimals = Number(asset?.decimals) || NATIVE_CURRENCY_DECIMALS; + const assetDecimals = isNaN(Number(asset?.decimals)) + ? NATIVE_CURRENCY_DECIMALS + : Number(asset?.decimals); const preferredCurrency = useSelector(getNativeCurrency); const secondaryCurrency = useSelector(getCurrentCurrency); diff --git a/ui/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/components/app/modals/qr-scanner/qr-scanner.component.js index ea64d1270f54..7eaf3f9549e1 100644 --- a/ui/components/app/modals/qr-scanner/qr-scanner.component.js +++ b/ui/components/app/modals/qr-scanner/qr-scanner.component.js @@ -12,6 +12,7 @@ import Spinner from '../../../ui/spinner'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app'; import { SECOND } from '../../../../../shared/constants/time'; +import { parseScanContent } from './scan-util'; const READY_STATE = { ACCESSING_CAMERA: 'ACCESSING_CAMERA', @@ -30,8 +31,8 @@ const parseContent = (content) => { // Ethereum address links - fox ex. ethereum:0x.....1111 if (content.split('ethereum:').length > 1) { type = 'address'; - values = { address: content.split('ethereum:')[1] }; - + // uses regex capture groups to match and extract address while ignoring everything else + values = { address: parseScanContent(content) }; // Regular ethereum addresses - fox ex. 0x.....1111 } else if (content.substring(0, 2).toLowerCase() === '0x') { type = 'address'; diff --git a/ui/components/app/modals/qr-scanner/qr-scanner.test.js b/ui/components/app/modals/qr-scanner/qr-scanner.test.js new file mode 100644 index 000000000000..f63cddb4bdcf --- /dev/null +++ b/ui/components/app/modals/qr-scanner/qr-scanner.test.js @@ -0,0 +1,24 @@ +import { parseScanContent } from './scan-util'; + +describe('Extract Address from string', () => { + it('should correctly extract the address from the string', () => { + const result = parseScanContent( + 'ethereum:0xCf464B40cb2419944138F24514C9aE4D1889ccC1', + ); + expect(result).toStrictEqual('0xCf464B40cb2419944138F24514C9aE4D1889ccC1'); + }); + + it('should correctly extract the address from the string when there is a 0x appended', () => { + const result = parseScanContent( + 'ethereum:0xCf464B40cb2419944138F24514C9aE4D1889ccC1@0x1', + ); + expect(result).toStrictEqual('0xCf464B40cb2419944138F24514C9aE4D1889ccC1'); + }); + + it('should return null if there is no address to extract that matches the pattern', () => { + const result = parseScanContent( + 'ethereum:0xCf464B40cb2419944138F24514C9aE4D1', + ); + expect(result).toStrictEqual(null); + }); +}); diff --git a/ui/components/app/modals/qr-scanner/scan-util.ts b/ui/components/app/modals/qr-scanner/scan-util.ts new file mode 100644 index 000000000000..78bdad26bb5f --- /dev/null +++ b/ui/components/app/modals/qr-scanner/scan-util.ts @@ -0,0 +1,7 @@ +export function parseScanContent(content: string): string | null { + const matches = content.match(/^[a-zA-Z]+:(0x[0-9a-fA-F]{40})(?:@.*)?/u); + if (!matches) { + return null; + } + return matches[1]; +} diff --git a/ui/components/app/multi-rpc-edit-modal/__snapshots__/multi-rpc-edit-modal.test.tsx.snap b/ui/components/app/multi-rpc-edit-modal/__snapshots__/multi-rpc-edit-modal.test.tsx.snap new file mode 100644 index 000000000000..7c77aff77a3e --- /dev/null +++ b/ui/components/app/multi-rpc-edit-modal/__snapshots__/multi-rpc-edit-modal.test.tsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MultiRpcEditModal renders correctly with required props 1`] = ` + +
+
+
+ `; diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx index 65b8e889e94b..510ca7d98f05 100644 --- a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx +++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx @@ -17,25 +17,19 @@ describe('AddRpcUrlModal', () => { }); it('should render correctly', () => { - const { container } = render(); + const { container } = render( undefined} />); expect(container).toMatchSnapshot(); }); - it('should render the input field with the correct label', () => { - render(); - const inputLabel = screen.getByLabelText('additionalRpcUrl'); - expect(inputLabel).toBeInTheDocument(); - }); - it('should render the "Add URL" button with correct text', () => { - render(); + render( undefined} />); const addButton = screen.getByRole('button', { name: 'addUrl' }); expect(addButton).toBeInTheDocument(); }); it('should call the appropriate function when "Add URL" button is clicked', () => { const mockAddUrl = jest.fn(); - render(); + render( null} />); const addButton = screen.getByRole('button', { name: 'addUrl' }); userEvent.click(addButton); expect(mockAddUrl).not.toHaveBeenCalled(); diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx index 8a9ff53a74b5..b2086aeab2f5 100644 --- a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx +++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx @@ -1,42 +1,109 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Box, ButtonPrimary, ButtonPrimarySize, FormTextField, + FormTextFieldSize, + HelpText, + HelpTextSeverity, } from '../../../component-library'; import { + BackgroundColor, BlockSize, + BorderRadius, Display, + FlexDirection, + JustifyContent, TextVariant, } from '../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { isWebUrl } from '../../../../../app/scripts/lib/util'; -const AddRpcUrlModal = () => { +const AddRpcUrlModal = ({ + onAdded, +}: { + onAdded: (url: string, name?: string) => void; +}) => { const t = useI18nContext(); + const [url, setUrl] = useState(); + const [error, setError] = useState(); + const nameRef = useRef(null); + + useEffect(() => { + if (url && !isWebUrl(url)) { + setError(isWebUrl(`https://${url}`) ? t('urlErrorMsg') : t('invalidRPC')); + } else { + setError(undefined); + } + }, [url]); + return ( - - + + + setUrl(e.target.value)} + /> + {error && ( + {error} + )} + + - ({})} > - {t('addUrl')} - + { + if (url && !error && nameRef.current) { + onAdded(url, nameRef.current.value || undefined); + } + }} + > + {t('addUrl')} + + ); }; diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/index.scss b/ui/components/multichain/network-list-menu/add-rpc-url-modal/index.scss new file mode 100644 index 000000000000..27d1fe4c20fd --- /dev/null +++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/index.scss @@ -0,0 +1,9 @@ +.add-rpc-modal { + overflow-y: auto; + + &__footer { + position: sticky; + bottom: 0; + box-shadow: 0 0 8px 0 var(--color-shadow-default); + } +} diff --git a/ui/components/multichain/network-list-menu/rpc-list-item.tsx b/ui/components/multichain/network-list-menu/rpc-list-item.tsx new file mode 100644 index 000000000000..086f9838d142 --- /dev/null +++ b/ui/components/multichain/network-list-menu/rpc-list-item.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { infuraProjectId } from '../../../../shared/constants/network'; +import { Box, Text } from '../../component-library'; +import { + Display, + FlexDirection, + BorderStyle, + BorderColor, + TextColor, + TextVariant, + BackgroundColor, + BlockSize, +} from '../../../helpers/constants/design-system'; + +// TODO: Use version from network controller with v21 upgrade +enum RpcEndpointType { + Custom = 'custom', + Infura = 'infura', +} + +export const stripKeyFromInfuraUrl = (endpoint: string) => { + let modifiedEndpoint = endpoint; + + if (modifiedEndpoint.endsWith('/v3/{infuraProjectId}')) { + modifiedEndpoint = modifiedEndpoint.replace('/v3/{infuraProjectId}', ''); + } else if (modifiedEndpoint.endsWith(`/v3/${infuraProjectId}`)) { + modifiedEndpoint = modifiedEndpoint.replace(`/v3/${infuraProjectId}`, ''); + } + + return modifiedEndpoint; +}; + +export const stripProtocol = (endpoint: string) => { + const url = new URL(endpoint); + return `${url.host}${url.pathname === '/' ? '' : url.pathname}`; +}; + +// This components represents an RPC endpoint in a list, +// currently when selecting or editing endpoints for a network. +const RpcListItem = ({ + rpcEndpoint, +}: { + rpcEndpoint: { + name?: string; + url: string; + type: RpcEndpointType; + }; +}) => { + const { url, type } = rpcEndpoint; + const name = type === RpcEndpointType.Infura ? 'Infura' : rpcEndpoint.name; + + const displayEndpoint = (endpoint?: string) => + endpoint ? stripProtocol(stripKeyFromInfuraUrl(endpoint)) : '\u00A0'; + + const padding = name ? 2 : 4; + + return ( + + + + {name || displayEndpoint(url)} + + + {name && ( + + + {displayEndpoint(url)} + + + )} + + ); +}; + +export default RpcListItem; diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/__snapshots__/select-rpc-url-modal.test.tsx.snap b/ui/components/multichain/network-list-menu/select-rpc-url-modal/__snapshots__/select-rpc-url-modal.test.tsx.snap new file mode 100644 index 000000000000..ebb23db52186 --- /dev/null +++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/__snapshots__/select-rpc-url-modal.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectRpcUrlModal Component renders select rpc url 1`] = ` +
+
+
+
+
+ Ethereum Mainnet logo +
+

+ Ethereum Mainnet +

+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+`; diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/index.scss b/ui/components/multichain/network-list-menu/select-rpc-url-modal/index.scss new file mode 100644 index 000000000000..0f8cce816d4b --- /dev/null +++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/index.scss @@ -0,0 +1,23 @@ +.select-rpc-url { + &__item { + position: relative; + } + + &__item:hover { + cursor: pointer; + background-color: var(--color-background-default-hover); + } + + &__item--selected, + &__item--selected:hover { + background-color: var(--color-primary-muted); + } + + &__item-selected-pill { + width: 4px; + height: calc(100% - 8px); + position: absolute; + top: 4px; + left: 4px; + } +} diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.test.tsx b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.test.tsx new file mode 100644 index 000000000000..8f3de3d488f9 --- /dev/null +++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.test.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import { + // TODO: Add this API with network controller v21 upgrade + // updateNetwork, + setActiveNetwork, + setEditedNetwork, + toggleNetworkMenu, +} from '../../../../store/actions'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network'; +import { stripProtocol } from '../rpc-list-item'; +import { SelectRpcUrlModal } from './select-rpc-url-modal'; // Adjust the path as needed + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})); + +jest.mock('../../../../store/actions', () => ({ + // TODO: Add this API with network controller v21 upgrade + // updateNetwork: jest.fn(), + setActiveNetwork: jest.fn(), + setEditedNetwork: jest.fn(), + toggleNetworkMenu: jest.fn(), +})); + +const mockStore = configureMockStore(); +const networkConfiguration = { + chainId: '0x1', + name: 'Ethereum Mainnet', + rpcEndpoints: [ + { url: 'https://mainnet.infura.io/v3/', networkClientId: 'mainnet' }, + { url: 'https://rpc.flashbots.net/', networkClientId: 'flashbots' }, + ], + defaultRpcEndpointIndex: 0, +}; + +const store = mockStore({ + metamask: { + networks: [networkConfiguration], + activeNetwork: '0x1', + }, +}); + +describe('SelectRpcUrlModal Component', () => { + beforeEach(() => { + mockDispatch.mockClear(); + }); + + it('renders select rpc url', () => { + const { container } = renderWithProvider( + , + store, + ); + expect(container).toMatchSnapshot(); + }); + + it('should render the component correctly with network image and name', () => { + const { getByRole, getByText } = renderWithProvider( + , + store, + ); + + const imageSrc = + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + networkConfiguration.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ]; + + const networkImage = getByRole('img'); + + expect(networkImage).toBeInTheDocument(); + expect(networkImage).toHaveAttribute('src', imageSrc); + expect(getByText(networkConfiguration.name)).toBeInTheDocument(); + }); + + it('should render all RPC endpoints and highlight the selected one', () => { + const { getByText } = renderWithProvider( + , + store, + ); + + networkConfiguration.rpcEndpoints.forEach((rpcEndpoint) => { + expect(getByText(stripProtocol(rpcEndpoint.url))).toBeInTheDocument(); + }); + + const selectedItem = getByText( + stripProtocol(networkConfiguration.rpcEndpoints[0].url), + ).closest('.select-rpc-url__item'); + + expect(selectedItem).toHaveClass('select-rpc-url__item--selected'); + }); + + it('should dispatch the correct actions when an RPC endpoint is clicked', () => { + const { getByText } = renderWithProvider( + , + store, + ); + + const rpcEndpoint = getByText( + stripProtocol(networkConfiguration.rpcEndpoints[1].url), + ); + fireEvent.click(rpcEndpoint); + + // TODO: Add this API with network controller v21 upgrade + // expect(mockDispatch).toHaveBeenCalledWith( + // updateNetwork({ + // ...networkConfiguration, + // defaultRpcEndpointIndex: 1, + // }), + // ); + expect(mockDispatch).toHaveBeenCalledWith(setActiveNetwork('flashbots')); + expect(mockDispatch).toHaveBeenCalledWith(setEditedNetwork()); + expect(mockDispatch).toHaveBeenCalledWith(toggleNetworkMenu()); + }); + + it('should render the selected indicator correctly for the default RPC', () => { + const { container } = renderWithProvider( + , + store, + ); + + const selectedPill = container.querySelector( + '.select-rpc-url__item-selected-pill', + ); + expect(selectedPill).toBeInTheDocument(); + }); + + it('should render the modal with a network image', () => { + renderWithProvider( + , + store, + ); + + const networkImage = screen.getByRole('img'); + expect(networkImage).toBeInTheDocument(); + expect(networkImage).toHaveAttribute( + 'src', + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + networkConfiguration.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ], + ); + }); + + it('should handle click on RPC URL and update the network', () => { + renderWithProvider( + , + store, + ); + + fireEvent.click( + screen.getByText(stripProtocol(networkConfiguration.rpcEndpoints[1].url)), + ); + + // TODO: Add this API with network controller v21 upgrade + // expect(mockDispatch).toHaveBeenCalledWith( + // updateNetwork({ + // ...networkConfiguration, + // defaultRpcEndpointIndex: 1, + // }), + // ); + expect(mockDispatch).toHaveBeenCalledWith( + setActiveNetwork(networkConfiguration.rpcEndpoints[1].networkClientId), + ); + expect(mockDispatch).toHaveBeenCalledWith(setEditedNetwork()); + expect(mockDispatch).toHaveBeenCalledWith(toggleNetworkMenu()); + }); +}); diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.tsx b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.tsx new file mode 100644 index 000000000000..b16dddf5f86e --- /dev/null +++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import classnames from 'classnames'; +import { useDispatch } from 'react-redux'; +import { + AvatarNetwork, + AvatarNetworkSize, + Box, + Text, +} from '../../../component-library'; +import { + AlignItems, + BackgroundColor, + BorderRadius, + Display, + TextColor, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network'; +import { + setActiveNetwork, + setEditedNetwork, + toggleNetworkMenu, +} from '../../../../store/actions'; +import RpcListItem from '../rpc-list-item'; + +export const SelectRpcUrlModal = ({ + networkConfiguration, +}: { + // TODO: `NetworkConfiguration` with network controller v21 upgrade + // eslint-disable-next-line @typescript-eslint/no-explicit-any + networkConfiguration: any; +}) => { + const dispatch = useDispatch(); + + const image = + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + networkConfiguration.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ]; + + return ( + + + + {image && ( + + )} + + {networkConfiguration.name} + + + + + {networkConfiguration.rpcEndpoints.map( + // TODO: types will be inferred with network controller v21 upgrade + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (rpcEndpoint: any, index: number) => ( + { + // TODO: When this API becomes available with network controller v21 upgrade + // dispatch( + // updateNetwork({ + // ...networkConfiguration, + // defaultRpcEndpointIndex: index, + // }), + // ); + dispatch(setActiveNetwork(rpcEndpoint.networkClientId)); + dispatch(setEditedNetwork()); + dispatch(toggleNetworkMenu()); + }} + className={classnames('select-rpc-url__item', { + 'select-rpc-url__item--selected': + index === networkConfiguration.defaultRpcEndpointIndex, + })} + > + {index === networkConfiguration.defaultRpcEndpointIndex && ( + + )} + + + ), + )} + + ); +}; + +export default SelectRpcUrlModal; diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index cc017421a63e..6d988f8a8bad 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -39,12 +39,14 @@ export const RAMPS_CARD_VARIANTS = { [RAMPS_CARD_VARIANT_TYPES.TOKEN]: { illustrationSrc: './images/ramps-card-token-illustration.png', gradient: + // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #0189EC 0%, #4B7AED 35%, #6774EE 58%, #706AF4 80.5%, #7C5BFC 100%)', title: 'fundYourWallet', - body: 'fundYourWalletDescription', + body: 'getStartedByFundingWallet', }, [RAMPS_CARD_VARIANT_TYPES.NFT]: { illustrationSrc: './images/ramps-card-nft-illustration.png', + // eslint-disable-next-line @metamask/design-tokens/color-no-hex gradient: 'linear-gradient(90deg, #F6822D 0%, #F894A7 52%, #ED94FB 92.5%)', title: 'getStartedWithNFTs', body: 'getStartedWithNFTsDescription', @@ -52,6 +54,7 @@ export const RAMPS_CARD_VARIANTS = { [RAMPS_CARD_VARIANT_TYPES.ACTIVITY]: { illustrationSrc: './images/ramps-card-activity-illustration.png', gradient: + // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #57C5DC 0%, #06BFDD 49.39%, #35A9C7 100%)', title: 'startYourJourney', @@ -60,6 +63,7 @@ export const RAMPS_CARD_VARIANTS = { [RAMPS_CARD_VARIANT_TYPES.BTC]: { illustrationSrc: './images/ramps-card-btc-illustration.png', gradient: + // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #017ED9 0%, #446FD9 35%, #5E6AD9 58%, #635ED9 80.5%, #6855D9 92.5%, #6A4FD9 100%)', title: 'fundYourWallet', body: 'fundYourWalletDescription', @@ -73,7 +77,7 @@ const metamaskEntryMap = { [RAMPS_CARD_VARIANT_TYPES.BTC]: RampsMetaMaskEntry.BtcBanner, }; -export const RampsCard = ({ variant }) => { +export const RampsCard = ({ variant, handleOnClick }) => { const t = useI18nContext(); const { gradient, illustrationSrc, title, body } = RAMPS_CARD_VARIANTS[variant]; @@ -83,6 +87,8 @@ export const RampsCard = ({ variant }) => { const { chainId, nickname } = useSelector(getMultichainCurrentNetwork); const { symbol } = useSelector(getMultichainDefaultToken); + const isTokenVariant = variant === RAMPS_CARD_VARIANT_TYPES.TOKEN; + useEffect(() => { trackEvent({ event: MetaMetricsEventName.EmptyBuyBannerDisplayed, @@ -129,8 +135,11 @@ export const RampsCard = ({ variant }) => { {t(title, [symbol])} {t(body, [symbol])} - - {t('buyToken', [symbol])} + + {isTokenVariant ? t('getStarted') : t('buyToken', [symbol])} ); @@ -138,4 +147,5 @@ export const RampsCard = ({ variant }) => { RampsCard.propTypes = { variant: PropTypes.oneOf(Object.values(RAMPS_CARD_VARIANT_TYPES)), + handleOnClick: PropTypes.oneOfType([PropTypes.func, PropTypes.undefined]), }; diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index a16508c9a45a..d053b32e4e6e 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -99,6 +99,8 @@ type AppState = { txId: string | null; accountDetailsAddress: string; snapsInstallPrivacyWarningShown: boolean; + isAddingNewNetwork: boolean; + isMultiRpcOnboarding: boolean; }; type AppSliceState = { @@ -181,6 +183,8 @@ const initialState: AppState = { txId: null, accountDetailsAddress: '', snapsInstallPrivacyWarningShown: false, + isAddingNewNetwork: false, + isMultiRpcOnboarding: false, }; export default function reduceApp( @@ -583,6 +587,12 @@ export default function reduceApp( ...appState, customTokenAmount: action.payload, }; + case actionConstants.TOGGLE_NETWORK_MENU: + return { + ...appState, + isAddingNewNetwork: Boolean(action.payload?.isAddingNewNetwork), + isMultiRpcOnboarding: Boolean(action.payload?.isMultiRpcOnboarding), + }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 672ee0ea4395..92eb9fb507b9 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -2679,7 +2679,7 @@ export function updateSendAsset( if (details.standard === TokenStandard.ERC20) { asset.balance = - details.balance && details.decimals + details.balance && typeof details.decimals === 'number' ? addHexPrefix( calcTokenAmount(details.balance, details.decimals).toString(16), ) diff --git a/ui/helpers/utils/portfolio.js b/ui/helpers/utils/portfolio.js index 3aa5d8f592b2..97d6eb528d81 100644 --- a/ui/helpers/utils/portfolio.js +++ b/ui/helpers/utils/portfolio.js @@ -4,6 +4,8 @@ export function getPortfolioUrl( metaMetricsId = '', metricsEnabled = false, marketingEnabled = false, + accountAddress, + tab, ) { const baseUrl = process.env.PORTFOLIO_URL || ''; const url = new URL(endpoint, baseUrl); @@ -15,5 +17,13 @@ export function getPortfolioUrl( url.searchParams.append('metricsEnabled', String(metricsEnabled)); url.searchParams.append('marketingEnabled', String(marketingEnabled)); + if (accountAddress) { + url.searchParams.append('accountAddress', accountAddress); + } + + if (tab) { + url.searchParams.append('tab', tab); + } + return url.href; } diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 6bf5ef82ae9f..0c6f61654d09 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3067,6 +3067,10 @@ export function setSmartTransactionsOptInStatus( }; } +export function setShowMultiRpcModal(value: boolean) { + return setPreference('showMultiRpcModal', value); +} + export function setAutoLockTimeLimit(value: number | null) { return setPreference('autoLockTimeLimit', value); } @@ -3178,9 +3182,13 @@ export function toggleAccountMenu() { }; } -export function toggleNetworkMenu() { +export function toggleNetworkMenu(payload?: { + isAddingNewNetwork: boolean; + isMultiRpcOnboarding: boolean; +}) { return { type: actionConstants.TOGGLE_NETWORK_MENU, + payload, }; } @@ -4058,9 +4066,10 @@ export function setNewNetworkAdded({ export function setEditedNetwork( payload: | { - networkConfigurationId: string; + chainId: string; + networkConfigurationId?: string; nickname: string; - editCompleted: boolean; + editCompleted?: boolean; } | undefined = undefined, ): PayloadAction {