diff --git a/README.md b/README.md index 531a1e4fc3b6..d70eb03a32b2 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,13 @@ If you are using VS Code and are unable to make commits from the source control To start a development build (e.g. with logging and file watching) run `yarn start`. -Alternatively, one can skip wallet onboarding and preload the vault state with a specific SRP by adding `TEST_SRP=''` and `PASSWORD=''` to the `.metamaskrc` file and running `yarn start:skip-onboarding`. +#### Development build with wallet state +You can start a development build with a preloaded wallet state, by adding `TEST_SRP=''` and `PASSWORD=''` to the `.metamaskrc` file. Then you have the following options: +1. Start the wallet with the default fixture flags, by running `yarn start:with-state`. +2. Check the list of available fixture flags, by running `yarn start:with-state --help`. +3. Start the wallet with custom fixture flags, by running `yarn start:with-state --FIXTURE_NAME=VALUE` for example `yarn start:with-state --withAccounts=100`. You can pass as many flags as you want. The rest of the fixtures will take the default values. +#### Development build with Webpack You can also start a development build using the `yarn webpack` command, or `yarn webpack --watch`. This uses an alternative build system that is much faster, but not yet production ready. See the [Webpack README](./development/webpack/README.md) for more information. #### React and Redux DevTools diff --git a/app/scripts/background.js b/app/scripts/background.js index 2a29de9af601..3f513e142729 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -77,7 +77,7 @@ import { getPlatform, shouldEmitDappViewedEvent, } from './lib/util'; -import { generateSkipOnboardingState } from './skip-onboarding'; +import { generateWalletState } from './fixtures/generate-wallet-state'; import { createOffscreen } from './offscreen'; /* eslint-enable import/first */ @@ -559,15 +559,15 @@ export async function loadStateFromPersistence() { // migrations const migrator = new Migrator({ migrations, - defaultVersion: process.env.SKIP_ONBOARDING + defaultVersion: process.env.WITH_STATE ? FIXTURE_STATE_METADATA_VERSION : null, }); migrator.on('error', console.warn); - if (process.env.SKIP_ONBOARDING) { - const skipOnboardingStateOverrides = await generateSkipOnboardingState(); - firstTimeState = { ...firstTimeState, ...skipOnboardingStateOverrides }; + if (process.env.WITH_STATE) { + const stateOverrides = await generateWalletState(); + firstTimeState = { ...firstTimeState, ...stateOverrides }; } // read from disk diff --git a/app/scripts/fixtures/generate-wallet-state.js b/app/scripts/fixtures/generate-wallet-state.js new file mode 100644 index 000000000000..43d8010bcea8 --- /dev/null +++ b/app/scripts/fixtures/generate-wallet-state.js @@ -0,0 +1,318 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import { KeyringController } from '@metamask/keyring-controller'; +import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; +import { UI_NOTIFICATIONS } from '../../../shared/notifications'; +import { E2E_SRP, defaultFixture } from '../../../test/e2e/default-fixture'; +import FixtureBuilder from '../../../test/e2e/fixture-builder'; +import { encryptorFactory } from '../lib/encryptor-factory'; +import { FIXTURES_APP_STATE } from './with-app-state'; +import { FIXTURES_NETWORKS } from './with-networks'; +import { FIXTURES_PREFERENCES } from './with-preferences'; +import { FIXTURES_ERC20_TOKENS } from './with-erc20-tokens'; +import { withAddressBook } from './with-address-book'; +import { withConfirmedTransactions } from './with-confirmed-transactions'; +import { withUnreadNotifications } from './with-unread-notifications'; + +const FIXTURES_CONFIG = process.env.WITH_STATE + ? JSON.parse(process.env.WITH_STATE) + : {}; + +/** + * Generates the wallet state based on the fixtures set in the environment variable. + * + * @returns {Promise} The generated wallet state. + */ +export async function generateWalletState() { + const fixtureBuilder = new FixtureBuilder({ inputChainId: '0xaa36a7' }); + + const { vault, accounts } = await generateVaultAndAccount( + process.env.TEST_SRP || E2E_SRP, + process.env.PASSWORD, + ); + + fixtureBuilder + .withAccountsController(generateAccountsControllerState(accounts)) + .withAddressBookController(generateAddressBookControllerState()) + .withAnnouncementController(generateAnnouncementControllerState()) + .withAppStateController(FIXTURES_APP_STATE) + .withKeyringController(generateKeyringControllerState(vault)) + .withNetworkController(generateNetworkControllerState()) + .withNotificationServicesController( + generateNotificationControllerState(accounts[0]), + ) + .withPreferencesController(generatePreferencesControllerState(accounts)) + .withTokensController(generateTokensControllerState(accounts[0])) + .withTransactionController(generateTransactionControllerState(accounts[0])); + + return fixtureBuilder.fixture.data; +} + +/** + * Generates a new vault and account based on the provided seed phrase and password. + * + * @param {string} encodedSeedPhrase - The encoded seed phrase. + * @param {string} password - The password for the vault. + * @returns {Promise<{vault: object, account: string}>} The generated vault and account. + */ +async function generateVaultAndAccount(encodedSeedPhrase, password) { + const controllerMessenger = new ControllerMessenger(); + const keyringControllerMessenger = controllerMessenger.getRestricted({ + name: 'KeyringController', + }); + const krCtrl = new KeyringController({ + encryptor: encryptorFactory(600_000), + messenger: keyringControllerMessenger, + }); + + const seedPhraseAsBuffer = Buffer.from(encodedSeedPhrase); + const _convertMnemonicToWordlistIndices = (mnemonic) => { + const indices = mnemonic + .toString() + .split(' ') + .map((word) => wordlist.indexOf(word)); + return new Uint8Array(new Uint16Array(indices).buffer); + }; + + await krCtrl.createNewVaultAndRestore( + password, + _convertMnemonicToWordlistIndices(seedPhraseAsBuffer), + ); + + const accounts = []; + const account = krCtrl.state.keyrings[0].accounts[0]; + accounts.push(account); + + for (let i = 1; i < FIXTURES_CONFIG.withAccounts; i++) { + const newAccount = await krCtrl.addNewAccount(i); + accounts.push(newAccount); + } + const { vault } = krCtrl.state; + + return { vault, accounts }; +} + +/** + * Generates the state for the KeyringController. + * + * @param {object} vault - The vault object. + * @returns {object} The generated KeyringController state. + */ +function generateKeyringControllerState(vault) { + console.log('Generating KeyringController state'); + + return { + ...defaultFixture().data.KeyringController, + vault, + }; +} + +/** + * Generates the state for the AccountsController. + * + * @param {string} accounts - The account addresses. + * @returns {object} The generated AccountsController state. + */ +function generateAccountsControllerState(accounts) { + console.log('Generating AccountsController state'); + const internalAccounts = { + selectedAccount: 'account-id', + accounts: {}, + }; + + accounts.forEach((account, index) => { + internalAccounts.accounts[`acount-id-${index}`] = { + selectedAccount: 'account-id', + id: 'account-id', + address: account, + metadata: { + name: `Account ${index + 1}`, + lastSelected: 1665507600000, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + }; + }); + return { + internalAccounts, + }; +} + +/** + * Generates the state for the AddressBookController. + * + * @returns {object} The generated AddressBookController state. + */ +function generateAddressBookControllerState() { + console.log('Generating AddressBookController state'); + + const numEntries = FIXTURES_CONFIG.withContacts; + if (numEntries > 0) { + return withAddressBook(numEntries); + } + + return {}; +} + +/** + * Generates the state for the AnnouncementController. + * All the what's new modals are dismissed for convenience. + * + * @returns {object} The generated AnnouncementController state. + */ +function generateAnnouncementControllerState() { + console.log('Generating AnnouncementController state'); + + const allAnnouncementsAlreadyShown = Object.keys(UI_NOTIFICATIONS).reduce( + (acc, val) => { + acc[val] = { + ...UI_NOTIFICATIONS[val], + isShown: true, + }; + return acc; + }, + {}, + ); + return allAnnouncementsAlreadyShown; +} + +/** + * Generates the state for the NotificationController. + * + * @param {string} account - The account address to add the notifications to. + * @returns {object} The generated NotificationController state. + */ +function generateNotificationControllerState(account) { + console.log('Generating NotificationController state'); + + let notifications = {}; + + if (FIXTURES_CONFIG.withUnreadNotifications > 0) { + notifications = withUnreadNotifications( + account, + FIXTURES_CONFIG.withUnreadNotifications, + ); + } + return notifications; +} + +/** + * Generates the state for the NetworkController. + * Sepolia is always pre-loaded and set as the active provider. + * + * @returns {object} The generated NetworkController state. + */ +function generateNetworkControllerState() { + console.log('Generating NetworkController state'); + + const defaultNetworkState = { + ...defaultFixture().data.NetworkController, + networkConfigurations: {}, + networksMetadata: { + sepolia: { + EIPS: { + 1559: true, + }, + status: 'available', + }, + }, + selectedNetworkClientId: 'sepolia', + }; + + if (FIXTURES_CONFIG.withNetworks) { + return { + ...defaultNetworkState, + ...FIXTURES_NETWORKS, + }; + } + return defaultNetworkState; +} + +/** + * Generates the state for the PreferencesController. + * + * @param {string} accounts - The account addresses. + * @returns {object} The generated PreferencesController state. + */ +function generatePreferencesControllerState(accounts) { + console.log('Generating PreferencesController state'); + let preferencesControllerState = {}; + + if (FIXTURES_CONFIG.withPreferences) { + preferencesControllerState = FIXTURES_PREFERENCES; + } + + // Add account identities + preferencesControllerState.identities = Object.assign( + ...accounts.map((address, index) => ({ + [address]: { + address, + lastSelected: 1725363500048, + name: `Account ${index + 1}`, + }, + })), + ); + + preferencesControllerState.lostIdentities = Object.assign( + ...accounts.map((address, index) => ({ + [address]: { + address, + lastSelected: 1725363500048, + name: `Account ${index + 1}`, + }, + })), + ); + + return preferencesControllerState; +} + +/** + * Generates the state for the TokensController. + * + * @param {string} account - The account address to add the transactions to. + * @returns {object} The generated TokensController state. + */ +function generateTokensControllerState(account) { + console.log('Generating TokensController state'); + + const tokens = FIXTURES_ERC20_TOKENS; + if (FIXTURES_CONFIG.withErc20Tokens) { + // Update `myAccount` key for the account address + for (const network of Object.values(tokens.allTokens)) { + network[account] = network.myAccount; + delete network.myAccount; + } + return tokens; + } + return {}; +} + +/** + * Generates the state for the TransactionController. + * + * @param {string} account - The account address to add the transactions to. + * @returns {object} The generated TransactionController state. + */ +function generateTransactionControllerState(account) { + console.log('Generating TransactionController state'); + + let transactions = {}; + + if (FIXTURES_CONFIG.withConfirmedTransactions > 0) { + transactions = withConfirmedTransactions( + account, + FIXTURES_CONFIG.withConfirmedTransactions, + ); + } + + return transactions; +} diff --git a/app/scripts/fixtures/with-address-book.js b/app/scripts/fixtures/with-address-book.js new file mode 100644 index 000000000000..f8b72ceae17a --- /dev/null +++ b/app/scripts/fixtures/with-address-book.js @@ -0,0 +1,45 @@ +import { CHAIN_IDS } from '../../../shared/constants/network'; + +/** + * Generates a random Ethereum address. + * + * @returns {string} A randomly generated Ethereum address. + */ +const generateRandomAddress = () => { + const hexChars = '0123456789abcdef'; + let address = '0x'; + for (let i = 0; i < 40; i++) { + address += hexChars[Math.floor(Math.random() * 16)]; + } + + return address; +}; + +/** + * Generates an address book with a specified number of entries, for the supported networks. + * + * @param {number} numEntries - The number of address book entries to generate for each network. + * @returns {object} The generated address book object. + */ +export const withAddressBook = (numEntries) => { + const networks = [CHAIN_IDS.MAINNET, CHAIN_IDS.SEPOLIA]; + + const addressBook = {}; + + networks.forEach((network) => { + addressBook[network] = {}; + + for (let i = 1; i <= numEntries; i++) { + const address = generateRandomAddress(); + addressBook[network][address] = { + address, + chainId: network, + isEns: false, + memo: '', + name: `Contact ${i}`, + }; + } + }); + + return { addressBook }; +}; diff --git a/app/scripts/fixtures/with-app-state.js b/app/scripts/fixtures/with-app-state.js new file mode 100644 index 000000000000..eff071079c81 --- /dev/null +++ b/app/scripts/fixtures/with-app-state.js @@ -0,0 +1,3 @@ +export const FIXTURES_APP_STATE = { + showProductTour: false, +}; diff --git a/app/scripts/fixtures/with-confirmed-transactions.js b/app/scripts/fixtures/with-confirmed-transactions.js new file mode 100644 index 000000000000..29a40b8185f7 --- /dev/null +++ b/app/scripts/fixtures/with-confirmed-transactions.js @@ -0,0 +1,99 @@ +import { v4 as uuidv4 } from 'uuid'; +import { CHAIN_IDS } from '../../../shared/constants/network'; + +/** + * Generates a specified number of confirmed transactions for each network. + * + * @param {string} from - The address from which the transactions are sent. + * @param {number} numEntries - The number of transactions to generate for each network. + * @returns {object} The generated transactions object. + */ +export const withConfirmedTransactions = (from, numEntries) => { + const networks = [CHAIN_IDS.MAINNET, CHAIN_IDS.SEPOLIA]; + const transactions = {}; + + networks.forEach((network) => { + for (let i = 0; i < numEntries; i++) { + const id = uuidv4(); + const transaction = { + chainId: network, + dappSuggestedGasFees: { + gas: '0x5208', + maxFeePerGas: '0x59682f0c', + maxPriorityFeePerGas: '0x59682f00', + }, + history: [ + { + chainId: network, + dappSuggestedGasFees: { + gas: '0x5208', + maxFeePerGas: '0x59682f0c', + maxPriorityFeePerGas: '0x59682f00', + }, + id, + loadingDefaults: true, + origin: 'https://metamask.github.io', + status: 'confirmed', + time: Date.now(), + txParams: { + from, + gas: '0x5208', + maxFeePerGas: '0x59682f0c', + maxPriorityFeePerGas: '0x59682f00', + to: '0x2f318c334780961fb129d2a6c30d0763d9a5c970', + value: '0x29a2241af62c0000', + }, + type: 'simpleSend', + }, + [ + { + note: 'Added new confirmed transaction.', + op: 'replace', + path: '/loadingDefaults', + timestamp: Date.now(), + value: false, + }, + { + op: 'add', + path: '/simulationData', + value: { + error: { + code: 'disabled', + message: 'Simulation disabled', + }, + tokenBalanceChanges: [], + }, + note: 'TransactionController#updateSimulationData - Update simulation data', + timestamp: Date.now(), + }, + ], + ], + simulationData: { + error: { + code: 'disabled', + message: 'Simulation disabled', + }, + tokenBalanceChanges: [], + }, + id, + loadingDefaults: false, + origin: 'https://metamask.github.io', + status: 'confirmed', + time: Date.now(), + txParams: { + from, + gas: '0x5208', + maxFeePerGas: '0x59682f0c', + maxPriorityFeePerGas: '0x59682f00', + to: '0x2f318c334780961fb129d2a6c30d0763d9a5c970', + value: '0x29a2241af62c0000', + }, + type: 'simpleSend', + }; + + transactions[id] = transaction; + } + }); + + return { transactions }; +}; diff --git a/app/scripts/fixtures/with-erc20-tokens.js b/app/scripts/fixtures/with-erc20-tokens.js new file mode 100644 index 000000000000..8088aef9e4c4 --- /dev/null +++ b/app/scripts/fixtures/with-erc20-tokens.js @@ -0,0 +1,87 @@ +const _TOKENS = [ + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + decimals: 18, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', + isERC721: false, + aggregators: [], + }, + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', + isERC721: false, + aggregators: [], + }, + { + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + symbol: 'USDT', + decimals: 6, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0xdAC17F958D2ee523a2206206994597C13D831ec7.png', + isERC721: false, + aggregators: [], + }, + { + address: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', + symbol: 'SNX', + decimals: 18, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F.png', + isERC721: false, + aggregators: [], + }, + { + address: '0x111111111117dC0aa78b770fA6A738034120C302', + symbol: '1INCH', + decimals: 18, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0x111111111117dC0aa78b770fA6A738034120C302.png', + isERC721: false, + aggregators: [], + }, + { + address: '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0', + symbol: 'MATIC', + decimals: 18, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0.png', + isERC721: false, + aggregators: [], + }, + { + address: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', + symbol: 'SHIB', + decimals: 18, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE.png', + isERC721: false, + aggregators: [], + }, + { + address: '0xFd09911130e6930Bf87F2B0554c44F400bD80D3e', + symbol: 'ETHIX', + decimals: 18, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0xFd09911130e6930Bf87F2B0554c44F400bD80D3e.png', + isERC721: false, + aggregators: [], + }, +]; + +export const FIXTURES_ERC20_TOKENS = { + tokens: _TOKENS, + ignoredTokens: [], + detectedTokens: [], + allTokens: { + '0x1': { + myAccount: _TOKENS, + }, + }, + allIgnoredTokens: {}, + allDetectedTokens: {}, +}; diff --git a/app/scripts/fixtures/with-networks.js b/app/scripts/fixtures/with-networks.js new file mode 100644 index 000000000000..1efb15e25f3c --- /dev/null +++ b/app/scripts/fixtures/with-networks.js @@ -0,0 +1,130 @@ +export const FIXTURES_NETWORKS = { + providerConfig: { + chainId: '0xaa36a7', + rpcPrefs: { + blockExplorerUrl: 'https://sepolia.etherscan.io', + }, + ticker: 'SepoliaETH', + type: 'sepolia', + }, + networkConfigurations: { + networkConfigurationId: { + chainId: '0xaa36a7', + nickname: 'Sepolia', + rpcPrefs: {}, + rpcUrl: 'https://sepolia.infura.io/v3/', + ticker: 'SepoliaETH', + networkConfigurationId: 'networkConfigurationId', + }, + optimism: { + chainId: '0xa', + id: 'optimism', + nickname: 'OP Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://optimistic.etherscan.io/', + imageUrl: './images/optimism.svg', + }, + rpcUrl: 'https://optimism-mainnet.infura.io/v3/', + ticker: 'ETH', + }, + base: { + chainId: '0x2105', + id: 'base', + nickname: 'Base Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://basescan.org', + imageUrl: './images/base.svg', + }, + rpcUrl: 'https://mainnet.base.org', + ticker: 'ETH', + }, + polygon: { + chainId: '0x89', + id: 'polygon', + nickname: 'Polygon Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://polygonscan.com/', + imageUrl: './images/matic-token.svg', + }, + rpcUrl: 'https://polygon-mainnet.infura.io/v3/', + ticker: 'MATIC', + }, + binance: { + chainId: '0x38', + id: 'binance', + nickname: 'BNB Chain', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + imageUrl: './images/bnb.svg', + }, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: 'BNB', + }, + gnosis: { + id: 'gnosis', + rpcUrl: 'https://rpc.gnosischain.com', + chainId: '0x64', + ticker: 'XDAI', + nickname: 'Gnosis', + rpcPrefs: { + blockExplorerUrl: 'https://gnosisscan.io', + imageUrl: './images/gnosis.svg', + }, + }, + arbitrum: { + id: 'arbitrum', + rpcUrl: 'https://arbitrum-mainnet.infura.io/v3/', + chainId: '0xa4b1', + ticker: 'ETH', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + imageUrl: './images/arbitrum.svg', + }, + }, + avalanche: { + id: 'avalanche', + rpcUrl: 'https://avalanche-mainnet.infura.io/v3/', + chainId: '0xa86a', + ticker: 'AVAX', + nickname: 'Avalanche Network C-Chain', + rpcPrefs: { + blockExplorerUrl: 'https://snowtrace.io/', + imageUrl: './images/avax-token.svg', + }, + }, + celo: { + id: 'celo', + rpcUrl: 'https://celo-mainnet.infura.io/v3/', + chainId: '0xa4ec', + ticker: 'CELO', + nickname: 'Celo Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://celoscan.io', + imageUrl: './images/celo.svg', + }, + }, + zkSync: { + id: 'zkSync', + rpcUrl: 'https://mainnet.era.zksync.io', + chainId: '0x144', + ticker: 'ETH', + nickname: 'zkSync Era Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.zksync.io/', + imageUrl: './images/zk-sync.svg', + }, + }, + localhost: { + id: 'localhost', + rpcUrl: 'http://localhost:8545', + chainId: '0x539', + ticker: 'ETH', + nickname: 'Localhost 8545', + rpcPrefs: { + blockExplorerUrl: '', + imageUrl: '', + }, + }, + }, +}; diff --git a/app/scripts/fixtures/with-preferences.js b/app/scripts/fixtures/with-preferences.js new file mode 100644 index 000000000000..8d1e4293e8a4 --- /dev/null +++ b/app/scripts/fixtures/with-preferences.js @@ -0,0 +1,40 @@ +export const FIXTURES_PREFERENCES = { + preferences: { + hideZeroBalanceTokens: false, + showExtensionInFullSizeView: false, + showFiatInTestnets: true, + showTestNetworks: true, + smartTransactionsOptInStatus: true, + useNativeCurrencyAsPrimaryCurrency: true, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + featureNotificationsEnabled: true, + showTokenAutodetectModal: false, + showNftAutodetectModal: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + }, + featureFlags: { + sendHexData: true, + }, + firstTimeFlowType: 'import', + completedOnboarding: true, + currentLocale: 'en', + knownMethodData: {}, + use4ByteResolution: true, + participateInMetaMetrics: true, + dataCollectionForMarketing: true, + useNftDetection: true, + useNonceField: true, + usePhishDetect: true, + useTokenDetection: true, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useRequestQueue: true, + theme: 'light', + useExternalNameSources: true, + useTransactionSimulations: true, + enableMV3TimestampSave: true, + useExternalServices: true, + isProfileSyncingEnabled: true, +}; diff --git a/app/scripts/fixtures/with-unread-notifications.js b/app/scripts/fixtures/with-unread-notifications.js new file mode 100644 index 000000000000..e99cefe43778 --- /dev/null +++ b/app/scripts/fixtures/with-unread-notifications.js @@ -0,0 +1,57 @@ +import { v4 as uuidv4 } from 'uuid'; + +/** + * Generates a specified number of unread notifications for the given account. + * + * @param {string} account - The account address to use in the notifications. + * @param {number} numNotifications - The number of unread notifications to generate. + * @returns {object} The generated unread notifications object. + */ +export const withUnreadNotifications = (account, numNotifications) => { + const notifications = []; + + for (let i = 0; i < numNotifications; i++) { + const id = uuidv4(); + const triggerId = uuidv4(); + const txHash = `0x${uuidv4().replace(/-/gu, '').padEnd(64, '0')}`; + + const notification = { + address: account, + block_number: 59796924, + block_timestamp: '1721922504', + chain_id: 1, + created_at: new Date().toISOString(), + data: { + to: account, + from: account, + kind: 'eth_received', + amount: { + eth: '0.000100000000000000', + usd: '0.27', + }, + network_fee: { + gas_price: '30000000078', + native_token_price_in_usd: '0.497927', + }, + }, + id, + trigger_id: triggerId, + tx_hash: txHash, + unread: true, + type: 'eth_received', + createdAt: new Date().toISOString(), + isRead: false, + }; + + notifications.push(notification); + } + + const notificationServicesController = { + isFeatureAnnouncementsEnabled: true, + isMetamaskNotificationsFeatureSeen: true, + isNotificationServicesEnabled: true, + metamaskNotificationsReadList: [], + metamaskNotificationsList: notifications, + }; + return notificationServicesController; +}; diff --git a/app/scripts/skip-onboarding.js b/app/scripts/skip-onboarding.js deleted file mode 100644 index 17ce107927ec..000000000000 --- a/app/scripts/skip-onboarding.js +++ /dev/null @@ -1,142 +0,0 @@ -import { ControllerMessenger } from '@metamask/base-controller'; -import { KeyringController } from '@metamask/keyring-controller'; -import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; -import { UI_NOTIFICATIONS } from '../../shared/notifications'; -import { E2E_SRP, defaultFixture } from '../../test/e2e/default-fixture'; -import { encryptorFactory } from './lib/encryptor-factory'; - -export async function generateSkipOnboardingState() { - const state = defaultFixture('0xaa36a7').data; - - state.AppStateController = generateAppStateControllerState(); - state.AnnouncementController = generateAnnouncementControllerState(); - state.NetworkController = generateNetworkControllerState(); - - if (process.env.PASSWORD) { - const { vault, account } = await generateVaultAndAccount( - process.env.TEST_SRP || E2E_SRP, - process.env.PASSWORD, - ); - - state.KeyringController = generateKeyringControllerState(vault); - state.AccountsController = generateAccountsControllerState(account); - } - - return state; -} - -// dismiss product tour -function generateAppStateControllerState() { - return { - ...defaultFixture().data.AppStateController, - showProductTour: false, - }; -} - -// dismiss 'what's new' modals -function generateAnnouncementControllerState() { - const allAnnouncementsAlreadyShown = Object.keys(UI_NOTIFICATIONS).reduce( - (acc, val) => { - acc[val] = { - ...UI_NOTIFICATIONS[val], - isShown: true, - }; - return acc; - }, - {}, - ); - - return { - ...defaultFixture().data.AnnouncementController, - announcements: { - ...defaultFixture().data.AnnouncementController.announcements, - ...allAnnouncementsAlreadyShown, - }, - }; -} - -// configure 'Sepolia' network -// TODO: Support for local node -function generateNetworkControllerState() { - return { - ...defaultFixture().data.NetworkController, - networkConfigurations: { - networkConfigurationId: { - chainId: '0xaa36a7', - nickname: 'Sepolia', - rpcPrefs: {}, - rpcUrl: 'https://sepolia.infura.io/v3/6c21df2a8dcb4a77b9bbcc1b65ee9ded', - ticker: 'SepoliaETH', - networkConfigurationId: 'networkConfigurationId', - }, - }, - }; -} - -async function generateVaultAndAccount(encodedSeedPhrase, password) { - const controllerMessenger = new ControllerMessenger(); - const keyringControllerMessenger = controllerMessenger.getRestricted({ - name: 'KeyringController', - }); - const krCtrl = new KeyringController({ - encryptor: encryptorFactory(600_000), - messenger: keyringControllerMessenger, - }); - - const seedPhraseAsBuffer = Buffer.from(encodedSeedPhrase); - const _convertMnemonicToWordlistIndices = (mnemonic) => { - const indices = mnemonic - .toString() - .split(' ') - .map((word) => wordlist.indexOf(word)); - return new Uint8Array(new Uint16Array(indices).buffer); - }; - - await krCtrl.createNewVaultAndRestore( - password, - _convertMnemonicToWordlistIndices(seedPhraseAsBuffer), - ); - - const { vault } = krCtrl.state; - const account = krCtrl.state.keyrings[0].accounts[0]; - - return { vault, account }; -} - -function generateKeyringControllerState(vault) { - return { - ...defaultFixture().data.KeyringController, - vault, - }; -} - -function generateAccountsControllerState(account) { - return { - ...defaultFixture().data.AccountsController, - internalAccounts: { - selectedAccount: 'account-id', - accounts: { - 'account-id': { - id: 'account-id', - address: account, - metadata: { - name: 'Account 1', - lastSelected: 1665507600000, - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: [ - 'personal_sign', - 'eth_signTransaction', - 'eth_signTypedData_v1', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', - ], - type: 'eip155:eoa', - }, - }, - }, - }; -} diff --git a/app/scripts/start-with-wallet-state.mjs b/app/scripts/start-with-wallet-state.mjs new file mode 100644 index 000000000000..2464b7b87097 --- /dev/null +++ b/app/scripts/start-with-wallet-state.mjs @@ -0,0 +1,122 @@ +import childProcess from 'child_process'; + +// Default fixtures and flags +const FIXTURES_FLAGS = { + '--withAccounts': { + type: 'number', + defaultValue: 30, + explanation: 'Specify the number of wallet accounts to generate.', + }, + '--withConfirmedTransactions': { + type: 'number', + defaultValue: 30, + explanation: 'Specify the number of confirmed transactions to generate.', + }, + '--withContacts': { + type: 'number', + defaultValue: 30, + explanation: + 'Specify the number of contacts to generate in the address book.', + }, + '--withErc20Tokens': { + type: 'boolean', + defaultValue: true, + explanation: 'Specify whether to import ERC20 tokens in Mainnet.', + }, + '--withNetworks': { + type: 'boolean', + defaultValue: true, + explanation: 'Specify whether to load suggested networks.', + }, + '--withPreferences': { + type: 'boolean', + defaultValue: true, + explanation: 'Specify whether to activate all preferences.', + }, + '--withUnreadNotifications': { + type: 'number', + defaultValue: 30, + explanation: 'Specify the number of unread notifications to load.', + }, + '--withStartCommand': { + type: 'string', + defaultValue: 'start', + explanation: + 'Specify the start command for the wallet (put double quotes around a multi-word command like --withStartCommand="webpack --watch").', + }, +}; + +function startWithWalletState() { + const args = process.argv.slice(2); + + if (args.includes('--help')) { + console.log('Available fixture flags:'); + Object.keys(FIXTURES_FLAGS).forEach((key) => { + console.log( + ` \x1b[36m${key}\x1b[0m ${FIXTURES_FLAGS[key].explanation} (default: \x1b[33m${FIXTURES_FLAGS[key].defaultValue}\x1b[0m)`, + ); + }); + return; + } + + const FIXTURES_CONFIG = {}; + + Object.keys(FIXTURES_FLAGS).forEach((key) => { + FIXTURES_CONFIG[key.replace(/^--/u, '')] = FIXTURES_FLAGS[key].defaultValue; + }); + + let invalidArguments = false; + + // Arguments parsing and validation + args.forEach((arg) => { + const [key, value] = arg.split('='); + if (Object.prototype.hasOwnProperty.call(FIXTURES_FLAGS, key)) { + let valueType; + switch (FIXTURES_FLAGS[key].type) { + case 'number': + valueType = !isNaN(parseFloat(value)) && isFinite(value); + break; + case 'boolean': + valueType = value === 'true' || value === 'false'; + break; + case 'string': + valueType = typeof value === 'string'; + break; + default: + throw new Error(`Unknown type for argument ${key}`); + } + if (valueType) { + const configKey = key.replace(/^--/u, ''); + if (FIXTURES_FLAGS[key].type === 'number') { + FIXTURES_CONFIG[configKey] = parseFloat(value); + } else if (FIXTURES_FLAGS[key].type === 'boolean') { + FIXTURES_CONFIG[configKey] = value === 'true'; + } else { + FIXTURES_CONFIG[configKey] = value; + } + } else { + console.error(`Invalid value for argument ${key}: ${value}`); + invalidArguments = true; + } + } else { + console.error(`Invalid argument: ${key}`); + invalidArguments = true; + } + }); + + console.log('Fixture flags:', FIXTURES_CONFIG); + if (invalidArguments) { + throw new Error('Invalid arguments'); + } + + const fixturesConfig = JSON.stringify(FIXTURES_CONFIG); + + // Start the wallet with state + process.env.WITH_STATE = fixturesConfig; + childProcess.spawn('yarn', [FIXTURES_CONFIG['withStartCommand']], { + stdio: 'inherit', + shell: true, + }); +} + +startWithWalletState(); diff --git a/builds.yml b/builds.yml index 573b0633b78c..62989545775f 100644 --- a/builds.yml +++ b/builds.yml @@ -145,7 +145,7 @@ env: # The unlock password - PASSWORD: null - TEST_SRP: null - - SKIP_ONBOARDING: null + - WITH_STATE: null # Also see METAMASK_DEBUG and NODE_DEBUG - DEBUG: null - SUPPORT_LINK: https://support.metamask.io diff --git a/package.json b/package.json index 99ed1e060cf5..51d5ac234bbc 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "postinstall": "yarn webpack:clearcache", "env:e2e": "SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' yarn", "start": "yarn build:dev dev --apply-lavamoat=false --snow=false", - "start:skip-onboarding": "SKIP_ONBOARDING=true yarn start", + "start:with-state": "node ./app/scripts/start-with-wallet-state.mjs", "start:mv2": "ENABLE_MV3=false yarn build:dev dev --apply-lavamoat=false --snow=false", "start:flask": "yarn start --build-type flask", "start:mmi": "yarn start --build-type mmi", diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 4dee1053aaee..e7fef587f533 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -39,5 +39,10 @@ export const VERIFYING_PAYMASTER = '0xbdbDEc38ed168331b1F7004cc9e5392A2272C1D7'; /* Default ganache ETH balance in decimal when first login */ export const DEFAULT_GANACHE_ETH_BALANCE_DEC = '25'; +/* Dapp host addresses and URL*/ +export const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; +export const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; +export const DAPP_ONE_URL = 'http://127.0.0.1:8081'; + /* Default BTC address created using test SRP */ export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252'; diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 1a4014bcc30a..1d9401bf227f 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -2,14 +2,18 @@ const { WALLET_SNAP_PERMISSION_KEY, SnapCaveatType, } = require('@metamask/snaps-utils'); -const { merge } = require('lodash'); +const { merge, mergeWith } = require('lodash'); const { toHex } = require('@metamask/controller-utils'); const { mockNetworkState } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); const { SMART_CONTRACTS } = require('./seeder/smart-contracts'); -const { DAPP_URL, DAPP_ONE_URL } = require('./helpers'); -const { DEFAULT_FIXTURE_ACCOUNT, ERC_4337_ACCOUNT } = require('./constants'); +const { + DAPP_URL, + DAPP_ONE_URL, + DEFAULT_FIXTURE_ACCOUNT, + ERC_4337_ACCOUNT, +} = require('./constants'); const { defaultFixture, FIXTURE_STATE_METADATA_VERSION, @@ -361,6 +365,20 @@ class FixtureBuilder { }); } + withNotificationServicesController(data) { + mergeWith( + this.fixture.data.NotificationServicesController, + data, + (objValue, srcValue) => { + if (Array.isArray(objValue)) { + objValue.concat(srcValue); + } + return undefined; + }, + ); + return this; + } + withOnboardingController(data) { merge(this.fixture.data.OnboardingController, data); return this;