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

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

This commit is contained in:
Matthias Kretschmann 2023-04-19 00:50:38 +01:00
commit 522f2a05ac
Signed by: m
GPG Key ID: 606EEEF3C479A91F
76 changed files with 1982 additions and 663 deletions

View File

@ -54,7 +54,7 @@ module.exports = {
os: false, os: false,
path: false, path: false,
stream: require.resolve('stream-browserify'), stream: require.resolve('stream-browserify'),
_stream_transform: false, _stream_transform: require.resolve('readable-stream/lib/_stream_transform.js'),
}; };
config.module.strictExportPresence = true; config.module.strictExportPresence = true;
config.module.rules.push({ config.module.rules.push({

View File

@ -1192,7 +1192,7 @@ describe('NetworkController', () => {
}); });
expect(oldChainIdResult).toBe('0x5'); expect(oldChainIdResult).toBe('0x5');
controller.setActiveNetwork('testNetworkConfigurationId'); await controller.setActiveNetwork('testNetworkConfigurationId');
const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( const promisifiedSendAsync2 = promisify(provider.sendAsync).bind(
provider, provider,
); );
@ -2431,15 +2431,9 @@ describe('NetworkController', () => {
}, },
}, },
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setActiveNetwork(
controller, 'testNetworkConfigurationId',
propertyPath: ['networkStatus'], );
operation: () => {
controller.setActiveNetwork(
'testNetworkConfigurationId',
);
},
});
}, },
}, },
], ],
@ -2506,15 +2500,9 @@ describe('NetworkController', () => {
network1.mockEssentialRpcCalls({ network1.mockEssentialRpcCalls({
eth_getBlockByNumber: { eth_getBlockByNumber: {
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setActiveNetwork(
controller, 'testNetworkConfigurationId',
propertyPath: ['networkStatus'], );
operation: () => {
controller.setActiveNetwork(
'testNetworkConfigurationId',
);
},
});
}, },
}, },
net_version: { net_version: {
@ -2578,15 +2566,9 @@ describe('NetworkController', () => {
latestBlock: POST_1559_BLOCK, latestBlock: POST_1559_BLOCK,
eth_getBlockByNumber: { eth_getBlockByNumber: {
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setActiveNetwork(
controller, 'testNetworkConfigurationId',
propertyPath: ['networkStatus'], );
operation: () => {
controller.setActiveNetwork(
'testNetworkConfigurationId',
);
},
});
}, },
}, },
}); });
@ -4032,9 +4014,9 @@ describe('NetworkController', () => {
async ({ controller, network }) => { async ({ controller, network }) => {
network.mockEssentialRpcCalls(); network.mockEssentialRpcCalls();
expect(() => await expect(() =>
controller.setActiveNetwork('invalid-network-configuration-id'), controller.setActiveNetwork('invalid-network-configuration-id'),
).toThrow( ).rejects.toThrow(
new Error( new Error(
'networkConfigurationId invalid-network-configuration-id does not match a configured networkConfiguration', 'networkConfigurationId invalid-network-configuration-id does not match a configured networkConfiguration',
), ),
@ -4075,7 +4057,7 @@ describe('NetworkController', () => {
}); });
network.mockEssentialRpcCalls(); network.mockEssentialRpcCalls();
controller.setActiveNetwork('testNetworkConfigurationId1'); await controller.setActiveNetwork('testNetworkConfigurationId1');
expect(controller.store.getState().provider).toStrictEqual({ expect(controller.store.getState().provider).toStrictEqual({
type: 'rpc', type: 'rpc',
@ -4144,6 +4126,8 @@ describe('NetworkController', () => {
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkWillChange, eventType: NetworkControllerEventType.NetworkWillChange,
operation: () => { operation: () => {
// Intentionally not awaited because we're checking state
// partway through the operation
controller.setActiveNetwork('testNetworkConfigurationId2'); controller.setActiveNetwork('testNetworkConfigurationId2');
}, },
beforeResolving: () => { beforeResolving: () => {
@ -4208,6 +4192,8 @@ describe('NetworkController', () => {
// before networkDidChange // before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we're checking state
// partway through the operation.
controller.setActiveNetwork('testNetworkConfigurationId1'); controller.setActiveNetwork('testNetworkConfigurationId1');
}, },
}); });
@ -4267,6 +4253,8 @@ describe('NetworkController', () => {
// before networkDidChange // before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we're checking state
// partway through the operation
controller.setActiveNetwork('testNetworkConfigurationId2'); controller.setActiveNetwork('testNetworkConfigurationId2');
}, },
}); });
@ -4308,7 +4296,7 @@ describe('NetworkController', () => {
}, },
}); });
controller.setActiveNetwork('testNetworkConfigurationId'); await controller.setActiveNetwork('testNetworkConfigurationId');
const { provider } = controller.getProviderAndBlockTracker(); const { provider } = controller.getProviderAndBlockTracker();
assert(provider, 'Provider is somehow unset'); assert(provider, 'Provider is somehow unset');
@ -4356,7 +4344,7 @@ describe('NetworkController', () => {
const { provider: providerBefore } = const { provider: providerBefore } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
controller.setActiveNetwork('testNetworkConfigurationId'); await controller.setActiveNetwork('testNetworkConfigurationId');
const { provider: providerAfter } = const { provider: providerAfter } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
@ -4392,8 +4380,8 @@ describe('NetworkController', () => {
const networkDidChange = await waitForPublishedEvents({ const networkDidChange = await waitForPublishedEvents({
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkDidChange, eventType: NetworkControllerEventType.NetworkDidChange,
operation: () => { operation: async () => {
controller.setActiveNetwork('testNetworkConfigurationId'); await controller.setActiveNetwork('testNetworkConfigurationId');
}, },
}); });
@ -4429,8 +4417,8 @@ describe('NetworkController', () => {
const infuraIsUnblocked = await waitForPublishedEvents({ const infuraIsUnblocked = await waitForPublishedEvents({
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.InfuraIsUnblocked, eventType: NetworkControllerEventType.InfuraIsUnblocked,
operation: () => { operation: async () => {
controller.setActiveNetwork('testNetworkConfigurationId'); await controller.setActiveNetwork('testNetworkConfigurationId');
}, },
}); });
@ -4462,13 +4450,7 @@ describe('NetworkController', () => {
response: SUCCESSFUL_NET_VERSION_RESPONSE, response: SUCCESSFUL_NET_VERSION_RESPONSE,
}, },
}); });
await waitForStateChanges({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
expect(controller.store.getState().networkStatus).toBe('available'); expect(controller.store.getState().networkStatus).toBe('available');
}, },
@ -4501,16 +4483,7 @@ describe('NetworkController', () => {
latestBlock: POST_1559_BLOCK, latestBlock: POST_1559_BLOCK,
}); });
await waitForStateChanges({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
propertyPath: ['networkDetails'],
// setActiveNetwork clears networkDetails first, and then updates it
// to what we expect it to be
count: 2,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
@ -5657,12 +5630,7 @@ describe('NetworkController', () => {
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId2');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId2');
},
});
expect(controller.store.getState().provider).toStrictEqual({ expect(controller.store.getState().provider).toStrictEqual({
type: 'rpc', type: 'rpc',
rpcUrl: 'https://mock-rpc-url-2', rpcUrl: 'https://mock-rpc-url-2',
@ -5730,12 +5698,7 @@ describe('NetworkController', () => {
}); });
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
controller, controller,
@ -5787,12 +5750,7 @@ describe('NetworkController', () => {
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
expect(controller.store.getState().networkStatus).toBe( expect(controller.store.getState().networkStatus).toBe(
'available', 'available',
); );
@ -5855,12 +5813,7 @@ describe('NetworkController', () => {
}); });
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
1559: true, 1559: true,
@ -5926,12 +5879,7 @@ describe('NetworkController', () => {
}); });
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
controller, controller,
@ -5986,12 +5934,7 @@ describe('NetworkController', () => {
}); });
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
const { provider: providerBefore } = const { provider: providerBefore } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
@ -6045,12 +5988,7 @@ describe('NetworkController', () => {
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
controller, controller,
@ -6103,12 +6041,7 @@ describe('NetworkController', () => {
response: BLOCKED_INFURA_RESPONSE, response: BLOCKED_INFURA_RESPONSE,
}, },
}); });
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
const promiseForNoInfuraIsUnblockedEvents = const promiseForNoInfuraIsUnblockedEvents =
waitForPublishedEvents({ waitForPublishedEvents({
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
@ -6173,13 +6106,7 @@ describe('NetworkController', () => {
}, },
}); });
await waitForStateChanges({ await controller.setActiveNetwork('currentNetworkConfiguration');
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setActiveNetwork('currentNetworkConfiguration');
},
});
expect(controller.store.getState().networkStatus).toBe( expect(controller.store.getState().networkStatus).toBe(
'unavailable', 'unavailable',
); );
@ -6228,12 +6155,7 @@ describe('NetworkController', () => {
latestBlock: POST_1559_BLOCK, latestBlock: POST_1559_BLOCK,
}); });
await waitForLookupNetworkToComplete({ await controller.setActiveNetwork('testNetworkConfigurationId');
controller,
operation: () => {
controller.setActiveNetwork('testNetworkConfigurationId');
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
1559: false, 1559: false,

View File

@ -704,7 +704,7 @@ export class NetworkController extends EventEmitter {
* @returns The URL of the RPC endpoint representing the newly switched * @returns The URL of the RPC endpoint representing the newly switched
* network. * network.
*/ */
setActiveNetwork(networkConfigurationId: NetworkConfigurationId): string { async setActiveNetwork(networkConfigurationId: NetworkConfigurationId) {
const targetNetwork = const targetNetwork =
this.store.getState().networkConfigurations[networkConfigurationId]; this.store.getState().networkConfigurations[networkConfigurationId];
@ -714,7 +714,7 @@ export class NetworkController extends EventEmitter {
); );
} }
this.#setProviderConfig({ await this.#setProviderConfig({
type: NETWORK_TYPES.RPC, type: NETWORK_TYPES.RPC,
...targetNetwork, ...targetNetwork,
}); });

View File

@ -52,6 +52,7 @@ const messageMock = {
const coreMessageMock = { const coreMessageMock = {
...messageMock, ...messageMock,
messageParams: messageParamsMock, messageParams: messageParamsMock,
securityProviderResponse: securityProviderResponseMock,
}; };
const stateMessageMock = { const stateMessageMock = {

View File

@ -21,6 +21,7 @@ import {
AbstractMessageParams, AbstractMessageParams,
AbstractMessageParamsMetamask, AbstractMessageParamsMetamask,
OriginalRequest, OriginalRequest,
SecurityProviderRequest,
} from '@metamask/message-manager/dist/AbstractMessageManager'; } from '@metamask/message-manager/dist/AbstractMessageManager';
import { import {
BaseControllerV2, BaseControllerV2,
@ -63,9 +64,10 @@ export type CoreMessage = AbstractMessage & {
messageParams: AbstractMessageParams; messageParams: AbstractMessageParams;
}; };
export type StateMessage = Required<AbstractMessage> & { export type StateMessage = Required<
Omit<AbstractMessage, 'securityProviderResponse'>
> & {
msgParams: Required<AbstractMessageParams>; msgParams: Required<AbstractMessageParams>;
securityProviderResponse: any;
}; };
export type SignControllerState = { export type SignControllerState = {
@ -107,10 +109,7 @@ export type SignControllerOptions = {
preferencesController: PreferencesController; preferencesController: PreferencesController;
getState: () => any; getState: () => any;
metricsEvent: (payload: any, options?: any) => void; metricsEvent: (payload: any, options?: any) => void;
securityProviderRequest: ( securityProviderRequest: SecurityProviderRequest;
requestData: any,
methodName: string,
) => Promise<any>;
}; };
/** /**
@ -143,11 +142,6 @@ export default class SignController extends BaseControllerV2<
private _metricsEvent: (payload: any, options?: any) => void; private _metricsEvent: (payload: any, options?: any) => void;
private _securityProviderRequest: (
requestData: any,
methodName: string,
) => Promise<any>;
/** /**
* Construct a Sign controller. * Construct a Sign controller.
* *
@ -178,12 +172,23 @@ export default class SignController extends BaseControllerV2<
this._preferencesController = preferencesController; this._preferencesController = preferencesController;
this._getState = getState; this._getState = getState;
this._metricsEvent = metricsEvent; this._metricsEvent = metricsEvent;
this._securityProviderRequest = securityProviderRequest;
this.hub = new EventEmitter(); this.hub = new EventEmitter();
this._messageManager = new MessageManager(); this._messageManager = new MessageManager(
this._personalMessageManager = new PersonalMessageManager(); undefined,
this._typedMessageManager = new TypedMessageManager(); undefined,
securityProviderRequest,
);
this._personalMessageManager = new PersonalMessageManager(
undefined,
undefined,
securityProviderRequest,
);
this._typedMessageManager = new TypedMessageManager(
undefined,
undefined,
securityProviderRequest,
);
this._messageManagers = [ this._messageManagers = [
this._messageManager, this._messageManager,
@ -412,27 +417,30 @@ export default class SignController extends BaseControllerV2<
* Used to cancel a message submitted via eth_sign. * Used to cancel a message submitted via eth_sign.
* *
* @param msgId - The id of the message to cancel. * @param msgId - The id of the message to cancel.
* @returns A full state update.
*/ */
cancelMessage(msgId: string) { cancelMessage(msgId: string) {
this._cancelAbstractMessage(this._messageManager, msgId); return this._cancelAbstractMessage(this._messageManager, msgId);
} }
/** /**
* Used to cancel a personal_sign type message. * Used to cancel a personal_sign type message.
* *
* @param msgId - The ID of the message to cancel. * @param msgId - The ID of the message to cancel.
* @returns A full state update.
*/ */
cancelPersonalMessage(msgId: string) { cancelPersonalMessage(msgId: string) {
this._cancelAbstractMessage(this._personalMessageManager, msgId); return this._cancelAbstractMessage(this._personalMessageManager, msgId);
} }
/** /**
* Used to cancel a eth_signTypedData type message. * Used to cancel a eth_signTypedData type message.
* *
* @param msgId - The ID of the message to cancel. * @param msgId - The ID of the message to cancel.
* @returns A full state update.
*/ */
cancelTypedMessage(msgId: string) { cancelTypedMessage(msgId: string) {
this._cancelAbstractMessage(this._typedMessageManager, msgId); return this._cancelAbstractMessage(this._typedMessageManager, msgId);
} }
/** /**
@ -586,15 +594,7 @@ export default class SignController extends BaseControllerV2<
origin: messageParams.origin as string, origin: messageParams.origin as string,
}, },
}; };
return stateMessage;
const messageId = coreMessage.id;
const existingMessage = this._getMessage(messageId);
const securityProviderResponse = existingMessage
? existingMessage.securityProviderResponse
: await this._securityProviderRequest(stateMessage, stateMessage.type);
return { ...stateMessage, securityProviderResponse };
} }
private _normalizeMsgData(data: string) { private _normalizeMsgData(data: string) {

View File

@ -304,6 +304,7 @@ export default class SwapsController {
Object.values(newQuotes).map(async (quote) => { Object.values(newQuotes).map(async (quote) => {
if (quote.trade) { if (quote.trade) {
const multiLayerL1TradeFeeTotal = await fetchEstimatedL1Fee( const multiLayerL1TradeFeeTotal = await fetchEstimatedL1Fee(
chainId,
{ {
txParams: quote.trade, txParams: quote.trade,
chainId, chainId,

View File

@ -156,12 +156,12 @@ const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => {
describe('SwapsController', function () { describe('SwapsController', function () {
let provider; let provider;
const getSwapsController = () => { const getSwapsController = (_provider = provider) => {
return new SwapsController({ return new SwapsController({
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
networkController: getMockNetworkController(), networkController: getMockNetworkController(),
onNetworkDidChange: sinon.stub(), onNetworkDidChange: sinon.stub(),
provider, provider: _provider,
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
getTokenRatesState: MOCK_TOKEN_RATES_STORE, getTokenRatesState: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
@ -722,6 +722,72 @@ describe('SwapsController', function () {
); );
}); });
it('calls returns the correct quotes on the optimism chain', async function () {
fetchTradesInfoStub.resetHistory();
const OPTIMISM_MOCK_FETCH_METADATA = {
...MOCK_FETCH_METADATA,
chainId: CHAIN_IDS.OPTIMISM,
};
const optimismProviderResultStub = {
// 1 gwei
eth_gasPrice: '0x0de0b6b3a7640000',
// by default, all accounts are external accounts (not contracts)
eth_getCode: '0x',
eth_call:
'0x000000000000000000000000000000000000000000000000000103c18816d4e8',
};
const optimismProvider = createTestProviderTools({
scaffold: optimismProviderResultStub,
networkId: 10,
chainId: 10,
}).provider;
swapsController = getSwapsController(optimismProvider);
fetchTradesInfoStub.resolves(getMockQuotes());
// Make it so approval is not required
sandbox
.stub(swapsController, '_getERC20Allowance')
.resolves(BigNumber.from(1));
const [newQuotes] = await swapsController.fetchAndSetQuotes(
MOCK_FETCH_PARAMS,
OPTIMISM_MOCK_FETCH_METADATA,
);
assert.deepStrictEqual(newQuotes[TEST_AGG_ID_BEST], {
...getMockQuotes()[TEST_AGG_ID_BEST],
sourceTokenInfo: undefined,
destinationTokenInfo: {
symbol: 'FOO',
decimals: 18,
},
isBestQuote: true,
// TODO: find a way to calculate these values dynamically
gasEstimate: 2000000,
gasEstimateWithRefund: '0xb8cae',
savings: {
fee: '-0.061067',
metaMaskFee: '0.5050505050505050505',
performance: '6',
total: '5.4338824949494949495',
medianMetaMaskFee: '0.44444444444444444444',
},
ethFee: '0.113822',
multiLayerL1TradeFeeTotal: '0x0103c18816d4e8',
overallValueOfQuote: '49.886178',
metaMaskFeeInEth: '0.5050505050505050505',
ethValueOfTokens: '50',
});
assert.strictEqual(
fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS, {
...OPTIMISM_MOCK_FETCH_METADATA,
}),
true,
);
});
it('performs the allowance check', async function () { it('performs the allowance check', async function () {
fetchTradesInfoStub.resolves(getMockQuotes()); fetchTradesInfoStub.resolves(getMockQuotes());

View File

@ -1,65 +0,0 @@
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
const fetchWithTimeout = getFetchWithTimeout();
export async function securityProviderCheck(
requestData,
methodName,
chainId,
currentLocale,
) {
let dataToValidate;
if (methodName === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
dataToValidate = {
host_name: requestData.msgParams.origin,
rpc_method_name: methodName,
chain_id: chainId,
data: requestData.msgParams.data,
currentLocale,
};
} else if (
methodName === MESSAGE_TYPE.ETH_SIGN ||
methodName === MESSAGE_TYPE.PERSONAL_SIGN
) {
dataToValidate = {
host_name: requestData.msgParams.origin,
rpc_method_name: methodName,
chain_id: chainId,
data: {
signer_address: requestData.msgParams.from,
msg_to_sign: requestData.msgParams.data,
},
currentLocale,
};
} else {
dataToValidate = {
host_name: requestData.origin,
rpc_method_name: methodName,
chain_id: chainId,
data: {
from_address: requestData?.txParams?.from,
to_address: requestData?.txParams?.to,
gas: requestData?.txParams?.gas,
gasPrice: requestData?.txParams?.gasPrice,
value: requestData?.txParams?.value,
data: requestData?.txParams?.data,
},
currentLocale,
};
}
const response = await fetchWithTimeout(
'https://proxy.metafi.codefi.network/opensea/security/v1/validate',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(dataToValidate),
},
);
return await response.json();
}

View File

@ -0,0 +1,117 @@
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import {
RequestData,
securityProviderCheck,
} from './security-provider-helpers';
describe('securityProviderCheck', () => {
let fetchSpy: jest.SpyInstance;
beforeEach(() => {
// Spy on the global fetch function
fetchSpy = jest.spyOn(global, 'fetch');
fetchSpy.mockImplementation(async () => {
return new Response(JSON.stringify('result_mocked'), { status: 200 });
});
});
const paramsMock = {
origin: 'https://example.com',
data: 'some_data',
from: '0x',
};
// Utility function to handle different data properties based on methodName
const getExpectedData = (methodName: string, requestData: RequestData) => {
switch (methodName) {
case MESSAGE_TYPE.ETH_SIGN:
case MESSAGE_TYPE.PERSONAL_SIGN:
return {
signer_address: requestData.msgParams?.from,
msg_to_sign: requestData.msgParams?.data,
};
case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA:
return requestData.messageParams?.data;
default:
return {
from_address: requestData.txParams?.from,
to_address: requestData.txParams?.to,
gas: requestData.txParams?.gas,
gasPrice: requestData.txParams?.gasPrice,
value: requestData.txParams?.value,
data: requestData.txParams?.data,
};
}
};
test.each([
[MESSAGE_TYPE.ETH_SIGN_TYPED_DATA],
[MESSAGE_TYPE.ETH_SIGN],
[MESSAGE_TYPE.PERSONAL_SIGN],
['some_other_method'],
])(
'should call fetch with the correct parameters for %s',
async (methodName: string) => {
let requestData: RequestData;
switch (methodName) {
case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA:
requestData = {
origin: 'https://example.com',
messageParams: paramsMock,
};
break;
case MESSAGE_TYPE.ETH_SIGN:
case MESSAGE_TYPE.PERSONAL_SIGN:
requestData = {
origin: 'https://example.com',
msgParams: paramsMock,
};
break;
default:
requestData = {
origin: 'https://example.com',
txParams: {
from: '0x',
to: '0x',
gas: 'some_gas',
gasPrice: 'some_gasPrice',
value: 'some_value',
data: 'some_data',
},
};
}
const result = await securityProviderCheck(
requestData,
methodName,
'1',
'en',
);
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(fetchSpy).toHaveBeenCalledWith(
'https://proxy.metafi.codefi.network/opensea/security/v1/validate',
expect.objectContaining({
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
host_name:
methodName === 'some_other_method'
? requestData.origin
: requestData.msgParams?.origin ||
requestData.messageParams?.origin,
rpc_method_name: methodName,
chain_id: '1',
data: getExpectedData(methodName, requestData),
currentLocale: 'en',
}),
}),
);
expect(result).toEqual('result_mocked');
},
);
});

View File

@ -0,0 +1,92 @@
import { Json } from '@metamask/utils';
import { MessageParams } from '@metamask/message-manager';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
const fetchWithTimeout = getFetchWithTimeout();
export type TransactionRequestData = {
txParams: Record<string, unknown>;
messageParams?: never;
msgParams?: never;
};
export type MessageRequestData =
| {
msgParams: MessageParams;
txParams?: never;
messageParams?: never;
}
| {
messageParams: MessageParams;
msgParams?: never;
txParams?: never;
}
| TransactionRequestData;
export type RequestData = {
origin: string;
} & MessageRequestData;
export async function securityProviderCheck(
requestData: RequestData,
methodName: string,
chainId: string,
currentLocale: string,
): Promise<Record<string, Json>> {
let dataToValidate;
// Core message managers use messageParams but frontend uses msgParams with lots of references
const params = requestData.msgParams || requestData.messageParams;
if (methodName === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
dataToValidate = {
host_name: params?.origin,
rpc_method_name: methodName,
chain_id: chainId,
data: params?.data,
currentLocale,
};
} else if (
methodName === MESSAGE_TYPE.ETH_SIGN ||
methodName === MESSAGE_TYPE.PERSONAL_SIGN
) {
dataToValidate = {
host_name: params?.origin,
rpc_method_name: methodName,
chain_id: chainId,
data: {
signer_address: params?.from,
msg_to_sign: params?.data,
},
currentLocale,
};
} else {
dataToValidate = {
host_name: requestData.origin,
rpc_method_name: methodName,
chain_id: chainId,
data: {
from_address: requestData.txParams?.from,
to_address: requestData.txParams?.to,
gas: requestData.txParams?.gas,
gasPrice: requestData.txParams?.gasPrice,
value: requestData.txParams?.value,
data: requestData.txParams?.data,
},
currentLocale,
};
}
const response: Response = await fetchWithTimeout(
'https://proxy.metafi.codefi.network/opensea/security/v1/validate',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(dataToValidate),
},
);
return await response.json();
}

View File

@ -348,18 +348,29 @@ export default class MetamaskController extends EventEmitter {
{ {
onPreferencesStateChange: (listener) => onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener), this.preferencesController.store.subscribe(listener),
onNetworkStateChange: (cb) => { // This handler is misnamed, and is a known issue that will be resolved
this.networkController.store.subscribe((networkState) => { // by planned refactors. It should be onNetworkDidChange which happens
const modifiedNetworkState = { // AFTER the provider in the network controller is updated to reflect
...networkState, // the new state of the network controller. In #18041 we changed this
providerConfig: { // handler to be triggered by the change in the network state because
...networkState.provider, // that is what the handler name implies, but this triggers too soon
chainId: hexToDecimal(networkState.provider.chainId), // causing the provider of the AssetsContractController to trail the
}, // network provider by one update.
}; onNetworkStateChange: (cb) =>
return cb(modifiedNetworkState); networkControllerMessenger.subscribe(
}); NetworkControllerEventType.NetworkDidChange,
}, () => {
const networkState = this.networkController.store.getState();
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
},
),
}, },
{ {
provider: this.provider, provider: this.provider,

View File

@ -0,0 +1,13 @@
# Confirmation Pages Refactoring
The following pages document the ongoing refactoring efforts of confirmation pages. They describe the current (2023) code and proposed changes.
1. [Signature Request Pages](./signature-request/README.md)
2. [Confirmation Pages Routing](./confirmation-pages-routing/README.md)
3. [Confirmation Page Structure](./confirmation-page=structure/README.md)
4. [Confirmation State Management](./confirmation-state-management/README.md)
5. [Confirmation Backend Architecture](./confirmation-backend-architecture/README.md)

View File

@ -0,0 +1,32 @@
# Confirmation Background Architecture and Code Cleanup
## Current Implementation:
Current confirmation implementation in the background consists of following pieces:
1. `TransactionController` and utility, helper classes used by it:
`TransactionController` is very important piece in transaction processing. It is described [here](https://github.com/MetaMask/metamask-extension/tree/develop/app/scripts/controllers/transactions#transaction-controller). It consists of 4 important parts:
- `txStateManager`: responsible for the state of a transaction and storing the transaction
- `pendingTxTracker`: watching blocks for transactions to be include and emitting confirmed events
- `txGasUtil`: gas calculations and safety buffering
- `nonceTracker`: calculating nonces
2. `MessageManagers`:
There are 3 different message managers responsible for processing signature requests. These are detailed [here](https://github.com/MetaMask/metamask-extension/tree/develop/docs/refactoring/signature-request#proposed-refactoring).
3. `MetamaskController `:
`MetamaskController ` is responsible for gluing together the different pieces in transaction processing. It is responsible to inject dependencies in `TransactionController`, `MessageManagers`, handling different events, responses to DAPP requests, etc.
## Areas of Code Cleanup:
1. Migrating to `@metamask/transaction-controller`. `TransactionController` in extension repo should eventually get replaced by core repo [TransactionController](https://github.com/MetaMask/core/tree/main/packages/transaction-controller). This controller is maintained by core team and also used in Metamask Mobile App.
2. Migrating to `@metamask/message-manager`. Message Managers in extension repo should be deprecated in favour of core repo [MessageManagers](https://github.com/MetaMask/core/tree/main/packages/message-manager).
3. Cleanup Code in `MetamaskController`. [Metamaskcontroller](https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/metamask-controller.js) is where `TransactionController` and different `MessageManagers` are initialized. It is responsible for injecting required dependencies. Also, it is responsible for handling incoming DAPP requests and invoking appropriate methods in these background classes. Over the period of time lot of code that should have been part of `TransactionController` and `MessageManagers` has ended up in `MetamaskController`. We need to cleanup this code and move to the appropriate classes.
- Code [here](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L3097) to check if `eth_sign` is enabled in preferences and perform other validation on the incoming request should be part of [MessageManager](https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/lib/message-manager.js)
- Method to sign messages [signMessage](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L3158), [signPersonalMessage](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L3217), [signTypedMessage](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L3470) can be simplified by injecting `KeyringController` into `MessageManagers`.
- There are about 11 different methods to `add`, `approve`, `reject` different types of signature requests. These can probably be moved to a helper class, thus reducing lines of code from `MetamaskController `.
- This [code](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L959) can better be placed in `TransactionController`.
- A lot of other methods in `MetamaskController` which are related to `TransactionController` and the state of `TransactionController` can be moved into `TransactionController` itself like [method1](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L1179), [method2](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L3570), [method3](https://github.com/MetaMask/metamask-extension/blob/bc19856d5d9ad1831e1722c84fe6161bed7a0a5a/app/scripts/metamask-controller.js#L4349), etc.
### Using ApprovalController for Confirmations
[ApprovalController](https://github.com/MetaMask/core/tree/main/packages/approval-controller) is written as a helper to `PermissionController`. Its role is to manage requests that require user approval. It can also be used in confirmation code to launch UI. Thus the use of `showUserConfirmation` function in `MetamaskController ` can be removed.
But `ApprovalController` will need some changes to be able to use it for confirmations, for example, it does not support multiple parallel requests from the same origin.

View File

@ -0,0 +1,67 @@
# Confirmation Pages Structure
### Current Implementation
Currently we have following confirmation pages mapping to confirmation routes:
1. `pages/confirm-deploy-contract`
2. `pages/confirm-send-ether`
3. `pages/confirm-send-token`
4. `pages/confirm-approve`
5. `pages/confirm-token-transaction-base`
6. `pages/confirm-contract-interaction`
![Confirmation Pages structure](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/confirmation-page-structure/current.png)
`confirm-page-container` component helps to define a structure for confirmation pages it includes:
1. `header`
2. `content` - transaction details and tabs for hexdata and insights if available
3. `footer`
4. `warnings`
`confirm-transaction-base` component is responsible for checking transaction details and pass required details like `gas-details`, `hex-data`, etc and passing over to `confirm-page-container`.
Other confirmation components listed above map to different types of transactions and are responsible for passing over to `confirm-transaction-base` values / components specific to their transaction type. For instance, `confirm-deploy-contract` passes data section to `confirm-transaction-base`.
## Areas of Refactoring:
1. ### [confirm-transaction-base](https://github.com/MetaMask/metamask-extension/tree/develop/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js) cleanup:
The `confirm-transaction-base` component is huge 1200 lines component taking care of lot of complexity. We need to break it down into smaller components and move logic to hooks or utility classes. Layout related part can be moved to `confirm-page-container`.
- Extract out code to render data into separate component from [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L641).
- Extract out component to render hex data from [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L675).
- Extract out code to render title [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L894) into separate component.
- Extract out code to render sub-title [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L921). It should return null if hideSubtitle is true.
- Extract out code to render gas details [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L444), this code can be used [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js#L171) and [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/send/gas-display/gas-display.js#L161) also.
- Extract renderDetails from [here](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L309) into a separate component. Function `setUserAcknowledgedGasMissing` can also be moved to it.
- Code to get error key [getErrorKey](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L230) can be moved to a util function.
- As new component for gas selection popups is created this code [handleEditGas, handleCloseEditGas](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L276) can be moved to it.
- Convert `confirm-transaction-base` into a functional components and extract out all of these functions into a hook - `handleEdit`, `handleCancelAll`, `handleCancel`, `handleSubmit`, `handleSetApprovalForAll`, etc.
2. ### [confirm-transaction-base-container](https://github.com/MetaMask/metamask-extension/tree/develop/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js) cleanup:
This container is doing much work to query and get required transaction related values from state and pass over to `confirm-transaction-base` component. As we refactor state we should get rid of this component.
- remove the use of `state.confirmTransaction` from the component
- create hook to get values derived from metamask state and active transaction.
State cleanup is detailed more in a separate document [here](https://github.com/MetaMask/metamask-extension/tree/develop/docs/confirmation-refactoring/confirmation-state-management).
3. ### [confirm-page-container](https://github.com/MetaMask/metamask-extension/tree/03ccc5366cf31c9fa0fedc2fac533ebc64e6f2b4/ui/components/app/confirm-page-container) cleanup:
As described we should continue to have `confirm-page-container` components taking care of layout. Also wherever possible more re-usable smaller layout components for different part of confirmation page like gas details, gas selection popover, etc should be added.
`confirm-page-container` defines a layout which is used by most comfirmation pages, but some pages like new token allowance implementation for `ERC20` differ from this layout. We will be able to use more and more of these re-usable components for other confirmation pages layouts also.
- Move code specific to transaction to their confirmation component, for instance code related to `ApproveForAll` should be moved to `/pages/confirm-approve`, code related to `hideTitle` can be moved to `/pages/confirm-contract-interaction` etc.
- All header related code [here](https://github.com/MetaMask/metamask-extension/blob/03ccc5366cf31c9fa0fedc2fac533ebc64e6f2b4/ui/components/app/confirm-page-container/confirm-page-container.component.js#L191) should be moved to [confirm-page-container-header](https://github.com/MetaMask/metamask-extension/tree/03ccc5366cf31c9fa0fedc2fac533ebc64e6f2b4/ui/components/app/confirm-page-container/confirm-page-container-header)
- All warnings related code can be moved to a new child component.
- Props passing to `confirm-page-component` should be reduced. A lot of passed props like `origin`, `supportEIP1559` can be obtained directly using selectors. Props passing from `confirm-page-container` down to its child components should also be reduced.
4. ### Edit gas popovers:
There are 2 different versions popovers for gas editing:
- Legacy gas popover - [component](https://github.com/MetaMask/metamask-extension/tree/develop/ui/components/app/edit-gas-popover)
- EIP-1559 V2 gas popover - [component1](https://github.com/MetaMask/metamask-extension/tree/develop/ui/components/app/edit-gas-fee-popover), [component2](https://github.com/MetaMask/metamask-extension/tree/develop/ui/components/app/advanced-gas-fee-popover).
Context [transaction-modal-context](https://github.com/MetaMask/metamask-extension/blob/develop/ui/contexts/transaction-modal.js) is used to show hide EIP-1559 gas popovers.
A parent component can be created for gas editing popover which will wrap both the legacy and EIP-1559 gas popover. Depending on the type of transaction appropriate gas popover can be shown. `transaction-modal-context` can be used to take care to open/close both popovers.
This parent component can be added to `confirm-transaction-base` and `token-allowance` components and thus will be available on all confirmation pages using gas editing.
Code [handleEditGas, handleCloseEditGas](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js#L276) can be moved to this new component.
5. ### Gas polling
Gas polling related code in `/pages/confirm-transaction` can be moved into a hook and included in `pages/confirm-transaction-base`, `/app/token-allowance` as only those confirmation pages need gas estimates.
**Note:** This document **does not cover signature request pages** which are covered separately.

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,67 @@
# Refactoring - Confirmation pages routing
This document details how routing to confirmation pages is currently done and the proposed improvements in routing.
## Current flow
The current flow of routing to confirmation pages is un-necessarily complicated and have issues.
![Confirmation Pages Routing - Current](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/confirmation-pages-routing/current.png)
- There are 2 ways in which confirmation pages can be opened:
1. User triggers send flow from within Metamask
- If the user triggers the send flow from within MetaMask and selects the recipient and amount on the send screen, an unapproved transaction is created in the background and the user is redirected to the **`/confirm-transaction`** route.
2. DAPP sends request to Metamask
- If DAPP sends request to Metamask an unapproved transaction or signature request is created in background and UI is triggered open (if it is not already open).
- The router by default renders `pages/home` component. The component looks at the state and if it finds an unapproved transaction or signature request in state it re-routes to **`/confirm-transaction`**.
- For **`/confirm-transaction/`** route, the router renders `pages/confirm-transaction` component.
- For **`/confirm-transaction`** route `pages/confirm-transaction` component renders `pages/confirm-transaction-switch` by default, for transactions with token methods it renders `pages/confirm-transaction/confirm-token-transaction-switch` which also open `pages/confirm-transaction-switch` by default.
- `pages/confirm-token-switch` redirect to specific confirmation page route depending on un-approved transaction or signature request in the state.
- For specific route **`/confirm-transaction/${id}/XXXXX`** routes again `pages/confirm-transaction` is rendered.
- Depending on confirmation route `pages/confirm-transaction` and `pages/confirm-transaction/confirm-token-transaction-switch` renders the specific confirmation page component.
## Proposed flow
The proposed routing of confirmation pages looks like.
![Confirmation Pages Routing - Proposed](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/confirmation-pages-routing/proposed.png)
- There are 2 ways in which confirmation pages can be opened:
1. User triggers send flow from within Metamask
- If the user triggers the send flow from within MetaMask and selects the recipient and amount on the send screen, an unapproved transaction is created in the background and the user is redirected to a specific transaction route, **`/confirm-transaction/${id}/XXXX`**, depending on the transaction type.
2. DAPP sends request to Metamask
- If DAPP send request to Metamask an unapproved transaction or signature request is created in background and UI is triggered to open (if it is not already open).
- Instead of rendering `pages/home`, `pages/routes` finds the unapproved transaction in state and reroutes to **`/confirm-transaction`**.
- Router renders `pages/confirm-transaction` component for **`/confirm-transaction`** route.
- `pages/confirm-transaction` component redirect to specific confirmation page route depending on unapproved transaction or signature request in the state.
- Again for specific route **`/confirm-transaction/${id}/XXXXX`** `pages/confirm-transaction` is rendered, it in-turn renders appropriate confirmation page for the specific route.
## Current Route component mapping
| Route | Component |
| ------------------------------------------------- | -------------------------------------- |
| `/confirm-transaction/${id}/deploy-contract` | `pages/confirm-deploy-contract` |
| `/confirm-transaction/${id}/send-ether` | `pages/confirm-send-ether` |
| `/confirm-transaction/${id}/send-token` | `pages/confirm-send-token` |
| `/confirm-transaction/${id}/approve` | `pages/confirm-approve` |
| `/confirm-transaction/${id}/set-approval-for-all` | `pages/confirm-approve` |
| `/confirm-transaction/${id}/transfer-from` | `pages/confirm-token-transaction-base` |
| `/confirm-transaction/${id}/safe-transfer-from` | `pages/confirm-token-transaction-base` |
| `/confirm-transaction/${id}/token-method` | `pages/confirm-contract-interaction` |
| `/confirm-transaction/${id}/signature-request` | `pages/confirm-signature-request.js` |
## Areas of code refactoring
Current routing code is complicated, it is also currently tied to state change in confirmation pages that makes it more complicated. State refactoring as discussed in this [document](https://github.com/MetaMask/metamask-extension/tree/develop/docs/confirmation-refactoring/confirmation-state-management) will also help simplify it.
- Any re-usable routing related code should be moved to [useRouting](https://github.com/MetaMask/metamask-extension/blob/develop/ui/hooks/useRouting.js) hook.
- Logic to initially check state and redirect to `/pages/confirm-transaction` can be moved from `/pages/home` to `pages/routes`
- All the route mapping code should be moved to `/pages/confirm-transaction`, this will require getting rid of route mappings in `/pages/confirm-transaction/confirm-token-transaction-switch`, `/pages/confirm-transaction-switch`.
- `/pages/confirm-transaction-switch` has the code that checks the un-approved transaction / message in the state, and based on its type and asset redirect to a specific route, a utility method can be created to do this mapping and can be included in `/pages/confirm-transaction` component.
- During the send flow initiated within metamask user can be redirected to specific confirmations route **`/confirm-transaction/${id}/XXXX`**
- Confirmation components have lot of props passing which needs to be reduced. Values can be obtained from redux state or other contexts directly using hooks. Component [confirm-token-transaction-switch](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/confirm-token-transaction-switch.js) has a lot of un-necessary props passing which should be removed and will help to further refactor routing.
- **Routing to mostRecentOverviewPage**
Across confirmation pages there is code to re-direct to `mostRecentOverviewPage`. `mostRecentOverviewPage` is equal to default route `/` or `/asset` whichever was last opened.
Also a lot of components check for state update and as soon as state has `0` pending un-approved transaction or signature request redirect is done to `mostRecentOverviewPage`. This logic can be handled at `/pages/confirm-transaction` which is always rendered for any confirmation page.
Also when the transaction is completed / rejected redirect is done to `mostRecentOverviewPage` explicitly which we should continue to do.

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -0,0 +1,47 @@
# Confirmation Pages - Frontend State Management
State Management is very important piece to keep frontend confirmation code simplified. Currently state management is fragmented over places and is complicated. Following guidelines will be useful for designing State Magagement:
1. Use state obtained from backend (redux store `state.metamask`) as single source of truth
2. For state derived from the backend state hooks can be written, these will internally use backend state
3. For temporary UI state shared across multiple components React Context can be used, minimise the scope of the context to just the components that need them (this is useful to avoid un-necessary re-rendering cycles in the app)
4. Confirmation React components fall into 2 categories:
- Smart components: state access should go here
- Dumb components: they are used for layout mainly and should ideally have required state data passed to them via props
5. Redux state is a good candidate for implementing state machine on frontend, if require anywhere in confirmation pages. Though currently transient state is mostly confined to single component state machine may not be needed.
Refactorings:
- There are confirmations related ducks [here](https://github.com/MetaMask/metamask-extension/tree/develop/ui/ducks):
- [confirm-transaction](https://github.com/MetaMask/metamask-extension/tree/develop/ui/ducks/confirm-transaction): this is redundant and we should be able to get rid of it.
- [gas](https://github.com/MetaMask/metamask-extension/tree/develop/ui/ducks/gas): this is not used anywhere and can be removed.
- [send](https://github.com/MetaMask/metamask-extension/tree/develop/ui/ducks/send): this duck is important state machine for send flow and we should continue to maintain.
- [gasFeeContext](https://github.com/MetaMask/metamask-extension/blob/develop/ui/contexts/gasFee.js) is huge context written on top of [gasFeeInput](https://github.com/MetaMask/metamask-extension/tree/develop/ui/hooks/gasFeeInput) hook. The context / hook provides about 20 different values used in different places in confirmation pages. We need to break this down:
- Context is required only to provide temporary UI state for confirmation pages which includes:
- `transaction` - active transaction on confirmation pages
- `editGasMode` - cancel, speedup, swap or default, this is also temporary UI state
The context can be included in `/pages/confirm-transaction-base` and around `TokenAllowance` in `/pages/confirm-approve`.
- Hooks can be created for values derived from values derived from above context and metamask state. This include:
- `maxFeePerGas`
- `maxPriorityFeePerGas`
- `supportEIP1559`
- `balanceError`
- `minimumCostInHexWei`
- `maximumCostInHexWei`
- `hasSimulationError`
- `estimateUsed`
- Values which can be obtained from metamask state using selectors should be removed from this context. This includes:
- `gasFeeEstimates`
- `isNetworkBusy`
- `minimumGasLimitDec` is a constant value 21000 should be removed from the context, this can be moved to constants file.
- Create separate hook for transaction functions [here](https://github.com/MetaMask/metamask-extension/blob/develop/ui/hooks/gasFeeInput/useTransactionFunctions.js), this hook can consume GasFeeContext.
- Setters and manual update functions are only used by legacy gas component [edit-gas-fee-popover](https://github.com/MetaMask/metamask-extension/tree/develop/ui/components/app/edit-gas-popover). This component uses useGasFeeInputs hook. We need to create a smaller hook just for this component using the above context and hooks.
* [confirm-transaction-base.container.js](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js) and [confirm-transaction-base.component.js](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js) has a lot of code to derive values from state and selected transactions. This can be simplified by using hooks that will he created.
* We will have a lot of hooks for transaction related fields, these can be grouped into same file / folder.
As we work on the components we will be able to identify more areas of improvement.

View File

@ -6,35 +6,35 @@ This document details the plan to refactor and cleanup Signature Request pages i
1. Simple ETH Signature 1. Simple ETH Signature
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/eth_sign.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/eth_sign.png" width="150"/>
1. Personal Signature 1. Personal Signature
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/personal_sign.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/personal_sign.png" width="150"/>
1. Typed Data - V1 1. Typed Data - V1
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/v1.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/v1.png" width="150"/>
1. Typed Data - V3 1. Typed Data - V3
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/v3.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/v3.png" width="150"/>
1. Typed Data - V4 1. Typed Data - V4
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/v4.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/v4.png" width="150"/>
1. SIWE Signature 1. SIWE Signature
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/siwe.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/siwe.png" width="150"/>
## The current flow of control for Signature Request looks like: ## The current flow of control for Signature Request looks like:
![Signature Request Flow - Current](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/signature_request_old.png) ![Signature Request Flow - Current](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/signature_request_old.png)
## The proposed flow of control: ## The proposed flow of control:
![Signature Request Flow - Proposed](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/signature_request_proposed.png) ![Signature Request Flow - Proposed](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/signature_request_proposed.png)
## Proposed Refactoring: ## Proposed Refactoring:
@ -48,33 +48,9 @@ There are many areas in above flow where the code can be improved upon to cleanu
- [PersonalMessageManager](https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/lib/personal-message-manager.js) - [PersonalMessageManager](https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/lib/personal-message-manager.js)
- [TypedMessageManager](https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/lib/typed-message-manager.js) - [TypedMessageManager](https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/lib/typed-message-manager.js)
Above message managers handle different types of message requests sent by DAPP. There is a lot of code duplication between the 3 classes. We can extract out a parent class and move duplicated code to it. Functions that can be moved to parent class: Above message managers handle different types of message requests sent by DAPP. There is a lot of code duplication between the 3 classes.
1. `constructor` - variable initialisation: We should migrate to use `MessageManagers` from `@metamask/core` repo [here](https://github.com/MetaMask/core/tree/main/packages/message-manager).
```
this.messages = [];
this.metricsEvent = metricsEvent;
```
1. `unapprovedMsgCount`
1. `getUnapprovedMsgs`
1. `addUnapprovedMessageAsync` - partially
1. `addUnapprovedMessage` - partially
1. `addMsg`
1. `getMsg`
1. `approveMessage`
1. `setMsgStatusApproved`
1. `setMsgStatusSigned`
1. `prepMsgForSigning`
1. `rejectMsg`
1. `errorMessage`
1. `clearUnapproved`
1. `_setMsgStatus`
1. `_updateMsg`
1. `_saveMsgList`
Much de-duplication can be achieved in message menagers.
2. ### Refactoring Routing to Signature Request pages: 2. ### Refactoring Routing to Signature Request pages:
@ -86,15 +62,13 @@ There are many areas in above flow where the code can be improved upon to cleanu
3. ### Refactoring in [conf-tx.js](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/conf-tx.js) 3. ### Refactoring in [conf-tx.js](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/conf-tx.js)
- While fixing routing [conf-tx.js](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/conf-tx.js) will be renamed to pages/confirm-signature-request component - [conf-tx.js](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/conf-tx.js) to be renamed to `pages/confirm-signature-request component`
- We are getting rid of [confirm-transaction](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/confirm-transaction.component.js) component from the flow. Thus, we need to ensure that any required logic from the component is extracted into a reusable hook and included in pages/confirm-signature-request. - Get rid of [confirm-transaction](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/confirm-transaction.component.js) component from signature request routing. Thus, we need to ensure that any required logic from the component is extracted into a reusable hook and included in pages/confirm-signature-request.
[This](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/confirm-transaction.component.js#L158) check for valid transaction id should be made re-usable and extracted out. - Convert to functional react component and use selectors to get state and get rid of `mapStateToProps`. [#17239](https://github.com/MetaMask/metamask-extension/issues/17239)
- The component can be converted to a function react component and use selectors to get state and get rid of `mapStatToProps`. [#17239](https://github.com/MetaMask/metamask-extension/issues/17239) - Various callbacks to `sign message`, `cancel request`, etc for different types of messages can be moved to respective child components.
- Various callbacks to sign message, cancel request, etc for different type of messaged can be moved to respective child components. - On component `mount/update` if there are no unapproved messages redirect to `mostRecentlyOverviewedPage` as [here](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L187).
- On component mount/update if there are no unapproved messages redirect to `mostRecentlyOverviewedPage` as [here](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L187). - Do not pass values like [these](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L260) to child components which can be obtained in child components using selectors.
- Not pass values like [these](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L260) to child components which can be obtained in child components using selectors.
- Extract logic [here](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L218) to show success modal for previously confirmed transaction into separate hook. - Extract logic [here](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L218) to show success modal for previously confirmed transaction into separate hook.
- **Question** - [this](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L241) check for message params look confusing - is it possible for a message request to not have message params or for other transactions to have message params. [Here](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/ui/app/pages/confirm-transaction/conf-tx.js#L185) we could have just checked for unapproved messages, why are we checking for pending transactions also ?
4. ### Refactoring component rendering Signature Request Pages 4. ### Refactoring component rendering Signature Request Pages
@ -110,28 +84,29 @@ There are many areas in above flow where the code can be improved upon to cleanu
5. ### Refactoring in signature-request-original 5. ### Refactoring in signature-request-original
- Rename, this component takes care of ETH sign, personal sign, sign typed data V1 requests. Let's rename it accordingly. - Rename, this component takes care of ETH sign, personal sign, sign typed data V1 requests. Let's rename it accordingly.
- Get rid of container components and for other components migrate to functional react components. - Get rid of container components
- Move this [metrics event](https://github.com/MetaMask/metamask-extension/blob/71a0bc8b3ff94478e61294c815770e6bc12a72f5/ui/app/components/app/signature-request-original/signature-request-original.component.js#L47) to pages/confirm-signature-request as it is applicable to all the signature requests types. - Migrate other classical components to functional react components.
- Move this [metrics event](https://github.com/MetaMask/metamask-extension/blob/71a0bc8b3ff94478e61294c815770e6bc12a72f5/ui/app/components/app/signature-request-original/signature-request-original.component.js#L50) to pages/confirm-signature-request as it is applicable to all the signature requests types.
- Header or we can say upper half of the page of all signature request pages (except SIWE) are very similar, this can be extracted into a reusable component used across both signature-request-original and signature-request: - Header or we can say upper half of the page of all signature request pages (except SIWE) are very similar, this can be extracted into a reusable component used across both signature-request-original and signature-request:
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/header.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/header.png" width="150"/>
- [LedgerInstructions](https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/app/signature-request-original/signature-request-original.component.js#L308) can also be moved to the header. - [LedgerInstructions](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/components/app/signature-request-original/signature-request-original.component.js#L312) can also be moved to the header.
- Create a reuable footer component and use it across all confirmation pages. [#17237](https://github.com/MetaMask/metamask-extension/issues/17237) - Create a reuable footer component and use it across all confirmation pages. [#17237](https://github.com/MetaMask/metamask-extension/issues/17237)
<img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/signature-request/footer.png" width="150"/> <img src="https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/confirmation-refactoring/signature-request/footer.png" width="150"/>
- Create a reuable component for Cancel All requests for use across signature request pages [Code](https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/app/signature-request-original/signature-request-original.component.js#L322). - Create a reusable component for Cancel All requests for use across signature request pages [Code](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/components/app/signature-request-original/signature-request-original.component.js#L326).
- Extract [getNetrowkName](https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/app/signature-request-original/signature-request-original.component.js#L56) into a reuable hook / utility method. - Extract [getNetrowkName](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/components/app/signature-request-original/signature-request-original.component.js#L60) into a reusable hook / utility method.
- [msgHexToText](https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/app/signature-request-original/signature-request-original.component.js#L75) to be made a utility method. - [msgHexToText](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/components/app/signature-request-original/signature-request-original.component.js#L79) to be made a utility method.
- Extract [renderBody](https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/app/signature-request-original/signature-request-original.component.js#L110) into a reusable component. - Extract [renderBody](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/components/app/signature-request-original/signature-request-original.component.js#L114) into a reusable component.
6. ### Refactoring in signature-request 6. ### Refactoring in signature-request
- Get rid of container components and for other components migrate to functional react components. - Get rid of container components and for other components migrate to functional react components.
- Reuse the Header component created for signature-request pages - Reuse the Header component created for signature-request pages
- Reuse the footer component created for confirmation pages. - Reuse the footer component created for confirmation pages.
- Extract [formatWallet](https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/app/signature-request/signature-request.component.js#L85) into a utility method. - Extract [formatWallet](https://github.com/MetaMask/metamask-extension/blob/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/components/app/signature-request/signature-request.component.js#L93) into a utility method.
7. ### Refactoring in signature-request-siwe 7. ### Refactoring in signature-request-siwe
- Footer component use `PageContainerFooter` can be converted into a footer component for all confirmation pages. [#17237](https://github.com/MetaMask/metamask-extension/issues/17237) - Footer component use `PageContainerFooter` can be used as footer component for all confirmation pages. [#17237](https://github.com/MetaMask/metamask-extension/issues/17237)

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,7 +0,0 @@
# Confirmation Pages Refactoring
The document details about refactoring confirmation pages. It describes the current code and improved proposed architecture.
1. [Signature Request Pages](https://github.com/MetaMask/metamask-extension/tree/develop/docs/refactoring/signature-request)
2. [Confirmation Pages Routing](https://github.com/MetaMask/metamask-extension/tree/develop/docs/refactoring/confirmation-pages-routing)

View File

@ -1,65 +0,0 @@
# Refactoring - Confirmation pages routing
This document details how routing to confirmation pages is currently done and the proposed improvements in routing.
## Current flow
The current flow of routing to confirmation pages is un-necessarily complicated and have issues.
![Confirmation Pages Routing - Current](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/confirmation-pages-routing/current.png)
- There are 2 ways in which confirmation pages can be opened:
1. User triggers send flow from within Metamask
- If the user triggers the send flow from within MetaMask and selects the recipient and amount on the send screen, an unapproved transaction is created in the background and the user is redirected to the **/confirm-transaction** route.
2. DAPP sends request to Metamask
- If DAPP sends request to Metamask an un-approved transaction or signature request is created in background and UI is triggered open (if it is not already open).
- The router by default renders `pages/home` component. The component looks at the state and if it finds an un-approved transaction or signature request in state it re-routes to **/confirm-transaction**.
- For **/confirm-transaction/** route, the router renders `pages/confirm-transaction` component.
- For **/confirm-transaction** route `pages/confirm-transaction` component renders `pages/confirm-transaction-switch` by default (for token methods it renders `pages/confirm-transaction/confirm-token-transaction-switch` which also open `pages/confirm-transaction-switch` by default).
- `pages/confirm-token-switch` redirect to specific confirmation page route depending on un-approved transaction or signature request in the state.
- For specific route **/confirm-transaction/${id}/XXXXX** routes also `pages/confirm-transaction` is rendered.
- Depending on confirmation route `pages/confirm-transaction` and `pages/confirm-transaction/confirm-token-transaction-switch` renders specific confirmation page component.
## Proposed flow
The proposed routing of confirmation pages looks like.
![Confirmation Pages Routing - Proposed](https://raw.githubusercontent.com/MetaMask/metamask-extension/develop/docs/refactoring/confirmation-pages-routing/proposed.png)
- There are 2 ways in which confirmation pages can be opened:
1. User triggers send flow from within Metamask
- [changed] If the user triggers the send flow from within MetaMask and selects the recipient and amount on the send screen, an unapproved transaction is created in the background and the user is redirected to a specific transaction route, **/confirm-transaction/${id}/XXXX**, depending on the transaction type.
2. DAPP sends request to Metamask
- If DAPP send request to Metamask an un-approved transaction or signature request is created in background and UI is triggered to open (if it is not already open).
- [changed] Instead of rendering `pages/home`, `pages/routes` finds the unapproved transaction in state and reroutes to **/confirm-transaction**.
- Router renders `pages/confirm-transaction` component for **/confirm-transaction** route.
- `pages/confirm-transaction` component redirect to specific confirmation page route depending on un-approved transaction or signature request in the state.
- Again for specific route **/confirm-transaction/${id}/XXXXX** `pages/confirm-transaction` is rendered, it in-turn renders appropriate confirmation page for the specific route.
## Current Route component mapping
| Route | Component |
| ----------------------------------------------- | ------------------------------------ |
| /confirm-transaction/${id}/deploy-contract | pages/confirm-deploy-contract |
| /confirm-transaction/${id}/send-ether | pages/confirm-send-ether |
| /confirm-transaction/${id}/send-token | pages/confirm-send-token |
| /confirm-transaction/${id}/approve | pages/confirm-approve |
| /confirm-transaction/${id}/set-approval-for-all | pages/confirm-approve |
| /confirm-transaction/${id}/transfer-from | pages/confirm-token-transaction-base |
| /confirm-transaction/${id}/safe-transfer-from | pages/confirm-token-transaction-base |
| /confirm-transaction/${id}/token-method | pages/confirm-contract-interaction |
| /confirm-transaction/${id}/signature-request | pages/confirm-signature-request.js |
## Areas of code refactoring
- **Routing to mostRecentOverviewPage**
Across confirmation pages there is code to re-direct to `mostRecentOverviewPage`. `mostRecentOverviewPage` is equal to default route `/` or `/asset` whichever was last opened.
Also a lot of components check for state update and as soon as state has `0` pending un-approved transaction or signature request redirect is done to `mostRecentOverviewPage`. This logic can be handled at `/pages/confirm-transaction` which is always rendered for any confirmation page.
Also when the transaction is completed / rejected redirect is done to `mostRecentOverviewPage` explicitly which we should continue to do.
- Any re-usable routing related code should be moved to [useRouting](https://github.com/MetaMask/metamask-extension/blob/develop/ui/hooks/useRouting.js) hook.
- Logic to initially check state and redirect to `/pages/confirm-transaction` can be moved from `/pages/home` to `pages/routes`
- Confirmation components have lot of props passing which needs to be reduced. Values can be obtained from redux state or other contexts directly using hooks. Component [confirm-token-transaction-switch](https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/confirm-transaction/confirm-token-transaction-switch.js) has a lot of un-necessary props passing which should be removed and will help to further refactor routing.
- All the route mapping code should be moved to `/pages/confirm-transaction`, this will require getting rid of route mappings in `/pages/confirm-transaction/confirm-token-transaction-switch`, `/pages/confirm-transaction-switch`.
- `/pages/confirm-transaction-switch` has the code that check the un-approved trancation / message in state and reditect to a specific route, a utility method can be create to do this mapping and can be included in `/pages/confirm-transaction` component.
- During the send flow initiated within metamask user should be redirected to specific confirmations route **/confirm-transaction/${id}/XXXX**

View File

@ -16,7 +16,8 @@
"name": null "name": null
} }
}, },
"warning": null "warning": null,
"customTokenAmount": "10"
}, },
"history": { "history": {
"mostRecentOverviewPage": "/mostRecentOverviewPage" "mostRecentOverviewPage": "/mostRecentOverviewPage"

View File

@ -0,0 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CustomSpendingCap should match snapshot 1`] = `
<div>
<div
class="box custom-spending-cap box--padding-top-2 box--padding-right-6 box--padding-left-6 box--display-flex box--gap-2 box--flex-direction-column box--align-items-flex-start box--background-color-background-alternative box--rounded-sm"
>
<div
class="box custom-spending-cap__input box--display-block box--flex-direction-row box--justify-content-center"
>
<label
for="custom-spending-cap"
>
<div
class="form-field"
>
<div
class="box box--flex-direction-row"
>
<div
class="form-field__heading"
>
<div
class="box form-field__heading-title box--display-flex box--flex-direction-row box--align-items-baseline"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--display-inline-block box--flex-direction-row typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Custom spending cap
</h6>
<div
class="box box--display-inline-block box--flex-direction-row"
>
<div>
<div
aria-describedby="tippy-tooltip-1"
class=""
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<span
class="box mm-icon mm-icon--size-md box--display-inline-block box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/question.svg');"
/>
</div>
</div>
</div>
</div>
<div
class="box form-field__heading-detail box--margin-bottom-2 box--flex-direction-row box--text-align-end"
>
<button
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
>
Use default
</button>
</div>
</div>
<input
class="form-field__input"
data-testid="custom-spending-cap-input"
id="custom-spending-cap"
placeholder="Enter a number"
type="text"
value="10"
/>
</div>
</div>
<div
class="box custom-spending-cap__max box--margin-left-auto box--padding-right-4 box--padding-bottom-2 box--flex-direction-row box--text-align-end box--width-max"
>
<button
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
>
Max
</button>
</div>
<div
class="box custom-spending-cap__description box--flex-direction-row"
>
<h6
class="box mm-text mm-text--body-sm box--padding-top-2 box--padding-bottom-2 box--flex-direction-row box--color-text-default"
>
<span>
This allows the third party to spend
<h6
class="box mm-text custom-spending-cap__input-value-and-token-name mm-text--body-sm-bold box--flex-direction-row box--color-text-default"
>
10
TST
</h6>
from your current balance.
</span>
</h6>
</div>
</label>
</div>
</div>
</div>
`;

View File

@ -3,6 +3,8 @@ import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box'; import Box from '../../ui/box';
import FormField from '../../ui/form-field'; import FormField from '../../ui/form-field';
@ -23,24 +25,31 @@ import {
import { getCustomTokenAmount } from '../../../selectors'; import { getCustomTokenAmount } from '../../../selectors';
import { setCustomTokenAmount } from '../../../ducks/app/app'; import { setCustomTokenAmount } from '../../../ducks/app/app';
import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils';
import { hexToDecimal } from '../../../../shared/modules/conversion.utils';
import { import {
MAX_TOKEN_ALLOWANCE_AMOUNT, MAX_TOKEN_ALLOWANCE_AMOUNT,
NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX, NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX,
DECIMAL_REGEX, DECIMAL_REGEX,
} from '../../../../shared/constants/tokens'; } from '../../../../shared/constants/tokens';
import { Numeric } from '../../../../shared/modules/Numeric'; import { Numeric } from '../../../../shared/modules/Numeric';
import { estimateGas } from '../../../store/actions';
import { getCustomTxParamsData } from '../../../pages/confirm-approve/confirm-approve.util';
import { useGasFeeContext } from '../../../contexts/gasFee';
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip'; import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
export default function CustomSpendingCap({ export default function CustomSpendingCap({
txParams,
tokenName, tokenName,
currentTokenBalance, currentTokenBalance,
dappProposedValue, dappProposedValue,
siteOrigin, siteOrigin,
passTheErrorText, passTheErrorText,
decimals, decimals,
setInputChangeInProgress,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { updateTransaction } = useGasFeeContext();
const inputRef = useRef(null); const inputRef = useRef(null);
const value = useSelector(getCustomTokenAmount); const value = useSelector(getCustomTokenAmount);
@ -97,12 +106,17 @@ export default function CustomSpendingCap({
getInputTextLogic(value).description, getInputTextLogic(value).description,
); );
const handleChange = (valueInput) => { const handleChange = async (valueInput) => {
if (!txParams) {
return;
}
setInputChangeInProgress(true);
let spendingCapError = ''; let spendingCapError = '';
const inputTextLogic = getInputTextLogic(valueInput); const inputTextLogic = getInputTextLogic(valueInput);
const inputTextLogicDescription = inputTextLogic.description; const inputTextLogicDescription = inputTextLogic.description;
const match = DECIMAL_REGEX.exec(replaceCommaToDot(valueInput)); const match = DECIMAL_REGEX.exec(replaceCommaToDot(valueInput));
if (match?.[1]?.length > decimals) { if (match?.[1]?.length > decimals) {
setInputChangeInProgress(false);
return; return;
} }
@ -128,6 +142,28 @@ export default function CustomSpendingCap({
} }
dispatch(setCustomTokenAmount(String(valueInput))); dispatch(setCustomTokenAmount(String(valueInput)));
try {
const newData = getCustomTxParamsData(txParams.data, {
customPermissionAmount: valueInput,
decimals,
});
const { from, to, value: txValue } = txParams;
const estimatedGasLimit = await estimateGas({
from,
to,
value: txValue,
data: newData,
});
if (estimatedGasLimit) {
await updateTransaction({
gasLimit: hexToDecimal(addHexPrefix(estimatedGasLimit)),
});
}
} catch (exp) {
console.error('Error in trying to update gas limit', exp);
}
setInputChangeInProgress(false);
}; };
useEffect(() => { useEffect(() => {
@ -278,6 +314,10 @@ export default function CustomSpendingCap({
} }
CustomSpendingCap.propTypes = { CustomSpendingCap.propTypes = {
/**
* Transaction params
*/
txParams: PropTypes.object.isRequired,
/** /**
* Displayed the token name currently tracked in description related to the input state * Displayed the token name currently tracked in description related to the input state
*/ */
@ -302,4 +342,8 @@ CustomSpendingCap.propTypes = {
* Number of decimals * Number of decimals
*/ */
decimals: PropTypes.string, decimals: PropTypes.string,
/**
* Updating input state to changing
*/
setInputChangeInProgress: PropTypes.func.isRequired,
}; };

View File

@ -0,0 +1,61 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import userEvent from '@testing-library/user-event';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import * as Actions from '../../../store/actions';
import * as GasFeeContext from '../../../contexts/gasFee';
import CustomSpendingCap from './custom-spending-cap';
const props = {
txParams: {
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
from: '0x8eeee1781fd885ff5ddef7789486676961873d12',
gas: '0xb41b',
maxFeePerGas: '0x4a817c800',
maxPriorityFeePerGas: '0x4a817c800',
to: '0x665933d73375e385bef40abcccea8b4cccc32d4c',
value: '0x0',
},
tokenName: 'TST',
currentTokenBalance: '10',
dappProposedValue: '7',
siteOrigin: 'https://metamask.github.io',
decimals: '4',
passTheErrorText: () => undefined,
setInputChangeInProgress: () => undefined,
};
describe('CustomSpendingCap', () => {
const store = configureMockStore([thunk])(mockState);
it('should match snapshot', () => {
const { container } = renderWithProvider(
<CustomSpendingCap {...props} />,
store,
);
expect(container).toMatchSnapshot();
});
it('should change in token allowance amount should call functions to update gas limit', async () => {
const user = userEvent.setup();
const spyEstimateGas = jest
.spyOn(Actions, 'estimateGas')
.mockReturnValue(Promise.resolve('1770'));
const updateTransactionMock = jest.fn();
jest
.spyOn(GasFeeContext, 'useGasFeeContext')
.mockImplementation(() => ({ updateTransaction: updateTransactionMock }));
const { getByRole } = renderWithProvider(
<CustomSpendingCap {...props} />,
store,
);
await user.type(getByRole('textbox'), '5');
expect(spyEstimateGas).toHaveBeenCalledTimes(1);
expect(updateTransactionMock).toHaveBeenCalledTimes(1);
});
});

View File

@ -105,7 +105,7 @@ const SetApproveForAllWarning = ({
key="non_custodial_bold" key="non_custodial_bold"
className="set-approval-for-all-warning__content__bold" className="set-approval-for-all-warning__content__bold"
> >
{t('nftWarningContentBold', [collectionName])} {t('nftWarningContentBold', [collectionName || ''])}
</strong>, </strong>,
<strong key="non_custodial_grey"> <strong key="non_custodial_grey">
{t('nftWarningContentGrey')} {t('nftWarningContentGrey')}

View File

@ -5,11 +5,7 @@ import InfoIconInverted from '../icon/info-icon-inverted.component';
import { SEVERITIES, Color } from '../../../helpers/constants/design-system'; import { SEVERITIES, Color } from '../../../helpers/constants/design-system';
import { MILLISECOND } from '../../../../shared/constants/time'; import { MILLISECOND } from '../../../../shared/constants/time';
import Typography from '../typography'; import Typography from '../typography';
import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ButtonIcon, IconName, IconSize } from '../../component-library';
import {
ICON_NAMES,
ICON_SIZES,
} from '../../component-library/icon/deprecated';
export default function Callout({ export default function Callout({
severity, severity,
@ -47,8 +43,8 @@ export default function Callout({
</Typography> </Typography>
{dismiss && ( {dismiss && (
<ButtonIcon <ButtonIcon
iconName={ICON_NAMES.CLOSE} iconName={IconName.Close}
size={ICON_SIZES.SM} size={IconSize.Sm}
className="callout__close-button" className="callout__close-button"
onClick={() => { onClick={() => {
setRemoved(true); setRemoved(true);

View File

@ -16,8 +16,7 @@ import {
Color, Color,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ButtonIcon, IconName } from '../../component-library';
import { ICON_NAMES } from '../../component-library/icon/deprecated';
export default function ContractTokenValues({ export default function ContractTokenValues({
address, address,
@ -51,7 +50,7 @@ export default function ContractTokenValues({
title={copied ? t('copiedExclamation') : t('copyToClipboard')} title={copied ? t('copiedExclamation') : t('copyToClipboard')}
> >
<ButtonIcon <ButtonIcon
iconName={copied ? ICON_NAMES.COPY_SUCCESS : ICON_NAMES.COPY} iconName={copied ? IconName.CopySuccess : IconName.Copy}
color={Color.iconMuted} color={Color.iconMuted}
onClick={() => handleCopy(address)} onClick={() => handleCopy(address)}
ariaLabel={copied ? t('copiedExclamation') : t('copyToClipboard')} ariaLabel={copied ? t('copiedExclamation') : t('copyToClipboard')}
@ -60,7 +59,7 @@ export default function ContractTokenValues({
<Tooltip position="top" title={t('openInBlockExplorer')}> <Tooltip position="top" title={t('openInBlockExplorer')}>
<ButtonIcon <ButtonIcon
display={DISPLAY.FLEX} display={DISPLAY.FLEX}
iconName={ICON_NAMES.EXPORT} iconName={IconName.Export}
color={Color.iconMuted} color={Color.iconMuted}
onClick={() => { onClick={() => {
const blockExplorerTokenLink = getAccountLink( const blockExplorerTokenLink = getAccountLink(

View File

@ -1,8 +1,7 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { Icon, IconName, IconSize } from '../../component-library';
import { Size } from '../../../helpers/constants/design-system';
const Disclosure = ({ children, title, size }) => { const Disclosure = ({ children, title, size }) => {
const disclosureFooterEl = useRef(null); const disclosureFooterEl = useRef(null);
@ -27,8 +26,8 @@ const Disclosure = ({ children, title, size }) => {
<summary className="disclosure__summary"> <summary className="disclosure__summary">
<Icon <Icon
className="disclosure__summary--icon" className="disclosure__summary--icon"
name={ICON_NAMES.ADD} name={IconName.Add}
size={Size.SM} size={IconSize.Sm}
marginInlineEnd={2} marginInlineEnd={2}
/> />
{title} {title}

View File

@ -1,11 +1,7 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { import { Icon, IconName, IconSize } from '../../component-library';
Icon,
ICON_NAMES,
ICON_SIZES,
} from '../../component-library/icon/deprecated';
const Dropdown = ({ const Dropdown = ({
className, className,
@ -46,8 +42,8 @@ const Dropdown = ({
})} })}
</select> </select>
<Icon <Icon
name={ICON_NAMES.ARROW_DOWN} name={IconName.ArrowDown}
size={ICON_SIZES.SM} size={IconSize.Sm}
className="dropdown__icon-caret-down" className="dropdown__icon-caret-down"
/> />
</div> </div>

View File

@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Color } from '../../../helpers/constants/design-system'; import { Color } from '../../../helpers/constants/design-system';
import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts'; import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts';
import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ButtonIcon, IconName } from '../../component-library';
import { ICON_NAMES } from '../../component-library/icon/deprecated';
export default class EditableLabel extends Component { export default class EditableLabel extends Component {
static propTypes = { static propTypes = {
@ -60,7 +59,7 @@ export default class EditableLabel extends Component {
autoFocus autoFocus
/> />
<ButtonIcon <ButtonIcon
iconName={ICON_NAMES.CHECK} iconName={IconName.Check}
className="editable-label__icon-button" className="editable-label__icon-button"
onClick={() => this.handleSubmit(isValidAccountName)} onClick={() => this.handleSubmit(isValidAccountName)}
/> />
@ -76,7 +75,7 @@ export default class EditableLabel extends Component {
<div className={classnames('editable-label', this.props.className)}> <div className={classnames('editable-label', this.props.className)}>
<div className="editable-label__value">{this.state.value}</div> <div className="editable-label__value">{this.state.value}</div>
<ButtonIcon <ButtonIcon
iconName={ICON_NAMES.EDIT} iconName={IconName.Edit}
ariaLabel={this.context.t('edit')} ariaLabel={this.context.t('edit')}
data-testid="editable-label-button" data-testid="editable-label-button"
onClick={() => this.setState({ isEditing: true })} onClick={() => this.setState({ isEditing: true })}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { Icon, IconName, IconSize } from '../../component-library';
import { IconColor, Size } from '../../../helpers/constants/design-system'; import { IconColor } from '../../../helpers/constants/design-system';
/** /**
* @deprecated - Please use ActionableMessage type danger * @deprecated - Please use ActionableMessage type danger
@ -19,8 +19,8 @@ const ErrorMessage = (props, context) => {
<div className="error-message"> <div className="error-message">
<Icon <Icon
className="error-message__icon" className="error-message__icon"
name={ICON_NAMES.WARNING} name={IconName.Warning}
size={Size.SM} size={IconSize.Sm}
color={IconColor.errorDefault} color={IconColor.errorDefault}
marginRight={2} marginRight={2}
/> />

View File

@ -4,7 +4,7 @@ import React, { useState } from 'react';
import Typography from '../typography'; import Typography from '../typography';
import Tooltip from '../tooltip'; import Tooltip from '../tooltip';
import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { Icon, IconName } from '../../component-library';
import { AlignItems } from '../../../helpers/constants/design-system'; import { AlignItems } from '../../../helpers/constants/design-system';
import README from './README.mdx'; import README from './README.mdx';
import FormField from '.'; import FormField from '.';
@ -70,7 +70,7 @@ export const FormFieldWithTitleDetail = (args) => {
Click Me Click Me
</button> </button>
), ),
checkmark: <Icon name={ICON_NAMES.CHECK} />, checkmark: <Icon name={IconName.Check} />,
}; };
return <FormField {...args} titleDetail={detailOptions[args.titleDetail]} />; return <FormField {...args} titleDetail={detailOptions[args.titleDetail]} />;
@ -108,7 +108,7 @@ export const CustomComponents = (args) => {
position="top" position="top"
html={<Typography>Custom tooltip</Typography>} html={<Typography>Custom tooltip</Typography>}
> >
<Icon name={ICON_NAMES.QUESTION} marginLeft={2} /> <Icon name={IconName.Question} marginLeft={2} />
</Tooltip> </Tooltip>
} }
titleDetail={<Typography>TitleDetail</Typography>} titleDetail={<Typography>TitleDetail</Typography>}

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { IconName } from '../../component-library';
import { Menu, MenuItem } from '.'; import { Menu, MenuItem } from '.';
export default { export default {
@ -10,11 +10,11 @@ export default {
export const DefaultStory = () => { export const DefaultStory = () => {
return ( return (
<Menu onHide={action('Hide')}> <Menu onHide={action('Hide')}>
<MenuItem iconName={ICON_NAMES.EYE} onClick={action('Menu Item 1')}> <MenuItem iconName={IconName.Eye} onClick={action('Menu Item 1')}>
Menu Item 1 Menu Item 1
</MenuItem> </MenuItem>
<MenuItem onClick={action('Menu Item 2')}>Menu Item 2</MenuItem> <MenuItem onClick={action('Menu Item 2')}>Menu Item 2</MenuItem>
<MenuItem iconName={ICON_NAMES.EYE_SLASH} onClick={action('Menu Item 3')}> <MenuItem iconName={IconName.EyeSlash} onClick={action('Menu Item 3')}>
Menu Item 3 Menu Item 3
</MenuItem> </MenuItem>
</Menu> </Menu>
@ -29,14 +29,11 @@ export const Anchored = () => {
<> <>
<button ref={setAnchorElement}>Menu</button> <button ref={setAnchorElement}>Menu</button>
<Menu anchorElement={anchorElement} onHide={action('Hide')}> <Menu anchorElement={anchorElement} onHide={action('Hide')}>
<MenuItem iconName={ICON_NAMES.EXPORT} onClick={action('Menu Item 1')}> <MenuItem iconName={IconName.Export} onClick={action('Menu Item 1')}>
Menu Item 1 Menu Item 1
</MenuItem> </MenuItem>
<MenuItem onClick={action('Menu Item 2')}>Menu Item 2</MenuItem> <MenuItem onClick={action('Menu Item 2')}>Menu Item 2</MenuItem>
<MenuItem <MenuItem iconName={IconName.EyeSlash} onClick={action('Menu Item 3')}>
iconName={ICON_NAMES.EYE_SLSH}
onClick={action('Menu Item 3')}
>
Menu Item 3 Menu Item 3
</MenuItem> </MenuItem>
</Menu> </Menu>

View File

@ -26,7 +26,7 @@ import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes';
import Chip from '../chip/chip'; import Chip from '../chip/chip';
import { setFirstTimeUsedNetwork } from '../../../store/actions'; import { setFirstTimeUsedNetwork } from '../../../store/actions';
import { NETWORK_TYPES } from '../../../../shared/constants/network'; import { NETWORK_TYPES } from '../../../../shared/constants/network';
import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { Icon, IconName } from '../../component-library';
const NewNetworkInfo = () => { const NewNetworkInfo = () => {
const t = useContext(I18nContext); const t = useContext(I18nContext);
@ -106,7 +106,7 @@ const NewNetworkInfo = () => {
) : ( ) : (
<Icon <Icon
className="question" className="question"
name={ICON_NAMES.QUESTION} name={IconName.Question}
color={Color.iconDefault} color={Color.iconDefault}
/> />
) )

View File

@ -11,11 +11,7 @@ import { shortenAddress } from '../../../helpers/utils/util';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
import { getTokenList, getBlockExplorerLinkText } from '../../../selectors'; import { getTokenList, getBlockExplorerLinkText } from '../../../selectors';
import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes';
import { import { ButtonIcon, IconName, IconSize } from '../../component-library';
ICON_NAMES,
ICON_SIZES,
} from '../../component-library/icon/deprecated';
import { ButtonIcon } from '../../component-library/button-icon/deprecated';
const NicknamePopover = ({ const NicknamePopover = ({
address, address,
@ -67,8 +63,8 @@ const NicknamePopover = ({
title={copied ? t('copiedExclamation') : t('copyToClipboard')} title={copied ? t('copiedExclamation') : t('copyToClipboard')}
> >
<ButtonIcon <ButtonIcon
iconName={copied ? ICON_NAMES.COPY_SUCCESS : ICON_NAMES.COPY} iconName={copied ? IconName.CopySuccess : IconName.Copy}
size={ICON_SIZES.SM} size={IconSize.Sm}
onClick={() => handleCopy(address)} onClick={() => handleCopy(address)}
/> />
</Tooltip> </Tooltip>

View File

@ -18,13 +18,14 @@ import {
TEXT_ALIGN, TEXT_ALIGN,
BLOCK_SIZES, BLOCK_SIZES,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { import {
ButtonIcon,
Icon, Icon,
ICON_NAMES, IconName,
ICON_SIZES, IconSize,
} from '../../component-library/icon/deprecated'; Text,
import { ButtonIcon } from '../../component-library/button-icon/deprecated'; } from '../../component-library';
import { Text } from '../../component-library';
const defaultHeaderProps = { const defaultHeaderProps = {
padding: [6, 4, 4], padding: [6, 4, 4],
@ -86,7 +87,7 @@ const Popover = ({
> >
{onBack ? ( {onBack ? (
<ButtonIcon <ButtonIcon
iconName={ICON_NAMES.ARROW_LEFT} iconName={IconName.ArrowLeft}
ariaLabel={t('back')} ariaLabel={t('back')}
onClick={onBack} onClick={onBack}
color={Color.iconDefault} color={Color.iconDefault}
@ -104,7 +105,7 @@ const Popover = ({
</Text> </Text>
{onClose ? ( {onClose ? (
<ButtonIcon <ButtonIcon
iconName={ICON_NAMES.CLOSE} iconName={IconName.Close}
ariaLabel={t('close')} ariaLabel={t('close')}
data-testid="popover-close" data-testid="popover-close"
onClick={onClose} onClick={onClose}
@ -151,9 +152,9 @@ const Popover = ({
data-testid="popover-scroll-button" data-testid="popover-scroll-button"
> >
<Icon <Icon
name={ICON_NAMES.ARROW_DOWN} name={IconName.ArrowDown}
color={IconColor.primaryDefault} color={IconColor.primaryDefault}
size={ICON_SIZES.MD} size={IconSize.Md}
aria-label={t('scrollDown')} aria-label={t('scrollDown')}
/> />
</Box> </Box>

View File

@ -9,11 +9,7 @@ import Tooltip from '../tooltip';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { AddressCopyButton } from '../../multichain/address-copy-button'; import { AddressCopyButton } from '../../multichain/address-copy-button';
import Box from '../box/box'; import Box from '../box/box';
import { import { Icon, IconName, IconSize } from '../../component-library';
Icon,
ICON_NAMES,
ICON_SIZES,
} from '../../component-library/icon/deprecated';
export default connect(mapStateToProps)(QrCodeView); export default connect(mapStateToProps)(QrCodeView);
@ -80,8 +76,8 @@ function QrCodeView(props) {
> >
<div className="qr-code__address">{toChecksumHexAddress(data)}</div> <div className="qr-code__address">{toChecksumHexAddress(data)}</div>
<Icon <Icon
name={copied ? ICON_NAMES.COPY_SUCCESS : ICON_NAMES.COPY} name={copied ? IconName.CopySuccess : IconName.Copy}
size={ICON_SIZES.SM} size={IconSize.Sm}
marginInlineStart={3} marginInlineStart={3}
/> />
</div> </div>

View File

@ -4,12 +4,7 @@ import { I18nContext } from '../../../contexts/i18n';
import Box from '../box'; import Box from '../box';
import Tooltip from '../tooltip'; import Tooltip from '../tooltip';
import Typography from '../typography'; import Typography from '../typography';
import { ButtonLink } from '../../component-library'; import { ButtonLink, Icon, IconName, IconSize } from '../../component-library';
import {
Icon,
ICON_NAMES,
ICON_SIZES,
} from '../../component-library/icon/deprecated';
import { import {
AlignItems, AlignItems,
DISPLAY, DISPLAY,
@ -86,7 +81,7 @@ export default function ReviewSpendingCap({
color={TextColor.errorDefault} color={TextColor.errorDefault}
> >
<Icon <Icon
name={ICON_NAMES.WARNING} name={IconName.Warning}
style={{ verticalAlign: 'middle' }} style={{ verticalAlign: 'middle' }}
/> />
{t('beCareful')} {t('beCareful')}
@ -100,16 +95,16 @@ export default function ReviewSpendingCap({
{valueIsGreaterThanBalance && ( {valueIsGreaterThanBalance && (
<Icon <Icon
className="review-spending-cap__heading-title__tooltip__warning-icon" className="review-spending-cap__heading-title__tooltip__warning-icon"
name={ICON_NAMES.DANGER} name={IconName.Danger}
color={IconColor.errorDefault} color={IconColor.errorDefault}
size={ICON_SIZES.SM} size={IconSize.Sm}
style={{ 'vertical-align': 'middle' }} style={{ 'vertical-align': 'middle' }}
/> />
)} )}
{Number(tokenValue) === 0 && ( {Number(tokenValue) === 0 && (
<Icon <Icon
className="review-spending-cap__heading-title__tooltip__question-icon" className="review-spending-cap__heading-title__tooltip__question-icon"
name={ICON_NAMES.QUESTION} name={IconName.Question}
color={IconColor.iconDefault} color={IconColor.iconDefault}
/> />
)} )}

View File

@ -9,7 +9,7 @@ import AccountMismatchWarning from '../account-mismatch-warning/account-mismatch
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import NicknamePopovers from '../../app/modals/nickname-popovers'; import NicknamePopovers from '../../app/modals/nickname-popovers';
import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { Icon, IconName } from '../../component-library';
import { import {
DEFAULT_VARIANT, DEFAULT_VARIANT,
CARDS_VARIANT, CARDS_VARIANT,
@ -199,7 +199,7 @@ function Arrow({ variant }) {
</div> </div>
) : ( ) : (
<div className="sender-to-recipient__arrow-container"> <div className="sender-to-recipient__arrow-container">
<Icon name={ICON_NAMES.ARROW_RIGHT} /> <Icon name={IconName.ArrowRight} />
</div> </div>
); );
} }

View File

@ -1,65 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Dropdown from '../../dropdown';
import Box from '../../box';
export const DropdownTab = (props) => {
const {
activeClassName,
className,
'data-testid': dataTestId,
isActive,
onClick,
onChange,
tabIndex,
options,
selectedOption,
} = props;
return (
<Box
as="li"
className={classnames('tab', className, {
'tab--active': isActive,
[activeClassName]: activeClassName && isActive,
})}
data-testid={dataTestId}
onClick={(event) => {
event.preventDefault();
onClick(tabIndex);
}}
>
<Dropdown
options={options}
selectedOption={selectedOption}
onChange={onChange}
/>
</Box>
);
};
DropdownTab.propTypes = {
activeClassName: PropTypes.string,
className: PropTypes.string,
'data-testid': PropTypes.string,
isActive: PropTypes.bool, // required, but added using React.cloneElement
options: PropTypes.arrayOf(
PropTypes.exact({
name: PropTypes.string,
value: PropTypes.string.isRequired,
}),
).isRequired,
selectedOption: PropTypes.string,
onChange: PropTypes.func,
onClick: PropTypes.func,
tabIndex: PropTypes.number, // required, but added using React.cloneElement
};
DropdownTab.defaultProps = {
activeClassName: undefined,
className: undefined,
onChange: undefined,
onClick: undefined,
selectedOption: undefined,
};

View File

@ -1,23 +0,0 @@
.tab {
.dropdown__select {
border: none;
font-size: unset;
width: 100%;
background-color: unset;
padding-left: 8px;
padding-right: 20px;
line-height: unset;
option {
background-color: var(--color-background-default);
}
&:focus-visible {
outline: none;
}
}
.dropdown__icon-caret-down {
right: 0;
}
}

View File

@ -0,0 +1,160 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Box from '../../../box';
import {
AlignItems,
BLOCK_SIZES,
BackgroundColor,
BorderColor,
BorderRadius,
BorderStyle,
DISPLAY,
FLEX_DIRECTION,
FLEX_WRAP,
TextVariant,
} from '../../../../../helpers/constants/design-system';
import { Icon, IconName, IconSize, Text } from '../../../../component-library';
export const DropdownTab = ({
activeClassName,
className,
'data-testid': dataTestId,
isActive,
onClick,
onChange,
tabIndex,
options,
selectedOption,
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const selectOption = useCallback(
(event, option) => {
event.stopPropagation();
onChange(option.value);
setIsOpen(false);
},
[onChange],
);
const openDropdown = (event) => {
event.preventDefault();
setIsOpen(true);
onClick(tabIndex);
};
const selectedOptionName = options.find(
(option) => option.value === selectedOption,
)?.name;
useEffect(() => {
function handleClickOutside(event) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target) &&
isOpen
) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [dropdownRef, isOpen]);
return (
<Box
as="li"
className={classnames('tab', className, {
'tab--active': isActive,
[activeClassName]: activeClassName && isActive,
})}
data-testid={dataTestId}
onClick={openDropdown}
dataTestId={dataTestId}
flexDirection={FLEX_DIRECTION.ROW}
flexWrap={FLEX_WRAP.NO_WRAP}
height={BLOCK_SIZES.FULL}
style={{ cursor: 'pointer', overflow: 'hidden' }}
title={selectedOptionName}
>
<Box alignItems={AlignItems.center} padding={2}>
<Text
variant={TextVariant.inherit}
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{selectedOptionName}
</Text>
<Icon marginLeft={2} name={IconName.ArrowDown} size={IconSize.Sm} />
</Box>
{isOpen && (
<Box
backgroundColor={BackgroundColor.backgroundDefault}
borderStyle={BorderStyle.solid}
borderColor={BorderColor.borderDefault}
borderRadius={BorderRadius.SM}
paddingLeft={2}
paddingRight={2}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
flexWrap={FLEX_WRAP.NO_WRAP}
style={{ position: 'absolute', maxWidth: '170px' }}
ref={dropdownRef}
>
{options.map((option, i) => (
<Text
key={i}
marginTop={1}
marginBottom={1}
variant={TextVariant.bodySm}
onClick={(event) => selectOption(event, option)}
style={{
cursor: 'pointer',
textTransform: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{option.name}
</Text>
))}
</Box>
)}
</Box>
);
};
DropdownTab.propTypes = {
activeClassName: PropTypes.string,
className: PropTypes.string,
'data-testid': PropTypes.string,
isActive: PropTypes.bool, // required, but added using React.cloneElement
options: PropTypes.arrayOf(
PropTypes.exact({
name: PropTypes.string,
value: PropTypes.string.isRequired,
}),
).isRequired,
selectedOption: PropTypes.string,
onChange: PropTypes.func,
onClick: PropTypes.func,
tabIndex: PropTypes.number, // required, but added using React.cloneElement
};
DropdownTab.defaultProps = {
activeClassName: undefined,
className: undefined,
onChange: undefined,
onClick: undefined,
selectedOption: undefined,
};

View File

@ -0,0 +1,48 @@
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import DropdownTab from '.';
describe('DropdownTab', () => {
const onChange = jest.fn();
const onClick = jest.fn();
let args;
beforeEach(() => {
args = {
activeClassName: 'active',
tabIndex: 1,
options: [
{ name: 'foo', value: 'foo' },
{ name: 'bar', value: 'bar' },
],
selectedOption: 'foo',
onChange,
onClick,
};
});
it('should render the DropdownTab component without crashing', () => {
const { getByText } = render(<DropdownTab {...args} />);
expect(getByText(args.options[0].name)).toBeDefined();
});
it('registers click', () => {
const { container } = render(<DropdownTab {...args} />);
fireEvent.click(container.firstChild);
expect(onClick).toHaveBeenCalledWith(args.tabIndex);
});
it('registers selection', () => {
const { container, getByText } = render(<DropdownTab {...args} />);
fireEvent.click(container.firstChild);
const element = getByText(args.options[1].name);
fireEvent.click(element);
expect(onClick).toHaveBeenCalledWith(args.tabIndex);
expect(onChange).toHaveBeenCalledWith(args.options[1].value);
});
});

View File

@ -1,5 +1,4 @@
import Tabs from './tabs.component'; import Tabs from './tabs.component';
import Tab from './tab'; import Tab from './tab';
import DropdownTab from './dropdown-tab';
export { Tabs, Tab, DropdownTab }; export { Tabs, Tab };

View File

@ -1,5 +1,4 @@
@import 'tab/index'; @import 'tab/index';
@import 'dropdown-tab/index';
.tabs { .tabs {
flex-grow: 1; flex-grow: 1;
@ -11,6 +10,5 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 2; z-index: 2;
overflow: hidden;
} }
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import DropdownTab from './dropdown-tab'; import DropdownTab from './flask/dropdown-tab';
import Tab from './tab/tab.component'; import Tab from './tab/tab.component';
import Tabs from './tabs.component'; import Tabs from './tabs.component';

View File

@ -5,7 +5,8 @@ import { CHAIN_ID_TO_NETWORK_ID_MAP } from '../../shared/constants/network';
import { stripHexPrefix } from '../../shared/modules/hexstring-utils'; import { stripHexPrefix } from '../../shared/modules/hexstring-utils';
import { TransactionType } from '../../shared/constants/transaction'; import { TransactionType } from '../../shared/constants/transaction';
import { getInsightSnaps } from '../selectors'; import { getInsightSnaps } from '../selectors';
import { DropdownTab, Tab } from '../components/ui/tabs'; import { Tab } from '../components/ui/tabs';
import DropdownTab from '../components/ui/tabs/flask/dropdown-tab';
import { SnapInsight } from '../components/app/confirm-page-container/flask/snap-insight'; import { SnapInsight } from '../components/app/confirm-page-container/flask/snap-insight';
const isAllowedTransactionTypes = (transactionType) => const isAllowedTransactionTypes = (transactionType) =>

View File

@ -144,6 +144,7 @@ export default function ConfirmApprove({
const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {}; const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {};
// Code below may need a additional look as ERC1155 tokens do not have a name
let tokensText; let tokensText;
if ( if (
assetStandard === TokenStandard.ERC721 || assetStandard === TokenStandard.ERC721 ||
@ -199,6 +200,7 @@ export default function ConfirmApprove({
toAddress={toAddress} toAddress={toAddress}
tokenSymbol={tokenSymbol} tokenSymbol={tokenSymbol}
decimals={decimals} decimals={decimals}
fromAddressIsLedger={fromAddressIsLedger}
/> />
{showCustomizeGasPopover && !supportsEIP1559 && ( {showCustomizeGasPopover && !supportsEIP1559 && (
<EditGasPopover <EditGasPopover

View File

@ -0,0 +1,677 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmSendEther should render correct information for for confirm send ether 1`] = `
[
<div
class="page-container"
data-testid="page-container"
>
<div
class="confirm-page-container-navigation"
style="display: flex;"
>
<div
class="confirm-page-container-navigation__container"
data-testid="navigation-container"
style="visibility: hidden;"
>
<button
class="confirm-page-container-navigation__arrow"
data-testid="first-page"
>
<i
class="fa fa-angle-double-left fa-2x"
/>
</button>
<button
class="confirm-page-container-navigation__arrow"
data-testid="previous-page"
>
<i
class="fa fa-angle-left fa-2x"
/>
</button>
</div>
<div
class="confirm-page-container-navigation__textcontainer"
>
<div
class="confirm-page-container-navigation__navtext"
>
0
of
2
</div>
<div
class="confirm-page-container-navigation__longtext"
>
requests waiting to be acknowledged
</div>
</div>
<div
class="confirm-page-container-navigation__container"
style="visibility: initial;"
>
<button
class="confirm-page-container-navigation__arrow"
data-testid="next-page"
>
<i
class="fa fa-angle-right fa-2x"
/>
</button>
<button
class="confirm-page-container-navigation__arrow"
data-testid="last-page"
>
<i
class="fa fa-angle-double-right fa-2x"
/>
</button>
</div>
</div>
<div
class="confirm-page-container-header"
data-testid="header-container"
>
<div
class="confirm-page-container-header__row"
>
<div
class="confirm-page-container-header__back-button-container"
style="visibility: initial;"
>
<span
class="box mm-icon mm-icon--size-md box--display-inline-block box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/arrow-left.svg');"
/>
<span
class="confirm-page-container-header__back-button"
data-testid="confirm-page-back-edit-button"
>
Edit
</span>
</div>
</div>
<div
class="sender-to-recipient sender-to-recipient--default"
data-testid="sender-to-recipient"
>
<div
class="sender-to-recipient__party sender-to-recipient__party--sender"
>
<div
class="sender-to-recipient__sender-icon"
>
<div
class=""
>
<div
class="identicon"
style="height: 24px; width: 24px; border-radius: 12px;"
>
<div
style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 24px; height: 24px; display: inline-block; background: rgb(250, 58, 0);"
>
<svg
height="24"
width="24"
x="0"
y="0"
>
<rect
fill="#18CDF2"
height="24"
transform="translate(-0.786295127845455 -2.478213052095374) rotate(328.9 12 12)"
width="24"
x="0"
y="0"
/>
<rect
fill="#035E56"
height="24"
transform="translate(-13.723846281624033 7.94434640381145) rotate(176.2 12 12)"
width="24"
x="0"
y="0"
/>
<rect
fill="#F26602"
height="24"
transform="translate(12.500881513667943 -10.653854792247811) rotate(468.9 12 12)"
width="24"
x="0"
y="0"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="sender-to-recipient__tooltip-wrapper"
>
<div
aria-describedby="tippy-tooltip-1"
class="sender-to-recipient__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<div
class="sender-to-recipient__name"
>
Test Account
</div>
</div>
</div>
</div>
<div
class="sender-to-recipient__arrow-container"
>
<div
class="sender-to-recipient__arrow-circle"
>
<i
class="fa fa-arrow-right sender-to-recipient__arrow-circle__icon"
/>
</div>
</div>
<div
class="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
>
<div
class="sender-to-recipient__sender-icon"
>
<div
class=""
>
<div
class="identicon"
style="height: 24px; width: 24px; border-radius: 12px;"
>
<div
style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 24px; height: 24px; display: inline-block; background: rgb(3, 73, 94);"
>
<svg
height="24"
width="24"
x="0"
y="0"
>
<rect
fill="#F5D800"
height="24"
transform="translate(-1.6948137315966292 7.490045892231985) rotate(238.2 12 12)"
width="24"
x="0"
y="0"
/>
<rect
fill="#1888F2"
height="24"
transform="translate(2.6987555575750655 10.47254609666851) rotate(211.2 12 12)"
width="24"
x="0"
y="0"
/>
<rect
fill="#017E8E"
height="24"
transform="translate(4.1286145552783005 -17.188975454864387) rotate(404.9 12 12)"
width="24"
x="0"
y="0"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="sender-to-recipient__tooltip-wrapper"
>
<div
class="sender-to-recipient__tooltip-container"
style="display: inline;"
tabindex="0"
title=""
>
<div
class="sender-to-recipient__name"
data-testid="sender-to-recipient__name"
>
0x0c5...AaFb
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="confirm-page-container-content"
>
<div
class="confirm-page-container-summary confirm-page-container-summary--border"
>
<div
class="site-origin confirm-page-container-summary__origin"
>
<bdi
dir="ltr"
>
https://metamask.github.io
</bdi>
</div>
<div
class="confirm-page-container-summary__action-row"
>
<div
class="confirm-page-container-summary__action"
>
<span
class="confirm-page-container-summary__action__name"
>
Sending ETH
</span>
</div>
</div>
<div
class="confirm-page-container-summary__title"
>
<h3
class="box mm-text mm-text--heading-md mm-text--font-weight-normal mm-text--ellipsis box--flex-direction-row box--color-text-default"
>
<div
class="currency-display-component"
title="0"
>
<span
class="currency-display-component__prefix"
>
<i
class="fab fa-ethereum"
style="font-size: 24px;"
/>
</span>
<span
class="currency-display-component__text"
>
0
</span>
</div>
</h3>
</div>
</div>
<div
class="confirm-page-container-content__details"
>
<div
class="transaction-alerts"
>
<div
class="box mm-banner-base mm-banner-alert mm-banner-alert--severity-warning box--padding-3 box--padding-left-2 box--display-flex box--gap-2 box--flex-direction-row box--background-color-warning-muted box--rounded-sm"
>
<span
class="box mm-icon mm-icon--size-lg box--display-inline-block box--flex-direction-row box--color-warning-default"
style="mask-image: url('./images/icons/warning.svg');"
/>
<div>
<p
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
Network is busy. Gas prices are high and estimates are less accurate.
</p>
</div>
</div>
</div>
<div
class="transaction-detail"
>
<div
class="box box--padding-top-5 box--display-flex box--flex-direction-row box--justify-content-flex-end"
>
<div
class="edit-gas-fee-button"
>
<button
data-testid="edit-gas-fee-button"
>
<span
class="edit-gas-fee-button__icon"
>
🌐
</span>
<span
class="edit-gas-fee-button__label"
>
Site suggested
</span>
<span
class="box mm-icon mm-icon--size-xs box--display-inline-block box--flex-direction-row box--color-primary-default"
style="mask-image: url('./images/icons/arrow-right.svg');"
/>
</button>
<div
class="info-tooltip"
>
<div>
<div
aria-describedby="tippy-tooltip-2"
class="info-tooltip__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 0C2.2 0 0 2.2 0 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 2c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.2-.7-.6.3-.8.7-.8zm.7 6H4.3V4.3h1.5V8z"
fill="var(--color-icon-alternative)"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div
class="transaction-detail-rows"
>
<div
class="transaction-detail-item"
>
<div
class="transaction-detail-item__row"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--display-flex box--flex-direction-row box--flex-wrap-nowrap box--align-items-center typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
<div
class="box box--display-flex box--flex-direction-row"
>
<div
class="box box--margin-right-1 box--flex-direction-row"
>
Gas
</div>
<span
class="gas-details-item-title__estimate"
>
(
estimated
)
</span>
<div
class="info-tooltip"
>
<div>
<div
aria-describedby="tippy-tooltip-3"
class="info-tooltip__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 0C2.2 0 0 2.2 0 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 2c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.2-.7-.6.3-.8.7-.8zm.7 6H4.3V4.3h1.5V8z"
fill="var(--color-icon-alternative)"
/>
</svg>
</div>
</div>
</div>
</div>
</h6>
<div
class="transaction-detail-item__detail-values"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h6 typography--weight-normal typography--style-normal typography--color-text-alternative"
>
<div
class="gas-details-item__currency-container"
>
<div
class="currency-display-component"
title="0.00021"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.00021
</span>
</div>
</div>
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
<div
class="gas-details-item__currency-container"
>
<div
class="currency-display-component"
title="0.00021 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.00021
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</h6>
</div>
</div>
<div
class="transaction-detail-item__row"
>
<div>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography gas-timing gas-timing--positive typography--h7 typography--weight-normal typography--style-normal typography--color-text-default"
>
Unknown processing time
</h6>
</div>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative"
>
<div
class="box gas-details-item__gasfee-label box--display-inline-flex box--flex-direction-row"
>
<div
class="box box--margin-right-1 box--flex-direction-row"
>
<strong>
Max fee:
</strong>
</div>
<div
class="gas-details-item__currency-container"
>
<div
class="currency-display-component"
title="0.00021 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.00021
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</div>
</h6>
</div>
</div>
<div
class="transaction-detail-item"
>
<div
class="transaction-detail-item__row"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--display-flex box--flex-direction-row box--flex-wrap-nowrap box--align-items-center typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Total
</h6>
<div
class="transaction-detail-item__detail-values"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h6 typography--weight-normal typography--style-normal typography--color-text-alternative"
>
<div
class="confirm-page-container-content__total-value"
>
<div
class="currency-display-component"
title="0.00021"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.00021
</span>
</div>
</div>
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
<div
class="confirm-page-container-content__total-value"
>
<div
class="currency-display-component"
title="0.00021 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.00021
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</h6>
</div>
</div>
<div
class="transaction-detail-item__row"
>
<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-text-alternative"
>
Amount + gas fee
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative"
>
<div
class="confirm-page-container-content__total-amount"
>
<strong>
Max amount:
</strong>
<div
class="currency-display-component"
title="0.00021 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.00021
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</h6>
</div>
</div>
</div>
</div>
</div>
<div
class="page-container__footer"
>
<footer>
<button
class="button btn--rounded btn-secondary page-container__footer-button page-container__footer-button__cancel"
data-testid="page-container-footer-cancel"
role="button"
tabindex="0"
>
Reject
</button>
<button
class="button btn--rounded btn-primary page-container__footer-button"
data-testid="page-container-footer-next"
role="button"
tabindex="0"
>
Confirm
</button>
</footer>
<div
class="page-container__footer-secondary"
>
<a>
Reject 2 transactions
</a>
</div>
</div>
</div>
</div>,
]
`;

View File

@ -1,33 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ConfirmTransactionBase from '../confirm-transaction-base';
import { SEND_ROUTE } from '../../helpers/constants/routes';
export default class ConfirmSendEther extends Component {
static contextTypes = {
t: PropTypes.func,
};
static propTypes = {
editTransaction: PropTypes.func,
history: PropTypes.object,
};
handleEdit({ txData }) {
const { editTransaction, history } = this.props;
editTransaction(txData).then(() => {
history.push(SEND_ROUTE);
});
}
render() {
return (
<ConfirmTransactionBase
actionKey="confirm"
onEdit={(confirmTransactionData) =>
this.handleEdit(confirmTransactionData)
}
/>
);
}
}

View File

@ -1,22 +0,0 @@
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { editExistingTransaction } from '../../ducks/send';
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { AssetType } from '../../../shared/constants/transaction';
import ConfirmSendEther from './confirm-send-ether.component';
const mapDispatchToProps = (dispatch) => {
return {
editTransaction: async (txData) => {
const { id } = txData;
await dispatch(editExistingTransaction(AssetType.native, id.toString()));
dispatch(clearConfirmTransaction());
},
};
};
export default compose(
withRouter,
connect(undefined, mapDispatchToProps),
)(ConfirmSendEther);

View File

@ -0,0 +1,35 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AssetType } from '../../../shared/constants/transaction';
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { editExistingTransaction } from '../../ducks/send';
import { SEND_ROUTE } from '../../helpers/constants/routes';
import ConfirmTransactionBase from '../confirm-transaction-base';
const ConfirmSendEther = () => {
const dispatch = useDispatch();
const history = useHistory();
const editTransaction = async (txData) => {
const { id } = txData;
await dispatch(editExistingTransaction(AssetType.native, id.toString()));
dispatch(clearConfirmTransaction());
};
const handleEdit = ({ txData }) => {
editTransaction(txData).then(() => {
history.push(SEND_ROUTE);
});
};
return (
<ConfirmTransactionBase
actionKey="confirm"
onEdit={(confirmTransactionData) => handleEdit(confirmTransactionData)}
/>
);
};
export default ConfirmSendEther;

View File

@ -1,26 +0,0 @@
import React from 'react';
import ConfirmSendEther from '.';
// eslint-disable-next-line import/no-anonymous-default-export
export default {
title: 'Pages/ConfirmSendEther',
component: ConfirmSendEther,
argTypes: {
editTransaction: {
action: 'editTransaction',
},
history: {
control: 'object',
},
txParams: {
control: 'object',
},
},
};
export const DefaultStory = (args) => {
return <ConfirmSendEther {...args} />;
};
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Provider } from 'react-redux';
import mockState from '../../../test/data/mock-state.json';
import configureStore from '../../store/store';
import ConfirmSendEther from './confirm-send-ether';
const sendEther = {
id: 9597986287241458,
time: 1681203297082,
status: 'unapproved',
metamaskNetworkId: '5',
originalGasEstimate: '0x5208',
userEditedGasLimit: false,
chainId: '0x5',
loadingDefaults: false,
dappSuggestedGasFees: {
maxPriorityFeePerGas: '0x3b9aca00',
maxFeePerGas: '0x2540be400',
},
sendFlowHistory: [],
txParams: {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
value: '0x0',
gas: '0x5208',
maxFeePerGas: '0x2540be400',
maxPriorityFeePerGas: '0x3b9aca00',
},
origin: 'https://metamask.github.io',
actionId: 1830698773,
type: 'simpleSend',
securityProviderResponse: null,
userFeeLevel: 'dappSuggested',
defaultGasEstimates: {
estimateType: 'dappSuggested',
gas: '0x5208',
maxFeePerGas: '0x2540be400',
maxPriorityFeePerGas: '0x3b9aca00',
},
};
mockState.metamask.unapprovedTxs[sendEther.id] = sendEther;
mockState.confirmTransaction = {
txData: sendEther,
};
const store = configureStore(mockState);
export default {
title: 'Pages/ConfirmSendEther',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};
export const DefaultStory = () => {
return <ConfirmSendEther />;
};
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,64 @@
import React from 'react';
import { renderWithProvider } from '../../../test/lib/render-helpers';
import { setBackgroundConnection } from '../../../test/jest';
import mockState from '../../../test/data/mock-state.json';
import configureStore from '../../store/store';
import ConfirmSendEther from './confirm-send-ether';
setBackgroundConnection({
getGasFeeTimeEstimate: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(),
promisifiedBackground: jest.fn(),
tryReverseResolveAddress: jest.fn(),
getNextNonce: jest.fn(),
addKnownMethodData: jest.fn(),
});
const sendEther = {
id: 9597986287241458,
time: 1681203297082,
status: 'unapproved',
metamaskNetworkId: '5',
originalGasEstimate: '0x5208',
userEditedGasLimit: false,
chainId: '0x5',
loadingDefaults: false,
dappSuggestedGasFees: {
maxPriorityFeePerGas: '0x3b9aca00',
maxFeePerGas: '0x2540be400',
},
sendFlowHistory: [],
txParams: {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
value: '0x0',
gas: '0x5208',
maxFeePerGas: '0x2540be400',
maxPriorityFeePerGas: '0x3b9aca00',
},
origin: 'https://metamask.github.io',
actionId: 1830698773,
type: 'simpleSend',
securityProviderResponse: null,
userFeeLevel: 'dappSuggested',
defaultGasEstimates: {
estimateType: 'dappSuggested',
gas: '0x5208',
maxFeePerGas: '0x2540be400',
maxPriorityFeePerGas: '0x3b9aca00',
},
};
mockState.metamask.unapprovedTxs[sendEther.id] = sendEther;
mockState.confirmTransaction = {
txData: sendEther,
};
const store = configureStore(mockState);
describe('ConfirmSendEther', () => {
it('should render correct information for for confirm send ether', () => {
const { getAllByTestId } = renderWithProvider(<ConfirmSendEther />, store);
expect(getAllByTestId('page-container')).toMatchSnapshot();
});
});

View File

@ -1 +1 @@
export { default } from './confirm-send-ether.container'; export { default } from './confirm-send-ether';

View File

@ -126,6 +126,7 @@ export default function ConfirmTokenTransactionBase({
assetName || `${getTitleTokenDescription('text')} #${tokenId}`; assetName || `${getTitleTokenDescription('text')} #${tokenId}`;
} else if (assetStandard === TokenStandard.ERC20) { } else if (assetStandard === TokenStandard.ERC20) {
title = `${tokenAmount} ${tokenSymbol}`; title = `${tokenAmount} ${tokenSymbol}`;
subtotalDisplay = `${tokenAmount} ${tokenSymbol}`;
} }
const hexWeiValue = useMemo(() => { const hexWeiValue = useMemo(() => {

View File

@ -813,6 +813,7 @@ export default class ConfirmTransactionBase extends Component {
image, image,
isApprovalOrRejection, isApprovalOrRejection,
assetStandard, assetStandard,
title,
} = this.props; } = this.props;
const { const {
submitting, submitting,
@ -873,6 +874,7 @@ export default class ConfirmTransactionBase extends Component {
showEdit={!isContractInteractionFromDapp && Boolean(onEdit)} showEdit={!isContractInteractionFromDapp && Boolean(onEdit)}
action={functionType} action={functionType}
image={image} image={image}
title={title}
titleComponent={this.renderTitleComponent()} titleComponent={this.renderTitleComponent()}
subtitleComponent={this.renderSubtitleComponent()} subtitleComponent={this.renderSubtitleComponent()}
detailsComponent={this.renderDetails()} detailsComponent={this.renderDetails()}

View File

@ -7,7 +7,6 @@ import {
ETH_GAS_PRICE_FETCH_WARNING_KEY, ETH_GAS_PRICE_FETCH_WARNING_KEY,
GAS_PRICE_FETCH_FAILURE_ERROR_KEY, GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
GAS_PRICE_EXCESSIVE_ERROR_KEY, GAS_PRICE_EXCESSIVE_ERROR_KEY,
INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY,
} from '../../../helpers/constants/error-keys'; } from '../../../helpers/constants/error-keys';
import { AssetType } from '../../../../shared/constants/transaction'; import { AssetType } from '../../../../shared/constants/transaction';
import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common'; import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common';
@ -30,7 +29,6 @@ export default class SendContent extends Component {
isEthGasPrice: PropTypes.bool, isEthGasPrice: PropTypes.bool,
noGasPrice: PropTypes.bool, noGasPrice: PropTypes.bool,
networkOrAccountNotSupports1559: PropTypes.bool, networkOrAccountNotSupports1559: PropTypes.bool,
getIsBalanceInsufficient: PropTypes.bool,
asset: PropTypes.object, asset: PropTypes.object,
assetError: PropTypes.string, assetError: PropTypes.string,
recipient: PropTypes.object, recipient: PropTypes.object,
@ -46,7 +44,6 @@ export default class SendContent extends Component {
isEthGasPrice, isEthGasPrice,
noGasPrice, noGasPrice,
networkOrAccountNotSupports1559, networkOrAccountNotSupports1559,
getIsBalanceInsufficient,
asset, asset,
assetError, assetError,
recipient, recipient,
@ -58,8 +55,6 @@ export default class SendContent extends Component {
gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY; gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY;
} else if (noGasPrice) { } else if (noGasPrice) {
gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY; gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY;
} else if (getIsBalanceInsufficient) {
gasError = INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY;
} }
const showHexData = const showHexData =
this.props.showHexData && this.props.showHexData &&

View File

@ -4,7 +4,6 @@ import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { renderWithProvider } from '../../../../test/lib/render-helpers';
import mockSendState from '../../../../test/data/mock-send-state.json'; import mockSendState from '../../../../test/data/mock-send-state.json';
import { INSUFFICIENT_FUNDS_ERROR } from '../send.constants';
import SendContent from '.'; import SendContent from '.';
jest.mock('../../../store/actions', () => ({ jest.mock('../../../store/actions', () => ({
@ -148,41 +147,6 @@ describe('SendContent Component', () => {
expect(gasWarning).toBeInTheDocument(); expect(gasWarning).toBeInTheDocument();
}); });
}); });
it('should show gas warning for gas error state in draft transaction', async () => {
const props = {
gasIsExcessive: false,
showHexData: false,
};
const gasErrorState = {
...mockSendState,
send: {
...mockSendState.send,
draftTransactions: {
'1-tx': {
...mockSendState.send.draftTransactions['1-tx'],
gas: {
error: INSUFFICIENT_FUNDS_ERROR,
},
},
},
},
};
const mockStore = configureMockStore()(gasErrorState);
const { queryByTestId } = renderWithProvider(
<SendContent {...props} />,
mockStore,
);
const gasWarning = queryByTestId('gas-warning-message');
await waitFor(() => {
expect(gasWarning).toBeInTheDocument();
});
});
}); });
describe('Recipient Warning', () => { describe('Recipient Warning', () => {

View File

@ -8,6 +8,7 @@ import {
} from '../../../helpers/constants/routes'; } from '../../../helpers/constants/routes';
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics';
import { SEND_STAGES } from '../../../ducks/send'; import { SEND_STAGES } from '../../../ducks/send';
import { INSUFFICIENT_FUNDS_ERROR } from '../send.constants';
export default class SendFooter extends Component { export default class SendFooter extends Component {
static propTypes = { static propTypes = {
@ -92,12 +93,14 @@ export default class SendFooter extends Component {
render() { render() {
const { t } = this.context; const { t } = this.context;
const { sendStage } = this.props; const { sendStage, sendErrors } = this.props;
return ( return (
<PageContainerFooter <PageContainerFooter
onCancel={() => this.onCancel()} onCancel={() => this.onCancel()}
onSubmit={(e) => this.onSubmit(e)} onSubmit={(e) => this.onSubmit(e)}
disabled={this.props.disabled} disabled={
this.props.disabled && sendErrors.gasFee !== INSUFFICIENT_FUNDS_ERROR
}
cancelText={sendStage === SEND_STAGES.EDIT ? t('reject') : t('cancel')} cancelText={sendStage === SEND_STAGES.EDIT ? t('reject') : t('cancel')}
/> />
); );

View File

@ -35,7 +35,6 @@ import {
getUnapprovedTxCount, getUnapprovedTxCount,
getUnapprovedTransactions, getUnapprovedTransactions,
getUseCurrencyRateCheck, getUseCurrencyRateCheck,
isHardwareWallet,
} from '../../selectors'; } from '../../selectors';
import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network'; import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
import { import {
@ -95,6 +94,7 @@ export default function TokenAllowance({
currentTokenBalance, currentTokenBalance,
toAddress, toAddress,
tokenSymbol, tokenSymbol,
fromAddressIsLedger,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -105,6 +105,7 @@ export default function TokenAllowance({
const thisOriginIsAllowedToSkipFirstPage = ALLOWED_HOSTS.includes(hostname); const thisOriginIsAllowedToSkipFirstPage = ALLOWED_HOSTS.includes(hostname);
const [showContractDetails, setShowContractDetails] = useState(false); const [showContractDetails, setShowContractDetails] = useState(false);
const [inputChangeInProgress, setInputChangeInProgress] = useState(false);
const [showFullTxDetails, setShowFullTxDetails] = useState(false); const [showFullTxDetails, setShowFullTxDetails] = useState(false);
const [isFirstPage, setIsFirstPage] = useState( const [isFirstPage, setIsFirstPage] = useState(
dappProposedTokenAmount !== '0' && !thisOriginIsAllowedToSkipFirstPage, dappProposedTokenAmount !== '0' && !thisOriginIsAllowedToSkipFirstPage,
@ -122,7 +123,6 @@ export default function TokenAllowance({
const unapprovedTxCount = useSelector(getUnapprovedTxCount); const unapprovedTxCount = useSelector(getUnapprovedTxCount);
const unapprovedTxs = useSelector(getUnapprovedTransactions); const unapprovedTxs = useSelector(getUnapprovedTransactions);
const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
const isHardwareWalletConnected = useSelector(isHardwareWallet);
let customTokenAmount = useSelector(getCustomTokenAmount); let customTokenAmount = useSelector(getCustomTokenAmount);
if (thisOriginIsAllowedToSkipFirstPage && dappProposedTokenAmount) { if (thisOriginIsAllowedToSkipFirstPage && dappProposedTokenAmount) {
customTokenAmount = dappProposedTokenAmount; customTokenAmount = dappProposedTokenAmount;
@ -403,12 +403,14 @@ export default function TokenAllowance({
<Box margin={[4, 4, 3, 4]}> <Box margin={[4, 4, 3, 4]}>
{isFirstPage ? ( {isFirstPage ? (
<CustomSpendingCap <CustomSpendingCap
txParams={txData?.txParams}
tokenName={tokenSymbol} tokenName={tokenSymbol}
currentTokenBalance={currentTokenBalance} currentTokenBalance={currentTokenBalance}
dappProposedValue={dappProposedTokenAmount} dappProposedValue={dappProposedTokenAmount}
siteOrigin={origin} siteOrigin={origin}
passTheErrorText={(value) => setErrorText(value)} passTheErrorText={(value) => setErrorText(value)}
decimals={decimals} decimals={decimals}
setInputChangeInProgress={setInputChangeInProgress}
/> />
) : ( ) : (
<ReviewSpendingCap <ReviewSpendingCap
@ -516,7 +518,7 @@ export default function TokenAllowance({
</Box> </Box>
</Box> </Box>
) : null} ) : null}
{!isFirstPage && isHardwareWalletConnected && ( {!isFirstPage && fromAddressIsLedger && (
<Box paddingLeft={2} paddingRight={2}> <Box paddingLeft={2} paddingRight={2}>
<LedgerInstructionField showDataInstruction /> <LedgerInstructionField showDataInstruction />
</Box> </Box>
@ -526,7 +528,9 @@ export default function TokenAllowance({
submitText={isFirstPage ? t('next') : t('approveButtonText')} submitText={isFirstPage ? t('next') : t('approveButtonText')}
onCancel={() => handleReject()} onCancel={() => handleReject()}
onSubmit={() => (isFirstPage ? handleNextClick() : handleApprove())} onSubmit={() => (isFirstPage ? handleNextClick() : handleApprove())}
disabled={disableNextButton || disableApproveButton} disabled={
inputChangeInProgress || disableNextButton || disableApproveButton
}
> >
{unapprovedTxCount > 1 && ( {unapprovedTxCount > 1 && (
<Button <Button
@ -643,4 +647,8 @@ TokenAllowance.propTypes = {
* Symbol of the token that is waiting to be allowed * Symbol of the token that is waiting to be allowed
*/ */
tokenSymbol: PropTypes.string, tokenSymbol: PropTypes.string,
/**
* Whether the address sending the transaction is a ledger address
*/
fromAddressIsLedger: PropTypes.bool,
}; };

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import { fireEvent } from '@testing-library/react'; import { act, fireEvent } from '@testing-library/react';
import { renderWithProvider } from '../../../test/lib/render-helpers'; import { renderWithProvider } from '../../../test/lib/render-helpers';
import { KeyringType } from '../../../shared/constants/keyring'; import { KeyringType } from '../../../shared/constants/keyring';
import TokenAllowance from './token-allowance'; import TokenAllowance from './token-allowance';
@ -65,10 +65,10 @@ const state = {
}, },
], ],
unapprovedTxs: {}, unapprovedTxs: {},
keyringTypes: [KeyringType.ledger], keyringTypes: [],
keyrings: [ keyrings: [
{ {
type: KeyringType.ledger, type: KeyringType.hdKeyTree,
accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
}, },
], ],
@ -96,12 +96,14 @@ jest.mock('../../store/actions', () => ({
updatePreviousGasParams: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }), updatePreviousGasParams: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }),
createTransactionEventFragment: jest.fn(), createTransactionEventFragment: jest.fn(),
updateCustomNonce: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }), updateCustomNonce: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }),
estimateGas: jest.fn().mockImplementation(() => Promise.resolve()),
})); }));
jest.mock('../../contexts/gasFee', () => ({ jest.mock('../../contexts/gasFee', () => ({
useGasFeeContext: () => ({ useGasFeeContext: () => ({
maxPriorityFeePerGas: '0.1', maxPriorityFeePerGas: '0.1',
maxFeePerGas: '0.1', maxFeePerGas: '0.1',
updateTransaction: jest.fn(),
}), }),
})); }));
@ -215,8 +217,10 @@ describe('TokenAllowancePage', () => {
store, store,
); );
const useDefault = getByText('Use default'); act(() => {
fireEvent.click(useDefault); const useDefault = getByText('Use default');
fireEvent.click(useDefault);
});
const input = getByTestId('custom-spending-cap-input'); const input = getByTestId('custom-spending-cap-input');
expect(input.value).toBe('1'); expect(input.value).toBe('1');
@ -256,9 +260,9 @@ describe('TokenAllowancePage', () => {
expect(gotIt).not.toBeInTheDocument(); expect(gotIt).not.toBeInTheDocument();
}); });
it('should show hardware wallet info text', () => { it('should show ledger info text if the sending address is ledger', () => {
const { queryByText, getByText, getByTestId } = renderWithProvider( const { queryByText, getByText, getByTestId } = renderWithProvider(
<TokenAllowance {...props} />, <TokenAllowance {...props} fromAddressIsLedger />,
store, store,
); );
@ -273,12 +277,20 @@ describe('TokenAllowancePage', () => {
expect(queryByText('Prior to clicking confirm:')).toBeInTheDocument(); expect(queryByText('Prior to clicking confirm:')).toBeInTheDocument();
}); });
it('should not show hardware wallet info text', () => { it('should not show ledger info text if the sending address is not ledger', () => {
const { queryByText } = renderWithProvider( const { queryByText, getByText, getByTestId } = renderWithProvider(
<TokenAllowance {...props} />, <TokenAllowance {...props} fromAddressIsLedger={false} />,
store, store,
); );
const textField = getByTestId('custom-spending-cap-input');
fireEvent.change(textField, { target: { value: '1' } });
expect(queryByText('Prior to clicking confirm:')).toBeNull();
const nextButton = getByText('Next');
fireEvent.click(nextButton);
expect(queryByText('Prior to clicking confirm:')).toBeNull(); expect(queryByText('Prior to clicking confirm:')).toBeNull();
}); });