From 00d2d43d45de982722a8132b8bbf933f4e00975e Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 18 Apr 2023 05:58:28 +0530 Subject: [PATCH 01/13] Fix error in console on rejecting signature request (#18614) --- app/scripts/controllers/sign.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/sign.ts b/app/scripts/controllers/sign.ts index d27de2f0605d..e042f831ce0e 100644 --- a/app/scripts/controllers/sign.ts +++ b/app/scripts/controllers/sign.ts @@ -412,27 +412,30 @@ export default class SignController extends BaseControllerV2< * Used to cancel a message submitted via eth_sign. * * @param msgId - The id of the message to cancel. + * @returns A full state update. */ cancelMessage(msgId: string) { - this._cancelAbstractMessage(this._messageManager, msgId); + return this._cancelAbstractMessage(this._messageManager, msgId); } /** * Used to cancel a personal_sign type message. * * @param msgId - The ID of the message to cancel. + * @returns A full state update. */ cancelPersonalMessage(msgId: string) { - this._cancelAbstractMessage(this._personalMessageManager, msgId); + return this._cancelAbstractMessage(this._personalMessageManager, msgId); } /** * Used to cancel a eth_signTypedData type message. * * @param msgId - The ID of the message to cancel. + * @returns A full state update. */ cancelTypedMessage(msgId: string) { - this._cancelAbstractMessage(this._typedMessageManager, msgId); + return this._cancelAbstractMessage(this._typedMessageManager, msgId); } /** From 09d00e1e4530161a45f375ca4b92f3d584c9ae0d Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 18 Apr 2023 05:59:06 +0530 Subject: [PATCH 02/13] Refactor confirm-send-ether into functional component (#18527) --- .storybook/main.js | 2 +- .../confirm-send-ether.test.js.snap | 677 ++++++++++++++++++ .../confirm-send-ether.component.js | 33 - .../confirm-send-ether.container.js | 22 - .../confirm-send-ether/confirm-send-ether.js | 35 + .../confirm-send-ether.stories-to-do.js | 26 - .../confirm-send-ether.stories.js | 58 ++ .../confirm-send-ether.test.js | 64 ++ ui/pages/confirm-send-ether/index.js | 2 +- 9 files changed, 836 insertions(+), 83 deletions(-) create mode 100644 ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap delete mode 100644 ui/pages/confirm-send-ether/confirm-send-ether.component.js delete mode 100644 ui/pages/confirm-send-ether/confirm-send-ether.container.js create mode 100644 ui/pages/confirm-send-ether/confirm-send-ether.js delete mode 100644 ui/pages/confirm-send-ether/confirm-send-ether.stories-to-do.js create mode 100644 ui/pages/confirm-send-ether/confirm-send-ether.stories.js create mode 100644 ui/pages/confirm-send-ether/confirm-send-ether.test.js diff --git a/.storybook/main.js b/.storybook/main.js index d14c65f90987..635eae9bee44 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -54,7 +54,7 @@ module.exports = { os: false, path: false, stream: require.resolve('stream-browserify'), - _stream_transform: false, + _stream_transform: require.resolve('readable-stream/lib/_stream_transform.js'), }; config.module.strictExportPresence = true; config.module.rules.push({ diff --git a/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap b/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap new file mode 100644 index 000000000000..b42547829b4f --- /dev/null +++ b/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap @@ -0,0 +1,677 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmSendEther should render correct information for for confirm send ether 1`] = ` +[ +
+
+ +
+
+ 0 + + of + + 2 +
+
+ requests waiting to be acknowledged +
+
+
+ + +
+
+
+
+
+ + + Edit + +
+
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+
+
+ Test Account +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+
+
+ 0x0c5...AaFb +
+
+
+
+
+
+
+ +
+
+ + https://metamask.github.io + +
+
+
+ + Sending ETH + +
+
+
+

+
+ + + + + 0 + +
+

+
+
+
+
+
+ +
+

+ Network is busy. Gas prices are high and estimates are less accurate. +

+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+ Gas +
+ + ( + estimated + ) + +
+
+
+ + + +
+
+
+
+
+
+
+
+
+ + + 0.00021 + +
+
+
+
+
+
+ + + 0.00021 + + + ETH + +
+
+
+
+
+
+
+
+ Unknown processing time +
+
+
+
+
+ + Max fee: + +
+
+
+ + + 0.00021 + + + ETH + +
+
+
+
+
+
+
+
+
+ Total +
+
+
+
+
+ + + 0.00021 + +
+
+
+
+
+
+ + + 0.00021 + + + ETH + +
+
+
+
+
+
+
+ Amount + gas fee +
+
+
+ + Max amount: + + +
+ + + 0.00021 + + + ETH + +
+
+
+
+
+
+
+
+ +
+
, +] +`; diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/pages/confirm-send-ether/confirm-send-ether.component.js deleted file mode 100644 index 02b113010fd8..000000000000 --- a/ui/pages/confirm-send-ether/confirm-send-ether.component.js +++ /dev/null @@ -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 ( - - this.handleEdit(confirmTransactionData) - } - /> - ); - } -} diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/pages/confirm-send-ether/confirm-send-ether.container.js deleted file mode 100644 index 9bca08091290..000000000000 --- a/ui/pages/confirm-send-ether/confirm-send-ether.container.js +++ /dev/null @@ -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); diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.js b/ui/pages/confirm-send-ether/confirm-send-ether.js new file mode 100644 index 000000000000..04a207f9ef8f --- /dev/null +++ b/ui/pages/confirm-send-ether/confirm-send-ether.js @@ -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 ( + handleEdit(confirmTransactionData)} + /> + ); +}; + +export default ConfirmSendEther; diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.stories-to-do.js b/ui/pages/confirm-send-ether/confirm-send-ether.stories-to-do.js deleted file mode 100644 index fe3d1314635a..000000000000 --- a/ui/pages/confirm-send-ether/confirm-send-ether.stories-to-do.js +++ /dev/null @@ -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 ; -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.stories.js b/ui/pages/confirm-send-ether/confirm-send-ether.stories.js new file mode 100644 index 000000000000..3f29014c21a0 --- /dev/null +++ b/ui/pages/confirm-send-ether/confirm-send-ether.stories.js @@ -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) => {story()}], +}; + +export const DefaultStory = () => { + return ; +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.test.js b/ui/pages/confirm-send-ether/confirm-send-ether.test.js new file mode 100644 index 000000000000..2ecab5826b82 --- /dev/null +++ b/ui/pages/confirm-send-ether/confirm-send-ether.test.js @@ -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(, store); + expect(getAllByTestId('page-container')).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirm-send-ether/index.js b/ui/pages/confirm-send-ether/index.js index eba4b48b1c4f..f57cbc84e29f 100644 --- a/ui/pages/confirm-send-ether/index.js +++ b/ui/pages/confirm-send-ether/index.js @@ -1 +1 @@ -export { default } from './confirm-send-ether.container'; +export { default } from './confirm-send-ether'; From ae0af1b2836afe130ca1965a603726ef533dce74 Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Tue, 18 Apr 2023 08:33:32 +0100 Subject: [PATCH 03/13] Adopt security provider request from core (#18520) --- app/scripts/controllers/sign.test.ts | 1 + app/scripts/controllers/sign.ts | 45 ++++--- app/scripts/lib/security-provider-helpers.js | 65 ---------- .../lib/security-provider-helpers.test.ts | 117 ++++++++++++++++++ app/scripts/lib/security-provider-helpers.ts | 92 ++++++++++++++ 5 files changed, 231 insertions(+), 89 deletions(-) delete mode 100644 app/scripts/lib/security-provider-helpers.js create mode 100644 app/scripts/lib/security-provider-helpers.test.ts create mode 100644 app/scripts/lib/security-provider-helpers.ts diff --git a/app/scripts/controllers/sign.test.ts b/app/scripts/controllers/sign.test.ts index dac749466cc7..078d99291a83 100644 --- a/app/scripts/controllers/sign.test.ts +++ b/app/scripts/controllers/sign.test.ts @@ -52,6 +52,7 @@ const messageMock = { const coreMessageMock = { ...messageMock, messageParams: messageParamsMock, + securityProviderResponse: securityProviderResponseMock, }; const stateMessageMock = { diff --git a/app/scripts/controllers/sign.ts b/app/scripts/controllers/sign.ts index e042f831ce0e..70c65596d6be 100644 --- a/app/scripts/controllers/sign.ts +++ b/app/scripts/controllers/sign.ts @@ -21,6 +21,7 @@ import { AbstractMessageParams, AbstractMessageParamsMetamask, OriginalRequest, + SecurityProviderRequest, } from '@metamask/message-manager/dist/AbstractMessageManager'; import { BaseControllerV2, @@ -63,9 +64,10 @@ export type CoreMessage = AbstractMessage & { messageParams: AbstractMessageParams; }; -export type StateMessage = Required & { +export type StateMessage = Required< + Omit +> & { msgParams: Required; - securityProviderResponse: any; }; export type SignControllerState = { @@ -107,10 +109,7 @@ export type SignControllerOptions = { preferencesController: PreferencesController; getState: () => any; metricsEvent: (payload: any, options?: any) => void; - securityProviderRequest: ( - requestData: any, - methodName: string, - ) => Promise; + securityProviderRequest: SecurityProviderRequest; }; /** @@ -143,11 +142,6 @@ export default class SignController extends BaseControllerV2< private _metricsEvent: (payload: any, options?: any) => void; - private _securityProviderRequest: ( - requestData: any, - methodName: string, - ) => Promise; - /** * Construct a Sign controller. * @@ -178,12 +172,23 @@ export default class SignController extends BaseControllerV2< this._preferencesController = preferencesController; this._getState = getState; this._metricsEvent = metricsEvent; - this._securityProviderRequest = securityProviderRequest; this.hub = new EventEmitter(); - this._messageManager = new MessageManager(); - this._personalMessageManager = new PersonalMessageManager(); - this._typedMessageManager = new TypedMessageManager(); + this._messageManager = new MessageManager( + undefined, + undefined, + securityProviderRequest, + ); + this._personalMessageManager = new PersonalMessageManager( + undefined, + undefined, + securityProviderRequest, + ); + this._typedMessageManager = new TypedMessageManager( + undefined, + undefined, + securityProviderRequest, + ); this._messageManagers = [ this._messageManager, @@ -589,15 +594,7 @@ export default class SignController extends BaseControllerV2< origin: messageParams.origin as string, }, }; - - const messageId = coreMessage.id; - const existingMessage = this._getMessage(messageId); - - const securityProviderResponse = existingMessage - ? existingMessage.securityProviderResponse - : await this._securityProviderRequest(stateMessage, stateMessage.type); - - return { ...stateMessage, securityProviderResponse }; + return stateMessage; } private _normalizeMsgData(data: string) { diff --git a/app/scripts/lib/security-provider-helpers.js b/app/scripts/lib/security-provider-helpers.js deleted file mode 100644 index 986165cd9877..000000000000 --- a/app/scripts/lib/security-provider-helpers.js +++ /dev/null @@ -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(); -} diff --git a/app/scripts/lib/security-provider-helpers.test.ts b/app/scripts/lib/security-provider-helpers.test.ts new file mode 100644 index 000000000000..6f3d13ef29a7 --- /dev/null +++ b/app/scripts/lib/security-provider-helpers.test.ts @@ -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'); + }, + ); +}); diff --git a/app/scripts/lib/security-provider-helpers.ts b/app/scripts/lib/security-provider-helpers.ts new file mode 100644 index 000000000000..4b4bca0d6914 --- /dev/null +++ b/app/scripts/lib/security-provider-helpers.ts @@ -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; + 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> { + 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(); +} From 76d79d9ccecc384d99f100b1c200139d706a0464 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Tue, 18 Apr 2023 12:46:38 +0200 Subject: [PATCH 04/13] [FLASK] Redesign `dropdown-tab` (#18546) --- .../ui/tabs/dropdown-tab/dropdown-tab.js | 65 ------- ui/components/ui/tabs/dropdown-tab/index.scss | 23 --- .../tabs/flask/dropdown-tab/dropdown-tab.js | 160 ++++++++++++++++++ .../flask/dropdown-tab/dropdown-tab.test.js | 48 ++++++ .../ui/tabs/{ => flask}/dropdown-tab/index.js | 0 ui/components/ui/tabs/index.js | 3 +- ui/components/ui/tabs/index.scss | 2 - ui/components/ui/tabs/tabs.stories.js | 2 +- ui/hooks/useTransactionInsights.js | 3 +- 9 files changed, 212 insertions(+), 94 deletions(-) delete mode 100644 ui/components/ui/tabs/dropdown-tab/dropdown-tab.js delete mode 100644 ui/components/ui/tabs/dropdown-tab/index.scss create mode 100644 ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.js create mode 100644 ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.test.js rename ui/components/ui/tabs/{ => flask}/dropdown-tab/index.js (100%) diff --git a/ui/components/ui/tabs/dropdown-tab/dropdown-tab.js b/ui/components/ui/tabs/dropdown-tab/dropdown-tab.js deleted file mode 100644 index 6da690afc6f2..000000000000 --- a/ui/components/ui/tabs/dropdown-tab/dropdown-tab.js +++ /dev/null @@ -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 ( - { - event.preventDefault(); - onClick(tabIndex); - }} - > - - - ); -}; - -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, -}; diff --git a/ui/components/ui/tabs/dropdown-tab/index.scss b/ui/components/ui/tabs/dropdown-tab/index.scss deleted file mode 100644 index 01ee7f077a27..000000000000 --- a/ui/components/ui/tabs/dropdown-tab/index.scss +++ /dev/null @@ -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; - } -} diff --git a/ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.js b/ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.js new file mode 100644 index 000000000000..bbb48602c70f --- /dev/null +++ b/ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.js @@ -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 ( + + + + {selectedOptionName} + + + + {isOpen && ( + + {options.map((option, i) => ( + selectOption(event, option)} + style={{ + cursor: 'pointer', + textTransform: 'none', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }} + > + {option.name} + + ))} + + )} + + ); +}; + +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, +}; diff --git a/ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.test.js b/ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.test.js new file mode 100644 index 000000000000..61b19a847a64 --- /dev/null +++ b/ui/components/ui/tabs/flask/dropdown-tab/dropdown-tab.test.js @@ -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(); + + expect(getByText(args.options[0].name)).toBeDefined(); + }); + + it('registers click', () => { + const { container } = render(); + + fireEvent.click(container.firstChild); + + expect(onClick).toHaveBeenCalledWith(args.tabIndex); + }); + + it('registers selection', () => { + const { container, getByText } = render(); + + 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); + }); +}); diff --git a/ui/components/ui/tabs/dropdown-tab/index.js b/ui/components/ui/tabs/flask/dropdown-tab/index.js similarity index 100% rename from ui/components/ui/tabs/dropdown-tab/index.js rename to ui/components/ui/tabs/flask/dropdown-tab/index.js diff --git a/ui/components/ui/tabs/index.js b/ui/components/ui/tabs/index.js index c20ebbfc1854..43366ec6f7e8 100644 --- a/ui/components/ui/tabs/index.js +++ b/ui/components/ui/tabs/index.js @@ -1,5 +1,4 @@ import Tabs from './tabs.component'; import Tab from './tab'; -import DropdownTab from './dropdown-tab'; -export { Tabs, Tab, DropdownTab }; +export { Tabs, Tab }; diff --git a/ui/components/ui/tabs/index.scss b/ui/components/ui/tabs/index.scss index 4398ddf501bb..20ee131b780a 100644 --- a/ui/components/ui/tabs/index.scss +++ b/ui/components/ui/tabs/index.scss @@ -1,5 +1,4 @@ @import 'tab/index'; -@import 'dropdown-tab/index'; .tabs { flex-grow: 1; @@ -11,6 +10,5 @@ position: sticky; top: 0; z-index: 2; - overflow: hidden; } } diff --git a/ui/components/ui/tabs/tabs.stories.js b/ui/components/ui/tabs/tabs.stories.js index d8d4c4b39d7a..09f19f0bc190 100644 --- a/ui/components/ui/tabs/tabs.stories.js +++ b/ui/components/ui/tabs/tabs.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import DropdownTab from './dropdown-tab'; +import DropdownTab from './flask/dropdown-tab'; import Tab from './tab/tab.component'; import Tabs from './tabs.component'; diff --git a/ui/hooks/useTransactionInsights.js b/ui/hooks/useTransactionInsights.js index ed7916c4f95a..d20dbfcc7630 100644 --- a/ui/hooks/useTransactionInsights.js +++ b/ui/hooks/useTransactionInsights.js @@ -5,7 +5,8 @@ import { CHAIN_ID_TO_NETWORK_ID_MAP } from '../../shared/constants/network'; import { stripHexPrefix } from '../../shared/modules/hexstring-utils'; import { TransactionType } from '../../shared/constants/transaction'; 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'; const isAllowedTransactionTypes = (transactionType) => From 66767d981c8e220c7a3b5d0bdd3578e683c01650 Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Tue, 18 Apr 2023 15:49:31 +0200 Subject: [PATCH 05/13] fix/18577: Add title back for approving ERC20 token (#18617) * fix/18577: Add title back for approving ERC20 token * Apply suggestions from code review Co-authored-by: Nidhi Kumari --------- Co-authored-by: Nidhi Kumari --- .../confirm-token-transaction-base.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js index e7f53a4dbdfc..ee0b5c869f6e 100644 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js @@ -126,6 +126,7 @@ export default function ConfirmTokenTransactionBase({ assetName || `${getTitleTokenDescription('text')} #${tokenId}`; } else if (assetStandard === TokenStandard.ERC20) { title = `${tokenAmount} ${tokenSymbol}`; + subtotalDisplay = `${tokenAmount} ${tokenSymbol}`; } const hexWeiValue = useMemo(() => { From e6f73f5fe9ee6c8a32150a2b35f4ffdaab01927e Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 18 Apr 2023 12:15:18 -0230 Subject: [PATCH 06/13] Pass correct params to fetchEstimatedL1Fee in the swaps controller (#18634) --- app/scripts/controllers/swaps.js | 1 + app/scripts/controllers/swaps.test.js | 70 ++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index ab6105ecfbc5..5030f1cd391f 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -304,6 +304,7 @@ export default class SwapsController { Object.values(newQuotes).map(async (quote) => { if (quote.trade) { const multiLayerL1TradeFeeTotal = await fetchEstimatedL1Fee( + chainId, { txParams: quote.trade, chainId, diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index 648f57cccc1d..f5a43e0dcab4 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -156,12 +156,12 @@ const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => { describe('SwapsController', function () { let provider; - const getSwapsController = () => { + const getSwapsController = (_provider = provider) => { return new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController: getMockNetworkController(), onNetworkDidChange: sinon.stub(), - provider, + provider: _provider, getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getTokenRatesState: MOCK_TOKEN_RATES_STORE, 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 () { fetchTradesInfoStub.resolves(getMockQuotes()); From 9d3cdd1b79d8754e79cb0bcd0e16c76766e09378 Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:10:32 -0400 Subject: [PATCH 07/13] Overriding gas estimate in the send page even if enough ETH is not available (#18554) --- .../send-content/send-content.component.js | 5 --- .../send-content.component.test.js | 36 ------------------- .../send/send-footer/send-footer.component.js | 7 ++-- 3 files changed, 5 insertions(+), 43 deletions(-) diff --git a/ui/pages/send/send-content/send-content.component.js b/ui/pages/send/send-content/send-content.component.js index b5f1c26e81d7..b57b59a515e0 100644 --- a/ui/pages/send/send-content/send-content.component.js +++ b/ui/pages/send/send-content/send-content.component.js @@ -7,7 +7,6 @@ import { ETH_GAS_PRICE_FETCH_WARNING_KEY, GAS_PRICE_FETCH_FAILURE_ERROR_KEY, GAS_PRICE_EXCESSIVE_ERROR_KEY, - INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY, } from '../../../helpers/constants/error-keys'; import { AssetType } from '../../../../shared/constants/transaction'; import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common'; @@ -30,7 +29,6 @@ export default class SendContent extends Component { isEthGasPrice: PropTypes.bool, noGasPrice: PropTypes.bool, networkOrAccountNotSupports1559: PropTypes.bool, - getIsBalanceInsufficient: PropTypes.bool, asset: PropTypes.object, assetError: PropTypes.string, recipient: PropTypes.object, @@ -46,7 +44,6 @@ export default class SendContent extends Component { isEthGasPrice, noGasPrice, networkOrAccountNotSupports1559, - getIsBalanceInsufficient, asset, assetError, recipient, @@ -58,8 +55,6 @@ export default class SendContent extends Component { gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY; } else if (noGasPrice) { gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY; - } else if (getIsBalanceInsufficient) { - gasError = INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY; } const showHexData = this.props.showHexData && diff --git a/ui/pages/send/send-content/send-content.component.test.js b/ui/pages/send/send-content/send-content.component.test.js index 3868da35e804..660ecf9bbdb2 100644 --- a/ui/pages/send/send-content/send-content.component.test.js +++ b/ui/pages/send/send-content/send-content.component.test.js @@ -4,7 +4,6 @@ import configureMockStore from 'redux-mock-store'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import mockSendState from '../../../../test/data/mock-send-state.json'; -import { INSUFFICIENT_FUNDS_ERROR } from '../send.constants'; import SendContent from '.'; jest.mock('../../../store/actions', () => ({ @@ -148,41 +147,6 @@ describe('SendContent Component', () => { 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( - , - mockStore, - ); - - const gasWarning = queryByTestId('gas-warning-message'); - - await waitFor(() => { - expect(gasWarning).toBeInTheDocument(); - }); - }); }); describe('Recipient Warning', () => { diff --git a/ui/pages/send/send-footer/send-footer.component.js b/ui/pages/send/send-footer/send-footer.component.js index f16ad7e09c69..82afae25741d 100644 --- a/ui/pages/send/send-footer/send-footer.component.js +++ b/ui/pages/send/send-footer/send-footer.component.js @@ -8,6 +8,7 @@ import { } from '../../../helpers/constants/routes'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { SEND_STAGES } from '../../../ducks/send'; +import { INSUFFICIENT_FUNDS_ERROR } from '../send.constants'; export default class SendFooter extends Component { static propTypes = { @@ -92,12 +93,14 @@ export default class SendFooter extends Component { render() { const { t } = this.context; - const { sendStage } = this.props; + const { sendStage, sendErrors } = this.props; return ( this.onCancel()} 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')} /> ); From c6aa4f38c55d4ad7b9ca8d9b2abbd8eba3d8ebd3 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 18 Apr 2023 21:22:16 +0530 Subject: [PATCH 08/13] Fix approve all warning modal (#18613) * Fix approve all warning modal * fix * fix * fix --- .../set-approval-for-all-warning.js | 2 +- ui/pages/confirm-approve/confirm-approve.js | 1 + .../confirm-transaction-base.component.js | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/components/app/set-approval-for-all-warning/set-approval-for-all-warning.js b/ui/components/app/set-approval-for-all-warning/set-approval-for-all-warning.js index 83baa2bde127..f6f0794f6a02 100644 --- a/ui/components/app/set-approval-for-all-warning/set-approval-for-all-warning.js +++ b/ui/components/app/set-approval-for-all-warning/set-approval-for-all-warning.js @@ -105,7 +105,7 @@ const SetApproveForAllWarning = ({ key="non_custodial_bold" className="set-approval-for-all-warning__content__bold" > - {t('nftWarningContentBold', [collectionName])} + {t('nftWarningContentBold', [collectionName || ''])} , {t('nftWarningContentGrey')} diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js index 6980d4862c79..38dfe3a2ec9d 100644 --- a/ui/pages/confirm-approve/confirm-approve.js +++ b/ui/pages/confirm-approve/confirm-approve.js @@ -144,6 +144,7 @@ export default function ConfirmApprove({ const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {}; + // Code below may need a additional look as ERC1155 tokens do not have a name let tokensText; if ( assetStandard === TokenStandard.ERC721 || diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index d4103634cbfe..2e9bfaa60db2 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -813,6 +813,7 @@ export default class ConfirmTransactionBase extends Component { image, isApprovalOrRejection, assetStandard, + title, } = this.props; const { submitting, @@ -873,6 +874,7 @@ export default class ConfirmTransactionBase extends Component { showEdit={!isContractInteractionFromDapp && Boolean(onEdit)} action={functionType} image={image} + title={title} titleComponent={this.renderTitleComponent()} subtitleComponent={this.renderSubtitleComponent()} detailsComponent={this.renderDetails()} From 1fe28ee15a4032eef9137fe8262542f0d730256b Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 18 Apr 2023 13:25:05 -0230 Subject: [PATCH 09/13] Make `setActiveNetwork` async (#18605) The network controller method `setActiveNetwork` is now async, and the asynchronous `_setProviderConfig` step is now awaited. The function will not resolve until the network has finished switching. This change affects the `eth_switchEthereumChain` and `eth_addEthereumChain` middleware, and it affects any network switching performed in our UI. Relates to https://github.com/MetaMask/metamask-extension/issues/18587 --- .../network/network-controller.test.ts | 152 +++++------------- .../controllers/network/network-controller.ts | 4 +- 2 files changed, 39 insertions(+), 117 deletions(-) diff --git a/app/scripts/controllers/network/network-controller.test.ts b/app/scripts/controllers/network/network-controller.test.ts index 503f23a72579..508d57fe7502 100644 --- a/app/scripts/controllers/network/network-controller.test.ts +++ b/app/scripts/controllers/network/network-controller.test.ts @@ -1192,7 +1192,7 @@ describe('NetworkController', () => { }); expect(oldChainIdResult).toBe('0x5'); - controller.setActiveNetwork('testNetworkConfigurationId'); + await controller.setActiveNetwork('testNetworkConfigurationId'); const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( provider, ); @@ -2431,15 +2431,9 @@ describe('NetworkController', () => { }, }, beforeCompleting: async () => { - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }); + await controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); }, }, ], @@ -2506,15 +2500,9 @@ describe('NetworkController', () => { network1.mockEssentialRpcCalls({ eth_getBlockByNumber: { beforeCompleting: async () => { - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }); + await controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); }, }, net_version: { @@ -2578,15 +2566,9 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, eth_getBlockByNumber: { beforeCompleting: async () => { - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }); + await controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); }, }, }); @@ -4032,9 +4014,9 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - expect(() => + await expect(() => controller.setActiveNetwork('invalid-network-configuration-id'), - ).toThrow( + ).rejects.toThrow( new Error( 'networkConfigurationId invalid-network-configuration-id does not match a configured networkConfiguration', ), @@ -4075,7 +4057,7 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - controller.setActiveNetwork('testNetworkConfigurationId1'); + await controller.setActiveNetwork('testNetworkConfigurationId1'); expect(controller.store.getState().provider).toStrictEqual({ type: 'rpc', @@ -4144,6 +4126,8 @@ describe('NetworkController', () => { messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkWillChange, operation: () => { + // Intentionally not awaited because we're checking state + // partway through the operation controller.setActiveNetwork('testNetworkConfigurationId2'); }, beforeResolving: () => { @@ -4208,6 +4192,8 @@ describe('NetworkController', () => { // before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we're checking state + // partway through the operation. controller.setActiveNetwork('testNetworkConfigurationId1'); }, }); @@ -4267,6 +4253,8 @@ describe('NetworkController', () => { // before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we're checking state + // partway through the operation controller.setActiveNetwork('testNetworkConfigurationId2'); }, }); @@ -4308,7 +4296,7 @@ describe('NetworkController', () => { }, }); - controller.setActiveNetwork('testNetworkConfigurationId'); + await controller.setActiveNetwork('testNetworkConfigurationId'); const { provider } = controller.getProviderAndBlockTracker(); assert(provider, 'Provider is somehow unset'); @@ -4356,7 +4344,7 @@ describe('NetworkController', () => { const { provider: providerBefore } = controller.getProviderAndBlockTracker(); - controller.setActiveNetwork('testNetworkConfigurationId'); + await controller.setActiveNetwork('testNetworkConfigurationId'); const { provider: providerAfter } = controller.getProviderAndBlockTracker(); @@ -4392,8 +4380,8 @@ describe('NetworkController', () => { const networkDidChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkDidChange, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); + operation: async () => { + await controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -4429,8 +4417,8 @@ describe('NetworkController', () => { const infuraIsUnblocked = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.InfuraIsUnblocked, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); + operation: async () => { + await controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -4462,13 +4450,7 @@ describe('NetworkController', () => { response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); expect(controller.store.getState().networkStatus).toBe('available'); }, @@ -4501,16 +4483,7 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, }); - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - // setActiveNetwork clears networkDetails first, and then updates it - // to what we expect it to be - count: 2, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); expect(controller.store.getState().networkDetails).toStrictEqual({ EIPS: { @@ -5657,12 +5630,7 @@ describe('NetworkController', () => { currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId2'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId2'); expect(controller.store.getState().provider).toStrictEqual({ type: 'rpc', rpcUrl: 'https://mock-rpc-url-2', @@ -5730,12 +5698,7 @@ describe('NetworkController', () => { }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); await waitForLookupNetworkToComplete({ controller, @@ -5787,12 +5750,7 @@ describe('NetworkController', () => { currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); expect(controller.store.getState().networkStatus).toBe( 'available', ); @@ -5855,12 +5813,7 @@ describe('NetworkController', () => { }); previousNetwork.mockEssentialRpcCalls(); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); expect(controller.store.getState().networkDetails).toStrictEqual({ EIPS: { 1559: true, @@ -5926,12 +5879,7 @@ describe('NetworkController', () => { }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); await waitForLookupNetworkToComplete({ controller, @@ -5986,12 +5934,7 @@ describe('NetworkController', () => { }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); const { provider: providerBefore } = controller.getProviderAndBlockTracker(); @@ -6045,12 +5988,7 @@ describe('NetworkController', () => { currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); await waitForLookupNetworkToComplete({ controller, @@ -6103,12 +6041,7 @@ describe('NetworkController', () => { response: BLOCKED_INFURA_RESPONSE, }, }); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ messenger: unrestrictedMessenger, @@ -6173,13 +6106,7 @@ describe('NetworkController', () => { }, }); - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: () => { - controller.setActiveNetwork('currentNetworkConfiguration'); - }, - }); + await controller.setActiveNetwork('currentNetworkConfiguration'); expect(controller.store.getState().networkStatus).toBe( 'unavailable', ); @@ -6228,12 +6155,7 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, }); - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); expect(controller.store.getState().networkDetails).toStrictEqual({ EIPS: { 1559: false, diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts index 51653ab4a209..62d89b69f8e0 100644 --- a/app/scripts/controllers/network/network-controller.ts +++ b/app/scripts/controllers/network/network-controller.ts @@ -704,7 +704,7 @@ export class NetworkController extends EventEmitter { * @returns The URL of the RPC endpoint representing the newly switched * network. */ - setActiveNetwork(networkConfigurationId: NetworkConfigurationId): string { + async setActiveNetwork(networkConfigurationId: NetworkConfigurationId) { const targetNetwork = this.store.getState().networkConfigurations[networkConfigurationId]; @@ -714,7 +714,7 @@ export class NetworkController extends EventEmitter { ); } - this.#setProviderConfig({ + await this.#setProviderConfig({ type: NETWORK_TYPES.RPC, ...targetNetwork, }); From 7fa7c0558602b1703efcfc69c48524614798fa6f Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Tue, 18 Apr 2023 11:33:12 -0500 Subject: [PATCH 10/13] use network did change instead of state update for assetsContractController (#18629) --- app/scripts/metamask-controller.js | 35 ++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index e7c7fcc7e08d..fa619c62f133 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -348,18 +348,29 @@ export default class MetamaskController extends EventEmitter { { onPreferencesStateChange: (listener) => this.preferencesController.store.subscribe(listener), - onNetworkStateChange: (cb) => { - this.networkController.store.subscribe((networkState) => { - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.provider, - chainId: hexToDecimal(networkState.provider.chainId), - }, - }; - return cb(modifiedNetworkState); - }); - }, + // This handler is misnamed, and is a known issue that will be resolved + // by planned refactors. It should be onNetworkDidChange which happens + // AFTER the provider in the network controller is updated to reflect + // the new state of the network controller. In #18041 we changed this + // handler to be triggered by the change in the network state because + // that is what the handler name implies, but this triggers too soon + // causing the provider of the AssetsContractController to trail the + // network provider by one update. + onNetworkStateChange: (cb) => + 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, From 13f429528700716d2ea0f5785901d13565796fe9 Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Tue, 18 Apr 2023 09:48:34 -0700 Subject: [PATCH 11/13] update Icon to TS version in UI Folder (#18551) * update Icon to TS version and using proper Enums * Update ui/components/ui/menu/menu.stories.js Co-authored-by: Nidhi Kumari * Update ui/components/ui/nickname-popover/nickname-popover.component.js Co-authored-by: Nidhi Kumari --------- Co-authored-by: Nidhi Kumari --- ui/components/ui/callout/callout.js | 10 +++------- .../contract-token-values.js | 7 +++---- ui/components/ui/disclosure/disclosure.js | 7 +++---- ui/components/ui/dropdown/dropdown.js | 10 +++------- .../ui/editable-label/editable-label.js | 7 +++---- .../error-message/error-message.component.js | 8 ++++---- .../ui/form-field/form-field.stories.js | 6 +++--- ui/components/ui/menu/menu.stories.js | 13 +++++-------- .../ui/new-network-info/new-network-info.js | 4 ++-- .../nickname-popover.component.js | 10 +++------- ui/components/ui/popover/popover.component.js | 19 ++++++++++--------- ui/components/ui/qr-code/qr-code.js | 10 +++------- .../review-spending-cap.js | 15 +++++---------- .../sender-to-recipient.component.js | 4 ++-- 14 files changed, 52 insertions(+), 78 deletions(-) diff --git a/ui/components/ui/callout/callout.js b/ui/components/ui/callout/callout.js index 8538d69cd2c6..6c28dc117206 100644 --- a/ui/components/ui/callout/callout.js +++ b/ui/components/ui/callout/callout.js @@ -5,11 +5,7 @@ import InfoIconInverted from '../icon/info-icon-inverted.component'; import { SEVERITIES, Color } from '../../../helpers/constants/design-system'; import { MILLISECOND } from '../../../../shared/constants/time'; import Typography from '../typography'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; +import { ButtonIcon, IconName, IconSize } from '../../component-library'; export default function Callout({ severity, @@ -47,8 +43,8 @@ export default function Callout({ {dismiss && ( { setRemoved(true); diff --git a/ui/components/ui/contract-token-values/contract-token-values.js b/ui/components/ui/contract-token-values/contract-token-values.js index b34b34b6aad1..61682859886f 100644 --- a/ui/components/ui/contract-token-values/contract-token-values.js +++ b/ui/components/ui/contract-token-values/contract-token-values.js @@ -16,8 +16,7 @@ import { Color, } from '../../../helpers/constants/design-system'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { ButtonIcon, IconName } from '../../component-library'; export default function ContractTokenValues({ address, @@ -51,7 +50,7 @@ export default function ContractTokenValues({ title={copied ? t('copiedExclamation') : t('copyToClipboard')} > handleCopy(address)} ariaLabel={copied ? t('copiedExclamation') : t('copyToClipboard')} @@ -60,7 +59,7 @@ export default function ContractTokenValues({ { const blockExplorerTokenLink = getAccountLink( diff --git a/ui/components/ui/disclosure/disclosure.js b/ui/components/ui/disclosure/disclosure.js index c45b60153f9b..e2074630824c 100644 --- a/ui/components/ui/disclosure/disclosure.js +++ b/ui/components/ui/disclosure/disclosure.js @@ -1,8 +1,7 @@ import React, { useState, useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; -import { Size } from '../../../helpers/constants/design-system'; +import { Icon, IconName, IconSize } from '../../component-library'; const Disclosure = ({ children, title, size }) => { const disclosureFooterEl = useRef(null); @@ -27,8 +26,8 @@ const Disclosure = ({ children, title, size }) => { {title} diff --git a/ui/components/ui/dropdown/dropdown.js b/ui/components/ui/dropdown/dropdown.js index a6f556f3cbef..802fd0a0b98b 100644 --- a/ui/components/ui/dropdown/dropdown.js +++ b/ui/components/ui/dropdown/dropdown.js @@ -1,11 +1,7 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { - Icon, - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; +import { Icon, IconName, IconSize } from '../../component-library'; const Dropdown = ({ className, @@ -46,8 +42,8 @@ const Dropdown = ({ })} diff --git a/ui/components/ui/editable-label/editable-label.js b/ui/components/ui/editable-label/editable-label.js index 98385776d255..7a0558cd8df5 100644 --- a/ui/components/ui/editable-label/editable-label.js +++ b/ui/components/ui/editable-label/editable-label.js @@ -3,8 +3,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Color } from '../../../helpers/constants/design-system'; import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { ButtonIcon, IconName } from '../../component-library'; export default class EditableLabel extends Component { static propTypes = { @@ -60,7 +59,7 @@ export default class EditableLabel extends Component { autoFocus /> this.handleSubmit(isValidAccountName)} /> @@ -76,7 +75,7 @@ export default class EditableLabel extends Component {
{this.state.value}
this.setState({ isEditing: true })} diff --git a/ui/components/ui/error-message/error-message.component.js b/ui/components/ui/error-message/error-message.component.js index ec7a5e3fcd80..53f50f90f9c4 100644 --- a/ui/components/ui/error-message/error-message.component.js +++ b/ui/components/ui/error-message/error-message.component.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; -import { IconColor, Size } from '../../../helpers/constants/design-system'; +import { Icon, IconName, IconSize } from '../../component-library'; +import { IconColor } from '../../../helpers/constants/design-system'; /** * @deprecated - Please use ActionableMessage type danger @@ -19,8 +19,8 @@ const ErrorMessage = (props, context) => {
diff --git a/ui/components/ui/form-field/form-field.stories.js b/ui/components/ui/form-field/form-field.stories.js index 101e092ab088..8a620b9e5f45 100644 --- a/ui/components/ui/form-field/form-field.stories.js +++ b/ui/components/ui/form-field/form-field.stories.js @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import Typography from '../typography'; 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 README from './README.mdx'; import FormField from '.'; @@ -70,7 +70,7 @@ export const FormFieldWithTitleDetail = (args) => { Click Me ), - checkmark: , + checkmark: , }; return ; @@ -108,7 +108,7 @@ export const CustomComponents = (args) => { position="top" html={Custom tooltip} > - + } titleDetail={TitleDetail} diff --git a/ui/components/ui/menu/menu.stories.js b/ui/components/ui/menu/menu.stories.js index 9ff6c660f920..1eed57ddbbff 100644 --- a/ui/components/ui/menu/menu.stories.js +++ b/ui/components/ui/menu/menu.stories.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { IconName } from '../../component-library'; import { Menu, MenuItem } from '.'; export default { @@ -10,11 +10,11 @@ export default { export const DefaultStory = () => { return ( - + Menu Item 1 Menu Item 2 - + Menu Item 3 @@ -29,14 +29,11 @@ export const Anchored = () => { <> - + Menu Item 1 Menu Item 2 - + Menu Item 3 diff --git a/ui/components/ui/new-network-info/new-network-info.js b/ui/components/ui/new-network-info/new-network-info.js index 7c1278ec4b86..e8637b285d03 100644 --- a/ui/components/ui/new-network-info/new-network-info.js +++ b/ui/components/ui/new-network-info/new-network-info.js @@ -26,7 +26,7 @@ import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes'; import Chip from '../chip/chip'; import { setFirstTimeUsedNetwork } from '../../../store/actions'; 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 t = useContext(I18nContext); @@ -106,7 +106,7 @@ const NewNetworkInfo = () => { ) : ( ) diff --git a/ui/components/ui/nickname-popover/nickname-popover.component.js b/ui/components/ui/nickname-popover/nickname-popover.component.js index 60785b18adc3..51a135fabe2b 100644 --- a/ui/components/ui/nickname-popover/nickname-popover.component.js +++ b/ui/components/ui/nickname-popover/nickname-popover.component.js @@ -11,11 +11,7 @@ import { shortenAddress } from '../../../helpers/utils/util'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { getTokenList, getBlockExplorerLinkText } from '../../../selectors'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; -import { - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; +import { ButtonIcon, IconName, IconSize } from '../../component-library'; const NicknamePopover = ({ address, @@ -67,8 +63,8 @@ const NicknamePopover = ({ title={copied ? t('copiedExclamation') : t('copyToClipboard')} > handleCopy(address)} /> diff --git a/ui/components/ui/popover/popover.component.js b/ui/components/ui/popover/popover.component.js index 2653d64c977d..c5b073d8639f 100644 --- a/ui/components/ui/popover/popover.component.js +++ b/ui/components/ui/popover/popover.component.js @@ -18,13 +18,14 @@ import { TEXT_ALIGN, BLOCK_SIZES, } from '../../../helpers/constants/design-system'; + import { + ButtonIcon, Icon, - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { Text } from '../../component-library'; + IconName, + IconSize, + Text, +} from '../../component-library'; const defaultHeaderProps = { padding: [6, 4, 4], @@ -86,7 +87,7 @@ const Popover = ({ > {onBack ? ( {onClose ? ( diff --git a/ui/components/ui/qr-code/qr-code.js b/ui/components/ui/qr-code/qr-code.js index 4907cee50850..d06d4c9f2629 100644 --- a/ui/components/ui/qr-code/qr-code.js +++ b/ui/components/ui/qr-code/qr-code.js @@ -9,11 +9,7 @@ import Tooltip from '../tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { AddressCopyButton } from '../../multichain/address-copy-button'; import Box from '../box/box'; -import { - Icon, - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; +import { Icon, IconName, IconSize } from '../../component-library'; export default connect(mapStateToProps)(QrCodeView); @@ -80,8 +76,8 @@ function QrCodeView(props) { >
{toChecksumHexAddress(data)}
diff --git a/ui/components/ui/review-spending-cap/review-spending-cap.js b/ui/components/ui/review-spending-cap/review-spending-cap.js index 9cbdd728bc16..1fe08023ed76 100644 --- a/ui/components/ui/review-spending-cap/review-spending-cap.js +++ b/ui/components/ui/review-spending-cap/review-spending-cap.js @@ -4,12 +4,7 @@ import { I18nContext } from '../../../contexts/i18n'; import Box from '../box'; import Tooltip from '../tooltip'; import Typography from '../typography'; -import { ButtonLink } from '../../component-library'; -import { - Icon, - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; +import { ButtonLink, Icon, IconName, IconSize } from '../../component-library'; import { AlignItems, DISPLAY, @@ -86,7 +81,7 @@ export default function ReviewSpendingCap({ color={TextColor.errorDefault} > {t('beCareful')} @@ -100,16 +95,16 @@ export default function ReviewSpendingCap({ {valueIsGreaterThanBalance && ( )} {Number(tokenValue) === 0 && ( )} diff --git a/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js index af6abb630e87..49cdcc8e0e32 100644 --- a/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -9,7 +9,7 @@ import AccountMismatchWarning from '../account-mismatch-warning/account-mismatch import { useI18nContext } from '../../../hooks/useI18nContext'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import NicknamePopovers from '../../app/modals/nickname-popovers'; -import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; +import { Icon, IconName } from '../../component-library'; import { DEFAULT_VARIANT, CARDS_VARIANT, @@ -199,7 +199,7 @@ function Arrow({ variant }) {
) : (
- +
); } From 599bef9dc536b9237e841736d60c5fd3df222ec2 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 18 Apr 2023 22:31:45 +0530 Subject: [PATCH 12/13] Adding documentation for confirmation code cleanup (#17975) --- docs/confirmation-refactoring/README.md | 13 +++ .../README.md | 32 +++++++ .../confirmation-page-structure/README.md | 67 ++++++++++++++ .../confirmation-page-structure/current.png | Bin 0 -> 52469 bytes .../confirmation-pages-routing/README.md | 67 ++++++++++++++ .../confirmation-pages-routing/current.png | Bin .../confirmation-pages-routing/proposed.png | Bin .../confirmation-state-management/README.md | 47 ++++++++++ .../signature-request/README.md | 83 ++++++------------ .../signature-request/eth_sign.png | Bin .../signature-request/footer.png | Bin .../signature-request/header.png | Bin .../signature-request/personal_sign.png | Bin .../signature_request_old.png | Bin .../signature_request_proposed.png | Bin .../signature-request/siwe.png | Bin .../signature-request/v1.png | Bin .../signature-request/v3.png | Bin .../signature-request/v4.png | Bin docs/refactoring/README.md | 7 -- .../confirmation-pages-routing/README.md | 65 -------------- 21 files changed, 255 insertions(+), 126 deletions(-) create mode 100644 docs/confirmation-refactoring/README.md create mode 100644 docs/confirmation-refactoring/confirmation-backend-architecture/README.md create mode 100644 docs/confirmation-refactoring/confirmation-page-structure/README.md create mode 100644 docs/confirmation-refactoring/confirmation-page-structure/current.png create mode 100644 docs/confirmation-refactoring/confirmation-pages-routing/README.md rename docs/{refactoring => confirmation-refactoring}/confirmation-pages-routing/current.png (100%) rename docs/{refactoring => confirmation-refactoring}/confirmation-pages-routing/proposed.png (100%) create mode 100644 docs/confirmation-refactoring/confirmation-state-management/README.md rename docs/{refactoring => confirmation-refactoring}/signature-request/README.md (52%) rename docs/{refactoring => confirmation-refactoring}/signature-request/eth_sign.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/footer.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/header.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/personal_sign.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/signature_request_old.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/signature_request_proposed.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/siwe.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/v1.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/v3.png (100%) rename docs/{refactoring => confirmation-refactoring}/signature-request/v4.png (100%) delete mode 100644 docs/refactoring/README.md delete mode 100644 docs/refactoring/confirmation-pages-routing/README.md diff --git a/docs/confirmation-refactoring/README.md b/docs/confirmation-refactoring/README.md new file mode 100644 index 000000000000..29abab167040 --- /dev/null +++ b/docs/confirmation-refactoring/README.md @@ -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) diff --git a/docs/confirmation-refactoring/confirmation-backend-architecture/README.md b/docs/confirmation-refactoring/confirmation-backend-architecture/README.md new file mode 100644 index 000000000000..453482cbf8ad --- /dev/null +++ b/docs/confirmation-refactoring/confirmation-backend-architecture/README.md @@ -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. diff --git a/docs/confirmation-refactoring/confirmation-page-structure/README.md b/docs/confirmation-refactoring/confirmation-page-structure/README.md new file mode 100644 index 000000000000..a0ecc658836d --- /dev/null +++ b/docs/confirmation-refactoring/confirmation-page-structure/README.md @@ -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. diff --git a/docs/confirmation-refactoring/confirmation-page-structure/current.png b/docs/confirmation-refactoring/confirmation-page-structure/current.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6458e6a4d37f543c5e7558a1610fc25c20b1a6 GIT binary patch literal 52469 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 Q3=BSZJAS10Q1GRH16`c}HUIzs literal 0 HcmV?d00001 diff --git a/docs/confirmation-refactoring/confirmation-pages-routing/README.md b/docs/confirmation-refactoring/confirmation-pages-routing/README.md new file mode 100644 index 000000000000..ed4d7aef0788 --- /dev/null +++ b/docs/confirmation-refactoring/confirmation-pages-routing/README.md @@ -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. diff --git a/docs/refactoring/confirmation-pages-routing/current.png b/docs/confirmation-refactoring/confirmation-pages-routing/current.png similarity index 100% rename from docs/refactoring/confirmation-pages-routing/current.png rename to docs/confirmation-refactoring/confirmation-pages-routing/current.png diff --git a/docs/refactoring/confirmation-pages-routing/proposed.png b/docs/confirmation-refactoring/confirmation-pages-routing/proposed.png similarity index 100% rename from docs/refactoring/confirmation-pages-routing/proposed.png rename to docs/confirmation-refactoring/confirmation-pages-routing/proposed.png diff --git a/docs/confirmation-refactoring/confirmation-state-management/README.md b/docs/confirmation-refactoring/confirmation-state-management/README.md new file mode 100644 index 000000000000..a24184154e5a --- /dev/null +++ b/docs/confirmation-refactoring/confirmation-state-management/README.md @@ -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. diff --git a/docs/refactoring/signature-request/README.md b/docs/confirmation-refactoring/signature-request/README.md similarity index 52% rename from docs/refactoring/signature-request/README.md rename to docs/confirmation-refactoring/signature-request/README.md index 35c1876a1699..a80cc046ca6c 100644 --- a/docs/refactoring/signature-request/README.md +++ b/docs/confirmation-refactoring/signature-request/README.md @@ -6,35 +6,35 @@ This document details the plan to refactor and cleanup Signature Request pages i 1. Simple ETH Signature - + 1. Personal Signature - + 1. Typed Data - V1 - + 1. Typed Data - V3 - + 1. Typed Data - V4 - + 1. SIWE Signature - + ## 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: -![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: @@ -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) - [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: - - 1. `constructor` - variable initialisation: - - ``` - 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. + Above message managers handle different types of message requests sent by DAPP. There is a lot of code duplication between the 3 classes. + + We should migrate to use `MessageManagers` from `@metamask/core` repo [here](https://github.com/MetaMask/core/tree/main/packages/message-manager). 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) - - 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 - - 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. - [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. - - 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 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). - - 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. + - [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` + - 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. + - 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) + - Various callbacks to `sign message`, `cancel request`, etc for different types of messages 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). + - 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. - 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 @@ -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 - 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. - - 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. + - Get rid of container components + - 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: - + - - [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 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). - - 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. - - [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. - - 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. + - 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/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/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/e07ec9dcf3d3f341f83e6b29a29d30edaf7f5b5b/ui/components/app/signature-request-original/signature-request-original.component.js#L114) into a reusable component. 6. ### Refactoring in signature-request - 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 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 - - 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) diff --git a/docs/refactoring/signature-request/eth_sign.png b/docs/confirmation-refactoring/signature-request/eth_sign.png similarity index 100% rename from docs/refactoring/signature-request/eth_sign.png rename to docs/confirmation-refactoring/signature-request/eth_sign.png diff --git a/docs/refactoring/signature-request/footer.png b/docs/confirmation-refactoring/signature-request/footer.png similarity index 100% rename from docs/refactoring/signature-request/footer.png rename to docs/confirmation-refactoring/signature-request/footer.png diff --git a/docs/refactoring/signature-request/header.png b/docs/confirmation-refactoring/signature-request/header.png similarity index 100% rename from docs/refactoring/signature-request/header.png rename to docs/confirmation-refactoring/signature-request/header.png diff --git a/docs/refactoring/signature-request/personal_sign.png b/docs/confirmation-refactoring/signature-request/personal_sign.png similarity index 100% rename from docs/refactoring/signature-request/personal_sign.png rename to docs/confirmation-refactoring/signature-request/personal_sign.png diff --git a/docs/refactoring/signature-request/signature_request_old.png b/docs/confirmation-refactoring/signature-request/signature_request_old.png similarity index 100% rename from docs/refactoring/signature-request/signature_request_old.png rename to docs/confirmation-refactoring/signature-request/signature_request_old.png diff --git a/docs/refactoring/signature-request/signature_request_proposed.png b/docs/confirmation-refactoring/signature-request/signature_request_proposed.png similarity index 100% rename from docs/refactoring/signature-request/signature_request_proposed.png rename to docs/confirmation-refactoring/signature-request/signature_request_proposed.png diff --git a/docs/refactoring/signature-request/siwe.png b/docs/confirmation-refactoring/signature-request/siwe.png similarity index 100% rename from docs/refactoring/signature-request/siwe.png rename to docs/confirmation-refactoring/signature-request/siwe.png diff --git a/docs/refactoring/signature-request/v1.png b/docs/confirmation-refactoring/signature-request/v1.png similarity index 100% rename from docs/refactoring/signature-request/v1.png rename to docs/confirmation-refactoring/signature-request/v1.png diff --git a/docs/refactoring/signature-request/v3.png b/docs/confirmation-refactoring/signature-request/v3.png similarity index 100% rename from docs/refactoring/signature-request/v3.png rename to docs/confirmation-refactoring/signature-request/v3.png diff --git a/docs/refactoring/signature-request/v4.png b/docs/confirmation-refactoring/signature-request/v4.png similarity index 100% rename from docs/refactoring/signature-request/v4.png rename to docs/confirmation-refactoring/signature-request/v4.png diff --git a/docs/refactoring/README.md b/docs/refactoring/README.md deleted file mode 100644 index a8b82d577dff..000000000000 --- a/docs/refactoring/README.md +++ /dev/null @@ -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) diff --git a/docs/refactoring/confirmation-pages-routing/README.md b/docs/refactoring/confirmation-pages-routing/README.md deleted file mode 100644 index b47f100554da..000000000000 --- a/docs/refactoring/confirmation-pages-routing/README.md +++ /dev/null @@ -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** \ No newline at end of file From 39f6042e6541e1badb9ee08098e55ce1f3c2dbef Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 18 Apr 2023 17:19:49 -0230 Subject: [PATCH 13/13] Ledger trezor display (#18637) * gst * Only display ledger info on approval screen for ledger hardware wallets --- ui/pages/confirm-approve/confirm-approve.js | 1 + ui/pages/token-allowance/token-allowance.js | 9 +++++--- .../token-allowance/token-allowance.test.js | 22 +++++++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js index 38dfe3a2ec9d..76d6db4c85ec 100644 --- a/ui/pages/confirm-approve/confirm-approve.js +++ b/ui/pages/confirm-approve/confirm-approve.js @@ -200,6 +200,7 @@ export default function ConfirmApprove({ toAddress={toAddress} tokenSymbol={tokenSymbol} decimals={decimals} + fromAddressIsLedger={fromAddressIsLedger} /> {showCustomizeGasPopover && !supportsEIP1559 && ( ) : null} - {!isFirstPage && isHardwareWalletConnected && ( + {!isFirstPage && fromAddressIsLedger && ( @@ -643,4 +642,8 @@ TokenAllowance.propTypes = { * Symbol of the token that is waiting to be allowed */ tokenSymbol: PropTypes.string, + /** + * Whether the address sending the transaction is a ledger address + */ + fromAddressIsLedger: PropTypes.bool, }; diff --git a/ui/pages/token-allowance/token-allowance.test.js b/ui/pages/token-allowance/token-allowance.test.js index 30d12b9e071c..d05c21f200ed 100644 --- a/ui/pages/token-allowance/token-allowance.test.js +++ b/ui/pages/token-allowance/token-allowance.test.js @@ -65,10 +65,10 @@ const state = { }, ], unapprovedTxs: {}, - keyringTypes: [KeyringType.ledger], + keyringTypes: [], keyrings: [ { - type: KeyringType.ledger, + type: KeyringType.hdKeyTree, accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], }, ], @@ -256,9 +256,9 @@ describe('TokenAllowancePage', () => { 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( - , + , store, ); @@ -273,12 +273,20 @@ describe('TokenAllowancePage', () => { expect(queryByText('Prior to clicking confirm:')).toBeInTheDocument(); }); - it('should not show hardware wallet info text', () => { - const { queryByText } = renderWithProvider( - , + it('should not show ledger info text if the sending address is not ledger', () => { + const { queryByText, getByText, getByTestId } = renderWithProvider( + , 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(); });