diff --git a/.yarnrc.yml b/.yarnrc.yml index 7fb86a083740..252333917781 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -43,17 +43,6 @@ npmAuditIgnoreAdvisories: # not appear to be used. - 1092461 - # Issue: path-to-regexp outputs backtracking regular expressions - # URL: https://github.com/advisories/GHSA-9wv6-86v2-598j - # path-to-regexp is used in react-router v5.1.2, which we use. However, the - # vulnerability in path-to-regexp could only be exploited within react-router - # if malicious properties were passed to react-router components or methods - # explicitly from our code. As such, this vulneratibility cannot be exploited - # by an external / malicious actor. Meanwhile, once we update to v6+, - # path-to-regexp will no longer be used. - - 1099518 - - 1099539 - # Temp fix for https://github.com/MetaMask/metamask-extension/pull/16920 for the sake of 11.7.1 hotfix # This will be removed in this ticket https://github.com/MetaMask/metamask-extension/issues/22299 - 'ts-custom-error (deprecation)' diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index ac10e5e077d8..7be4ca71e56d 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 6e3f98ef623d..e9c4753a54cb 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 905d80ed131e..61a8b29a3597 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." }, @@ -1833,6 +1839,9 @@ "ensUnknownError": { "message": "ENS lookup failed." }, + "enterANameToIdentifyTheUrl": { + "message": "Enter a name to identify the URL" + }, "enterANumber": { "message": "Enter a number" }, @@ -1848,6 +1857,9 @@ "enterPasswordContinue": { "message": "Enter password to continue" }, + "enterRpcUrl": { + "message": "Enter RPC URL" + }, "enterTokenNameOrAddress": { "message": "Enter token name or paste address" }, @@ -2150,6 +2162,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" @@ -2714,6 +2732,9 @@ "link": { "message": "Link" }, + "linkCentralizedExchanges": { + "message": "Link your Coinbase or Binance accounts to transfer crypto to MetaMask for free." + }, "links": { "message": "Links" }, @@ -4245,6 +4266,9 @@ "recipientAddressPlaceholderNew": { "message": "Enter public address (0x) or domain name" }, + "receiveCrypto": { + "message": "Receive crypto" + }, "recommendedGasLabel": { "message": "Recommended" }, @@ -4510,6 +4534,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" }, @@ -4651,6 +4678,9 @@ "selectEnableDisplayMediaPrivacyPreference": { "message": "Turn on Display NFT Media" }, + "selectFundingMethod": { + "message": "Select a funding method" + }, "selectHdPath": { "message": "Select HD path" }, @@ -5417,6 +5447,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" }, @@ -6131,6 +6164,9 @@ "transfer": { "message": "Transfer" }, + "transferCrypto": { + "message": "Transfer crypto" + }, "transferFrom": { "message": "Transfer from" }, @@ -6279,6 +6315,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 5005a4810ca5..9bdbe804c65c 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 750e0af005c4..9fa069312aea 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 12fc10506224..3906abfbe181 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 06a1e8fd39b1..77fdbaa50ce3 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 877ff3631269..1a762f1c713c 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 7fb4ce5a7a92..f33c56f89bc5 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 aaf4dd82d900..c35e80ab3e41 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 a280948aadb8..94b3eda386b8 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 bc2fe5d97d95..f859afba4482 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 e5c15d883589..bf7844af15f1 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 4af2e0e9f469..5c75f22b7e13 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 5e4e425210be..55a4e0a70036 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/controllers/push-notifications/get-notification-message.ts b/app/scripts/controllers/push-notifications/get-notification-message.ts index dd47b766ce1a..f3a3cc02c05a 100644 --- a/app/scripts/controllers/push-notifications/get-notification-message.ts +++ b/app/scripts/controllers/push-notifications/get-notification-message.ts @@ -1,70 +1,64 @@ import type { NotificationServicesController } from '@metamask/notification-services-controller'; -import { NotificationsServicesPushController } from '@metamask/notification-services-controller'; +import { NotificationServicesPushController } from '@metamask/notification-services-controller'; import { t as translate } from '../../translate'; const t = (...args: Parameters) => translate(...args) ?? ''; -const translations: NotificationsServicesPushController.Utils.TranslationKeys = - { - pushPlatformNotificationsFundsSentTitle: () => - t('pushPlatformNotificationsFundsSentTitle'), - pushPlatformNotificationsFundsSentDescriptionDefault: () => - t('pushPlatformNotificationsFundsSentDescriptionDefault'), - pushPlatformNotificationsFundsSentDescription: (amount, symbol) => - t('pushPlatformNotificationsFundsSentDescription', amount, symbol), - pushPlatformNotificationsFundsReceivedTitle: () => - t('pushPlatformNotificationsFundsReceivedTitle'), - pushPlatformNotificationsFundsReceivedDescriptionDefault: () => - t('pushPlatformNotificationsFundsReceivedDescriptionDefault'), - pushPlatformNotificationsFundsReceivedDescription: (amount, symbol) => - t('pushPlatformNotificationsFundsReceivedDescription', amount, symbol), - pushPlatformNotificationsSwapCompletedTitle: () => - t('pushPlatformNotificationsSwapCompletedTitle'), - pushPlatformNotificationsSwapCompletedDescription: () => - t('pushPlatformNotificationsSwapCompletedDescription'), - pushPlatformNotificationsNftSentTitle: () => - t('pushPlatformNotificationsNftSentTitle'), - pushPlatformNotificationsNftSentDescription: () => - t('pushPlatformNotificationsNftSentDescription'), - pushPlatformNotificationsNftReceivedTitle: () => - t('pushPlatformNotificationsNftReceivedTitle'), - pushPlatformNotificationsNftReceivedDescription: () => - t('pushPlatformNotificationsNftReceivedDescription'), - pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle: () => - t('pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle'), - pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription: () => - t('pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription'), - pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle: () => - t('pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle'), - pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription: () => - t( - 'pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription', - ), - pushPlatformNotificationsStakingLidoStakeCompletedTitle: () => - t('pushPlatformNotificationsStakingLidoStakeCompletedTitle'), - pushPlatformNotificationsStakingLidoStakeCompletedDescription: () => - t('pushPlatformNotificationsStakingLidoStakeCompletedDescription'), - pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle: () => - t('pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle'), - pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription: - () => - t( - 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription', - ), - pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle: () => - t('pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle'), - pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription: () => - t('pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription'), - pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle: () => - t('pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle'), - pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription: () => - t('pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription'), - }; +const translations: NotificationServicesPushController.Utils.TranslationKeys = { + pushPlatformNotificationsFundsSentTitle: () => + t('pushPlatformNotificationsFundsSentTitle'), + pushPlatformNotificationsFundsSentDescriptionDefault: () => + t('pushPlatformNotificationsFundsSentDescriptionDefault'), + pushPlatformNotificationsFundsSentDescription: (amount, symbol) => + t('pushPlatformNotificationsFundsSentDescription', amount, symbol), + pushPlatformNotificationsFundsReceivedTitle: () => + t('pushPlatformNotificationsFundsReceivedTitle'), + pushPlatformNotificationsFundsReceivedDescriptionDefault: () => + t('pushPlatformNotificationsFundsReceivedDescriptionDefault'), + pushPlatformNotificationsFundsReceivedDescription: (amount, symbol) => + t('pushPlatformNotificationsFundsReceivedDescription', amount, symbol), + pushPlatformNotificationsSwapCompletedTitle: () => + t('pushPlatformNotificationsSwapCompletedTitle'), + pushPlatformNotificationsSwapCompletedDescription: () => + t('pushPlatformNotificationsSwapCompletedDescription'), + pushPlatformNotificationsNftSentTitle: () => + t('pushPlatformNotificationsNftSentTitle'), + pushPlatformNotificationsNftSentDescription: () => + t('pushPlatformNotificationsNftSentDescription'), + pushPlatformNotificationsNftReceivedTitle: () => + t('pushPlatformNotificationsNftReceivedTitle'), + pushPlatformNotificationsNftReceivedDescription: () => + t('pushPlatformNotificationsNftReceivedDescription'), + pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle: () => + t('pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle'), + pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription: () => + t('pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription'), + pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle: () => + t('pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle'), + pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription: () => + t('pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription'), + pushPlatformNotificationsStakingLidoStakeCompletedTitle: () => + t('pushPlatformNotificationsStakingLidoStakeCompletedTitle'), + pushPlatformNotificationsStakingLidoStakeCompletedDescription: () => + t('pushPlatformNotificationsStakingLidoStakeCompletedDescription'), + pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle: () => + t('pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle'), + pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription: () => + t('pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription'), + pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle: () => + t('pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle'), + pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription: () => + t('pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription'), + pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle: () => + t('pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle'), + pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription: () => + t('pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription'), +}; export function createNotificationMessage( n: NotificationServicesController.Types.INotification, ) { - return NotificationsServicesPushController.Utils.createOnChainPushNotificationMessage( + return NotificationServicesPushController.Utils.createOnChainPushNotificationMessage( n, translations, ); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0e658195a50f..b5d370599ca1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -148,7 +148,7 @@ import { UserStorageController, } from '@metamask/profile-sync-controller'; import { - NotificationsServicesPushController, + NotificationServicesPushController, NotificationServicesController, } from '@metamask/notification-services-controller'; import { @@ -1485,7 +1485,12 @@ export default class MetamaskController extends EventEmitter { 'NotificationServicesController:disableNotificationServices', 'NotificationServicesController:selectIsNotificationServicesEnabled', ], - allowedEvents: ['KeyringController:lock', 'KeyringController:unlock'], + allowedEvents: [ + 'KeyringController:lock', + 'KeyringController:unlock', + 'AccountsController:accountAdded', + 'AccountsController:accountRenamed', + ], }), }); @@ -1496,9 +1501,9 @@ export default class MetamaskController extends EventEmitter { allowedEvents: [], }); this.notificationServicesPushController = - new NotificationsServicesPushController.Controller({ + new NotificationServicesPushController.Controller({ messenger: notificationServicesPushControllerMessenger, - state: initState.NotificationsServicesPushController, + state: initState.NotificationServicesPushController, env: { apiKey: process.env.FIREBASE_API_KEY ?? '', authDomain: process.env.FIREBASE_AUTH_DOMAIN ?? '', @@ -1560,6 +1565,7 @@ export default class MetamaskController extends EventEmitter { 'UserStorageController:performSetStorage', 'NotificationServicesPushController:enablePushNotifications', 'NotificationServicesPushController:disablePushNotifications', + 'NotificationServicesPushController:subscribeToPushNotifications', 'NotificationServicesPushController:updateTriggerPushNotifications', ], allowedEvents: [ 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/development/webpack/test/loaders.swcLoader.test.ts b/development/webpack/test/loaders.swcLoader.test.ts index 4e2e787c0e73..b5dd1b7dcc20 100644 --- a/development/webpack/test/loaders.swcLoader.test.ts +++ b/development/webpack/test/loaders.swcLoader.test.ts @@ -121,6 +121,7 @@ describe('swcLoader', () => { assert.deepStrictEqual(loader.options.jsc.parser, { syntax, [syntax === 'typescript' ? 'tsx' : 'jsx']: enableJsx, + importAttributes: true, }); assert.deepStrictEqual(loader.options.jsc.transform.react, { development: isDevelopment, diff --git a/development/webpack/utils/loaders/swcLoader.ts b/development/webpack/utils/loaders/swcLoader.ts index b6976ff8d66d..b2511a809a27 100644 --- a/development/webpack/utils/loaders/swcLoader.ts +++ b/development/webpack/utils/loaders/swcLoader.ts @@ -110,6 +110,12 @@ const schema = { default: false, type: 'boolean', }, + importAttributes: { + description: + 'Enable parsing of import attributes. Defaults to `false`.', + type: 'boolean', + default: false, + }, }, additionalProperties: false, required: ['syntax'], @@ -123,6 +129,12 @@ const schema = { default: false, type: 'boolean', }, + importAttributes: { + description: + 'Enable parsing of import attributes. Defaults to `false`.', + type: 'boolean', + default: false, + }, }, additionalProperties: false, required: ['syntax'], @@ -201,6 +213,7 @@ export function getSwcLoader( parser: { syntax, [syntax === 'typescript' ? 'tsx' : 'jsx']: enableJsx, + importAttributes: true, }, }, } as const satisfies SwcLoaderOptions, diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 20b28a748be0..5a53f4a118e0 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1750,9 +1750,9 @@ "removeEventListener": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, - "@metamask/notification-services-controller>@metamask/base-controller": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1765,14 +1765,6 @@ "SuppressedError": true } }, - "@metamask/notification-services-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2136,22 +2128,14 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, "@metamask/message-signing-snap>@noble/ciphers": true, - "@metamask/profile-sync-controller>@metamask/base-controller": true, "@metamask/profile-sync-controller>siwe": true, "@noble/hashes": true, "browserify>buffer": true, "loglevel": true } }, - "@metamask/profile-sync-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/profile-sync-controller>siwe": { "globals": { "console.error": true, @@ -5283,7 +5267,7 @@ "react-router-dom>react-router>mini-create-react-context": true, "react-router-dom>tiny-invariant": true, "react-router-dom>tiny-warning": true, - "sinon>nise>path-to-regexp": true + "serve-handler>path-to-regexp": true } }, "react-router-dom>react-router>history": { @@ -5433,9 +5417,9 @@ "process": true } }, - "sinon>nise>path-to-regexp": { + "serve-handler>path-to-regexp": { "packages": { - "sinon>nise>path-to-regexp>isarray": true + "serve-handler>path-to-regexp>isarray": true } }, "stream-browserify": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 20b28a748be0..5a53f4a118e0 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1750,9 +1750,9 @@ "removeEventListener": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, - "@metamask/notification-services-controller>@metamask/base-controller": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1765,14 +1765,6 @@ "SuppressedError": true } }, - "@metamask/notification-services-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2136,22 +2128,14 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, "@metamask/message-signing-snap>@noble/ciphers": true, - "@metamask/profile-sync-controller>@metamask/base-controller": true, "@metamask/profile-sync-controller>siwe": true, "@noble/hashes": true, "browserify>buffer": true, "loglevel": true } }, - "@metamask/profile-sync-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/profile-sync-controller>siwe": { "globals": { "console.error": true, @@ -5283,7 +5267,7 @@ "react-router-dom>react-router>mini-create-react-context": true, "react-router-dom>tiny-invariant": true, "react-router-dom>tiny-warning": true, - "sinon>nise>path-to-regexp": true + "serve-handler>path-to-regexp": true } }, "react-router-dom>react-router>history": { @@ -5433,9 +5417,9 @@ "process": true } }, - "sinon>nise>path-to-regexp": { + "serve-handler>path-to-regexp": { "packages": { - "sinon>nise>path-to-regexp>isarray": true + "serve-handler>path-to-regexp>isarray": true } }, "stream-browserify": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 20b28a748be0..5a53f4a118e0 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1750,9 +1750,9 @@ "removeEventListener": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, - "@metamask/notification-services-controller>@metamask/base-controller": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1765,14 +1765,6 @@ "SuppressedError": true } }, - "@metamask/notification-services-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2136,22 +2128,14 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, "@metamask/message-signing-snap>@noble/ciphers": true, - "@metamask/profile-sync-controller>@metamask/base-controller": true, "@metamask/profile-sync-controller>siwe": true, "@noble/hashes": true, "browserify>buffer": true, "loglevel": true } }, - "@metamask/profile-sync-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/profile-sync-controller>siwe": { "globals": { "console.error": true, @@ -5283,7 +5267,7 @@ "react-router-dom>react-router>mini-create-react-context": true, "react-router-dom>tiny-invariant": true, "react-router-dom>tiny-warning": true, - "sinon>nise>path-to-regexp": true + "serve-handler>path-to-regexp": true } }, "react-router-dom>react-router>history": { @@ -5433,9 +5417,9 @@ "process": true } }, - "sinon>nise>path-to-regexp": { + "serve-handler>path-to-regexp": { "packages": { - "sinon>nise>path-to-regexp>isarray": true + "serve-handler>path-to-regexp>isarray": true } }, "stream-browserify": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 0bc53f16bd1c..ab7f80e77aa4 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1842,9 +1842,9 @@ "removeEventListener": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, - "@metamask/notification-services-controller>@metamask/base-controller": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1857,14 +1857,6 @@ "SuppressedError": true } }, - "@metamask/notification-services-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2228,22 +2220,14 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, "@metamask/message-signing-snap>@noble/ciphers": true, - "@metamask/profile-sync-controller>@metamask/base-controller": true, "@metamask/profile-sync-controller>siwe": true, "@noble/hashes": true, "browserify>buffer": true, "loglevel": true } }, - "@metamask/profile-sync-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/profile-sync-controller>siwe": { "globals": { "console.error": true, @@ -5351,7 +5335,7 @@ "react-router-dom>react-router>mini-create-react-context": true, "react-router-dom>tiny-invariant": true, "react-router-dom>tiny-warning": true, - "sinon>nise>path-to-regexp": true + "serve-handler>path-to-regexp": true } }, "react-router-dom>react-router>history": { @@ -5501,9 +5485,9 @@ "process": true } }, - "sinon>nise>path-to-regexp": { + "serve-handler>path-to-regexp": { "packages": { - "sinon>nise>path-to-regexp>isarray": true + "serve-handler>path-to-regexp>isarray": true } }, "stream-browserify": { diff --git a/package.json b/package.json index b16ac1183555..1ce60ff7fff0 100644 --- a/package.json +++ b/package.json @@ -267,7 +267,8 @@ "@metamask/nonce-tracker@npm:^5.0.0": "patch:@metamask/nonce-tracker@npm%3A5.0.0#~/.yarn/patches/@metamask-nonce-tracker-npm-5.0.0-d81478218e.patch", "@metamask/keyring-controller@npm:^16.0.0": "patch:@metamask/keyring-controller@npm%3A17.1.1#~/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch", "@metamask/keyring-controller@npm:^17.1.0": "patch:@metamask/keyring-controller@npm%3A17.1.1#~/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch", - "@trezor/connect-web@npm:^9.1.11": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch" + "@trezor/connect-web@npm:^9.1.11": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", + "path-to-regexp": "1.9.0" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -336,7 +337,7 @@ "@metamask/name-controller": "^8.0.0", "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A20.2.0#~/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch", "@metamask/notification-controller": "^6.0.0", - "@metamask/notification-services-controller": "^0.2.1", + "@metamask/notification-services-controller": "^0.5.0", "@metamask/object-multiplex": "^2.0.0", "@metamask/obs-store": "^9.0.0", "@metamask/permission-controller": "^10.0.0", @@ -344,7 +345,7 @@ "@metamask/phishing-controller": "^12.0.1", "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", - "@metamask/profile-sync-controller": "^0.2.1", + "@metamask/profile-sync-controller": "^0.5.0", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", 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/mock-e2e.js b/test/e2e/mock-e2e.js index c71cc00c7a32..929a424a36ef 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -637,7 +637,7 @@ async function setupMocking( }); // Notification APIs - mockNotificationServices(server); + await mockNotificationServices(server); await server.forGet(/^https:\/\/sourcify.dev\/(.*)/u).thenCallback(() => { return { 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/test/e2e/tests/notifications/mocks.ts b/test/e2e/tests/notifications/mocks.ts index 7a2c3fdfef92..290db7024360 100644 --- a/test/e2e/tests/notifications/mocks.ts +++ b/test/e2e/tests/notifications/mocks.ts @@ -5,13 +5,13 @@ import { } from '@metamask/profile-sync-controller'; import { NotificationServicesController, - NotificationsServicesPushController, + NotificationServicesPushController, } from '@metamask/notification-services-controller'; const AuthMocks = AuthenticationController.Mocks; const StorageMocks = UserStorageController.Mocks; const NotificationMocks = NotificationServicesController.Mocks; -const PushMocks = NotificationsServicesPushController.Mocks; +const PushMocks = NotificationServicesPushController.Mocks; type MockResponse = { url: string | RegExp; @@ -24,15 +24,15 @@ type MockResponse = { * * @param server - server obj used to mock our endpoints */ -export function mockNotificationServices(server: Mockttp) { +export async function mockNotificationServices(server: Mockttp) { // Auth mockAPICall(server, AuthMocks.getMockAuthNonceResponse()); mockAPICall(server, AuthMocks.getMockAuthLoginResponse()); mockAPICall(server, AuthMocks.getMockAuthAccessTokenResponse()); // Storage - mockAPICall(server, StorageMocks.getMockUserStorageGetResponse()); - mockAPICall(server, StorageMocks.getMockUserStoragePutResponse()); + mockAPICall(server, await StorageMocks.getMockUserStorageGetResponse()); + mockAPICall(server, await StorageMocks.getMockUserStoragePutResponse()); // Notifications mockAPICall(server, NotificationMocks.getMockFeatureAnnouncementResponse()); 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 5d199c7daee7..09cc2412d995 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -2669,7 +2669,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 { diff --git a/yarn.lock b/yarn.lock index cb6af941a035..886d0ab55a0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1759,16 +1759,6 @@ __metadata: languageName: node linkType: hard -"@contentful/content-source-maps@npm:^0.6.0": - version: 0.6.1 - resolution: "@contentful/content-source-maps@npm:0.6.1" - dependencies: - "@vercel/stega": "npm:^0.1.2" - json-pointer: "npm:^0.6.2" - checksum: 10/c29ae369959504f6e57912d1419c76c9e42549588f560f5f3b105fafd39ee709d57dc73d85afaf122acb5aa7964c301831292c654985133737592bb3d4f47482 - languageName: node - linkType: hard - "@contentful/rich-text-html-renderer@npm:^16.5.2": version: 16.6.8 resolution: "@contentful/rich-text-html-renderer@npm:16.6.8" @@ -1779,7 +1769,7 @@ __metadata: languageName: node linkType: hard -"@contentful/rich-text-types@npm:^16.0.2, @contentful/rich-text-types@npm:^16.8.3": +"@contentful/rich-text-types@npm:^16.8.3": version: 16.8.3 resolution: "@contentful/rich-text-types@npm:16.8.3" checksum: 10/790b40930e89d6da910bc1522375c657f167d0002db04d8673fb4ec91f8044a4dbc49afd138e3cbad4d5586ddc8e71887ee6daf65319e42d4391462b5df4fae6 @@ -5861,22 +5851,21 @@ __metadata: languageName: node linkType: hard -"@metamask/notification-services-controller@npm:^0.2.1": - version: 0.2.1 - resolution: "@metamask/notification-services-controller@npm:0.2.1" +"@metamask/notification-services-controller@npm:^0.5.0": + version: 0.5.0 + resolution: "@metamask/notification-services-controller@npm:0.5.0" dependencies: "@contentful/rich-text-html-renderer": "npm:^16.5.2" - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/controller-utils": "npm:^11.0.2" + "@metamask/base-controller": "npm:^7.0.0" + "@metamask/controller-utils": "npm:^11.2.0" bignumber.js: "npm:^4.1.0" - contentful: "npm:^10.3.6" firebase: "npm:^10.11.0" loglevel: "npm:^1.8.1" uuid: "npm:^8.3.2" peerDependencies: "@metamask/keyring-controller": ^17.0.0 - "@metamask/profile-sync-controller": ^0.2.1 - checksum: 10/f2969150edcd91bda44b46baee977d45ab6f0a23b9d34e5212b37aed462e36b2af99a7983e3585f0be6d4a5c32336acdab4e48d96fb3772fcdc1bd2524e09326 + "@metamask/profile-sync-controller": ^0.5.0 + checksum: 10/e945cbf0b7138d22f1078844575dc89c990ddc6669724d1ce0db5ad43c74a86da1c5e1ab12614afbcc5c49125b09365689de2a2c9548af426932f4c1397a66bd languageName: node linkType: hard @@ -6088,11 +6077,11 @@ __metadata: languageName: node linkType: hard -"@metamask/profile-sync-controller@npm:^0.2.1": - version: 0.2.1 - resolution: "@metamask/profile-sync-controller@npm:0.2.1" +"@metamask/profile-sync-controller@npm:^0.5.0": + version: 0.5.0 + resolution: "@metamask/profile-sync-controller@npm:0.5.0" dependencies: - "@metamask/base-controller": "npm:^6.0.2" + "@metamask/base-controller": "npm:^7.0.0" "@metamask/snaps-sdk": "npm:^6.1.1" "@metamask/snaps-utils": "npm:^7.8.1" "@noble/ciphers": "npm:^0.5.2" @@ -6101,9 +6090,10 @@ __metadata: loglevel: "npm:^1.8.1" siwe: "npm:^2.3.2" peerDependencies: - "@metamask/keyring-controller": ^17.0.0 + "@metamask/accounts-controller": ^18.1.1 + "@metamask/keyring-controller": ^17.2.0 "@metamask/snaps-controllers": ^9.3.0 - checksum: 10/2eecdeca7d4f136b4d498cfa2e7b1c91322812b644cd744be1b3c70218dc8f348291023adf76335fbfe6d03af55513f26b6d05490ddddf182f29466e6b6597f5 + checksum: 10/96a31ca2692af85100bd690aa290d6c954378aa14be0dd507afda46c37d761c5e398da4e93ed0f6f7f97267a82def7f4b12b45805cd7673381a6404f810511ff languageName: node linkType: hard @@ -11587,13 +11577,6 @@ __metadata: languageName: node linkType: hard -"@vercel/stega@npm:^0.1.2": - version: 0.1.2 - resolution: "@vercel/stega@npm:0.1.2" - checksum: 10/67ade78b77f579e39bbdd010cc31c39e75b1c1a1ef5a8accdd78df57d156ecd163c767a1feb61b95df190f8ab10642d038776bc5f2fab48bf705d49ee9cced01 - languageName: node - linkType: hard - "@vue/compiler-core@npm:3.1.4": version: 3.1.4 resolution: "@vue/compiler-core@npm:3.1.4" @@ -13165,7 +13148,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.1.3, axios@npm:^1.7.4": +"axios@npm:^1.1.3": version: 1.7.4 resolution: "axios@npm:1.7.4" dependencies: @@ -15478,43 +15461,6 @@ __metadata: languageName: node linkType: hard -"contentful-resolve-response@npm:^1.9.0": - version: 1.9.0 - resolution: "contentful-resolve-response@npm:1.9.0" - dependencies: - fast-copy: "npm:^2.1.7" - checksum: 10/0c5daa59a3a6b020f9b5316812e3f14e62833b4aead008527c5bfde6549365e8e8e9f373af8836f06ca7b2023d861fdd2c241d3e74d3797133d18983be8bb3b3 - languageName: node - linkType: hard - -"contentful-sdk-core@npm:^8.3.1": - version: 8.3.1 - resolution: "contentful-sdk-core@npm:8.3.1" - dependencies: - fast-copy: "npm:^2.1.7" - lodash.isplainobject: "npm:^4.0.6" - lodash.isstring: "npm:^4.0.1" - p-throttle: "npm:^4.1.1" - qs: "npm:^6.11.2" - checksum: 10/645d3a5d296d0e2a5ce87cceb04cf1ddf572183b5946cb1b3b717436bc7be96864216225fb845e61850d580436021c6284e7c95da0600a16c89c0af81a5f0d2c - languageName: node - linkType: hard - -"contentful@npm:^10.3.6": - version: 10.14.0 - resolution: "contentful@npm:10.14.0" - dependencies: - "@contentful/content-source-maps": "npm:^0.6.0" - "@contentful/rich-text-types": "npm:^16.0.2" - axios: "npm:^1.7.4" - contentful-resolve-response: "npm:^1.9.0" - contentful-sdk-core: "npm:^8.3.1" - json-stringify-safe: "npm:^5.0.1" - type-fest: "npm:^4.0.0" - checksum: 10/93d6268150b4ae28c85e718b70c6fff8b0a2c4c9ecad4d5f88d775fee0ee2382f00d267f2a30456be962c010af731962491413bccdf07be8c8632aa979fb77a6 - languageName: node - linkType: hard - "continuable-cache@npm:^0.3.1": version: 0.3.1 resolution: "continuable-cache@npm:0.3.1" @@ -19238,13 +19184,6 @@ __metadata: languageName: node linkType: hard -"fast-copy@npm:^2.1.7": - version: 2.1.7 - resolution: "fast-copy@npm:2.1.7" - checksum: 10/0651a334d42e567f48e6efa9595202f3613353d7b46a1c4e6e055e3ab508583e4819f1bc06722588bd121680722923ba26bcfa6100661e8171a914893fb77e62 - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -19916,13 +19855,6 @@ __metadata: languageName: node linkType: hard -"foreach@npm:^2.0.4": - version: 2.0.6 - resolution: "foreach@npm:2.0.6" - checksum: 10/93b0e65b3f03d9f696418d45f589d0135268b97bf71b4c2628687ce77ce49c20abd60f3c1b23052306b4e789435683a467a7828beac486d2ea17ba8b80933d38 - languageName: node - linkType: hard - "foreground-child@npm:^2.0.0": version: 2.0.0 resolution: "foreground-child@npm:2.0.0" @@ -24320,15 +24252,6 @@ __metadata: languageName: node linkType: hard -"json-pointer@npm:^0.6.2": - version: 0.6.2 - resolution: "json-pointer@npm:0.6.2" - dependencies: - foreach: "npm:^2.0.4" - checksum: 10/1d8fc507008cf28815ad398baa7a6d62a73cce2d5ca7859097bb56043b3b6889e393bf5285db9674ddcdb8bc10551146cf8048d3d6430d55ce922105813661e2 - languageName: node - linkType: hard - "json-rpc-engine@npm:^6.1.0": version: 6.1.0 resolution: "json-rpc-engine@npm:6.1.0" @@ -25412,13 +25335,6 @@ __metadata: languageName: node linkType: hard -"lodash.isstring@npm:^4.0.1": - version: 4.0.1 - resolution: "lodash.isstring@npm:4.0.1" - checksum: 10/eaac87ae9636848af08021083d796e2eea3d02e80082ab8a9955309569cb3a463ce97fd281d7dc119e402b2e7d8c54a23914b15d2fc7fff56461511dc8937ba0 - languageName: node - linkType: hard - "lodash.memoize@npm:~3.0.3": version: 3.0.4 resolution: "lodash.memoize@npm:3.0.4" @@ -26289,7 +26205,7 @@ __metadata: "@metamask/name-controller": "npm:^8.0.0" "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A20.2.0#~/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch" "@metamask/notification-controller": "npm:^6.0.0" - "@metamask/notification-services-controller": "npm:^0.2.1" + "@metamask/notification-services-controller": "npm:^0.5.0" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/obs-store": "npm:^9.0.0" "@metamask/permission-controller": "npm:^10.0.0" @@ -26298,7 +26214,7 @@ __metadata: "@metamask/phishing-warning": "npm:^4.0.0" "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" - "@metamask/profile-sync-controller": "npm:^0.2.1" + "@metamask/profile-sync-controller": "npm:^0.5.0" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" @@ -28688,13 +28604,6 @@ __metadata: languageName: node linkType: hard -"p-throttle@npm:^4.1.1": - version: 4.1.1 - resolution: "p-throttle@npm:4.1.1" - checksum: 10/fe8709f3c3b1da7c033479375c2c302e80c1a5d86449013afa7cd46d1dc210bc824a7e4a9d088e66d31987d00878c2b5491bb2fe76246d4d2fc9a1636f5f8298 - languageName: node - linkType: hard - "p-try@npm:^1.0.0": version: 1.0.0 resolution: "p-try@npm:1.0.0" @@ -29090,26 +28999,12 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:0.1.7": - version: 0.1.7 - resolution: "path-to-regexp@npm:0.1.7" - checksum: 10/701c99e1f08e3400bea4d701cf6f03517474bb1b608da71c78b1eb261415b645c5670dfae49808c89e12cea2dccd113b069f040a80de012da0400191c6dbd1c8 - languageName: node - linkType: hard - -"path-to-regexp@npm:2.2.1": - version: 2.2.1 - resolution: "path-to-regexp@npm:2.2.1" - checksum: 10/1a7125f8c1b5904d556a29722333219df4aa779039e903efe2fbfe0cc3ae9246672846fc8ad285664020b70e434347e0bc9af691fd7d61df8eaa7b018dcd56fb - languageName: node - linkType: hard - -"path-to-regexp@npm:^1.7.0": - version: 1.7.0 - resolution: "path-to-regexp@npm:1.7.0" +"path-to-regexp@npm:1.9.0": + version: 1.9.0 + resolution: "path-to-regexp@npm:1.9.0" dependencies: isarray: "npm:0.0.1" - checksum: 10/7e1275a34fcfed7ba9d0d82ea7149f0c87d8c941c9b34109ab455cceb783b6387ce9275deeb6519eb0f880777a44bcb387cd579d3bb0cfbf4e7fe93c0e3b1a69 + checksum: 10/67f0f4823f7aab356523d93a83f9f8222bdd119fa0b27a8f8b587e8e6c9825294bb4ccd16ae619def111ff3fe5d15ff8f658cdd3b0d58b9c882de6fd15bc1b76 languageName: node linkType: hard @@ -35337,7 +35232,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.0.0, type-fest@npm:^4.12.0": +"type-fest@npm:^4.12.0": version: 4.18.1 resolution: "type-fest@npm:4.18.1" checksum: 10/ff76e19cb969854161fea2de854073f346e159f5efff05906ece93cbde8a7161b9374121aca53782b44f754152cbacc70264c90ca1acc81ca917723acce5054f