1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +01:00

Merge branch 'develop' of github.com:MetaMask/metamask-extension into minimal

This commit is contained in:
Matthias Kretschmann 2023-04-12 13:02:03 +01:00
commit 27e9cb7f88
Signed by: m
GPG Key ID: 606EEEF3C479A91F
41 changed files with 1874 additions and 844 deletions

View File

@ -815,7 +815,7 @@
"message": "Contract deployment"
},
"contractDescription": {
"message": "To protect yourself against scammers, take a moment to verify contract details."
"message": "To protect yourself against scammers, take a moment to verify third-party details."
},
"contractInteraction": {
"message": "Contract interaction"
@ -830,10 +830,10 @@
"message": "Contract requesting signature"
},
"contractRequestingSpendingCap": {
"message": "Contract requesting spending cap"
"message": "Third party requesting spending cap"
},
"contractTitle": {
"message": "Contract details"
"message": "Third-party details"
},
"contractToken": {
"message": "Token contract"
@ -1808,14 +1808,14 @@
"message": "Your initial transaction was confirmed by the network. Click OK to go back."
},
"inputLogicEmptyState": {
"message": "Only enter a number that you're comfortable with the contract spending now or in the future. You can always increase the spending cap later."
"message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later."
},
"inputLogicEqualOrSmallerNumber": {
"message": "This allows the contract to spend $1 from your current balance.",
"message": "This allows the third party to spend $1 from your current balance.",
"description": "$1 is the current token balance in the account and the name of the current token"
},
"inputLogicHigherNumber": {
"message": "This allows the contract to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap."
"message": "This allows the third party to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap."
},
"insightsFromSnap": {
"message": "Insights from $1",
@ -2486,6 +2486,9 @@
"message": "OpenSea is the first provider for this feature. More providers coming soon!",
"description": "Description of a notification in the 'See What's New' popup. Describes Opensea Security Provider feature."
},
"notifications18Title": {
"message": "Stay safe with security alerts"
},
"notifications19ActionText": {
"message": "Enable NFT autodetection"
},
@ -3328,7 +3331,7 @@
"description": "$1 is a token symbol"
},
"revokeSpendingCapTooltipText": {
"message": "This contract will be unable to spend any more of your current or future tokens."
"message": "This third party will be unable to spend any more of your current or future tokens."
},
"rpcUrl": {
"message": "New RPC URL"
@ -4660,7 +4663,7 @@
"message": "Username"
},
"verifyContractDetails": {
"message": "Verify contract details"
"message": "Verify third-party details"
},
"verifyThisTokenDecimalOn": {
"message": "Token decimal can be found on $1",
@ -4746,7 +4749,7 @@
"message": "Warning"
},
"warningTooltipText": {
"message": "$1 The contract could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.",
"message": "$1 The third party could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.",
"description": "$1 is a warning icon with text 'Be careful' in 'warning' colour"
},
"weak": {

View File

@ -727,7 +727,6 @@ export function setupController(
}
function getUnapprovedTransactionCount() {
const unapprovedTxCount = controller.txController.getUnapprovedTxCount();
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager;
const { unapprovedEncryptionPublicKeyMsgCount } =
controller.encryptionPublicKeyManager;
@ -736,7 +735,6 @@ export function setupController(
const waitingForUnlockCount =
controller.appStateController.waitingForUnlock.length;
return (
unapprovedTxCount +
unapprovedDecryptMsgCount +
unapprovedEncryptionPublicKeyMsgCount +
pendingApprovalCount +

View File

@ -13,7 +13,7 @@ import { convertHexToDecimal } from '@metamask/controller-utils';
import { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens';
import NetworkController, { NetworkControllerEventTypes } from './network';
import { NetworkController, NetworkControllerEventType } from './network';
import PreferencesController from './preferences';
describe('DetectTokensController', function () {
@ -248,7 +248,7 @@ describe('DetectTokensController', function () {
),
onNetworkStateChange: (cb) =>
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
() => {
const networkState = network.store.getState();
const modifiedNetworkState = {

View File

@ -1 +0,0 @@
export { default, NetworkControllerEventTypes } from './network-controller';

View File

@ -0,0 +1 @@
export * from './network-controller';

View File

@ -1,679 +0,0 @@
import { strict as assert } from 'assert';
import EventEmitter from 'events';
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import {
createSwappableProxy,
createEventEmitterProxy,
} from '@metamask/swappable-obj-proxy';
import EthQuery from 'eth-query';
// ControllerMessenger is referred to in the JSDocs
// eslint-disable-next-line no-unused-vars
import { ControllerMessenger } from '@metamask/base-controller';
import { v4 as random } from 'uuid';
import { hasProperty, isPlainObject } from '@metamask/utils';
import { errorCodes } from 'eth-rpc-errors';
import {
INFURA_PROVIDER_TYPES,
BUILT_IN_NETWORKS,
INFURA_BLOCKED_KEY,
TEST_NETWORK_TICKER_MAP,
CHAIN_IDS,
NETWORK_TYPES,
NetworkStatus,
} from '../../../../shared/constants/network';
import {
isPrefixedFormattedHexString,
isSafeChainId,
} from '../../../../shared/modules/network.utils';
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics';
import { createNetworkClient } from './create-network-client';
/**
* @typedef {object} NetworkConfiguration
* @property {string} rpcUrl - RPC target URL.
* @property {string} chainId - Network ID as per EIP-155
* @property {string} ticker - Currency ticker.
* @property {object} [rpcPrefs] - Personalized preferences.
* @property {string} [nickname] - Personalized network name.
*/
function buildDefaultProviderConfigState() {
if (process.env.IN_TEST) {
return {
type: NETWORK_TYPES.RPC,
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
nickname: 'Localhost 8545',
ticker: 'ETH',
};
} else if (
process.env.METAMASK_DEBUG ||
process.env.METAMASK_ENV === 'test'
) {
return {
type: NETWORK_TYPES.GOERLI,
chainId: CHAIN_IDS.GOERLI,
ticker: TEST_NETWORK_TICKER_MAP.GOERLI,
};
}
return {
type: NETWORK_TYPES.MAINNET,
chainId: CHAIN_IDS.MAINNET,
ticker: 'ETH',
};
}
function buildDefaultNetworkIdState() {
return null;
}
function buildDefaultNetworkStatusState() {
return NetworkStatus.Unknown;
}
function buildDefaultNetworkDetailsState() {
return {
EIPS: {
1559: undefined,
},
};
}
function buildDefaultNetworkConfigurationsState() {
return {};
}
/**
* The name of the controller.
*/
const name = 'NetworkController';
/**
* The set of event types that this controller can publish via its messenger.
*/
export const NetworkControllerEventTypes = {
/**
* Fired after the current network is changed.
*/
NetworkDidChange: `${name}:networkDidChange`,
/**
* Fired when there is a request to change the current network, but no state
* changes have occurred yet.
*/
NetworkWillChange: `${name}:networkWillChange`,
/**
* Fired after the network is changed to an Infura network, but when Infura
* returns an error denying support for the user's location.
*/
InfuraIsBlocked: `${name}:infuraIsBlocked`,
/**
* Fired after the network is changed to an Infura network and Infura does not
* return an error denying support for the user's location, or after the
* network is changed to a custom network.
*/
InfuraIsUnblocked: `${name}:infuraIsUnblocked`,
};
export default class NetworkController extends EventEmitter {
/**
* Construct a NetworkController.
*
* @param {object} options - Options for this controller.
* @param {ControllerMessenger} options.messenger - The controller messenger.
* @param {object} [options.state] - Initial controller state.
* @param {string} [options.infuraProjectId] - The Infura project ID.
* @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController
*/
constructor({
messenger,
state = {},
infuraProjectId,
trackMetaMetricsEvent,
} = {}) {
super();
this.messenger = messenger;
// create stores
this.providerStore = new ObservableStore(
state.provider || buildDefaultProviderConfigState(),
);
this.previousProviderStore = new ObservableStore(
this.providerStore.getState(),
);
this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState());
this.networkStatusStore = new ObservableStore(
buildDefaultNetworkStatusState(),
);
// We need to keep track of a few details about the current network.
// Ideally we'd merge this.networkStatusStore with this new store, but doing
// so will require a decent sized refactor of how we're accessing network
// state. Currently this is only used for detecting EIP-1559 support but can
// be extended to track other network details.
this.networkDetails = new ObservableStore(
state.networkDetails || buildDefaultNetworkDetailsState(),
);
this.networkConfigurationsStore = new ObservableStore(
state.networkConfigurations || buildDefaultNetworkConfigurationsState(),
);
this.store = new ComposedStore({
provider: this.providerStore,
previousProviderStore: this.previousProviderStore,
networkId: this.networkIdStore,
networkStatus: this.networkStatusStore,
networkDetails: this.networkDetails,
networkConfigurations: this.networkConfigurationsStore,
});
// provider and block tracker
this._provider = null;
this._blockTracker = null;
// provider and block tracker proxies - because the network changes
this._providerProxy = null;
this._blockTrackerProxy = null;
if (!infuraProjectId || typeof infuraProjectId !== 'string') {
throw new Error('Invalid Infura project ID');
}
this._infuraProjectId = infuraProjectId;
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
}
/**
* Destroy the network controller, stopping any ongoing polling.
*
* In-progress requests will not be aborted.
*/
async destroy() {
await this._blockTracker?.destroy();
}
async initializeProvider() {
const { type, rpcUrl, chainId } = this.providerStore.getState();
this._configureProvider({ type, rpcUrl, chainId });
await this.lookupNetwork();
}
// return the proxies so the references will always be good
getProviderAndBlockTracker() {
const provider = this._providerProxy;
const blockTracker = this._blockTrackerProxy;
return { provider, blockTracker };
}
/**
* Determines whether the network supports EIP-1559 by checking whether the
* latest block has a `baseFeePerGas` property, then updates state
* appropriately.
*
* @returns {Promise<boolean>} A promise that resolves to true if the network
* supports EIP-1559 and false otherwise.
*/
async getEIP1559Compatibility() {
const { EIPS } = this.networkDetails.getState();
// NOTE: This isn't necessary anymore because the block cache middleware
// already prevents duplicate requests from taking place
if (EIPS[1559] !== undefined) {
return EIPS[1559];
}
const supportsEIP1559 = await this._determineEIP1559Compatibility();
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
},
});
return supportsEIP1559;
}
/**
* Captures information about the currently selected network namely,
* the network ID and whether the network supports EIP-1559 and then uses
* the results of these requests to determine the status of the network.
*/
async lookupNetwork() {
const { chainId, type } = this.providerStore.getState();
let networkChanged = false;
let networkId;
let supportsEIP1559;
let networkStatus;
if (!this._provider) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing provider',
);
return;
}
if (!chainId) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing chainId',
);
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
return;
}
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
const listener = () => {
networkChanged = true;
this.messenger.unsubscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
};
this.messenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
try {
const results = await Promise.all([
this._getNetworkId(),
this._determineEIP1559Compatibility(),
]);
networkId = results[0];
supportsEIP1559 = results[1];
networkStatus = NetworkStatus.Available;
} catch (error) {
if (hasProperty(error, 'code')) {
let responseBody;
try {
responseBody = JSON.parse(error.message);
} catch {
// error.message must not be JSON
}
if (
isPlainObject(responseBody) &&
responseBody.error === INFURA_BLOCKED_KEY
) {
networkStatus = NetworkStatus.Blocked;
} else if (error.code === errorCodes.rpc.internal) {
networkStatus = NetworkStatus.Unknown;
} else {
networkStatus = NetworkStatus.Unavailable;
}
} else {
log.warn(
'NetworkController - could not determine network status',
error,
);
networkStatus = NetworkStatus.Unknown;
}
}
if (networkChanged) {
// If the network has changed, then `lookupNetwork` either has been or is
// in the process of being called, so we don't need to go further.
return;
}
this.messenger.unsubscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
this.networkStatusStore.putState(networkStatus);
if (networkStatus === NetworkStatus.Available) {
this.networkIdStore.putState(networkId);
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
},
});
} else {
this._resetNetworkId();
this._resetNetworkDetails();
}
if (isInfura) {
if (networkStatus === NetworkStatus.Available) {
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
} else if (networkStatus === NetworkStatus.Blocked) {
this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked);
}
} else {
// Always publish infuraIsUnblocked regardless of network status to
// prevent consumers from being stuck in a blocked state if they were
// previously connected to an Infura network that was blocked
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
}
}
/**
* A method for setting the currently selected network provider by networkConfigurationId.
*
* @param {string} networkConfigurationId - the universal unique identifier that corresponds to the network configuration to set as active.
* @returns {string} The rpcUrl of the network that was just set as active
*/
setActiveNetwork(networkConfigurationId) {
const targetNetwork =
this.networkConfigurationsStore.getState()[networkConfigurationId];
if (!targetNetwork) {
throw new Error(
`networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`,
);
}
this._setProviderConfig({
type: NETWORK_TYPES.RPC,
...targetNetwork,
});
return targetNetwork.rpcUrl;
}
setProviderType(type) {
assert.notStrictEqual(
type,
NETWORK_TYPES.RPC,
`NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setActiveNetwork"`,
);
assert.ok(
INFURA_PROVIDER_TYPES.includes(type),
`Unknown Infura provider type "${type}".`,
);
const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[type];
this._setProviderConfig({
type,
rpcUrl: '',
chainId,
ticker: ticker ?? 'ETH',
nickname: '',
rpcPrefs: { blockExplorerUrl },
});
}
resetConnection() {
this._setProviderConfig(this.providerStore.getState());
}
rollbackToPreviousProvider() {
const config = this.previousProviderStore.getState();
this.providerStore.putState(config);
this._switchNetwork(config);
}
//
// Private
//
/**
* Method to return the latest block for the current network
*
* @returns {object} Block header
*/
_getLatestBlock() {
const { provider } = this.getProviderAndBlockTracker();
const ethQuery = new EthQuery(provider);
return new Promise((resolve, reject) => {
ethQuery.sendAsync(
{ method: 'eth_getBlockByNumber', params: ['latest', false] },
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
},
);
});
}
/**
* Get the network ID for the current selected network
*
* @returns {string} The network ID for the current network.
*/
async _getNetworkId() {
const { provider } = this.getProviderAndBlockTracker();
const ethQuery = new EthQuery(provider);
return await new Promise((resolve, reject) => {
ethQuery.sendAsync({ method: 'net_version' }, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
/**
* Clears the stored network ID.
*/
_resetNetworkId() {
this.networkIdStore.putState(buildDefaultNetworkIdState());
}
/**
* Resets network status to the default ("unknown").
*/
_resetNetworkStatus() {
this.networkStatusStore.putState(buildDefaultNetworkStatusState());
}
/**
* Clears details previously stored for the network.
*/
_resetNetworkDetails() {
this.networkDetails.putState(buildDefaultNetworkDetailsState());
}
/**
* Sets the provider config and switches the network.
*
* @param config
*/
_setProviderConfig(config) {
this.previousProviderStore.putState(this.providerStore.getState());
this.providerStore.putState(config);
this._switchNetwork(config);
}
/**
* Retrieves the latest block from the currently selected network; if the
* block has a `baseFeePerGas` property, then we know that the network
* supports EIP-1559; otherwise it doesn't.
*
* @returns {Promise<boolean>} A promise that resolves to true if the network
* supports EIP-1559 and false otherwise.
*/
async _determineEIP1559Compatibility() {
const latestBlock = await this._getLatestBlock();
return latestBlock && latestBlock.baseFeePerGas !== undefined;
}
_switchNetwork(opts) {
this.messenger.publish(NetworkControllerEventTypes.NetworkWillChange);
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
this._configureProvider(opts);
this.messenger.publish(NetworkControllerEventTypes.NetworkDidChange);
this.lookupNetwork();
}
_configureProvider({ type, rpcUrl, chainId }) {
// infura type-based endpoints
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
if (isInfura) {
this._configureInfuraProvider({
type,
infuraProjectId: this._infuraProjectId,
});
// url-based rpc endpoints
} else if (type === NETWORK_TYPES.RPC) {
this._configureStandardProvider(rpcUrl, chainId);
} else {
throw new Error(
`NetworkController - _configureProvider - unknown type "${type}"`,
);
}
}
_configureInfuraProvider({ type, infuraProjectId }) {
log.info('NetworkController - configureInfuraProvider', type);
const { provider, blockTracker } = createNetworkClient({
network: type,
infuraProjectId,
type: 'infura',
});
this._setProviderAndBlockTracker({ provider, blockTracker });
}
_configureStandardProvider(rpcUrl, chainId) {
log.info('NetworkController - configureStandardProvider', rpcUrl);
const { provider, blockTracker } = createNetworkClient({
chainId,
rpcUrl,
type: 'custom',
});
this._setProviderAndBlockTracker({ provider, blockTracker });
}
_setProviderAndBlockTracker({ provider, blockTracker }) {
// update or initialize proxies
if (this._providerProxy) {
this._providerProxy.setTarget(provider);
} else {
this._providerProxy = createSwappableProxy(provider);
}
if (this._blockTrackerProxy) {
this._blockTrackerProxy.setTarget(blockTracker);
} else {
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, {
eventFilter: 'skipInternal',
});
}
// set new provider and blockTracker
this._provider = provider;
this._blockTracker = blockTracker;
}
/**
* Network Configuration management functions
*/
/**
* Adds a network configuration if the rpcUrl is not already present on an
* existing network configuration. Otherwise updates the entry with the matching rpcUrl.
*
* @param {NetworkConfiguration} networkConfiguration - The network configuration to add or, if rpcUrl matches an existing entry, to modify.
* @param {object} options
* @param {boolean} options.setActive - An option to set the newly added networkConfiguration as the active provider.
* @param {string} options.referrer - The site from which the call originated, or 'metamask' for internal calls - used for event metrics.
* @param {string} options.source - Where the upsertNetwork event originated (i.e. from a dapp or from the network form)- used for event metrics.
* @returns {string} id for the added or updated network configuration
*/
upsertNetworkConfiguration(
{ rpcUrl, chainId, ticker, nickname, rpcPrefs },
{ setActive = false, referrer, source },
) {
assert.ok(
isPrefixedFormattedHexString(chainId),
`Invalid chain ID "${chainId}": invalid hex string.`,
);
assert.ok(
isSafeChainId(parseInt(chainId, 16)),
`Invalid chain ID "${chainId}": numerical value greater than max safe value.`,
);
if (!rpcUrl) {
throw new Error(
'An rpcUrl is required to add or update network configuration',
);
}
if (!referrer || !source) {
throw new Error(
'referrer and source are required arguments for adding or updating a network configuration',
);
}
try {
// eslint-disable-next-line no-new
new URL(rpcUrl);
} catch (e) {
if (e.message.includes('Invalid URL')) {
throw new Error('rpcUrl must be a valid URL');
}
}
if (!ticker) {
throw new Error(
'A ticker is required to add or update networkConfiguration',
);
}
const networkConfigurations = this.networkConfigurationsStore.getState();
const newNetworkConfiguration = {
rpcUrl,
chainId,
ticker,
nickname,
rpcPrefs,
};
const oldNetworkConfigurationId = Object.values(networkConfigurations).find(
(networkConfiguration) =>
networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(),
)?.id;
const newNetworkConfigurationId = oldNetworkConfigurationId || random();
this.networkConfigurationsStore.putState({
...networkConfigurations,
[newNetworkConfigurationId]: {
...newNetworkConfiguration,
id: newNetworkConfigurationId,
},
});
if (!oldNetworkConfigurationId) {
this._trackMetaMetricsEvent({
event: 'Custom Network Added',
category: MetaMetricsEventCategory.Network,
referrer: {
url: referrer,
},
properties: {
chain_id: chainId,
symbol: ticker,
source,
},
});
}
if (setActive) {
this.setActiveNetwork(newNetworkConfigurationId);
}
return newNetworkConfigurationId;
}
/**
* Removes network configuration from state.
*
* @param {string} networkConfigurationId - the unique id for the network configuration to remove.
*/
removeNetworkConfiguration(networkConfigurationId) {
const networkConfigurations = {
...this.networkConfigurationsStore.getState(),
};
delete networkConfigurations[networkConfigurationId];
this.networkConfigurationsStore.putState(networkConfigurations);
}
}

View File

@ -6,7 +6,7 @@ import sinon from 'sinon';
import { ControllerMessenger } from '@metamask/base-controller';
import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network';
import { MetaMetricsNetworkEventSource } from '../../../../shared/constants/metametrics';
import NetworkController from './network-controller';
import { NetworkController } from './network-controller';
jest.mock('uuid', () => {
const actual = jest.requireActual('uuid');
@ -1100,7 +1100,7 @@ describe('NetworkController', () => {
});
describe('when the request for the latest block responds with null', () => {
it('stores null as whether the network supports EIP-1559', async () => {
it('persists false to state as whether the network supports EIP-1559', async () => {
await withController(
{
state: {
@ -1118,13 +1118,13 @@ describe('NetworkController', () => {
await controller.getEIP1559Compatibility();
expect(controller.store.getState().networkDetails.EIPS[1559]).toBe(
null,
false,
);
},
);
});
it('returns null', async () => {
it('returns false', async () => {
await withController(async ({ controller, network }) => {
network.mockEssentialRpcCalls({
latestBlock: null,
@ -1133,7 +1133,7 @@ describe('NetworkController', () => {
const supportsEIP1559 = await controller.getEIP1559Compatibility();
expect(supportsEIP1559).toBe(null);
expect(supportsEIP1559).toBe(false);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import { ControllerMessenger } from '@metamask/base-controller';
import { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences';
import NetworkController from './network';
import { NetworkController } from './network';
describe('preferences controller', function () {
let preferencesController;

View File

@ -52,7 +52,10 @@ import {
determineTransactionType,
isEIP1559Transaction,
} from '../../../../shared/modules/transaction.utils';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import {
ORIGIN_METAMASK,
MESSAGE_TYPE,
} from '../../../../shared/constants/app';
import {
calcGasTotal,
getSwapsTokensReceivedFromTxMeta,
@ -156,6 +159,7 @@ export default class TransactionController extends EventEmitter {
this.getAccountType = opts.getAccountType;
this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails;
this.securityProviderRequest = opts.securityProviderRequest;
this.messagingSystem = opts.messenger;
this.memStore = new ObservableStore({});
@ -798,6 +802,7 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.getTransactionWithActionId(actionId);
if (existingTxMeta) {
this.emit('newUnapprovedTx', existingTxMeta);
this._requestApproval(existingTxMeta);
existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta);
return existingTxMeta;
}
@ -870,6 +875,7 @@ export default class TransactionController extends EventEmitter {
this.addTransaction(txMeta);
this.emit('newUnapprovedTx', txMeta);
this._requestApproval(txMeta);
txMeta = await this.addTransactionGasDefaults(txMeta);
@ -1355,6 +1361,7 @@ export default class TransactionController extends EventEmitter {
try {
// approve
this.txStateManager.setTxStatusApproved(txId);
this._acceptApproval(txMeta);
// get next nonce
const fromAddress = txMeta.txParams.from;
// wait for a nonce
@ -1734,6 +1741,7 @@ export default class TransactionController extends EventEmitter {
async cancelTransaction(txId, actionId) {
const txMeta = this.txStateManager.getTransaction(txId);
this.txStateManager.setTxStatusRejected(txId);
this._rejectApproval(txMeta);
this._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.rejected,
@ -2596,4 +2604,54 @@ export default class TransactionController extends EventEmitter {
},
);
}
_requestApproval(txMeta) {
const id = this._getApprovalId(txMeta);
const { origin } = txMeta;
const type = MESSAGE_TYPE.TRANSACTION;
const requestData = { txId: txMeta.id };
this.messagingSystem
.call(
'ApprovalController:addRequest',
{
id,
origin,
type,
requestData,
},
true,
)
.catch(() => {
// Intentionally ignored as promise not currently used
});
}
_acceptApproval(txMeta) {
const id = this._getApprovalId(txMeta);
try {
this.messagingSystem.call('ApprovalController:acceptRequest', id);
} catch (error) {
log.error('Failed to accept transaction approval request', error);
}
}
_rejectApproval(txMeta) {
const id = this._getApprovalId(txMeta);
try {
this.messagingSystem.call(
'ApprovalController:rejectRequest',
id,
new Error('Rejected'),
);
} catch (error) {
log.error('Failed to reject transaction approval request', error);
}
}
_getApprovalId(txMeta) {
return String(txMeta.id);
}
}

View File

@ -29,7 +29,10 @@ import {
GasRecommendations,
} from '../../../../shared/constants/gas';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import {
MESSAGE_TYPE,
ORIGIN_METAMASK,
} from '../../../../shared/constants/app';
import { NetworkStatus } from '../../../../shared/constants/network';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
import TransactionController from '.';
@ -52,7 +55,8 @@ describe('Transaction Controller', function () {
fromAccount,
fragmentExists,
networkStatusStore,
getCurrentChainId;
getCurrentChainId,
messengerMock;
beforeEach(function () {
fragmentExists = false;
@ -76,6 +80,7 @@ describe('Transaction Controller', function () {
blockTrackerStub.getLatestBlock = noop;
getCurrentChainId = sinon.stub().callsFake(() => currentChainId);
messengerMock = { call: sinon.stub().returns(Promise.resolve()) };
txController = new TransactionController({
provider,
@ -108,6 +113,7 @@ describe('Transaction Controller', function () {
getAccountType: () => 'MetaMask',
getDeviceModel: () => 'N/A',
securityProviderRequest: () => undefined,
messenger: messengerMock,
});
txController.nonceTracker.getNonceLock = () =>
Promise.resolve({ nextNonce: 0, releaseLock: noop });
@ -489,6 +495,67 @@ describe('Transaction Controller', function () {
{ message: 'MetaMask is having trouble connecting to the network' },
);
});
it('should create an approval request', async function () {
const txMeta = await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
ORIGIN_METAMASK,
);
assert.equal(messengerMock.call.callCount, 1);
assert.deepEqual(messengerMock.call.getCall(0).args, [
'ApprovalController:addRequest',
{
id: String(txMeta.id),
origin: ORIGIN_METAMASK,
requestData: { txId: txMeta.id },
type: MESSAGE_TYPE.TRANSACTION,
},
true, // Show popup
]);
});
it('should still create an approval request when called twice with same actionId', async function () {
await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
ORIGIN_METAMASK,
undefined,
undefined,
'12345',
);
const secondTxMeta = await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
undefined,
undefined,
undefined,
'12345',
);
assert.equal(messengerMock.call.callCount, 2);
assert.deepEqual(messengerMock.call.getCall(1).args, [
'ApprovalController:addRequest',
{
id: String(secondTxMeta.id),
origin: ORIGIN_METAMASK,
requestData: { txId: secondTxMeta.id },
type: MESSAGE_TYPE.TRANSACTION,
},
true, // Show popup
]);
});
});
describe('#createCancelTransaction', function () {
@ -997,9 +1064,11 @@ describe('Transaction Controller', function () {
});
describe('#approveTransaction', function () {
it('does not overwrite set values', async function () {
const originalValue = '0x01';
const txMeta = {
let originalValue, txMeta, signStub, pubStub;
beforeEach(function () {
originalValue = '0x01';
txMeta = {
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
@ -1019,17 +1088,22 @@ describe('Transaction Controller', function () {
providerResultStub.eth_gasPrice = wrongValue;
providerResultStub.eth_estimateGas = '0x5209';
const signStub = sinon
signStub = sinon
.stub(txController, 'signTransaction')
.callsFake(() => Promise.resolve());
const pubStub = sinon
.stub(txController, 'publishTransaction')
.callsFake(() => {
pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
txController.setTxHash('1', originalValue);
txController.txStateManager.setTxStatusSubmitted('1');
});
});
afterEach(function () {
signStub.restore();
pubStub.restore();
});
it('does not overwrite set values', async function () {
await txController.approveTransaction(txMeta.id);
const result = txController.txStateManager.getTransaction(txMeta.id);
const params = result.txParams;
@ -1042,8 +1116,21 @@ describe('Transaction Controller', function () {
TransactionStatus.submitted,
'should have reached the submitted status.',
);
signStub.restore();
pubStub.restore();
});
it('should accept the approval request', async function () {
await txController.approveTransaction(txMeta.id);
assert.equal(messengerMock.call.callCount, 1);
assert.deepEqual(messengerMock.call.getCall(0).args, [
'ApprovalController:acceptRequest',
txMeta.id,
]);
});
it('should not throw if accepting approval request throws', async function () {
messengerMock.call.throws();
await txController.approveTransaction(txMeta.id);
});
});
@ -1108,7 +1195,7 @@ describe('Transaction Controller', function () {
});
describe('#cancelTransaction', function () {
it('should emit a status change to rejected', function (done) {
beforeEach(function () {
txController.txStateManager._addTransactionsToState([
{
id: 0,
@ -1181,7 +1268,9 @@ describe('Transaction Controller', function () {
history: [{}],
},
]);
});
it('should emit a status change to rejected', function (done) {
txController.once('tx:status-update', (txId, status) => {
try {
assert.equal(
@ -1198,6 +1287,22 @@ describe('Transaction Controller', function () {
txController.cancelTransaction(0);
});
it('should reject the approval request', function () {
txController.cancelTransaction(0);
assert.equal(messengerMock.call.callCount, 1);
assert.deepEqual(messengerMock.call.getCall(0).args, [
'ApprovalController:rejectRequest',
'0',
new Error('Rejected'),
]);
});
it('should not throw if rejecting approval request throws', async function () {
messengerMock.call.throws();
txController.cancelTransaction(0);
});
});
describe('#createSpeedUpTransaction', function () {

View File

@ -143,8 +143,9 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware';
import createOnboardingMiddleware from './lib/createOnboardingMiddleware';
import { setupMultiplex } from './lib/stream-utils';
import EnsController from './controllers/ens';
import NetworkController, {
NetworkControllerEventTypes,
import {
NetworkController,
NetworkControllerEventType,
} from './controllers/network';
import PreferencesController from './controllers/preferences';
import AppStateController from './controllers/app-state';
@ -263,7 +264,7 @@ export default class MetamaskController extends EventEmitter {
const networkControllerMessenger = this.controllerMessenger.getRestricted({
name: 'NetworkController',
allowedEvents: Object.values(NetworkControllerEventTypes),
allowedEvents: Object.values(NetworkControllerEventType),
});
this.networkController = new NetworkController({
messenger: networkControllerMessenger,
@ -310,11 +311,11 @@ export default class MetamaskController extends EventEmitter {
initLangCode: opts.initLangCode,
onInfuraIsBlocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsBlocked,
NetworkControllerEventType.InfuraIsBlocked,
),
onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsUnblocked,
NetworkControllerEventType.InfuraIsUnblocked,
),
tokenListController: this.tokenListController,
provider: this.provider,
@ -452,7 +453,7 @@ export default class MetamaskController extends EventEmitter {
preferencesStore: this.preferencesController.store,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
getNetworkIdentifier: () => {
const { type, rpcUrl } =
@ -491,7 +492,7 @@ export default class MetamaskController extends EventEmitter {
// onNetworkDidChange
onNetworkStateChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
getCurrentNetworkEIP1559Compatibility:
this.networkController.getEIP1559Compatibility.bind(
@ -609,7 +610,7 @@ export default class MetamaskController extends EventEmitter {
this.networkController.store.getState().provider.chainId,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
});
@ -621,7 +622,7 @@ export default class MetamaskController extends EventEmitter {
blockTracker: this.blockTracker,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
@ -1007,8 +1008,15 @@ export default class MetamaskController extends EventEmitter {
getDeviceModel: this.getDeviceModel.bind(this),
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
securityProviderRequest: this.securityProviderRequest.bind(this),
messenger: this.controllerMessenger.getRestricted({
name: 'TransactionController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
],
}),
});
this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation());
this.txController.on(`tx:status-update`, async (txId, status) => {
if (
@ -1099,7 +1107,7 @@ export default class MetamaskController extends EventEmitter {
});
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
async () => {
const { ticker } = this.networkController.store.getState().provider;
try {
@ -1145,7 +1153,7 @@ export default class MetamaskController extends EventEmitter {
networkController: this.networkController,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
provider: this.provider,
getProviderConfig: () => this.networkController.store.getState().provider,
@ -1188,7 +1196,7 @@ export default class MetamaskController extends EventEmitter {
// ensure accountTracker updates balances after network change
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
() => {
this.accountTracker._updateAccounts();
},
@ -1196,7 +1204,7 @@ export default class MetamaskController extends EventEmitter {
// clear unapproved transactions and messages when the network will change
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkWillChange,
NetworkControllerEventType.NetworkWillChange,
() => {
this.txController.txStateManager.clearUnapprovedTxs();
this.encryptionPublicKeyManager.clearUnapproved();

View File

@ -0,0 +1,254 @@
import { v4 } from 'uuid';
import { migrate, version } from './084';
jest.mock('uuid', () => {
const actual = jest.requireActual('uuid');
return {
...actual,
v4: jest.fn(),
};
});
describe('migration #84', () => {
beforeEach(() => {
v4.mockImplementationOnce(() => 'network-configuration-id-1')
.mockImplementationOnce(() => 'network-configuration-id-2')
.mockImplementationOnce(() => 'network-configuration-id-3')
.mockImplementationOnce(() => 'network-configuration-id-4');
});
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
version: 83,
},
data: {},
};
const newStorage = await migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version,
});
});
it('should use the key of the networkConfigurations object to set the id of each network configuration', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: {
networkConfigurations: {
'network-configuration-id-1': {
chainId: '0x539',
nickname: 'Localhost 8545',
rpcPrefs: {},
rpcUrl: 'http://localhost:8545',
ticker: 'ETH',
},
'network-configuration-id-2': {
chainId: '0xa4b1',
nickname: 'Arbitrum One',
rpcPrefs: {
blockExplorerUrl: 'https://explorer.arbitrum.io',
},
rpcUrl:
'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'ETH',
},
'network-configuration-id-3': {
chainId: '0x4e454152',
nickname: 'Aurora Mainnet',
rpcPrefs: {
blockExplorerUrl: 'https://aurorascan.dev/',
},
rpcUrl:
'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'Aurora ETH',
},
'network-configuration-id-4': {
chainId: '0x38',
nickname:
'BNB Smart Chain (previously Binance Smart Chain Mainnet)',
rpcPrefs: {
blockExplorerUrl: 'https://bscscan.com/',
},
rpcUrl: 'https://bsc-dataseed.binance.org/',
ticker: 'BNB',
},
},
},
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: {
networkConfigurations: {
'network-configuration-id-1': {
chainId: '0x539',
nickname: 'Localhost 8545',
rpcPrefs: {},
rpcUrl: 'http://localhost:8545',
ticker: 'ETH',
id: 'network-configuration-id-1',
},
'network-configuration-id-2': {
chainId: '0xa4b1',
nickname: 'Arbitrum One',
rpcPrefs: {
blockExplorerUrl: 'https://explorer.arbitrum.io',
},
rpcUrl:
'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'ETH',
id: 'network-configuration-id-2',
},
'network-configuration-id-3': {
chainId: '0x4e454152',
nickname: 'Aurora Mainnet',
rpcPrefs: {
blockExplorerUrl: 'https://aurorascan.dev/',
},
rpcUrl:
'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'Aurora ETH',
id: 'network-configuration-id-3',
},
'network-configuration-id-4': {
chainId: '0x38',
nickname:
'BNB Smart Chain (previously Binance Smart Chain Mainnet)',
rpcPrefs: {
blockExplorerUrl: 'https://bscscan.com/',
},
rpcUrl: 'https://bsc-dataseed.binance.org/',
ticker: 'BNB',
id: 'network-configuration-id-4',
},
},
},
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController is undefined', async () => {
const oldStorage = {
meta: {
version,
},
data: {
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController is not an object', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: false,
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: false,
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController.networkConfigurations is undefined', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: undefined,
},
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: undefined,
},
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController.networkConfigurations is an empty object', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: {},
},
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: {},
},
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
});

View File

@ -0,0 +1,58 @@
import { cloneDeep } from 'lodash';
import { isObject } from '@metamask/utils';
export const version = 84;
/**
* Ensure that each networkConfigurations object in state.NetworkController.networkConfigurations has an
* `id` property which matches the key pointing that object
*
* @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: {
meta: { version: number };
data: Record<string, unknown>;
}) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
versionedData.data = transformState(versionedData.data);
return versionedData;
}
function transformState(state: Record<string, unknown>) {
if (!isObject(state.NetworkController)) {
return state;
}
const { NetworkController } = state;
if (!isObject(NetworkController.networkConfigurations)) {
return state;
}
const { networkConfigurations } = NetworkController;
const newNetworkConfigurations: Record<string, Record<string, unknown>> = {};
for (const networkConfigurationId of Object.keys(networkConfigurations)) {
const networkConfiguration = networkConfigurations[networkConfigurationId];
if (!isObject(networkConfiguration)) {
return state;
}
newNetworkConfigurations[networkConfigurationId] = {
...networkConfiguration,
id: networkConfigurationId,
};
}
return {
...state,
NetworkController: {
...NetworkController,
networkConfigurations: newNetworkConfigurations,
},
};
}

View File

@ -87,6 +87,7 @@ import m080 from './080';
import * as m081 from './081';
import * as m082 from './082';
import * as m083 from './083';
import * as m084 from './084';
const migrations = [
m002,
@ -171,6 +172,7 @@ const migrations = [
m081,
m082,
m083,
m084,
];
export default migrations;

View File

@ -51,7 +51,7 @@ module.exports = {
'<rootDir>/ui/**/*.test.(js|ts|tsx)',
'<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)',
],
testTimeout: 2500,
testTimeout: 5500,
// We have to specify the environment we are running in, which is jsdom. The
// default is 'node'. This can be modified *per file* using a comment at the
// head of the file. So it may be worthwhile to switch to 'node' in any

View File

@ -1193,7 +1193,7 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
@ -1201,11 +1201,6 @@
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,

View File

@ -1265,7 +1265,7 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
@ -1273,11 +1273,6 @@
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,

View File

@ -1265,7 +1265,7 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
@ -1273,11 +1273,6 @@
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,

View File

@ -1193,7 +1193,7 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
@ -1201,11 +1201,6 @@
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,

View File

@ -242,14 +242,14 @@
"@metamask/eth-ledger-bridge-keyring": "^0.13.0",
"@metamask/eth-token-tracker": "^4.0.0",
"@metamask/etherscan-link": "^2.2.0",
"@metamask/gas-fee-controller": "^1.0.0",
"@metamask/gas-fee-controller": "^3.0.0",
"@metamask/jazzicon": "^2.0.0",
"@metamask/key-tree": "^7.0.0",
"@metamask/logo": "^3.1.1",
"@metamask/message-manager": "^2.1.0",
"@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/notification-controller": "^1.0.0",
"@metamask/obs-store": "^8.0.0",
"@metamask/obs-store": "^8.1.0",
"@metamask/permission-controller": "^3.1.0",
"@metamask/phishing-controller": "^2.0.0",
"@metamask/post-message-stream": "^6.0.0",

View File

@ -53,6 +53,7 @@ export const MESSAGE_TYPE = {
PERSONAL_SIGN: 'personal_sign',
SEND_METADATA: 'metamask_sendDomainMetadata',
SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain',
TRANSACTION: 'transaction',
WALLET_REQUEST_PERMISSIONS: 'wallet_requestPermissions',
WATCH_ASSET: 'wallet_watchAsset',
WATCH_ASSET_LEGACY: 'metamask_watchAsset',

View File

@ -238,7 +238,7 @@ export const INFURA_PROVIDER_TYPES = [
NETWORK_TYPES.MAINNET,
NETWORK_TYPES.GOERLI,
NETWORK_TYPES.SEPOLIA,
];
] as const;
export const TEST_CHAINS = [
CHAIN_IDS.GOERLI,

View File

@ -1535,7 +1535,18 @@
"origin": "tmashuang.github.io"
}
],
"desktopEnabled": false
"desktopEnabled": false,
"pendingApprovals": {
"testApprovalId": {
"id": "testApprovalId",
"time": 1528133319641,
"origin": "metamask",
"type": "transaction",
"requestData": { "txId": "testTransactionId" },
"requestState": { "test": "value" }
}
},
"pendingApprovalCount": 1
},
"send": {
"amountMode": "INPUT",

View File

@ -62,9 +62,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
);
// Verify transaction
const completedTx = await driver.findElement('.list-item__title');
const completedTxText = await completedTx.getText();
assert.equal(completedTxText, 'Send Token');
await driver.findElement({ text: 'Send TDC' });
},
);
});

View File

@ -119,16 +119,16 @@ describe('Create token, approve token and approve token without gas', function (
);
await driver.clickElement({
text: 'Verify contract details',
text: 'Verify third-party details',
css: '.token-allowance-container__verify-link',
});
const modalTitle = await driver.waitForSelector({
text: 'Contract details',
text: 'Third-party details',
tag: 'h5',
});
assert.equal(await modalTitle.getText(), 'Contract details');
assert.equal(await modalTitle.getText(), 'Third-party details');
await driver.clickElement({
text: 'Got it',

View File

@ -60,7 +60,7 @@ describe('Sign Typed Data V4 Signature Request', function () {
assert.equal(await origin.getText(), 'http://127.0.0.1:8080');
verifyContractDetailsButton.click();
await driver.findElement({ text: 'Contract details', tag: 'h5' });
await driver.findElement({ text: 'Third-party details', tag: 'h5' });
await driver.findElement('[data-testid="recipient"]');
await driver.clickElement({ text: 'Got it', tag: 'button' });
@ -142,7 +142,7 @@ describe('Sign Typed Data V3 Signature Request', function () {
assert.equal(await origin.getText(), 'http://127.0.0.1:8080');
verifyContractDetailsButton.click();
await driver.findElement({ text: 'Contract details', tag: 'h5' });
await driver.findElement({ text: 'Third-party details', tag: 'h5' });
await driver.findElement('[data-testid="recipient"]');
await driver.clickElement({ text: 'Got it', tag: 'button' });

50
types/eth-query.d.ts vendored Normal file
View File

@ -0,0 +1,50 @@
declare module 'eth-query' {
// What it says on the tin. We omit `null` because confusingly, this is used
// for a successful response to indicate a lack of an error.
type EverythingButNull =
| string
| number
| boolean
| object
| symbol
| undefined;
type ProviderSendAsyncResponse<Result> = {
error?: { message: string };
result?: Result;
};
type ProviderSendAsyncCallback<Result> = (
error: unknown,
response: ProviderSendAsyncResponse<Result>,
) => void;
type Provider = {
sendAsync<Params, Result>(
payload: SendAsyncPayload<Params>,
callback: ProviderSendAsyncCallback<Result>,
): void;
};
type SendAsyncPayload<Params> = {
id: number;
jsonrpc: '2.0';
method: string;
params: Params;
};
type SendAsyncCallback<Result> = (
...args:
| [error: EverythingButNull, result: undefined]
| [error: null, result: Result]
) => void;
export default class EthQuery {
constructor(provider: Provider);
sendAsync<Params, Result>(
opts: Partial<SendAsyncPayload<Params>>,
callback: SendAsyncCallback<Result>,
): void;
}
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Provider } from 'react-redux';
import { object } from '@storybook/addon-knobs';
import { panel, text, heading, divider, copyable } from '@metamask/snaps-ui';
import configureStore from '../../../../store/store';
import testData from '../../../../../.storybook/test-data';
@ -10,8 +9,13 @@ const store = configureStore(testData);
export default {
title: 'Components/App/SnapUIRenderer',
component: SnapUIRenderer,
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
argTypes: {
data: {
control: 'object',
},
},
};
const DATA = panel([
@ -22,13 +26,18 @@ const DATA = panel([
copyable('Text you can copy'),
]);
export const DefaultStory = () => (
<SnapUIRenderer
snapId="local:http://localhost:8080/"
data={object('data', DATA)}
/>
export const DefaultStory = (args) => (
<SnapUIRenderer snapId="local:http://localhost:8080/" data={args.data} />
);
export const ErrorStory = () => (
<SnapUIRenderer snapId="local:http://localhost:8080/" data="foo" />
DefaultStory.args = {
data: DATA,
};
export const ErrorStory = (args) => (
<SnapUIRenderer snapId="local:http://localhost:8080/" data={args.data} />
);
ErrorStory.args = {
data: 'foo',
};

View File

@ -170,8 +170,9 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className="hold-to-reveal-button__button-hold"
textProps={{ display: DISPLAY.FLEX, alignItems: AlignItems.center }}
>
<Box className="hold-to-reveal-button__icon-container">
<Box className="hold-to-reveal-button__icon-container" marginRight={2}>
{renderPreCompleteContent()}
{renderPostCompleteContent()}
</Box>

View File

@ -224,7 +224,7 @@ exports[`Signature Request Component render should match snapshot when we are us
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h7 typography--weight-normal typography--style-normal typography--color-primary-default"
>
Verify contract details
Verify third-party details
</h6>
</a>
</div>
@ -999,7 +999,7 @@ exports[`Signature Request Component render should match snapshot when we want t
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h7 typography--weight-normal typography--style-normal typography--color-primary-default"
>
Verify contract details
Verify third-party details
</h6>
</a>
</div>

View File

@ -152,7 +152,7 @@ export function useTransactionDisplayData(transactionGroup) {
async function getAndSetAssetDetails() {
if (isTokenCategory && !token) {
const assetDetails = await getAssetDetails(
recipientAddress,
to,
senderAddress,
initialTransaction?.txParams?.data,
knownNfts,
@ -168,6 +168,7 @@ export function useTransactionDisplayData(transactionGroup) {
senderAddress,
initialTransaction?.txParams?.data,
knownNfts,
to,
]);
if (currentAssetDetails) {
token = {

View File

@ -57,7 +57,7 @@ exports[`ConfirmApproveContent Component should render Confirm approve page corr
role="button"
tabindex="0"
>
Verify contract details
Verify third-party details
</a>
</div>
<div
@ -244,7 +244,7 @@ exports[`ConfirmApproveContent Component should render Confirm approve page corr
role="button"
tabindex="0"
>
Verify contract details
Verify third-party details
</a>
</div>
<div
@ -431,7 +431,7 @@ exports[`ConfirmApproveContent Component should render Confirm approve page corr
role="button"
tabindex="0"
>
Verify contract details
Verify third-party details
</a>
</div>
<div
@ -618,7 +618,7 @@ exports[`ConfirmApproveContent Component should render Confirm approve page corr
role="button"
tabindex="0"
>
Verify contract details
Verify third-party details
</a>
</div>
<div

View File

@ -56,7 +56,7 @@ describe('ConfirmApproveContent Component', () => {
'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.',
),
).toBeInTheDocument();
expect(queryByText('Verify contract details')).toBeInTheDocument();
expect(queryByText('Verify third-party details')).toBeInTheDocument();
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
@ -119,7 +119,7 @@ describe('ConfirmApproveContent Component', () => {
'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.',
),
).toBeInTheDocument();
expect(queryByText('Verify contract details')).toBeInTheDocument();
expect(queryByText('Verify third-party details')).toBeInTheDocument();
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
@ -181,7 +181,7 @@ describe('ConfirmApproveContent Component', () => {
'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.',
),
).toBeInTheDocument();
expect(queryByText('Verify contract details')).toBeInTheDocument();
expect(queryByText('Verify third-party details')).toBeInTheDocument();
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
@ -239,7 +239,7 @@ describe('ConfirmApproveContent Component', () => {
'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.',
),
).toBeInTheDocument();
expect(queryByText('Verify contract details')).toBeInTheDocument();
expect(queryByText('Verify third-party details')).toBeInTheDocument();
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',

View File

@ -223,7 +223,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h7 typography--weight-normal typography--style-normal typography--color-primary-default"
>
Verify contract details
Verify third-party details
</h6>
</a>
</div>

View File

@ -5,10 +5,10 @@ import UrlIcon from '../../../components/ui/url-icon';
import Popover from '../../../components/ui/popover';
import Button from '../../../components/ui/button';
import Box from '../../../components/ui/box';
import Typography from '../../../components/ui/typography';
import { Text } from '../../../components/component-library';
import ActionableMessage from '../../../components/ui/actionable-message/actionable-message';
import {
TypographyVariant,
TextVariant,
FONT_WEIGHT,
AlignItems,
DISPLAY,
@ -62,21 +62,26 @@ export default function ImportToken({
fallbackClassName="import-token__token-icon"
name={tokenForImport.symbol}
/>
<Typography
ariant={TypographyVariant.H4}
<Text
variant={TextVariant.headingSm}
as="h4"
fontWeight={FONT_WEIGHT.BOLD}
boxProps={{ marginTop: 2, marginBottom: 3 }}
marginTop={2}
marginBottom={3}
>
{tokenForImport.name || ''}
</Typography>
<Typography variant={TypographyVariant.H6}>{t('contract')}:</Typography>
<Typography
</Text>
<Text variant={TextVariant.bodySm} as="h6">
{t('contract')}:
</Text>
<Text
variant={TextVariant.bodySm}
className="import-token__contract-address"
variant={TypographyVariant.H7}
boxProps={{ marginBottom: 6 }}
as="h6"
marginBottom={6}
>
{tokenForImport.address || ''}
</Typography>
</Text>
</Box>
</Popover>
);

View File

@ -14,6 +14,7 @@
border-radius: 8px;
background-color: var(--color-background-alternative);
padding: 5px 10px;
font-size: 0.75rem;
}
&__token-icon {

View File

@ -16,8 +16,8 @@
a.token-allowance-container__verify-link {
width: fit-content;
margin-inline-start: 96px;
margin-inline-end: 96px;
margin-inline-start: auto;
margin-inline-end: auto;
padding: 0;
}

View File

@ -237,16 +237,16 @@ describe('TokenAllowancePage', () => {
expect(getByText('Set a spending cap for your')).toBeInTheDocument();
});
it('should click Verify contract details and show popup Contract details, then close popup', () => {
it('should click Verify third-party details and show popup Third-party details, then close popup', () => {
const { getByText } = renderWithProvider(
<TokenAllowance {...props} />,
store,
);
const verifyContractDetails = getByText('Verify contract details');
fireEvent.click(verifyContractDetails);
const verifyThirdPartyDetails = getByText('Verify third-party details');
fireEvent.click(verifyThirdPartyDetails);
expect(getByText('Contract details')).toBeInTheDocument();
expect(getByText('Third-party details')).toBeInTheDocument();
const gotIt = getByText('Got it');
fireEvent.click(gotIt);

View File

@ -484,21 +484,14 @@ export function getCurrentCurrency(state) {
export function getTotalUnapprovedCount(state) {
const {
unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0,
unapprovedDecryptMsgCount = 0,
unapprovedEncryptionPublicKeyMsgCount = 0,
unapprovedTypedMessagesCount = 0,
pendingApprovalCount = 0,
} = state.metamask;
return (
unapprovedMsgCount +
unapprovedPersonalMsgCount +
unapprovedDecryptMsgCount +
unapprovedEncryptionPublicKeyMsgCount +
unapprovedTypedMessagesCount +
getUnapprovedTxCount(state) +
pendingApprovalCount +
getSuggestedAssetCount(state)
);

View File

@ -3984,13 +3984,13 @@ __metadata:
languageName: node
linkType: hard
"@metamask/gas-fee-controller@npm:^1.0.0":
version: 1.0.0
resolution: "@metamask/gas-fee-controller@npm:1.0.0"
"@metamask/gas-fee-controller@npm:^3.0.0":
version: 3.0.0
resolution: "@metamask/gas-fee-controller@npm:3.0.0"
dependencies:
"@metamask/base-controller": ~1.0.0
"@metamask/controller-utils": ~1.0.0
"@metamask/network-controller": ~1.0.0
"@metamask/base-controller": ^1.1.2
"@metamask/controller-utils": ^2.0.0
"@metamask/network-controller": ^3.0.0
"@types/uuid": ^8.3.0
babel-runtime: ^6.26.0
eth-query: ^2.1.2
@ -3998,7 +3998,9 @@ __metadata:
ethjs-unit: ^0.1.6
immer: ^9.0.6
uuid: ^8.3.2
checksum: fef5255532a6cd5325ddfbbfec11140e6629c011a8cc6b126672ef7a6e93a327d059935cdc6fc7089562f3277fb70541b5ea54cd31c0e5b350ceebbe73d5d59f
peerDependencies:
"@metamask/network-controller": ^3.0.0
checksum: 8cdd43a265094dd5e41f0094c278cde351d290446711e6b39de26f842faa993c050e5506cafe8d1c2fb0c4ee3f0f97c5af5fa6528de10e76d071b56fb9673da8
languageName: node
linkType: hard
@ -4074,6 +4076,22 @@ __metadata:
languageName: node
linkType: hard
"@metamask/network-controller@npm:^3.0.0":
version: 3.0.0
resolution: "@metamask/network-controller@npm:3.0.0"
dependencies:
"@metamask/base-controller": ^1.1.2
"@metamask/controller-utils": ^2.0.0
async-mutex: ^0.2.6
babel-runtime: ^6.26.0
eth-json-rpc-infura: ^5.1.0
eth-query: ^2.1.2
immer: ^9.0.6
web3-provider-engine: ^16.0.3
checksum: 3ae56a252c11dbd6dc843f9db8b30768d2475afd499c99bdccdc850517031b447bab9ca4f6647da7e64c7a0efd61d029f59a89e4ec702e34a99733dd8e7f93ff
languageName: node
linkType: hard
"@metamask/network-controller@npm:^4.0.0":
version: 4.0.0
resolution: "@metamask/network-controller@npm:4.0.0"
@ -4090,22 +4108,6 @@ __metadata:
languageName: node
linkType: hard
"@metamask/network-controller@npm:~1.0.0":
version: 1.0.0
resolution: "@metamask/network-controller@npm:1.0.0"
dependencies:
"@metamask/base-controller": ~1.0.0
"@metamask/controller-utils": ~1.0.0
async-mutex: ^0.2.6
babel-runtime: ^6.26.0
eth-json-rpc-infura: ^5.1.0
eth-query: ^2.1.2
immer: ^9.0.6
web3-provider-engine: ^16.0.3
checksum: a138943fecc27630e6fe392b9d237405e61b55e17b9dcfc7c434ccc59582fc775aec54e765c2e98f2b1579f760c7d163156450184172128079ce3c4d8e4bc725
languageName: node
linkType: hard
"@metamask/notification-controller@npm:^1.0.0":
version: 1.0.0
resolution: "@metamask/notification-controller@npm:1.0.0"
@ -4150,13 +4152,13 @@ __metadata:
languageName: node
linkType: hard
"@metamask/obs-store@npm:^8.0.0":
version: 8.0.0
resolution: "@metamask/obs-store@npm:8.0.0"
"@metamask/obs-store@npm:^8.1.0":
version: 8.1.0
resolution: "@metamask/obs-store@npm:8.1.0"
dependencies:
"@metamask/safe-event-emitter": ^2.0.0
through2: ^2.0.3
checksum: 232362e65a3563f0bd3299cec48f5adb37e68d4f066b7de90f2b044480d3b16c2d918c12d672c825e1d9b55344ae818fb8494d91129e4613555097653b9bb887
checksum: 92356067fa3517526d656f2f0bdfbc4d39f65e27fb30d84240cfc9c1aa9cd5d743498952df18ed8efbb8887b6cc1bc1fab37bde3fb0fc059539e0dfcc67ff86f
languageName: node
linkType: hard
@ -24299,14 +24301,14 @@ __metadata:
"@metamask/eth-token-tracker": ^4.0.0
"@metamask/etherscan-link": ^2.2.0
"@metamask/forwarder": ^1.1.0
"@metamask/gas-fee-controller": ^1.0.0
"@metamask/gas-fee-controller": ^3.0.0
"@metamask/jazzicon": ^2.0.0
"@metamask/key-tree": ^7.0.0
"@metamask/logo": ^3.1.1
"@metamask/message-manager": ^2.1.0
"@metamask/metamask-eth-abis": ^3.0.0
"@metamask/notification-controller": ^1.0.0
"@metamask/obs-store": ^8.0.0
"@metamask/obs-store": ^8.1.0
"@metamask/permission-controller": ^3.1.0
"@metamask/phishing-controller": ^2.0.0
"@metamask/phishing-warning": ^2.1.0
@ -34616,14 +34618,14 @@ __metadata:
linkType: hard
"vm2@npm:^3.9.3":
version: 3.9.11
resolution: "vm2@npm:3.9.11"
version: 3.9.15
resolution: "vm2@npm:3.9.15"
dependencies:
acorn: ^8.7.0
acorn-walk: ^8.2.0
bin:
vm2: bin/vm2
checksum: aab39e6e4b59146d24abacd79f490e854a6e058a8b23d93d2be5aca7720778e2605d2cc028ccc4a5f50d3d91b0c38be9a6247a80d2da1a6de09425cc437770b4
checksum: 1df70d5a88173651c0062901aba67e5edfeeb3f699fe6c305f5efb6a5a7391e5724cbf98a6516600b65016c6824dc07cc79947ea4222f8537ae1d9ce0b730ad7
languageName: node
linkType: hard