1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

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

This commit is contained in:
Matthias Kretschmann 2023-04-17 23:50:34 +01:00
commit bd45b0eca2
Signed by: m
GPG Key ID: 606EEEF3C479A91F
70 changed files with 2188 additions and 918 deletions

View File

@ -263,12 +263,12 @@ const state = {
enabled: true, enabled: true,
id: 'local:http://localhost:8080/', id: 'local:http://localhost:8080/',
initialPermissions: { initialPermissions: {
snap_confirm: {}, snap_dialog: {},
}, },
manifest: { manifest: {
description: 'An example MetaMask Snap.', description: 'An example MetaMask Snap.',
initialPermissions: { initialPermissions: {
snap_confirm: {}, snap_dialog: {},
}, },
manifestVersion: '0.1', manifestVersion: '0.1',
proposedName: 'MetaMask Example Snap', proposedName: 'MetaMask Example Snap',
@ -298,7 +298,7 @@ const state = {
enabled: true, enabled: true,
id: 'npm:http://localhost:8080/', id: 'npm:http://localhost:8080/',
initialPermissions: { initialPermissions: {
snap_confirm: {}, snap_dialog: {},
eth_accounts: {}, eth_accounts: {},
snap_manageState: {}, snap_manageState: {},
}, },
@ -306,7 +306,7 @@ const state = {
description: description:
'This swap provides developers everywhere access to an entirely new data storage paradigm, even letting your programs store data autonomously. Learn more.', 'This swap provides developers everywhere access to an entirely new data storage paradigm, even letting your programs store data autonomously. Learn more.',
initialPermissions: { initialPermissions: {
snap_confirm: {}, snap_dialog: {},
eth_accounts: {}, eth_accounts: {},
snap_manageState: {}, snap_manageState: {},
}, },
@ -1349,9 +1349,9 @@ const state = {
}, },
'local:http://localhost:8080/': { 'local:http://localhost:8080/': {
permissions: { permissions: {
snap_confirm: { snap_dialog: {
invoker: 'local:http://localhost:8080/', invoker: 'local:http://localhost:8080/',
parentCapability: 'snap_confirm', parentCapability: 'snap_dialog',
id: 'a7342F4b-beae-4525-a36c-c0635fd03359', id: 'a7342F4b-beae-4525-a36c-c0635fd03359',
date: 1620710693178, date: 1620710693178,
caveats: [], caveats: [],

View File

@ -2697,10 +2697,6 @@
"message": "Regelmäßige Transaktionen planen und ausführen.", "message": "Regelmäßige Transaktionen planen und ausführen.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Bestätigung in MetaMask anzeigen.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Dialogfenster in MetaMask anzeigen.", "message": "Dialogfenster in MetaMask anzeigen.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Προγραμματισμός και εκτέλεση περιοδικών ενεργειών.", "message": "Προγραμματισμός και εκτέλεση περιοδικών ενεργειών.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Εμφάνιση επιβεβαίωσης στο MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Εμφάνιση παραθύρων διαλόγου στο MetaMask.", "message": "Εμφάνιση παραθύρων διαλόγου στο MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2914,14 +2914,6 @@
"message": "Allow the snap to perform actions that run periodically at fixed times, dates, or intervals. This can be used to trigger time-sensitive interactions or notifications.", "message": "Allow the snap to perform actions that run periodically at fixed times, dates, or intervals. This can be used to trigger time-sensitive interactions or notifications.",
"description": "An extended description for the `snap_cronjob` permission" "description": "An extended description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Display a confirmation in MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_customConfirmationDescription": {
"message": "Allow the snap to display MetaMask popups with custom text, and buttons to approve or reject an action.",
"description": "An extended description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Display dialog windows in MetaMask.", "message": "Display dialog windows in MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Programar y ejecutar acciones periódicas.", "message": "Programar y ejecutar acciones periódicas.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Mostrar una confirmación en MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Mostrar ventanas de diálogo en MetaMask.", "message": "Mostrar ventanas de diálogo en MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Planifiez et exécutez des actions périodiques.", "message": "Planifiez et exécutez des actions périodiques.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Afficher une confirmation dans MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Afficher les boîtes de dialogue dans MetaMask.", "message": "Afficher les boîtes de dialogue dans MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "समय-समय पर आने वाले क्रियाओं को शेड्यूल और निष्पादित करें।", "message": "समय-समय पर आने वाले क्रियाओं को शेड्यूल और निष्पादित करें।",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "MetaMask में पुष्टि को दर्शाएं।",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "MetaMask में डायलॉग विंडो प्रदर्शित करें।", "message": "MetaMask में डायलॉग विंडो प्रदर्शित करें।",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Jadwalkan dan lakukan tindakan berkala.", "message": "Jadwalkan dan lakukan tindakan berkala.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Tampilkan konfirmasi di MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Tampilkan jendela dialog di MetaMask.", "message": "Tampilkan jendela dialog di MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "定期的なアクションのスケジュール設定と実行。", "message": "定期的なアクションのスケジュール設定と実行。",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "MetaMask に確認を表示します。",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "MetaMask にダイアログウィンドウを表示します。", "message": "MetaMask にダイアログウィンドウを表示します。",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "정기적 활동 예약 및 실행", "message": "정기적 활동 예약 및 실행",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "MetaMask에 확인을 표시합니다.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "MetaMask 대화창 표시", "message": "MetaMask 대화창 표시",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Agende e execute ações periódicas.", "message": "Agende e execute ações periódicas.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Exibir uma confirmação na MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Exibir janelas de diálogo na MetaMask.", "message": "Exibir janelas de diálogo na MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Планируйте и выполняйте периодические действия.", "message": "Планируйте и выполняйте периодические действия.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Показать подтверждение в MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Отображение диалоговых окон в MetaMask.", "message": "Отображение диалоговых окон в MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Mag-iskedyul at magsagawa ng mga pana-panahong mga aksyon.", "message": "Mag-iskedyul at magsagawa ng mga pana-panahong mga aksyon.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Ipakita ang kumpirmasyon sa MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Ipakita ang mga dialog window sa MetaMask.", "message": "Ipakita ang mga dialog window sa MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Periyodik eylemleri planla ve gerçekleştir.", "message": "Periyodik eylemleri planla ve gerçekleştir.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "MetaMask'te bir onay görüntüle.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "MetaMask'te iletişim kutusu pencerelerini göster.", "message": "MetaMask'te iletişim kutusu pencerelerini göster.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Lên lịch và thực hiện các hành động theo định kỳ.", "message": "Lên lịch và thực hiện các hành động theo định kỳ.",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "Hiển thị xác nhận trong MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "Hiển thị cửa sổ hộp thoại trong MetaMask.", "message": "Hiển thị cửa sổ hộp thoại trong MetaMask.",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "规划并执行定期操作。", "message": "规划并执行定期操作。",
"description": "The description for the `snap_cronjob` permission" "description": "The description for the `snap_cronjob` permission"
}, },
"permission_customConfirmation": {
"message": "在MetaMask中显示确认。",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": { "permission_dialog": {
"message": "在 MetaMask 中显示对话框窗口。", "message": "在 MetaMask 中显示对话框窗口。",
"description": "The description for the `snap_dialog` permission" "description": "The description for the `snap_dialog` permission"

View File

@ -278,7 +278,7 @@ describe('DetectTokensController', function () {
it('should be called on every polling period', async function () { it('should be called on every polling period', async function () {
const clock = sandbox.useFakeTimers(); const clock = sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET); await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({ const controller = new DetectTokensController({
preferences, preferences,
network, network,
@ -304,7 +304,7 @@ describe('DetectTokensController', function () {
it('should not check and add tokens while on unsupported networks', async function () { it('should not check and add tokens while on unsupported networks', async function () {
sandbox.useFakeTimers(); sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.SEPOLIA); await network.setProviderType(NETWORK_TYPES.SEPOLIA);
const tokenListMessengerSepolia = new ControllerMessenger().getRestricted({ const tokenListMessengerSepolia = new ControllerMessenger().getRestricted({
name: 'TokenListController', name: 'TokenListController',
}); });
@ -337,7 +337,7 @@ describe('DetectTokensController', function () {
it('should skip adding tokens listed in ignoredTokens array', async function () { it('should skip adding tokens listed in ignoredTokens array', async function () {
sandbox.useFakeTimers(); sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET); await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({ const controller = new DetectTokensController({
preferences, preferences,
network, network,
@ -388,7 +388,7 @@ describe('DetectTokensController', function () {
it('should check and add tokens while on supported networks', async function () { it('should check and add tokens while on supported networks', async function () {
sandbox.useFakeTimers(); sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET); await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({ const controller = new DetectTokensController({
preferences, preferences,
network, network,
@ -483,7 +483,7 @@ describe('DetectTokensController', function () {
it('should not trigger detect new tokens when not unlocked', async function () { it('should not trigger detect new tokens when not unlocked', async function () {
const clock = sandbox.useFakeTimers(); const clock = sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET); await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({ const controller = new DetectTokensController({
preferences, preferences,
network, network,
@ -504,7 +504,7 @@ describe('DetectTokensController', function () {
it('should not trigger detect new tokens when not open', async function () { it('should not trigger detect new tokens when not open', async function () {
const clock = sandbox.useFakeTimers(); const clock = sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET); await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({ const controller = new DetectTokensController({
preferences, preferences,
network, network,

View File

@ -730,7 +730,7 @@ describe('NetworkController', () => {
await expect(async () => { await expect(async () => {
await controller.initializeProvider(); await controller.initializeProvider();
}).rejects.toThrow( }).rejects.toThrow(
'NetworkController - _configureProvider - unknown type "undefined"', 'NetworkController - #configureProvider - unknown type "undefined"',
); );
}, },
); );
@ -1135,7 +1135,7 @@ describe('NetworkController', () => {
}); });
expect(oldChainIdResult).toBe('0x1337'); expect(oldChainIdResult).toBe('0x1337');
controller.setProviderType(networkType); await controller.setProviderType(networkType);
const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( const promisifiedSendAsync2 = promisify(provider.sendAsync).bind(
provider, provider,
); );
@ -2652,21 +2652,9 @@ describe('NetworkController', () => {
network1.mockEssentialRpcCalls({ network1.mockEssentialRpcCalls({
eth_getBlockByNumber: { eth_getBlockByNumber: {
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForPublishedEvents({ await controller.setProviderType(
messenger: unrestrictedMessenger, anotherNetwork.networkType,
eventType: NetworkControllerEventType.NetworkDidChange, );
operation: async () => {
await waitForStateChanges({
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setProviderType(
anotherNetwork.networkType,
);
},
});
},
});
}, },
}, },
}); });
@ -3513,13 +3501,7 @@ describe('NetworkController', () => {
{ {
response: SUCCESSFUL_NET_VERSION_RESPONSE, response: SUCCESSFUL_NET_VERSION_RESPONSE,
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
], ],
@ -3618,13 +3600,7 @@ describe('NetworkController', () => {
result: '111', result: '111',
}, },
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
}); });
@ -3675,13 +3651,7 @@ describe('NetworkController', () => {
net_version: { net_version: {
response: SUCCESSFUL_NET_VERSION_RESPONSE, response: SUCCESSFUL_NET_VERSION_RESPONSE,
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkDetails'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
}); });
@ -3738,13 +3708,7 @@ describe('NetworkController', () => {
network1.mockEssentialRpcCalls({ network1.mockEssentialRpcCalls({
net_version: { net_version: {
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkDetails'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
}); });
@ -3841,13 +3805,7 @@ describe('NetworkController', () => {
}, },
}, },
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
], ],
@ -3904,13 +3862,7 @@ describe('NetworkController', () => {
network1.mockEssentialRpcCalls({ network1.mockEssentialRpcCalls({
eth_getBlockByNumber: { eth_getBlockByNumber: {
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
net_version: { net_version: {
@ -3965,13 +3917,7 @@ describe('NetworkController', () => {
latestBlock: POST_1559_BLOCK, latestBlock: POST_1559_BLOCK,
eth_getBlockByNumber: { eth_getBlockByNumber: {
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkDetails'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
}); });
@ -4028,13 +3974,7 @@ describe('NetworkController', () => {
network1.mockEssentialRpcCalls({ network1.mockEssentialRpcCalls({
eth_getBlockByNumber: { eth_getBlockByNumber: {
beforeCompleting: async () => { beforeCompleting: async () => {
await waitForStateChanges({ await controller.setProviderType('goerli');
controller,
propertyPath: ['networkDetails'],
operation: () => {
controller.setProviderType('goerli');
},
});
}, },
}, },
}); });
@ -4629,7 +4569,7 @@ describe('NetworkController', () => {
}); });
network.mockEssentialRpcCalls(); network.mockEssentialRpcCalls();
controller.setProviderType(networkType); await controller.setProviderType(networkType);
expect(controller.store.getState().provider).toStrictEqual({ expect(controller.store.getState().provider).toStrictEqual({
type: networkType, type: networkType,
@ -4662,6 +4602,8 @@ describe('NetworkController', () => {
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkWillChange, eventType: NetworkControllerEventType.NetworkWillChange,
operation: () => { operation: () => {
// Intentionally not awaited because we're capturing an event
// emitted partway through the operation
controller.setProviderType(networkType); controller.setProviderType(networkType);
}, },
}); });
@ -4714,6 +4656,8 @@ describe('NetworkController', () => {
// happens before networkDidChange // happens before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we're checking the state
// partway through the operation
controller.setProviderType(networkType); controller.setProviderType(networkType);
}, },
}); });
@ -4767,6 +4711,8 @@ describe('NetworkController', () => {
// happens before networkDidChange // happens before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we're checking the state
// partway through the operation
controller.setProviderType(networkType); controller.setProviderType(networkType);
}, },
}); });
@ -4786,7 +4732,7 @@ describe('NetworkController', () => {
}); });
network.mockEssentialRpcCalls(); network.mockEssentialRpcCalls();
controller.setProviderType(networkType); await controller.setProviderType(networkType);
const { provider } = controller.getProviderAndBlockTracker(); const { provider } = controller.getProviderAndBlockTracker();
assert(provider, 'Provider is somehow unset'); assert(provider, 'Provider is somehow unset');
@ -4813,7 +4759,7 @@ describe('NetworkController', () => {
const { provider: providerBefore } = const { provider: providerBefore } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
controller.setProviderType(networkType); await controller.setProviderType(networkType);
const { provider: providerAfter } = const { provider: providerAfter } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
@ -4836,8 +4782,8 @@ describe('NetworkController', () => {
const networkDidChange = await waitForPublishedEvents({ const networkDidChange = await waitForPublishedEvents({
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkDidChange, eventType: NetworkControllerEventType.NetworkDidChange,
operation: () => { operation: async () => {
controller.setProviderType(networkType); await controller.setProviderType(networkType);
}, },
}); });
@ -4872,7 +4818,7 @@ describe('NetworkController', () => {
eventType: NetworkControllerEventType.InfuraIsBlocked, eventType: NetworkControllerEventType.InfuraIsBlocked,
}); });
controller.setProviderType(networkType); await controller.setProviderType(networkType);
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
expect(await promiseForInfuraIsBlocked).toBeTruthy(); expect(await promiseForInfuraIsBlocked).toBeTruthy();
@ -4891,13 +4837,7 @@ describe('NetworkController', () => {
latestBlock: BLOCK, latestBlock: BLOCK,
}); });
await waitForStateChanges({ await controller.setProviderType(networkType);
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.setProviderType(networkType);
},
});
expect(controller.store.getState().networkStatus).toBe('available'); expect(controller.store.getState().networkStatus).toBe('available');
}); });
@ -4921,16 +4861,7 @@ describe('NetworkController', () => {
latestBlock: POST_1559_BLOCK, latestBlock: POST_1559_BLOCK,
}); });
await waitForStateChanges({ await controller.setProviderType(networkType);
controller,
propertyPath: ['networkDetails'],
// setProviderType clears networkDetails first, and then updates
// it to what we expect it to be
count: 2,
operation: () => {
controller.setProviderType(networkType);
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
@ -4946,7 +4877,7 @@ describe('NetworkController', () => {
describe('given a type of "rpc"', () => { describe('given a type of "rpc"', () => {
it('throws', async () => { it('throws', async () => {
await withController(async ({ controller }) => { await withController(async ({ controller }) => {
expect(() => controller.setProviderType('rpc')).toThrow( await expect(() => controller.setProviderType('rpc')).rejects.toThrow(
new Error( new Error(
'NetworkController - cannot call "setProviderType" with type "rpc". Use "setActiveNetwork"', 'NetworkController - cannot call "setProviderType" with type "rpc". Use "setActiveNetwork"',
), ),
@ -4958,7 +4889,9 @@ describe('NetworkController', () => {
describe('given an invalid Infura network name', () => { describe('given an invalid Infura network name', () => {
it('throws', async () => { it('throws', async () => {
await withController(async ({ controller }) => { await withController(async ({ controller }) => {
expect(() => controller.setProviderType('sadlflaksdj')).toThrow( await expect(() =>
controller.setProviderType('sadlflaksdj'),
).rejects.toThrow(
new Error('Unknown Infura provider type "sadlflaksdj".'), new Error('Unknown Infura provider type "sadlflaksdj".'),
); );
}); });
@ -4992,6 +4925,8 @@ describe('NetworkController', () => {
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkWillChange, eventType: NetworkControllerEventType.NetworkWillChange,
operation: () => { operation: () => {
// Intentionally not awaited because we want to capture an
// event emitted partway throught this operation
controller.resetConnection(); controller.resetConnection();
}, },
}); });
@ -5032,6 +4967,8 @@ describe('NetworkController', () => {
// happens before networkDidChange // happens before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we want to capture a
// state change made partway through the operation
controller.resetConnection(); controller.resetConnection();
}, },
}); });
@ -5074,6 +5011,8 @@ describe('NetworkController', () => {
// happens before networkDidChange // happens before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we want to check state
// partway through the operation
controller.resetConnection(); controller.resetConnection();
}, },
}); });
@ -5101,7 +5040,7 @@ describe('NetworkController', () => {
async ({ controller, network }) => { async ({ controller, network }) => {
network.mockEssentialRpcCalls(); network.mockEssentialRpcCalls();
controller.resetConnection(); await controller.resetConnection();
const { provider } = controller.getProviderAndBlockTracker(); const { provider } = controller.getProviderAndBlockTracker();
assert(provider, 'Provider is somehow unset'); assert(provider, 'Provider is somehow unset');
@ -5140,7 +5079,7 @@ describe('NetworkController', () => {
const { provider: providerBefore } = const { provider: providerBefore } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
controller.resetConnection(); await controller.resetConnection();
const { provider: providerAfter } = const { provider: providerAfter } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
@ -5171,8 +5110,8 @@ describe('NetworkController', () => {
const networkDidChange = await waitForPublishedEvents({ const networkDidChange = await waitForPublishedEvents({
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkDidChange, eventType: NetworkControllerEventType.NetworkDidChange,
operation: () => { operation: async () => {
controller.resetConnection(); await controller.resetConnection();
}, },
}); });
@ -5214,7 +5153,7 @@ describe('NetworkController', () => {
eventType: NetworkControllerEventType.InfuraIsBlocked, eventType: NetworkControllerEventType.InfuraIsBlocked,
}); });
controller.resetConnection(); await controller.resetConnection();
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
expect(await promiseForInfuraIsBlocked).toBeTruthy(); expect(await promiseForInfuraIsBlocked).toBeTruthy();
@ -5237,13 +5176,7 @@ describe('NetworkController', () => {
async ({ controller, network }) => { async ({ controller, network }) => {
network.mockEssentialRpcCalls(); network.mockEssentialRpcCalls();
await waitForStateChanges({ await controller.resetConnection();
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.resetConnection();
},
});
expect(controller.store.getState().networkStatus).toBe( expect(controller.store.getState().networkStatus).toBe(
'available', 'available',
@ -5275,13 +5208,7 @@ describe('NetworkController', () => {
}, },
}); });
await waitForStateChanges({ await controller.resetConnection();
controller,
propertyPath: ['networkDetails'],
operation: () => {
controller.resetConnection();
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
@ -5326,6 +5253,8 @@ describe('NetworkController', () => {
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkWillChange, eventType: NetworkControllerEventType.NetworkWillChange,
operation: () => { operation: () => {
// Intentionally not awaited because we're capturing an event
// emitted partway through the operation
controller.resetConnection(); controller.resetConnection();
}, },
}); });
@ -5376,6 +5305,8 @@ describe('NetworkController', () => {
// before networkDidChange // before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we want to check state
// partway through the operation
controller.resetConnection(); controller.resetConnection();
}, },
}); });
@ -5426,6 +5357,8 @@ describe('NetworkController', () => {
// before networkDidChange // before networkDidChange
count: 1, count: 1,
operation: () => { operation: () => {
// Intentionally not awaited because we want to check state
// partway through the operation
controller.resetConnection(); controller.resetConnection();
}, },
}); });
@ -5461,7 +5394,7 @@ describe('NetworkController', () => {
async ({ controller, network }) => { async ({ controller, network }) => {
network.mockEssentialRpcCalls(); network.mockEssentialRpcCalls();
controller.resetConnection(); await controller.resetConnection();
const { provider } = controller.getProviderAndBlockTracker(); const { provider } = controller.getProviderAndBlockTracker();
assert(provider, 'Provider is somehow unset'); assert(provider, 'Provider is somehow unset');
@ -5508,12 +5441,7 @@ describe('NetworkController', () => {
const { provider: providerBefore } = const { provider: providerBefore } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
await waitForLookupNetworkToComplete({ await controller.resetConnection();
controller,
operation: () => {
controller.resetConnection();
},
});
const { provider: providerAfter } = const { provider: providerAfter } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
@ -5552,8 +5480,8 @@ describe('NetworkController', () => {
const networkDidChange = await waitForPublishedEvents({ const networkDidChange = await waitForPublishedEvents({
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.NetworkDidChange, eventType: NetworkControllerEventType.NetworkDidChange,
operation: () => { operation: async () => {
controller.resetConnection(); await controller.resetConnection();
}, },
}); });
@ -5592,8 +5520,8 @@ describe('NetworkController', () => {
const infuraIsUnblocked = await waitForPublishedEvents({ const infuraIsUnblocked = await waitForPublishedEvents({
messenger: unrestrictedMessenger, messenger: unrestrictedMessenger,
eventType: NetworkControllerEventType.InfuraIsUnblocked, eventType: NetworkControllerEventType.InfuraIsUnblocked,
operation: () => { operation: async () => {
controller.resetConnection(); await controller.resetConnection();
}, },
}); });
@ -5630,13 +5558,7 @@ describe('NetworkController', () => {
}); });
expect(controller.store.getState().networkStatus).toBe('unknown'); expect(controller.store.getState().networkStatus).toBe('unknown');
await waitForStateChanges({ await controller.resetConnection();
controller,
propertyPath: ['networkStatus'],
operation: () => {
controller.resetConnection();
},
});
expect(controller.store.getState().networkStatus).toBe('available'); expect(controller.store.getState().networkStatus).toBe('available');
}, },
@ -5674,13 +5596,7 @@ describe('NetworkController', () => {
}, },
}); });
await waitForStateChanges({ await controller.resetConnection();
controller,
propertyPath: ['networkDetails'],
operation: () => {
controller.resetConnection();
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
@ -6393,12 +6309,7 @@ describe('NetworkController', () => {
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
expect(controller.store.getState().provider).toStrictEqual({ expect(controller.store.getState().provider).toStrictEqual({
type: 'goerli', type: 'goerli',
rpcUrl: '', rpcUrl: '',
@ -6466,12 +6377,7 @@ describe('NetworkController', () => {
}); });
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
controller, controller,
@ -6518,12 +6424,7 @@ describe('NetworkController', () => {
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
expect(controller.store.getState().networkStatus).toBe('available'); expect(controller.store.getState().networkStatus).toBe('available');
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
@ -6579,12 +6480,7 @@ describe('NetworkController', () => {
}); });
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
1559: true, 1559: true,
@ -6645,12 +6541,7 @@ describe('NetworkController', () => {
}); });
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
controller, controller,
@ -6700,12 +6591,7 @@ describe('NetworkController', () => {
}); });
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
const { provider: providerBefore } = const { provider: providerBefore } =
controller.getProviderAndBlockTracker(); controller.getProviderAndBlockTracker();
@ -6754,12 +6640,7 @@ describe('NetworkController', () => {
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
controller, controller,
@ -6809,12 +6690,7 @@ describe('NetworkController', () => {
currentNetwork.mockEssentialRpcCalls(); currentNetwork.mockEssentialRpcCalls();
previousNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls();
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
controller, controller,
@ -6869,12 +6745,7 @@ describe('NetworkController', () => {
}, },
}); });
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
expect(controller.store.getState().networkStatus).toBe('available'); expect(controller.store.getState().networkStatus).toBe('available');
await waitForLookupNetworkToComplete({ await waitForLookupNetworkToComplete({
@ -6919,12 +6790,7 @@ describe('NetworkController', () => {
latestBlock: POST_1559_BLOCK, latestBlock: POST_1559_BLOCK,
}); });
await waitForLookupNetworkToComplete({ await controller.setProviderType('goerli');
controller,
operation: () => {
controller.setProviderType('goerli');
},
});
expect(controller.store.getState().networkDetails).toStrictEqual({ expect(controller.store.getState().networkDetails).toStrictEqual({
EIPS: { EIPS: {
1559: false, 1559: false,

View File

@ -1,6 +1,6 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { ComposedStore, ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel'; import log from 'loglevel';
import { import {
createSwappableProxy, createSwappableProxy,
@ -379,6 +379,21 @@ function buildDefaultNetworkConfigurationsState(): NetworkConfigurations {
return {}; return {};
} }
/**
* Builds the default state for the network controller.
*
* @returns The default network controller state.
*/
function buildDefaultState() {
return {
provider: buildDefaultProviderConfigState(),
networkId: buildDefaultNetworkIdState(),
networkStatus: buildDefaultNetworkStatusState(),
networkDetails: buildDefaultNetworkDetailsState(),
networkConfigurations: buildDefaultNetworkConfigurationsState(),
};
}
/** /**
* Returns whether the given argument is a type that our Infura middleware * Returns whether the given argument is a type that our Infura middleware
* recognizes. We can't calculate this inline because the usual type of `type`, * recognizes. We can't calculate this inline because the usual type of `type`,
@ -412,12 +427,7 @@ export class NetworkController extends EventEmitter {
/** /**
* The messenger that NetworkController uses to publish events. * The messenger that NetworkController uses to publish events.
*/ */
messenger: NetworkControllerMessenger; #messenger: NetworkControllerMessenger;
/**
* Observable store containing the provider configuration.
*/
providerStore: ObservableStore<ProviderConfiguration>;
/** /**
* Observable store containing the provider configuration for the previously * Observable store containing the provider configuration for the previously
@ -425,44 +435,23 @@ export class NetworkController extends EventEmitter {
*/ */
#previousProviderConfig: ProviderConfiguration; #previousProviderConfig: ProviderConfiguration;
/**
* Observable store containing the network ID for the current network or null
* if there is no current network.
*/
networkIdStore: ObservableStore<NetworkIdState>;
/**
* Observable store for the network status.
*/
networkStatusStore: ObservableStore<NetworkStatus>;
/**
* Observable store for details about the network.
*/
networkDetails: ObservableStore<NetworkDetails>;
/**
* Observable store for network configurations.
*/
networkConfigurationsStore: ObservableStore<NetworkConfigurations>;
/** /**
* Observable store containing a combination of data from all of the * Observable store containing a combination of data from all of the
* individual stores. * individual stores.
*/ */
store: ComposedStore<NetworkControllerState>; store: ObservableStore<NetworkControllerState>;
_provider: SafeEventEmitterProvider | null; #provider: SafeEventEmitterProvider | null;
_blockTracker: PollingBlockTracker | null; #blockTracker: PollingBlockTracker | null;
_providerProxy: SwappableProxy<SafeEventEmitterProvider> | null; #providerProxy: SwappableProxy<SafeEventEmitterProvider> | null;
_blockTrackerProxy: SwappableProxy<PollingBlockTracker> | null; #blockTrackerProxy: SwappableProxy<PollingBlockTracker> | null;
_infuraProjectId: NetworkControllerOptions['infuraProjectId']; #infuraProjectId: NetworkControllerOptions['infuraProjectId'];
_trackMetaMetricsEvent: NetworkControllerOptions['trackMetaMetricsEvent']; #trackMetaMetricsEvent: NetworkControllerOptions['trackMetaMetricsEvent'];
/** /**
* Constructs a network controller. * Constructs a network controller.
@ -482,51 +471,27 @@ export class NetworkController extends EventEmitter {
}: NetworkControllerOptions) { }: NetworkControllerOptions) {
super(); super();
this.messenger = messenger; this.#messenger = messenger;
// create stores this.store = new ObservableStore({
this.providerStore = new ObservableStore( ...buildDefaultState(),
state.provider || buildDefaultProviderConfigState(), ...state,
);
this.#previousProviderConfig = this.providerStore.getState();
this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState());
this.networkStatusStore = new ObservableStore(
buildDefaultNetworkStatusState(),
);
// We need to keep track of a few details about the current network.
// Ideally we'd merge this.networkStatusStore with this new store, but doing
// so will require a decent sized refactor of how we're accessing network
// state. Currently this is only used for detecting EIP-1559 support but can
// be extended to track other network details.
this.networkDetails = new ObservableStore(
state.networkDetails || buildDefaultNetworkDetailsState(),
);
this.networkConfigurationsStore = new ObservableStore(
state.networkConfigurations || buildDefaultNetworkConfigurationsState(),
);
this.store = new ComposedStore<NetworkControllerState>({
provider: this.providerStore,
networkId: this.networkIdStore,
networkStatus: this.networkStatusStore,
networkDetails: this.networkDetails,
networkConfigurations: this.networkConfigurationsStore,
}); });
this.#previousProviderConfig = this.store.getState().provider;
// provider and block tracker // provider and block tracker
this._provider = null; this.#provider = null;
this._blockTracker = null; this.#blockTracker = null;
// provider and block tracker proxies - because the network changes // provider and block tracker proxies - because the network changes
this._providerProxy = null; this.#providerProxy = null;
this._blockTrackerProxy = null; this.#blockTrackerProxy = null;
if (!infuraProjectId || typeof infuraProjectId !== 'string') { if (!infuraProjectId || typeof infuraProjectId !== 'string') {
throw new Error('Invalid Infura project ID'); throw new Error('Invalid Infura project ID');
} }
this._infuraProjectId = infuraProjectId; this.#infuraProjectId = infuraProjectId;
this._trackMetaMetricsEvent = trackMetaMetricsEvent; this.#trackMetaMetricsEvent = trackMetaMetricsEvent;
} }
/** /**
@ -535,7 +500,7 @@ export class NetworkController extends EventEmitter {
* In-progress requests will not be aborted. * In-progress requests will not be aborted.
*/ */
async destroy(): Promise<void> { async destroy(): Promise<void> {
await this._blockTracker?.destroy(); await this.#blockTracker?.destroy();
} }
/** /**
@ -543,8 +508,8 @@ export class NetworkController extends EventEmitter {
* using the provider to gather details about the network. * using the provider to gather details about the network.
*/ */
async initializeProvider(): Promise<void> { async initializeProvider(): Promise<void> {
const { type, rpcUrl, chainId } = this.providerStore.getState(); const { type, rpcUrl, chainId } = this.store.getState().provider;
this._configureProvider({ type, rpcUrl, chainId }); this.#configureProvider({ type, rpcUrl, chainId });
await this.lookupNetwork(); await this.lookupNetwork();
} }
@ -555,8 +520,8 @@ export class NetworkController extends EventEmitter {
provider: SwappableProxy<SafeEventEmitterProvider> | null; provider: SwappableProxy<SafeEventEmitterProvider> | null;
blockTracker: SwappableProxy<PollingBlockTracker> | null; blockTracker: SwappableProxy<PollingBlockTracker> | null;
} { } {
const provider = this._providerProxy; const provider = this.#providerProxy;
const blockTracker = this._blockTrackerProxy; const blockTracker = this.#blockTrackerProxy;
return { provider, blockTracker }; return { provider, blockTracker };
} }
@ -569,7 +534,7 @@ export class NetworkController extends EventEmitter {
* and false otherwise. * and false otherwise.
*/ */
async getEIP1559Compatibility(): Promise<boolean> { async getEIP1559Compatibility(): Promise<boolean> {
const { EIPS } = this.networkDetails.getState(); const { EIPS } = this.store.getState().networkDetails;
// NOTE: This isn't necessary anymore because the block cache middleware // NOTE: This isn't necessary anymore because the block cache middleware
// already prevents duplicate requests from taking place // already prevents duplicate requests from taking place
if (EIPS[1559] !== undefined) { if (EIPS[1559] !== undefined) {
@ -584,11 +549,15 @@ export class NetworkController extends EventEmitter {
return false; return false;
} }
const supportsEIP1559 = await this._determineEIP1559Compatibility(provider); const supportsEIP1559 = await this.#determineEIP1559Compatibility(provider);
this.networkDetails.updateState({ const { networkDetails } = this.store.getState();
EIPS: { this.store.updateState({
...this.networkDetails.getState().EIPS, networkDetails: {
1559: supportsEIP1559, ...networkDetails,
EIPS: {
...networkDetails.EIPS,
1559: supportsEIP1559,
},
}, },
}); });
return supportsEIP1559; return supportsEIP1559;
@ -606,7 +575,7 @@ export class NetworkController extends EventEmitter {
* blocking requests, or if the network is not Infura-supported. * blocking requests, or if the network is not Infura-supported.
*/ */
async lookupNetwork(): Promise<void> { async lookupNetwork(): Promise<void> {
const { chainId, type } = this.providerStore.getState(); const { chainId, type } = this.store.getState().provider;
const { provider } = this.getProviderAndBlockTracker(); const { provider } = this.getProviderAndBlockTracker();
let networkChanged = false; let networkChanged = false;
let networkId: NetworkIdState = null; let networkId: NetworkIdState = null;
@ -624,9 +593,9 @@ export class NetworkController extends EventEmitter {
log.warn( log.warn(
'NetworkController - lookupNetwork aborted due to missing chainId', 'NetworkController - lookupNetwork aborted due to missing chainId',
); );
this._resetNetworkId(); this.#resetNetworkId();
this._resetNetworkStatus(); this.#resetNetworkStatus();
this._resetNetworkDetails(); this.#resetNetworkDetails();
return; return;
} }
@ -634,20 +603,20 @@ export class NetworkController extends EventEmitter {
const listener = () => { const listener = () => {
networkChanged = true; networkChanged = true;
this.messenger.unsubscribe( this.#messenger.unsubscribe(
NetworkControllerEventType.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
listener, listener,
); );
}; };
this.messenger.subscribe( this.#messenger.subscribe(
NetworkControllerEventType.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
listener, listener,
); );
try { try {
const results = await Promise.all([ const results = await Promise.all([
this._getNetworkId(provider), this.#getNetworkId(provider),
this._determineEIP1559Compatibility(provider), this.#determineEIP1559Compatibility(provider),
]); ]);
const possibleNetworkId = results[0]; const possibleNetworkId = results[0];
assertNetworkId(possibleNetworkId); assertNetworkId(possibleNetworkId);
@ -687,37 +656,43 @@ export class NetworkController extends EventEmitter {
// in the process of being called, so we don't need to go further. // in the process of being called, so we don't need to go further.
return; return;
} }
this.messenger.unsubscribe( this.#messenger.unsubscribe(
NetworkControllerEventType.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
listener, listener,
); );
this.networkStatusStore.putState(networkStatus); this.store.updateState({
networkStatus,
});
if (networkStatus === NetworkStatus.Available) { if (networkStatus === NetworkStatus.Available) {
this.networkIdStore.putState(networkId); const { networkDetails } = this.store.getState();
this.networkDetails.updateState({ this.store.updateState({
EIPS: { networkId,
...this.networkDetails.getState().EIPS, networkDetails: {
1559: supportsEIP1559, ...networkDetails,
EIPS: {
...networkDetails.EIPS,
1559: supportsEIP1559,
},
}, },
}); });
} else { } else {
this._resetNetworkId(); this.#resetNetworkId();
this._resetNetworkDetails(); this.#resetNetworkDetails();
} }
if (isInfura) { if (isInfura) {
if (networkStatus === NetworkStatus.Available) { if (networkStatus === NetworkStatus.Available) {
this.messenger.publish(NetworkControllerEventType.InfuraIsUnblocked); this.#messenger.publish(NetworkControllerEventType.InfuraIsUnblocked);
} else if (networkStatus === NetworkStatus.Blocked) { } else if (networkStatus === NetworkStatus.Blocked) {
this.messenger.publish(NetworkControllerEventType.InfuraIsBlocked); this.#messenger.publish(NetworkControllerEventType.InfuraIsBlocked);
} }
} else { } else {
// Always publish infuraIsUnblocked regardless of network status to // Always publish infuraIsUnblocked regardless of network status to
// prevent consumers from being stuck in a blocked state if they were // prevent consumers from being stuck in a blocked state if they were
// previously connected to an Infura network that was blocked // previously connected to an Infura network that was blocked
this.messenger.publish(NetworkControllerEventType.InfuraIsUnblocked); this.#messenger.publish(NetworkControllerEventType.InfuraIsUnblocked);
} }
} }
@ -731,7 +706,7 @@ export class NetworkController extends EventEmitter {
*/ */
setActiveNetwork(networkConfigurationId: NetworkConfigurationId): string { setActiveNetwork(networkConfigurationId: NetworkConfigurationId): string {
const targetNetwork = const targetNetwork =
this.networkConfigurationsStore.getState()[networkConfigurationId]; this.store.getState().networkConfigurations[networkConfigurationId];
if (!targetNetwork) { if (!targetNetwork) {
throw new Error( throw new Error(
@ -739,7 +714,7 @@ export class NetworkController extends EventEmitter {
); );
} }
this._setProviderConfig({ this.#setProviderConfig({
type: NETWORK_TYPES.RPC, type: NETWORK_TYPES.RPC,
...targetNetwork, ...targetNetwork,
}); });
@ -754,7 +729,7 @@ export class NetworkController extends EventEmitter {
* @throws if the `type` is "rpc" or if it is not a known Infura-supported * @throws if the `type` is "rpc" or if it is not a known Infura-supported
* network. * network.
*/ */
setProviderType(type: string): void { async setProviderType(type: string) {
assert.notStrictEqual( assert.notStrictEqual(
type, type,
NETWORK_TYPES.RPC, NETWORK_TYPES.RPC,
@ -765,7 +740,7 @@ export class NetworkController extends EventEmitter {
`Unknown Infura provider type "${type}".`, `Unknown Infura provider type "${type}".`,
); );
const network = BUILT_IN_INFURA_NETWORKS[type]; const network = BUILT_IN_INFURA_NETWORKS[type];
this._setProviderConfig({ await this.#setProviderConfig({
type, type,
rpcUrl: '', rpcUrl: '',
chainId: network.chainId, chainId: network.chainId,
@ -778,8 +753,8 @@ export class NetworkController extends EventEmitter {
/** /**
* Re-initializes the provider and block tracker for the current network. * Re-initializes the provider and block tracker for the current network.
*/ */
resetConnection(): void { async resetConnection() {
this._setProviderConfig(this.providerStore.getState()); await this.#setProviderConfig(this.store.getState().provider);
} }
/** /**
@ -789,8 +764,10 @@ export class NetworkController extends EventEmitter {
*/ */
async rollbackToPreviousProvider() { async rollbackToPreviousProvider() {
const config = this.#previousProviderConfig; const config = this.#previousProviderConfig;
this.providerStore.putState(config); this.store.updateState({
await this._switchNetwork(config); provider: config,
});
await this.#switchNetwork(config);
} }
/** /**
@ -800,7 +777,7 @@ export class NetworkController extends EventEmitter {
* @returns A promise that either resolves to the block header or null if * @returns A promise that either resolves to the block header or null if
* there is no latest block, or rejects with an error. * there is no latest block, or rejects with an error.
*/ */
_getLatestBlock(provider: SafeEventEmitterProvider): Promise<Block | null> { #getLatestBlock(provider: SafeEventEmitterProvider): Promise<Block | null> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ethQuery = new EthQuery(provider); const ethQuery = new EthQuery(provider);
ethQuery.sendAsync<['latest', false], Block | null>( ethQuery.sendAsync<['latest', false], Block | null>(
@ -823,7 +800,7 @@ export class NetworkController extends EventEmitter {
* @returns A promise that either resolves to the network ID, or rejects with * @returns A promise that either resolves to the network ID, or rejects with
* an error. * an error.
*/ */
async _getNetworkId(provider: SafeEventEmitterProvider): Promise<string> { async #getNetworkId(provider: SafeEventEmitterProvider): Promise<string> {
const ethQuery = new EthQuery(provider); const ethQuery = new EthQuery(provider);
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
ethQuery.sendAsync<never[], string>( ethQuery.sendAsync<never[], string>(
@ -842,22 +819,28 @@ export class NetworkController extends EventEmitter {
/** /**
* Clears the stored network ID. * Clears the stored network ID.
*/ */
_resetNetworkId(): void { #resetNetworkId(): void {
this.networkIdStore.putState(buildDefaultNetworkIdState()); this.store.updateState({
networkId: buildDefaultNetworkIdState(),
});
} }
/** /**
* Resets network status to the default ("unknown"). * Resets network status to the default ("unknown").
*/ */
_resetNetworkStatus(): void { #resetNetworkStatus(): void {
this.networkStatusStore.putState(buildDefaultNetworkStatusState()); this.store.updateState({
networkStatus: buildDefaultNetworkStatusState(),
});
} }
/** /**
* Clears details previously stored for the network. * Clears details previously stored for the network.
*/ */
_resetNetworkDetails(): void { #resetNetworkDetails(): void {
this.networkDetails.putState(buildDefaultNetworkDetailsState()); this.store.updateState({
networkDetails: buildDefaultNetworkDetailsState(),
});
} }
/** /**
@ -866,10 +849,10 @@ export class NetworkController extends EventEmitter {
* *
* @param providerConfig - The provider configuration. * @param providerConfig - The provider configuration.
*/ */
async _setProviderConfig(providerConfig: ProviderConfiguration) { async #setProviderConfig(providerConfig: ProviderConfiguration) {
this.#previousProviderConfig = this.providerStore.getState(); this.#previousProviderConfig = this.store.getState().provider;
this.providerStore.putState(providerConfig); this.store.updateState({ provider: providerConfig });
await this._switchNetwork(providerConfig); await this.#switchNetwork(providerConfig);
} }
/** /**
@ -881,10 +864,10 @@ export class NetworkController extends EventEmitter {
* @returns A promise that resolves to true if the network supports EIP-1559 * @returns A promise that resolves to true if the network supports EIP-1559
* and false otherwise. * and false otherwise.
*/ */
async _determineEIP1559Compatibility( async #determineEIP1559Compatibility(
provider: SafeEventEmitterProvider, provider: SafeEventEmitterProvider,
): Promise<boolean> { ): Promise<boolean> {
const latestBlock = await this._getLatestBlock(provider); const latestBlock = await this.#getLatestBlock(provider);
return latestBlock?.baseFeePerGas !== undefined; return latestBlock?.baseFeePerGas !== undefined;
} }
@ -900,13 +883,13 @@ export class NetworkController extends EventEmitter {
* @param providerConfig - The provider configuration object that specifies * @param providerConfig - The provider configuration object that specifies
* the new network. * the new network.
*/ */
async _switchNetwork(providerConfig: ProviderConfiguration) { async #switchNetwork(providerConfig: ProviderConfiguration) {
this.messenger.publish(NetworkControllerEventType.NetworkWillChange); this.#messenger.publish(NetworkControllerEventType.NetworkWillChange);
this._resetNetworkId(); this.#resetNetworkId();
this._resetNetworkStatus(); this.#resetNetworkStatus();
this._resetNetworkDetails(); this.#resetNetworkDetails();
this._configureProvider(providerConfig); this.#configureProvider(providerConfig);
this.messenger.publish(NetworkControllerEventType.NetworkDidChange); this.#messenger.publish(NetworkControllerEventType.NetworkDidChange);
await this.lookupNetwork(); await this.lookupNetwork();
} }
@ -924,20 +907,20 @@ export class NetworkController extends EventEmitter {
* any Infura-supported network). * any Infura-supported network).
* @throws if the `type` if not a known Infura-supported network. * @throws if the `type` if not a known Infura-supported network.
*/ */
_configureProvider({ type, rpcUrl, chainId }: ProviderConfiguration): void { #configureProvider({ type, rpcUrl, chainId }: ProviderConfiguration): void {
const isInfura = isInfuraProviderType(type); const isInfura = isInfuraProviderType(type);
if (isInfura) { if (isInfura) {
// infura type-based endpoints // infura type-based endpoints
this._configureInfuraProvider({ this.#configureInfuraProvider({
type, type,
infuraProjectId: this._infuraProjectId, infuraProjectId: this.#infuraProjectId,
}); });
} else if (type === NETWORK_TYPES.RPC && rpcUrl) { } else if (type === NETWORK_TYPES.RPC && rpcUrl) {
// url-based rpc endpoints // url-based rpc endpoints
this._configureStandardProvider(rpcUrl, chainId); this.#configureStandardProvider(rpcUrl, chainId);
} else { } else {
throw new Error( throw new Error(
`NetworkController - _configureProvider - unknown type "${type}"`, `NetworkController - #configureProvider - unknown type "${type}"`,
); );
} }
} }
@ -952,20 +935,20 @@ export class NetworkController extends EventEmitter {
* @param args.infuraProjectId - An Infura API key. ("Project ID" is a * @param args.infuraProjectId - An Infura API key. ("Project ID" is a
* now-obsolete term we've retained for backward compatibility.) * now-obsolete term we've retained for backward compatibility.)
*/ */
_configureInfuraProvider({ #configureInfuraProvider({
type, type,
infuraProjectId, infuraProjectId,
}: { }: {
type: BuiltInInfuraNetwork; type: BuiltInInfuraNetwork;
infuraProjectId: NetworkControllerOptions['infuraProjectId']; infuraProjectId: NetworkControllerOptions['infuraProjectId'];
}): void { }): void {
log.info('NetworkController - configureInfuraProvider', type); log.info('NetworkController - #configureInfuraProvider', type);
const { provider, blockTracker } = createNetworkClient({ const { provider, blockTracker } = createNetworkClient({
network: type, network: type,
infuraProjectId, infuraProjectId,
type: NetworkClientType.Infura, type: NetworkClientType.Infura,
}); });
this._setProviderAndBlockTracker({ provider, blockTracker }); this.#setProviderAndBlockTracker({ provider, blockTracker });
} }
/** /**
@ -975,14 +958,14 @@ export class NetworkController extends EventEmitter {
* @param rpcUrl - The URL of the RPC endpoint that represents the network. * @param rpcUrl - The URL of the RPC endpoint that represents the network.
* @param chainId - The chain ID of the network (as per EIP-155). * @param chainId - The chain ID of the network (as per EIP-155).
*/ */
_configureStandardProvider(rpcUrl: string, chainId: ChainId): void { #configureStandardProvider(rpcUrl: string, chainId: ChainId): void {
log.info('NetworkController - configureStandardProvider', rpcUrl); log.info('NetworkController - #configureStandardProvider', rpcUrl);
const { provider, blockTracker } = createNetworkClient({ const { provider, blockTracker } = createNetworkClient({
chainId, chainId,
rpcUrl, rpcUrl,
type: NetworkClientType.Custom, type: NetworkClientType.Custom,
}); });
this._setProviderAndBlockTracker({ provider, blockTracker }); this.#setProviderAndBlockTracker({ provider, blockTracker });
} }
/** /**
@ -994,7 +977,7 @@ export class NetworkController extends EventEmitter {
* @param args.provider - The provider. * @param args.provider - The provider.
* @param args.blockTracker - The block tracker. * @param args.blockTracker - The block tracker.
*/ */
_setProviderAndBlockTracker({ #setProviderAndBlockTracker({
provider, provider,
blockTracker, blockTracker,
}: { }: {
@ -1002,21 +985,21 @@ export class NetworkController extends EventEmitter {
blockTracker: PollingBlockTracker; blockTracker: PollingBlockTracker;
}): void { }): void {
// update or initialize proxies // update or initialize proxies
if (this._providerProxy) { if (this.#providerProxy) {
this._providerProxy.setTarget(provider); this.#providerProxy.setTarget(provider);
} else { } else {
this._providerProxy = createSwappableProxy(provider); this.#providerProxy = createSwappableProxy(provider);
} }
if (this._blockTrackerProxy) { if (this.#blockTrackerProxy) {
this._blockTrackerProxy.setTarget(blockTracker); this.#blockTrackerProxy.setTarget(blockTracker);
} else { } else {
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, { this.#blockTrackerProxy = createEventEmitterProxy(blockTracker, {
eventFilter: 'skipInternal', eventFilter: 'skipInternal',
}); });
} }
// set new provider and blockTracker // set new provider and blockTracker
this._provider = provider; this.#provider = provider;
this._blockTracker = blockTracker; this.#blockTracker = blockTracker;
} }
/** /**
@ -1105,7 +1088,7 @@ export class NetworkController extends EventEmitter {
); );
} }
const networkConfigurations = this.networkConfigurationsStore.getState(); const { networkConfigurations } = this.store.getState();
const newNetworkConfiguration = { const newNetworkConfiguration = {
rpcUrl, rpcUrl,
chainId, chainId,
@ -1120,16 +1103,18 @@ export class NetworkController extends EventEmitter {
)?.id; )?.id;
const newNetworkConfigurationId = oldNetworkConfigurationId || uuid(); const newNetworkConfigurationId = oldNetworkConfigurationId || uuid();
this.networkConfigurationsStore.putState({ this.store.updateState({
...networkConfigurations, networkConfigurations: {
[newNetworkConfigurationId]: { ...networkConfigurations,
...newNetworkConfiguration, [newNetworkConfigurationId]: {
id: newNetworkConfigurationId, ...newNetworkConfiguration,
id: newNetworkConfigurationId,
},
}, },
}); });
if (!oldNetworkConfigurationId) { if (!oldNetworkConfigurationId) {
this._trackMetaMetricsEvent({ this.#trackMetaMetricsEvent({
event: 'Custom Network Added', event: 'Custom Network Added',
category: MetaMetricsEventCategory.Network, category: MetaMetricsEventCategory.Network,
referrer: { referrer: {
@ -1160,9 +1145,11 @@ export class NetworkController extends EventEmitter {
networkConfigurationId: NetworkConfigurationId, networkConfigurationId: NetworkConfigurationId,
): void { ): void {
const networkConfigurations = { const networkConfigurations = {
...this.networkConfigurationsStore.getState(), ...this.store.getState().networkConfigurations,
}; };
delete networkConfigurations[networkConfigurationId]; delete networkConfigurations[networkConfigurationId];
this.networkConfigurationsStore.putState(networkConfigurations); this.store.updateState({
networkConfigurations,
});
} }
} }

View File

@ -16,7 +16,6 @@ describe('buildSnapRestrictedMethodSpecifications', () => {
getSnap: () => undefined, getSnap: () => undefined,
getSnapRpcHandler: () => undefined, getSnapRpcHandler: () => undefined,
getSnapState: () => undefined, getSnapState: () => undefined,
showConfirmation: () => undefined,
updateSnapState: () => undefined, updateSnapState: () => undefined,
}; };

View File

@ -2,36 +2,13 @@ import { strict as assert } from 'assert';
import sinon from 'sinon'; import sinon from 'sinon';
import { ControllerMessenger } from '@metamask/base-controller'; import { ControllerMessenger } from '@metamask/base-controller';
import { TokenListController } from '@metamask/assets-controllers'; import { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
import { NetworkController } from './network';
describe('preferences controller', function () { describe('preferences controller', function () {
let preferencesController; let preferencesController;
let network;
let currentChainId;
let provider;
let tokenListController; let tokenListController;
beforeEach(function () { beforeEach(function () {
const sandbox = sinon.createSandbox();
currentChainId = CHAIN_IDS.MAINNET;
const networkControllerProviderConfig = {
getAccounts: () => undefined,
};
const networkControllerMessenger = new ControllerMessenger();
network = new NetworkController({
infuraProjectId: 'foo',
messenger: networkControllerMessenger,
state: {
provider: {
type: 'mainnet',
chainId: currentChainId,
},
},
});
network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
const tokenListMessenger = new ControllerMessenger().getRestricted({ const tokenListMessenger = new ControllerMessenger().getRestricted({
name: 'TokenListController', name: 'TokenListController',
}); });
@ -43,14 +20,8 @@ describe('preferences controller', function () {
messenger: tokenListMessenger, messenger: tokenListMessenger,
}); });
sandbox
.stub(network, '_getLatestBlock')
.callsFake(() => Promise.resolve({}));
preferencesController = new PreferencesController({ preferencesController = new PreferencesController({
initLangCode: 'en_US', initLangCode: 'en_US',
network,
provider,
tokenListController, tokenListController,
onInfuraIsBlocked: sinon.spy(), onInfuraIsBlocked: sinon.spy(),
onInfuraIsUnblocked: sinon.spy(), onInfuraIsUnblocked: sinon.spy(),

View File

@ -1,5 +1,7 @@
import { errorCodes } from 'eth-rpc-errors'; import { errorCodes } from 'eth-rpc-errors';
import { detectSIWE } from '@metamask/controller-utils'; import { detectSIWE } from '@metamask/controller-utils';
import { isValidAddress } from 'ethereumjs-util';
import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app';
import { TransactionStatus } from '../../../shared/constants/transaction'; import { TransactionStatus } from '../../../shared/constants/transaction';
import { SECOND } from '../../../shared/constants/time'; import { SECOND } from '../../../shared/constants/time';
@ -168,8 +170,17 @@ export default function createRPCMethodTrackingMiddleware({
if (event === MetaMetricsEventName.SignatureRequested) { if (event === MetaMetricsEventName.SignatureRequested) {
eventProperties.signature_type = method; eventProperties.signature_type = method;
const data = req?.params?.[0]; // In personal messages the first param is data while in typed messages second param is data
const from = req?.params?.[1]; // if condition below is added to ensure that the right params are captured as data and address.
let data;
let from;
if (isValidAddress(req?.params?.[1])) {
data = req?.params?.[0];
from = req?.params?.[1];
} else {
data = req?.params?.[1];
from = req?.params?.[0];
}
const paramsExamplePassword = req?.params?.[2]; const paramsExamplePassword = req?.params?.[2];
const msgData = { const msgData = {

View File

@ -383,5 +383,66 @@ describe('createRPCMethodTrackingMiddleware', () => {
}); });
}); });
}); });
describe('when signature requests are received', () => {
let securityProviderReq, fnHandler;
beforeEach(() => {
securityProviderReq = jest.fn().mockReturnValue(() =>
Promise.resolve({
flagAsDangerous: 0,
}),
);
fnHandler = createRPCMethodTrackingMiddleware({
trackEvent,
getMetricsState,
rateLimitSeconds: 1,
securityProviderRequest: securityProviderReq,
});
});
it(`should pass correct data for personal sign`, async () => {
const req = {
method: 'personal_sign',
params: [
'0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765',
'0x8eeee1781fd885ff5ddef7789486676961873d12',
'Example password',
],
jsonrpc: '2.0',
id: 1142196570,
origin: 'https://metamask.github.io',
tabId: 1048582817,
};
const res = { id: 1142196570, jsonrpc: '2.0' };
const { next } = getNext();
await fnHandler(req, res, next);
expect(securityProviderReq).toHaveBeenCalledTimes(1);
const call = securityProviderReq.mock.calls[0][0];
expect(call.msgParams.data).toStrictEqual(req.params[0]);
});
it(`should pass correct data for typed sign`, async () => {
const req = {
method: 'eth_signTypedData_v4',
params: [
'0x8eeee1781fd885ff5ddef7789486676961873d12',
'{"domain":{"chainId":"5","name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Group":[{"name":"name","type":"string"},{"name":"members","type":"Person[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}',
],
jsonrpc: '2.0',
id: 1142196571,
origin: 'https://metamask.github.io',
tabId: 1048582817,
};
const res = { id: 1142196571, jsonrpc: '2.0' };
const { next } = getNext();
await fnHandler(req, res, next);
expect(securityProviderReq).toHaveBeenCalledTimes(1);
const call = securityProviderReq.mock.calls[0][0];
expect(call.msgParams.data).toStrictEqual(req.params[1]);
});
});
}); });
}); });

View File

@ -113,7 +113,7 @@ async function switchEthereumChainHandler(
approvedRequestData.type !== NETWORK_TYPES.LOCALHOST && approvedRequestData.type !== NETWORK_TYPES.LOCALHOST &&
approvedRequestData.type !== NETWORK_TYPES.LINEA_TESTNET approvedRequestData.type !== NETWORK_TYPES.LINEA_TESTNET
) { ) {
setProviderType(approvedRequestData.type); await setProviderType(approvedRequestData.type);
} else { } else {
await setActiveNetwork(approvedRequestData.id); await setActiveNetwork(approvedRequestData.id);
} }

View File

@ -544,7 +544,7 @@ export default class MetamaskController extends EventEmitter {
messenger: currencyRateMessenger, messenger: currencyRateMessenger,
state: { state: {
...initState.CurrencyController, ...initState.CurrencyController,
nativeCurrency: this.networkController.providerStore.getState().ticker, nativeCurrency: this.networkController.store.getState().provider.ticker,
}, },
}); });
@ -981,8 +981,16 @@ export default class MetamaskController extends EventEmitter {
getNetworkId: () => this.networkController.store.getState().networkId, getNetworkId: () => this.networkController.store.getState().networkId,
getNetworkStatus: () => getNetworkStatus: () =>
this.networkController.store.getState().networkStatus, this.networkController.store.getState().networkStatus,
onNetworkStateChange: (listener) => onNetworkStateChange: (listener) => {
this.networkController.networkIdStore.subscribe(listener), let previousNetworkId =
this.networkController.store.getState().networkId;
this.networkController.store.subscribe((state) => {
if (previousNetworkId !== state.networkId) {
listener();
previousNetworkId = state.networkId;
}
});
},
getCurrentChainId: () => getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId, this.networkController.store.getState().provider.chainId,
preferencesStore: this.preferencesController.store, preferencesStore: this.preferencesController.store,
@ -1166,6 +1174,9 @@ export default class MetamaskController extends EventEmitter {
preferencesController: this.preferencesController, preferencesController: this.preferencesController,
getState: this.getState.bind(this), getState: this.getState.bind(this),
securityProviderRequest: this.securityProviderRequest.bind(this), securityProviderRequest: this.securityProviderRequest.bind(this),
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
}); });
this.swapsController = new SwapsController({ this.swapsController = new SwapsController({
@ -1524,12 +1535,6 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger, this.controllerMessenger,
'SnapController:getSnapState', 'SnapController:getSnapState',
), ),
showConfirmation: (origin, confirmationData) =>
this.approvalController.addAndShowApprovalRequest({
origin,
type: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,
requestData: confirmationData,
}),
showDialog: (origin, type, content, placeholder) => showDialog: (origin, type, content, placeholder) =>
this.approvalController.addAndShowApprovalRequest({ this.approvalController.addAndShowApprovalRequest({
origin, origin,
@ -3237,32 +3242,6 @@ export default class MetamaskController extends EventEmitter {
return await this.txController.newUnapprovedTransaction(txParams, req); return await this.txController.newUnapprovedTransaction(txParams, req);
} }
///: BEGIN:ONLY_INCLUDE_IN(flask)
/**
* Gets an "app key" corresponding to an Ethereum address. An app key is more
* or less an addrdess hashed together with some string, in this case a
* subject identifier / origin.
*
* @todo Figure out a way to derive app keys that doesn't depend on the user's
* Ethereum addresses.
* @param {string} subject - The identifier of the subject whose app key to
* retrieve.
* @param {string} [requestedAccount] - The account whose app key to retrieve.
* The first account in the keyring will be used by default.
*/
async getAppKeyForSubject(subject, requestedAccount) {
let account;
if (requestedAccount) {
account = requestedAccount;
} else {
[account] = await this.keyringController.getAccounts();
}
return this.keyringController.exportAppKeyForAddress(account, subject);
}
///: END:ONLY_INCLUDE_IN
// eth_decrypt methods // eth_decrypt methods
/** /**
@ -3868,7 +3847,6 @@ export default class MetamaskController extends EventEmitter {
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
engine.push( engine.push(
createSnapMethodMiddleware(subjectType === SubjectType.Snap, { createSnapMethodMiddleware(subjectType === SubjectType.Snap, {
getAppKey: this.getAppKeyForSubject.bind(this, origin),
getUnlockPromise: this.appStateController.getUnlockPromise.bind( getUnlockPromise: this.appStateController.getUnlockPromise.bind(
this.appStateController, this.appStateController,
), ),

View File

@ -17,7 +17,7 @@ import {
TextVariant, TextVariant,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import GasDetailsItem from '../gas-details-item/gas-details-item'; import { ConfirmGasDisplay } from '../confirm-gas-display';
import MultiLayerFeeMessage from '../multilayer-fee-message/multi-layer-fee-message'; import MultiLayerFeeMessage from '../multilayer-fee-message/multi-layer-fee-message';
import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'; import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
@ -111,7 +111,7 @@ export default function ApproveContentCard({
(!isMultiLayerFeeNetwork && (!isMultiLayerFeeNetwork &&
supportsEIP1559 && supportsEIP1559 &&
!renderSimulationFailureWarning ? ( !renderSimulationFailureWarning ? (
<GasDetailsItem <ConfirmGasDisplay
userAcknowledgedGasMissing={userAcknowledgedGasMissing} userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/> />
) : ( ) : (

View File

@ -0,0 +1,157 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmGasDisplay should match snapshot 1`] = `
<div>
<div
class="transaction-detail-item"
>
<div
class="transaction-detail-item__row"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--display-flex box--flex-direction-row box--flex-wrap-nowrap box--align-items-center typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
<div
class="box box--display-flex box--flex-direction-row"
>
<div
class="box box--margin-right-1 box--flex-direction-row"
>
Gas
</div>
<span
class="gas-details-item-title__estimate"
>
(
estimated
)
</span>
<div
class="info-tooltip"
>
<div>
<div
aria-describedby="tippy-tooltip-1"
class="info-tooltip__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 0C2.2 0 0 2.2 0 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 2c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.2-.7-.6.3-.8.7-.8zm.7 6H4.3V4.3h1.5V8z"
fill="var(--color-icon-alternative)"
/>
</svg>
</div>
</div>
</div>
</div>
</h6>
<div
class="transaction-detail-item__detail-values"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h6 typography--weight-normal typography--style-normal typography--color-text-alternative"
>
<div
class="gas-details-item__currency-container"
>
<div
class="currency-display-component"
title="0"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0
</span>
</div>
</div>
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
<div
class="gas-details-item__currency-container"
>
<div
class="currency-display-component"
title="0 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</h6>
</div>
</div>
<div
class="transaction-detail-item__row"
>
<div>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography gas-timing gas-timing--positive typography--h7 typography--weight-normal typography--style-normal typography--color-text-default"
>
Maybe in 1 seconds
</h6>
</div>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative"
>
<div
class="box gas-details-item__gasfee-label box--display-inline-flex box--flex-direction-row"
>
<div
class="box box--margin-right-1 box--flex-direction-row"
>
<strong>
Max fee:
</strong>
</div>
<div
class="gas-details-item__currency-container"
>
<div
class="currency-display-component"
title="0 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</div>
</h6>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import {
checkNetworkAndAccountSupports1559,
txDataSelector,
} from '../../../selectors';
import { isLegacyTransaction } from '../../../helpers/utils/transactions.util';
import GasDetailsItem from '../gas-details-item';
import { getCurrentDraftTransaction } from '../../../ducks/send';
import { TransactionEnvelopeType } from '../../../../shared/constants/transaction';
import { ConfirmLegacyGasDisplay } from './confirm-legacy-gas-display';
const ConfirmGasDisplay = ({ userAcknowledgedGasMissing = false }) => {
const { txParams } = useSelector((state) => txDataSelector(state));
const draftTransaction = useSelector(getCurrentDraftTransaction);
const transactionType = draftTransaction?.transactionType;
let isLegacyTxn;
if (transactionType) {
isLegacyTxn = transactionType === TransactionEnvelopeType.legacy;
} else {
isLegacyTxn = isLegacyTransaction(txParams);
}
const networkAndAccountSupports1559 = useSelector(
checkNetworkAndAccountSupports1559,
);
const supportsEIP1559 = networkAndAccountSupports1559 && !isLegacyTxn;
return supportsEIP1559 ? (
<GasDetailsItem userAcknowledgedGasMissing={userAcknowledgedGasMissing} />
) : (
<ConfirmLegacyGasDisplay />
);
};
ConfirmGasDisplay.propTypes = {
userAcknowledgedGasMissing: PropTypes.bool,
};
export default ConfirmGasDisplay;

View File

@ -0,0 +1,101 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { GasEstimateTypes } from '../../../../shared/constants/gas';
import mockEstimates from '../../../../test/data/mock-estimates.json';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import ConfirmGasDisplay from './confirm-gas-display';
jest.mock('../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(),
getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()),
}));
const render = ({ transactionProp = {}, contextProps = {} } = {}) => {
const store = configureStore({
...mockState,
...contextProps,
metamask: {
...mockState.metamask,
accounts: {
[mockState.metamask.selectedAddress]: {
address: mockState.metamask.selectedAddress,
balance: '0x1F4',
},
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket],
},
});
return renderWithProvider(
<GasFeeContextProvider transaction={transactionProp}>
<ConfirmGasDisplay />
</GasFeeContextProvider>,
store,
);
};
describe('ConfirmGasDisplay', () => {
it('should match snapshot', async () => {
const { container } = render();
expect(container).toMatchSnapshot();
});
it('should render gas display labels for EIP1559 transcations', () => {
render({
transactionProp: {
txParams: {
gas: '0x5208',
maxFeePerGas: '0x59682f10',
maxPriorityFeePerGas: '0x59682f00',
},
userFeeLevel: 'medium',
},
});
expect(screen.queryByText('Gas')).toBeInTheDocument();
expect(screen.queryByText('(estimated)')).toBeInTheDocument();
expect(screen.queryByText('Max fee:')).toBeInTheDocument();
expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0);
});
it('should render gas display labels for legacy transcations', () => {
render({
contextProps: {
metamask: {
networkDetails: {
EIPS: {
1559: false,
},
},
},
confirmTransaction: {
txData: {
id: 8393540981007587,
status: 'unapproved',
chainId: '0x5',
txParams: {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
to: '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
value: '0x0',
gas: '0x5208',
gasPrice: '0x3b9aca00',
type: '0x0',
},
},
},
},
});
expect(screen.queryByText('Estimated gas fee')).toBeInTheDocument();
expect(screen.queryByText('Max fee:')).toBeInTheDocument();
expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0);
});
});

View File

@ -0,0 +1,9 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { ConfirmLegacyGasDisplay } from '.';
# Confirm Legacy Gas Display
Confirm Legacy Gas Display is used on confirmation screen and send screen to display gas details for legacy transaction.
<Canvas>
<Story id="components-app-ConfirmLegacyGasDisplay--default-story" />
</Canvas>

View File

@ -0,0 +1,124 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmLegacyGasDisplay should match snapshot 1`] = `
<div>
<div
class="transaction-detail-item"
>
<div
class="transaction-detail-item__row"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--display-flex box--flex-direction-row box--flex-wrap-nowrap box--align-items-center typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Estimated gas fee
<div
class="info-tooltip"
>
<div>
<div
aria-describedby="tippy-tooltip-1"
class="info-tooltip__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 0C2.2 0 0 2.2 0 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 2c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.2-.7-.6.3-.8.7-.8zm.7 6H4.3V4.3h1.5V8z"
fill="var(--color-icon-alternative)"
/>
</svg>
</div>
</div>
</div>
</h6>
<div
class="transaction-detail-item__detail-values"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h6 typography--weight-normal typography--style-normal typography--color-text-alternative"
>
<div>
<div
class="currency-display-component"
title="0.000021"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.000021
</span>
</div>
</div>
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
<div>
<div
class="currency-display-component"
title="0.000021 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.000021
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</h6>
</div>
</div>
<div
class="transaction-detail-item__row"
>
<div />
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative"
>
<strong>
Max fee:
</strong>
<div>
<div
class="currency-display-component"
title="0.000021 ETH"
>
<span
class="currency-display-component__prefix"
/>
<span
class="currency-display-component__text"
>
0.000021
</span>
<span
class="currency-display-component__suffix"
>
ETH
</span>
</div>
</div>
</h6>
</div>
</div>
</div>
`;
exports[`ConfirmLegacyGasDisplay should match snapshot 2`] = `<div />`;

View File

@ -0,0 +1,149 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
getIsMainnet,
getPreferences,
getUnapprovedTransactions,
getUseCurrencyRateCheck,
transactionFeeSelector,
txDataSelector,
} from '../../../../selectors';
import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common';
import TransactionDetailItem from '../../transaction-detail-item';
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display';
import InfoTooltip from '../../../ui/info-tooltip';
import LoadingHeartBeat from '../../../ui/loading-heartbeat';
import { Text } from '../../../component-library/text';
import {
FONT_STYLE,
TextVariant,
TextColor,
} from '../../../../helpers/constants/design-system';
import { useDraftTransactionGasValues } from '../../../../hooks/useDraftTransactionGasValues';
const renderHeartBeatIfNotInTest = () =>
process.env.IN_TEST ? null : <LoadingHeartBeat />;
const ConfirmLegacyGasDisplay = () => {
const t = useI18nContext();
// state selectors
const isMainnet = useSelector(getIsMainnet);
const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences);
const unapprovedTxs = useSelector(getUnapprovedTransactions);
const { transactionData } = useDraftTransactionGasValues();
const txData = useSelector((state) => txDataSelector(state));
const { id: transactionId, dappSuggestedGasFees } = txData;
const transaction = Object.keys(transactionData).length
? transactionData
: unapprovedTxs[transactionId] || {};
const { hexMinimumTransactionFee, hexMaximumTransactionFee } = useSelector(
(state) => transactionFeeSelector(state, transaction),
);
return (
<TransactionDetailItem
key="legacy-gas-details"
detailTitle={
dappSuggestedGasFees ? (
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
contentText={t('transactionDetailDappGasTooltip')}
position="top"
>
<i className="fa fa-info-circle" />
</InfoTooltip>
</>
) : (
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
contentText={
<>
<p>
{t('transactionDetailGasTooltipIntro', [
isMainnet ? t('networkNameEthereum') : '',
])}
</p>
<p>{t('transactionDetailGasTooltipExplanation')}</p>
<p>
<a
href="https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172"
target="_blank"
rel="noopener noreferrer"
>
{t('transactionDetailGasTooltipConversion')}
</a>
</p>
</>
}
position="top"
>
<i className="fa fa-info-circle" />
</InfoTooltip>
</>
)
}
detailText={
useCurrencyRateCheck && (
<div>
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexMinimumTransactionFee}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</div>
)
}
detailTotal={
<div>
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={hexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
numberOfDecimals={6}
/>
</div>
}
subText={
<>
<strong key="editGasSubTextFeeLabel">
{t('editGasSubTextFeeLabel')}
</strong>
<div key="editGasSubTextFeeValue">
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
</>
}
subTitle={
<>
{dappSuggestedGasFees && (
<Text
variant={TextVariant.bodySm}
fontStyle={FONT_STYLE.ITALIC}
color={TextColor.textAlternative}
as="h6"
>
{t('transactionDetailDappGasMoreInfo')}
</Text>
)}
</>
}
/>
);
};
export default ConfirmLegacyGasDisplay;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Provider } from 'react-redux';
import mockState from '../../../../../test/data/mock-state.json';
import configureStore from '../../../../store/store';
import README from './README.mdx';
import ConfirmLegacyGasDisplay from './confirm-legacy-gas-display';
const store = configureStore(mockState);
export default {
title: 'Components/App/ConfirmLegacyGasDisplay',
component: ConfirmLegacyGasDisplay,
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
parameters: {
docs: {
page: README,
},
},
};
export const DefaultStory = () => {
return <ConfirmLegacyGasDisplay />;
};
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,110 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import mockState from '../../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import ConfirmLegacyGasDisplay from './confirm-legacy-gas-display';
const render = ({ contextProps } = {}) => {
const store = configureStore({
...mockState,
...contextProps,
metamask: {
...mockState.metamask,
accounts: {
[mockState.metamask.selectedAddress]: {
address: mockState.metamask.selectedAddress,
balance: '0x1F4',
},
},
unapprovedTxs: {
8393540981007587: {
...mockState.metamask.unapprovedTxs[8393540981007587],
txParams: {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
to: '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
value: '0x0',
gas: '0x5208',
gasPrice: '0x3b9aca00',
type: '0x0',
},
},
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
},
confirmTransaction: {
txData: {
id: 8393540981007587,
status: 'unapproved',
chainId: '0x5',
txParams: {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
to: '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
value: '0x0',
gas: '0x5208',
gasPrice: '0x3b9aca00',
type: '0x0',
},
},
},
});
return renderWithProvider(<ConfirmLegacyGasDisplay />, store);
};
describe('ConfirmLegacyGasDisplay', () => {
it('should match snapshot', async () => {
const { container } = render();
await waitFor(() => {
expect(container).toMatchSnapshot();
});
});
it('should render label', async () => {
render();
await waitFor(() => {
expect(screen.queryByText('Estimated gas fee')).toBeInTheDocument();
expect(screen.queryByText('Max fee:')).toBeInTheDocument();
expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0);
});
});
it('should render gas fee details', async () => {
render();
await waitFor(() => {
expect(screen.queryAllByTitle('0.000021 ETH').length).toBeGreaterThan(0);
expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0);
});
});
it('should render label and gas details with draftTransaction', async () => {
render({
send: {
currentTransactionUUID: '1d40b578-6184-4607-8513-762c24d0a19b',
draftTransactions: {
'1d40b578-6184-4607-8513-762c24d0a19b': {
gas: {
error: null,
gasLimit: '0x5208',
gasPrice: '0x3b9aca00',
gasTotal: '0x157c9fbb9a000',
maxFeePerGas: '0x0',
maxPriorityFeePerGas: '0x0',
wasManuallyEdited: false,
},
},
},
},
});
await waitFor(() => {
expect(screen.queryByText('Estimated gas fee')).toBeInTheDocument();
expect(screen.queryByText('Max fee:')).toBeInTheDocument();
expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0);
expect(screen.queryAllByTitle('0.000021 ETH').length).toBeGreaterThan(0);
});
});
});

View File

@ -0,0 +1 @@
export { default as ConfirmLegacyGasDisplay } from './confirm-legacy-gas-display';

View File

@ -0,0 +1 @@
export { default as ConfirmGasDisplay } from './confirm-gas-display';

View File

@ -5,7 +5,12 @@ import { useSelector } from 'react-redux';
import { TextColor } from '../../../helpers/constants/design-system'; import { TextColor } from '../../../helpers/constants/design-system';
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common';
import { getPreferences, getUseCurrencyRateCheck } from '../../../selectors'; import {
getPreferences,
getUseCurrencyRateCheck,
transactionFeeSelector,
} from '../../../selectors';
import { getCurrentDraftTransaction } from '../../../ducks/send';
import { useGasFeeContext } from '../../../contexts/gasFee'; import { useGasFeeContext } from '../../../contexts/gasFee';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
@ -14,10 +19,20 @@ import LoadingHeartBeat from '../../ui/loading-heartbeat';
import GasTiming from '../gas-timing/gas-timing.component'; import GasTiming from '../gas-timing/gas-timing.component';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display';
import { hexWEIToDecGWEI } from '../../../../shared/modules/conversion.utils';
import { useDraftTransactionGasValues } from '../../../hooks/useDraftTransactionGasValues';
import GasDetailsItemTitle from './gas-details-item-title'; import GasDetailsItemTitle from './gas-details-item-title';
const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => { const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
const t = useI18nContext(); const t = useI18nContext();
const draftTransaction = useSelector(getCurrentDraftTransaction);
const { transactionData } = useDraftTransactionGasValues();
const {
hexMinimumTransactionFee: draftHexMinimumTransactionFee,
hexMaximumTransactionFee: draftHexMaximumTransactionFee,
} = useSelector((state) => transactionFeeSelector(state, transactionData));
const { const {
estimateUsed, estimateUsed,
hasSimulationError, hasSimulationError,
@ -41,7 +56,8 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
detailTitle={<GasDetailsItemTitle />} detailTitle={<GasDetailsItemTitle />}
detailTitleColor={TextColor.textDefault} detailTitleColor={TextColor.textDefault}
detailText={ detailText={
useCurrencyRateCheck && ( useCurrencyRateCheck &&
Object.keys(draftTransaction).length === 0 && (
<div className="gas-details-item__currency-container"> <div className="gas-details-item__currency-container">
<LoadingHeartBeat estimateUsed={estimateUsed} /> <LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay <UserPreferencedCurrencyDisplay
@ -57,7 +73,7 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
<LoadingHeartBeat estimateUsed={estimateUsed} /> <LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay <UserPreferencedCurrencyDisplay
type={PRIMARY} type={PRIMARY}
value={hexMinimumTransactionFee} value={hexMinimumTransactionFee || draftHexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency} hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/> />
</div> </div>
@ -86,7 +102,9 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
<UserPreferencedCurrencyDisplay <UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount" key="editGasSubTextFeeAmount"
type={PRIMARY} type={PRIMARY}
value={hexMaximumTransactionFee} value={
hexMaximumTransactionFee || draftHexMaximumTransactionFee
}
hideLabel={!useNativeCurrencyAsPrimaryCurrency} hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/> />
</div> </div>
@ -95,8 +113,14 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
} }
subTitle={ subTitle={
<GasTiming <GasTiming
maxPriorityFeePerGas={maxPriorityFeePerGas.toString()} maxPriorityFeePerGas={(
maxFeePerGas={maxFeePerGas.toString()} maxPriorityFeePerGas ||
hexWEIToDecGWEI(transactionData.txParams.maxPriorityFeePerGas)
).toString()}
maxFeePerGas={(
maxFeePerGas ||
hexWEIToDecGWEI(transactionData.txParams.maxFeePerGas)
).toString()}
/> />
} }
/> />

View File

@ -112,11 +112,11 @@ import { TextColor, BackgroundColor } from '../../../helpers/constants/design-sy
### Font Weight ### Font Weight
Use the `fontWeight` prop and the `FONT_WEIGHT` object from `./ui/helpers/constants/design-system.js` to change the font weight of the `Text` component. There are 3 font weights: Use the `fontWeight` prop and the `FontWeight` enum from `./ui/helpers/constants/design-system.js` to change the font weight of the `Text` component. There are 3 font weights:
- `FONT_WEIGHT.NORMAL` = `normal` || `400` - `FontWeight.Normal` = `normal` || `400`
- `FONT_WEIGHT.MEDIUM` = `medium` || `500` - `FontWeight.Medium` = `medium` || `500`
- `FONT_WEIGHT.BOLD` = `bold` || `700` - `FontWeight.Bold` = `bold` || `700`
<Canvas> <Canvas>
<Story id="components-componentlibrary-text--font-weight" /> <Story id="components-componentlibrary-text--font-weight" />
@ -124,25 +124,25 @@ Use the `fontWeight` prop and the `FONT_WEIGHT` object from `./ui/helpers/consta
```jsx ```jsx
import { Text } from '../../component-library'; import { Text } from '../../component-library';
import { FONT_WEIGHT } from '../../../helpers/constants/design-system'; import { FontWeight } from '../../../helpers/constants/design-system';
<Text fontWeight={FONT_WEIGHT.NORMAL}> <Text fontWeight={FontWeight.Normal}>
normal normal
</Text> </Text>
<Text fontWeight={FONT_WEIGHT.MEDIUM}> <Text fontWeight={FontWeight.Medium}>
medium medium
</Text> </Text>
<Text fontWeight={FONT_WEIGHT.BOLD}> <Text fontWeight={FontWeight.Bold}>
bold bold
</Text> </Text>
``` ```
### Font Style ### Font Style
Use the `fontStyle` prop and the `FONT_STYLE` object from `./ui/helpers/constants/design-system.js` to change the font style of the `Text` component. There are 2 font styles: Use the `fontStyle` prop and the `FontStyle` enum from `./ui/helpers/constants/design-system.js` to change the font style of the `Text` component. There are 2 font styles:
- `FONT_STYLE.NORMAL` - `FontStyle.Normal`
- `FONT_STYLE.ITALIC` - `FontStyle.Italic`
<Canvas> <Canvas>
<Story id="components-componentlibrary-text--font-style" /> <Story id="components-componentlibrary-text--font-style" />
@ -150,19 +150,19 @@ Use the `fontStyle` prop and the `FONT_STYLE` object from `./ui/helpers/constant
```jsx ```jsx
import { Text } from '../../component-library'; import { Text } from '../../component-library';
import { FONT_STYLE } from '../../../helpers/constants/design-system'; import { FontStyle } from '../../../helpers/constants/design-system';
<Text fontStyle={FONT_STYLE.NORMAL}> <Text fontStyle={FontStyle.Normal}>
normal normal
</Text> </Text>
<Text fontStyle={FONT_STYLE.ITALIC}> <Text fontStyle={FontStyle.Italic}>
bold bold
</Text> </Text>
``` ```
### Text Transform ### Text Transform
Use the `textTransform` prop and the `TEXT_TRANSFORM` object from `./ui/helpers/constants/design-system.js` to change the text alignment of the `Text` component Use the `textTransform` prop and the `TextTransform` enum from `./ui/helpers/constants/design-system.ts` to change the text alignment of the `Text` component
<Canvas> <Canvas>
<Story id="components-componentlibrary-text--text-transform" /> <Story id="components-componentlibrary-text--text-transform" />
@ -170,22 +170,22 @@ Use the `textTransform` prop and the `TEXT_TRANSFORM` object from `./ui/helpers/
```jsx ```jsx
import { Text } from '../../component-library'; import { Text } from '../../component-library';
import { TEXT_TRANSFORM } from '../../../helpers/constants/design-system'; import { TextTransform } from '../../../helpers/constants/design-system';
<Text textAlign={TEXT_TRANSFORM.UPPERCASE}> <Text textAlign={TextTransform.Uppercase}>
uppercase uppercase
</Text> </Text>
<Text textAlign={TEXT_TRANSFORM.LOWERCASE}> <Text textAlign={TextTransform.Lowercase}>
lowercase lowercase
</Text> </Text>
<Text textAlign={TEXT_TRANSFORM.CAPITALIZE}> <Text textAlign={TextTransform.Capitalize}>
capitalize capitalize
</Text> </Text>
``` ```
### Text Align ### Text Align
Use the `textAlign` prop and the `TEXT_ALIGN` object from `./ui/helpers/constants/design-system.js` to change the text alignment of the `Text` component Use the `textAlign` prop and the `TextAlign` enum from `./ui/helpers/constants/design-system.ts` to change the text alignment of the `Text` component
<Canvas> <Canvas>
<Story id="components-componentlibrary-text--text-align" /> <Story id="components-componentlibrary-text--text-align" />
@ -193,28 +193,28 @@ Use the `textAlign` prop and the `TEXT_ALIGN` object from `./ui/helpers/constant
```jsx ```jsx
import { Text } from '../../component-library'; import { Text } from '../../component-library';
import { TEXT_ALIGN } from '../../../helpers/constants/design-system'; import { TextAlign } from '../../../helpers/constants/design-system';
<Text textAlign={TEXT_ALIGN.LEFT}> <Text textAlign={TextAlign.Left}>
left left
</Text> </Text>
<Text textAlign={TEXT_ALIGN.CENTER}> <Text textAlign={TextAlign.Center}>
center center
</Text> </Text>
<Text textAlign={TEXT_ALIGN.RIGHT}> <Text textAlign={TextAlign.Right}>
right right
</Text> </Text>
<Text textAlign={TEXT_ALIGN.JUSTIFY}> <Text textAlign={TextAlign.Justify}>
justify justify
</Text> </Text>
<Text textAlign={TEXT_ALIGN.END}> <Text textAlign={TextAlign.End}>
end end
</Text> </Text>
``` ```
### Overflow Wrap ### Overflow Wrap
Use the `overflowWrap` prop and the `OVERFLOW_WRAP` object from `./ui/helpers/constants/design-system.js` to change the overflow wrap of the `Text` component Use the `overflowWrap` prop and the `OverflowWrap` enum from `./ui/helpers/constants/design-system.ts` to change the overflow wrap of the `Text` component
<Canvas> <Canvas>
<Story id="components-componentlibrary-text--overflow-wrap" /> <Story id="components-componentlibrary-text--overflow-wrap" />
@ -222,7 +222,7 @@ Use the `overflowWrap` prop and the `OVERFLOW_WRAP` object from `./ui/helpers/co
```jsx ```jsx
import { Text } from '../../component-library'; import { Text } from '../../component-library';
import { OVERFLOW_WRAP } from '../../../helpers/constants/design-system'; import { OverflowWrap } from '../../../helpers/constants/design-system';
<div <div
style={{ style={{
@ -231,11 +231,11 @@ import { OVERFLOW_WRAP } from '../../../helpers/constants/design-system';
display: 'block', display: 'block',
}} }}
> >
<Text overflowWrap={OVERFLOW_WRAP.NORMAL}> <Text overflowWrap={OverflowWrap.Normal}>
{OVERFLOW_WRAP.NORMAL}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d {OverflowWrap.Normal}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d
</Text> </Text>
<Text overflowWrap={OVERFLOW_WRAP.BREAK_WORD}> <Text overflowWrap={OverflowWrap.BreakWord}>
{OVERFLOW_WRAP.BREAK_WORD}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d {OverflowWrap.BreakWord}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d
</Text> </Text>
</div>; </div>;
``` ```
@ -538,14 +538,14 @@ import { TextVariant } from '../../../helpers/constants/design-system';
The prop name `align` has been deprecated in favor of `textAlign` The prop name `align` has been deprecated in favor of `textAlign`
Values and using the `TEXT_ALIGN` object from `./ui/helpers/constants/design-system.js` remain the same Values using the `TextAlign` object from `./ui/helpers/constants/design-system.js` remain the same
```jsx ```jsx
// Before // Before
<Typograpghy align={TEXT_ALIGN.CENTER}>Demo</Typograpghy>; <Typography align={TEXT_ALIGN.CENTER}>Demo</Typography>;
// After // After
<Text textAlign={TEXT_ALIGN.CENTER}>Demo</Text>; <Text textAlign={TextAlign.Center}>Demo</Text>;
``` ```
### Box Props ### Box Props

View File

@ -1,12 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { import {
FONT_STYLE, FontStyle,
FONT_WEIGHT, FontWeight,
OVERFLOW_WRAP, OverflowWrap,
TEXT_ALIGN, TextAlign,
TextColor, TextColor,
TEXT_TRANSFORM, TextTransform,
TextVariant, TextVariant,
Color, Color,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
@ -99,9 +99,9 @@ describe('Text', () => {
it('should render the Text with proper font weight class name', () => { it('should render the Text with proper font weight class name', () => {
const { getByText } = render( const { getByText } = render(
<> <>
<Text fontWeight={FONT_WEIGHT.BOLD}>bold</Text> <Text fontWeight={FontWeight.Bold}>bold</Text>
<Text fontWeight={FONT_WEIGHT.MEDIUM}>medium</Text> <Text fontWeight={FontWeight.Medium}>medium</Text>
<Text fontWeight={FONT_WEIGHT.NORMAL}>normal</Text> <Text fontWeight={FontWeight.Normal}>normal</Text>
</>, </>,
); );
expect(getByText('bold')).toHaveClass('mm-text--font-weight-bold'); expect(getByText('bold')).toHaveClass('mm-text--font-weight-bold');
@ -156,8 +156,8 @@ describe('Text', () => {
it('should render the Text with proper font style class name', () => { it('should render the Text with proper font style class name', () => {
const { getByText } = render( const { getByText } = render(
<> <>
<Text fontStyle={FONT_STYLE.ITALIC}>italic</Text> <Text fontStyle={FontStyle.Italic}>italic</Text>
<Text fontStyle={FONT_STYLE.NORMAL}>normal</Text> <Text fontStyle={FontStyle.Normal}>normal</Text>
</>, </>,
); );
expect(getByText('italic')).toHaveClass('mm-text--font-style-italic'); expect(getByText('italic')).toHaveClass('mm-text--font-style-italic');
@ -167,11 +167,11 @@ describe('Text', () => {
it('should render the Text with proper text align class name', () => { it('should render the Text with proper text align class name', () => {
const { getByText } = render( const { getByText } = render(
<> <>
<Text textAlign={TEXT_ALIGN.LEFT}>left</Text> <Text textAlign={TextAlign.Left}>left</Text>
<Text textAlign={TEXT_ALIGN.CENTER}>center</Text> <Text textAlign={TextAlign.Center}>center</Text>
<Text textAlign={TEXT_ALIGN.RIGHT}>right</Text> <Text textAlign={TextAlign.Right}>right</Text>
<Text textAlign={TEXT_ALIGN.JUSTIFY}>justify</Text> <Text textAlign={TextAlign.Justify}>justify</Text>
<Text textAlign={TEXT_ALIGN.END}>end</Text> <Text textAlign={TextAlign.End}>end</Text>
</>, </>,
); );
@ -185,8 +185,8 @@ describe('Text', () => {
it('should render the Text with proper overflow wrap class name', () => { it('should render the Text with proper overflow wrap class name', () => {
const { getByText } = render( const { getByText } = render(
<> <>
<Text overflowWrap={OVERFLOW_WRAP.BREAK_WORD}>break-word</Text> <Text overflowWrap={OverflowWrap.BreakWord}>break-word</Text>
<Text overflowWrap={OVERFLOW_WRAP.NORMAL}>normal</Text> <Text overflowWrap={OverflowWrap.Normal}>normal</Text>
</>, </>,
); );
expect(getByText('break-word')).toHaveClass( expect(getByText('break-word')).toHaveClass(
@ -207,9 +207,9 @@ describe('Text', () => {
it('should render the Text with proper text transform class name', () => { it('should render the Text with proper text transform class name', () => {
const { getByText } = render( const { getByText } = render(
<> <>
<Text textTransform={TEXT_TRANSFORM.UPPERCASE}>uppercase</Text> <Text textTransform={TextTransform.Uppercase}>uppercase</Text>
<Text textTransform={TEXT_TRANSFORM.LOWERCASE}>lowercase</Text> <Text textTransform={TextTransform.Lowercase}>lowercase</Text>
<Text textTransform={TEXT_TRANSFORM.CAPITALIZE}>capitalize</Text> <Text textTransform={TextTransform.Capitalize}>capitalize</Text>
</>, </>,
); );
expect(getByText('uppercase')).toHaveClass( expect(getByText('uppercase')).toHaveClass(

View File

@ -2,7 +2,7 @@ import React, { forwardRef, Ref } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Box from '../../ui/box'; import Box from '../../ui/box';
import { import {
FONT_WEIGHT, FontWeight,
TextVariant, TextVariant,
TextColor, TextColor,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
@ -49,7 +49,7 @@ export const Text = forwardRef(function Text(
let strongTagFontWeight; let strongTagFontWeight;
if (Tag === 'strong') { if (Tag === 'strong') {
strongTagFontWeight = FONT_WEIGHT.BOLD; strongTagFontWeight = FontWeight.Bold;
} }
const computedClassName = classnames( const computedClassName = classnames(

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import type { BoxProps } from '../../ui/box/box.d'; import type { BoxProps } from '../../ui/box/box.d';
import { import {
FONT_WEIGHT, FontWeight,
FONT_STYLE, FontStyle,
TextVariant, TextVariant,
TEXT_ALIGN, TextAlign,
TEXT_TRANSFORM, TextTransform,
OVERFLOW_WRAP, OverflowWrap,
TextColor, TextColor,
Color, Color,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
@ -70,35 +70,35 @@ export interface TextProps extends BoxProps {
*/ */
color?: TextColor | Color; color?: TextColor | Color;
/** /**
* The font-weight of the Text component. Should use the FONT_WEIGHT object from * The font-weight of the Text component. Should use the FontWeight enum from
* ./ui/helpers/constants/design-system.js * ./ui/helpers/constants/design-system.js
*/ */
fontWeight?: keyof typeof FONT_WEIGHT; fontWeight?: FontWeight;
/** /**
* The font-style of the Text component. Should use the FONT_STYLE object from * The font-style of the Text component. Should use the FontStyle enum from
* ./ui/helpers/constants/design-system.js * ./ui/helpers/constants/design-system.js
*/ */
fontStyle?: keyof typeof FONT_STYLE; fontStyle?: FontStyle;
/** /**
* The textTransform of the Text component. Should use the TEXT_TRANSFORM object from * The textTransform of the Text component. Should use the TextTransform enum from
* ./ui/helpers/constants/design-system.js * ./ui/helpers/constants/design-system.js
*/ */
textTransform?: keyof typeof TEXT_TRANSFORM; textTransform?: TextTransform;
/** /**
* The text-align of the Text component. Should use the TEXT_ALIGN object from * The text-align of the Text component. Should use the TextAlign enum from
* ./ui/helpers/constants/design-system.js * ./ui/helpers/constants/design-system.js
*/ */
textAlign?: keyof typeof TEXT_ALIGN; textAlign?: TextAlign;
/** /**
* Change the dir (direction) global attribute of text to support the direction a language is written * Change the dir (direction) global attribute of text to support the direction a language is written
* Possible values: `LEFT_TO_RIGHT` (default), `RIGHT_TO_LEFT`, `AUTO` (user agent decides) * Possible values: `LEFT_TO_RIGHT` (default), `RIGHT_TO_LEFT`, `AUTO` (user agent decides)
*/ */
textDirection?: TextDirection; textDirection?: TextDirection;
/** /**
* The overflow-wrap of the Text component. Should use the OVERFLOW_WRAP object from * The overflow-wrap of the Text component. Should use the OverflowWrap enum from
* ./ui/helpers/constants/design-system.js * ./ui/helpers/constants/design-system.js
*/ */
overflowWrap?: keyof typeof OVERFLOW_WRAP; overflowWrap?: OverflowWrap;
/** /**
* Used for long strings that can be cut off... * Used for long strings that can be cut off...
*/ */

View File

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CustodyLabels Component should render correctly 1`] = `
<div>
<label
class="box mm-text mm-label mm-label--html-for mm-text--body-md mm-text--font-weight-bold box--display-flex box--flex-direction-row box--align-items-center box--color-text-default"
for="address-index"
>
<p
class="box mm-text custody-label mm-text--h9 mm-text--font-weight-normal mm-text--text-transform-uppercase box--margin-top-1 box--margin-right-1 box--margin-bottom-2 box--padding-top-1 box--padding-right-2 box--padding-bottom-1 box--padding-left-2 box--flex-direction-row box--color-text-muted box--background-color-background-alternative box--rounded-sm"
>
value
</p>
</label>
</div>
`;

View File

@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, Label } from '../../component-library';
import {
TEXT_TRANSFORM,
BackgroundColor,
TextColor,
FONT_WEIGHT,
BorderRadius,
TypographyVariant,
} from '../../../helpers/constants/design-system';
const CustodyLabels = (props) => {
const { labels, index, background, hideNetwork } = props;
const filteredLabels = hideNetwork
? labels.filter((item) => item.key !== 'network_name')
: labels;
return (
<Label
display={['flex']}
flexDirection={['row']}
htmlFor={`address-${index || 0}`}
>
{filteredLabels.map((item) => (
<Text
key={item.key}
textTransform={TEXT_TRANSFORM.UPPERCASE}
className="custody-label"
style={background ? { background } : {}}
marginTop={1}
marginRight={1}
marginBottom={2}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
backgroundColor={BackgroundColor.backgroundAlternative}
color={TextColor.textMuted}
fontWeight={FONT_WEIGHT.NORMAL}
borderRadius={BorderRadius.SM}
variant={TypographyVariant.H9}
>
{item.value}
</Text>
))}
</Label>
);
};
CustodyLabels.propTypes = {
labels: PropTypes.array,
index: PropTypes.string,
background: PropTypes.string,
hideNetwork: PropTypes.bool,
};
export default CustodyLabels;

View File

@ -0,0 +1,9 @@
.custody-label {
z-index: 1;
letter-spacing: 0.5px;
white-space: nowrap;
max-width: 80px;
text-overflow: ellipsis;
overflow: hidden;
display: block;
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import CustodyLabels from '.';
export default {
title: 'Components/Institutional/CustodyLabels',
component: CustodyLabels,
args: {
labels: [{ key: 'testKey', value: 'value' }],
index: 'index',
hideNetwork: 'true',
},
};
export const DefaultStory = (args) => <CustodyLabels {...args} />;
DefaultStory.storyName = 'CustodyLabels';

View File

@ -0,0 +1,17 @@
import React from 'react';
import { render } from '@testing-library/react';
import CustodyLabels from './custody-labels';
describe('CustodyLabels Component', () => {
it('should render correctly', () => {
const props = {
labels: [{ key: 'testKey', value: 'value' }],
index: 'index',
hideNetwork: 'true',
};
const { container } = render(<CustodyLabels {...props} />);
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1 @@
export { default } from './custody-labels';

View File

@ -297,6 +297,15 @@ export const BLOCK_SIZES = {
FULL: 'full', FULL: 'full',
}; };
export enum TextAlign {
Left = 'left',
Center = 'center',
Right = 'right',
Justify = 'justify',
End = 'end',
Start = 'start',
}
export const TEXT_ALIGN = { export const TEXT_ALIGN = {
LEFT: 'left', LEFT: 'left',
CENTER: 'center', CENTER: 'center',
@ -306,24 +315,50 @@ export const TEXT_ALIGN = {
START: 'start', START: 'start',
}; };
export enum TextTransform {
// eslint-disable-next-line @typescript-eslint/no-shadow
Uppercase = 'uppercase',
// eslint-disable-next-line @typescript-eslint/no-shadow
Lowercase = 'lowercase',
// eslint-disable-next-line @typescript-eslint/no-shadow
Capitalize = 'capitalize',
}
export const TEXT_TRANSFORM = { export const TEXT_TRANSFORM = {
UPPERCASE: 'uppercase', UPPERCASE: 'uppercase',
LOWERCASE: 'lowercase', LOWERCASE: 'lowercase',
CAPITALIZE: 'capitalize', CAPITALIZE: 'capitalize',
}; };
export enum FontWeight {
Bold = 'bold',
Medium = 'medium',
Normal = 'normal',
}
export const FONT_WEIGHT = { export const FONT_WEIGHT = {
BOLD: 'bold', BOLD: 'bold',
MEDIUM: 'medium', MEDIUM: 'medium',
NORMAL: 'normal', NORMAL: 'normal',
}; };
export enum OverflowWrap {
BreakWord = 'break-word',
Anywhere = 'anywhere',
Normal = 'normal',
}
export const OVERFLOW_WRAP = { export const OVERFLOW_WRAP = {
BREAK_WORD: 'break-word', BREAK_WORD: 'break-word',
ANYWHERE: 'anywhere', ANYWHERE: 'anywhere',
NORMAL: 'normal', NORMAL: 'normal',
}; };
export enum FontStyle {
Italic = 'italic',
Normal = 'normal',
}
export const FONT_STYLE = { export const FONT_STYLE = {
ITALIC: 'italic', ITALIC: 'italic',
NORMAL: 'normal', NORMAL: 'normal',

View File

@ -71,12 +71,6 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
weight: 2, weight: 2,
}), }),
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
[RestrictedMethods.snap_confirm]: ({ t }) => ({
label: t('permission_customConfirmation'),
description: t('permission_customConfirmationDescription'),
leftIcon: ICON_NAMES.SECURITY_TICK,
weight: 3,
}),
[RestrictedMethods.snap_dialog]: ({ t }) => ({ [RestrictedMethods.snap_dialog]: ({ t }) => ({
label: t('permission_dialog'), label: t('permission_dialog'),
description: t('permission_dialogDescription'), description: t('permission_dialogDescription'),

View File

@ -0,0 +1,40 @@
import { useSelector } from 'react-redux';
import { getCurrentDraftTransaction } from '../ducks/send';
import { getUnapprovedTransactions } from '../selectors';
/**
* Returns an object that resembles the txData.txParams from the Transactions state.
* While processing gas details for send transaction and edit transaction,
* the gas data from draftTransaction and unapprovedTx has to be reorganized
* to mimic the txdata.txParam from a confirmTransaction
*
* @returns {Object txData.txParams}
*/
export const useDraftTransactionGasValues = () => {
const draftTransaction = useSelector(getCurrentDraftTransaction);
const unapprovedTxs = useSelector(getUnapprovedTransactions);
let transactionData = {};
if (Object.keys(draftTransaction).length !== 0) {
const editingTransaction = unapprovedTxs[draftTransaction.id];
transactionData = {
txParams: {
gasPrice: draftTransaction.gas?.gasPrice,
gas: editingTransaction?.userEditedGasLimit
? editingTransaction?.txParams?.gas
: draftTransaction.gas?.gasLimit,
maxFeePerGas: editingTransaction?.txParams?.maxFeePerGas
? editingTransaction?.txParams?.maxFeePerGas
: draftTransaction.gas?.maxFeePerGas,
maxPriorityFeePerGas: editingTransaction?.txParams?.maxPriorityFeePerGas
? editingTransaction?.txParams?.maxPriorityFeePerGas
: draftTransaction.gas?.maxPriorityFeePerGas,
value: draftTransaction.amount?.value,
type: draftTransaction.transactionType,
},
userFeeLevel: editingTransaction?.userFeeLevel,
};
}
return { transactionData };
};

View File

@ -23,7 +23,6 @@ import {
AlignItems, AlignItems,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { ConfirmPageContainerWarning } from '../../../components/app/confirm-page-container/confirm-page-container-content'; import { ConfirmPageContainerWarning } from '../../../components/app/confirm-page-container/confirm-page-container-content';
import GasDetailsItem from '../../../components/app/gas-details-item';
import LedgerInstructionField from '../../../components/app/ledger-instruction-field'; import LedgerInstructionField from '../../../components/app/ledger-instruction-field';
import { TokenStandard } from '../../../../shared/constants/transaction'; import { TokenStandard } from '../../../../shared/constants/transaction';
import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network'; import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network';
@ -34,6 +33,7 @@ import {
} from '../../../components/component-library/icon/deprecated'; } from '../../../components/component-library/icon/deprecated';
import { ButtonIcon } from '../../../components/component-library/button-icon/deprecated'; import { ButtonIcon } from '../../../components/component-library/button-icon/deprecated';
import { Text } from '../../../components/component-library'; import { Text } from '../../../components/component-library';
import { ConfirmGasDisplay } from '../../../components/app/confirm-gas-display';
export default class ConfirmApproveContent extends Component { export default class ConfirmApproveContent extends Component {
static contextTypes = { static contextTypes = {
@ -170,7 +170,7 @@ export default class ConfirmApproveContent extends Component {
!renderSimulationFailureWarning !renderSimulationFailureWarning
) { ) {
return ( return (
<GasDetailsItem <ConfirmGasDisplay
userAcknowledgedGasMissing={userAcknowledgedGasMissing} userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/> />
); );

View File

@ -374,9 +374,7 @@ exports[`Confirm Transaction Base should match snapshot 1`] = `
<h6 <h6
class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default" class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
> >
<div <div>
class="confirm-page-container-content__currency-container"
>
<div <div
class="currency-display-component" class="currency-display-component"
title="0.000021" title="0.000021"
@ -397,18 +395,14 @@ exports[`Confirm Transaction Base should match snapshot 1`] = `
<div <div
class="transaction-detail-item__row" class="transaction-detail-item__row"
> >
<div> <div />
</div>
<h6 <h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative" class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative"
> >
<strong> <strong>
Max fee: Max fee:
</strong> </strong>
<div <div>
class="confirm-page-container-content__currency-container"
>
<div <div
class="currency-display-component" class="currency-display-component"
title="0.000021" title="0.000021"

View File

@ -28,18 +28,9 @@ import {
import { TransactionModalContextProvider } from '../../contexts/transaction-modal'; import { TransactionModalContextProvider } from '../../contexts/transaction-modal';
import TransactionDetail from '../../components/app/transaction-detail/transaction-detail.component'; import TransactionDetail from '../../components/app/transaction-detail/transaction-detail.component';
import TransactionDetailItem from '../../components/app/transaction-detail-item/transaction-detail-item.component'; import TransactionDetailItem from '../../components/app/transaction-detail-item/transaction-detail-item.component';
import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
import LoadingHeartBeat from '../../components/ui/loading-heartbeat'; import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
import GasDetailsItem from '../../components/app/gas-details-item';
import GasTiming from '../../components/app/gas-timing/gas-timing.component';
import LedgerInstructionField from '../../components/app/ledger-instruction-field'; import LedgerInstructionField from '../../components/app/ledger-instruction-field';
import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message'; import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message';
import Typography from '../../components/ui/typography/typography';
import {
TextColor,
FONT_STYLE,
TypographyVariant,
} from '../../helpers/constants/design-system';
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling, getGasFeeEstimatesAndStartPolling,
@ -53,16 +44,13 @@ import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
import { import {
addHexes, addHexes,
hexToDecimal, hexToDecimal,
hexWEIToDecGWEI,
} from '../../../shared/modules/conversion.utils'; } from '../../../shared/modules/conversion.utils';
import TransactionAlerts from '../../components/app/transaction-alerts'; import TransactionAlerts from '../../components/app/transaction-alerts';
import { ConfirmHexData } from '../../components/app/confirm-hexdata'; import { ConfirmHexData } from '../../components/app/confirm-hexdata';
import { ConfirmData } from '../../components/app/confirm-data'; import { ConfirmData } from '../../components/app/confirm-data';
import { ConfirmTitle } from '../../components/app/confirm-title'; import { ConfirmTitle } from '../../components/app/confirm-title';
import { ConfirmSubTitle } from '../../components/app/confirm-subtitle'; import { ConfirmSubTitle } from '../../components/app/confirm-subtitle';
import { ConfirmGasDisplay } from '../../components/app/confirm-gas-display';
const renderHeartBeatIfNotInTest = () =>
process.env.IN_TEST ? null : <LoadingHeartBeat />;
export default class ConfirmTransactionBase extends Component { export default class ConfirmTransactionBase extends Component {
static contextTypes = { static contextTypes = {
@ -136,7 +124,6 @@ export default class ConfirmTransactionBase extends Component {
maxFeePerGas: PropTypes.string, maxFeePerGas: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string, maxPriorityFeePerGas: PropTypes.string,
baseFeePerGas: PropTypes.string, baseFeePerGas: PropTypes.string,
isMainnet: PropTypes.bool,
gasFeeIsCustom: PropTypes.bool, gasFeeIsCustom: PropTypes.bool,
showLedgerSteps: PropTypes.bool.isRequired, showLedgerSteps: PropTypes.bool.isRequired,
nativeCurrency: PropTypes.string, nativeCurrency: PropTypes.string,
@ -319,11 +306,7 @@ export default class ConfirmTransactionBase extends Component {
txData, txData,
useNativeCurrencyAsPrimaryCurrency, useNativeCurrencyAsPrimaryCurrency,
primaryTotalTextOverrideMaxAmount, primaryTotalTextOverrideMaxAmount,
maxFeePerGas,
maxPriorityFeePerGas,
isMainnet,
showLedgerSteps, showLedgerSteps,
supportsEIP1559,
isMultiLayerFeeNetwork, isMultiLayerFeeNetwork,
nativeCurrency, nativeCurrency,
isBuyableChain, isBuyableChain,
@ -439,128 +422,6 @@ export default class ConfirmTransactionBase extends Component {
</div> </div>
) : null; ) : null;
const renderGasDetailsItem = () => {
return this.supportsEIP1559 ? (
<GasDetailsItem
key="gas_details"
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
) : (
<TransactionDetailItem
key="gas-item"
detailTitle={
txData.dappSuggestedGasFees ? (
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
contentText={t('transactionDetailDappGasTooltip')}
position="top"
>
<i className="fa fa-info-circle" />
</InfoTooltip>
</>
) : (
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
contentText={
<>
<p>
{t('transactionDetailGasTooltipIntro', [
isMainnet ? t('networkNameEthereum') : '',
])}
</p>
<p>{t('transactionDetailGasTooltipExplanation')}</p>
<p>
<a
href="https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172"
target="_blank"
rel="noopener noreferrer"
>
{t('transactionDetailGasTooltipConversion')}
</a>
</p>
</>
}
position="top"
>
<i className="fa fa-info-circle" />
</InfoTooltip>
</>
)
}
detailText={
useCurrencyRateCheck && (
<div className="confirm-page-container-content__currency-container test">
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexMinimumTransactionFee}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</div>
)
}
detailTotal={
<div className="confirm-page-container-content__currency-container">
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={hexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
numberOfDecimals={6}
/>
</div>
}
subText={
<>
<strong key="editGasSubTextFeeLabel">
{t('editGasSubTextFeeLabel')}
</strong>
<div
key="editGasSubTextFeeValue"
className="confirm-page-container-content__currency-container"
>
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
</>
}
subTitle={
<>
{txData.dappSuggestedGasFees ? (
<Typography
variant={TypographyVariant.H7}
fontStyle={FONT_STYLE.ITALIC}
color={TextColor.textAlternative}
>
{t('transactionDetailDappGasMoreInfo')}
</Typography>
) : (
''
)}
{supportsEIP1559 && (
<GasTiming
maxPriorityFeePerGas={hexWEIToDecGWEI(
maxPriorityFeePerGas ||
txData.txParams.maxPriorityFeePerGas,
).toString()}
maxFeePerGas={hexWEIToDecGWEI(
maxFeePerGas || txData.txParams.maxFeePerGas,
).toString()}
/>
)}
</>
}
/>
);
};
const simulationFailureWarning = () => ( const simulationFailureWarning = () => (
<div className="confirm-page-container-content__error-container"> <div className="confirm-page-container-content__error-container">
<SimulationErrorMessage <SimulationErrorMessage
@ -594,9 +455,11 @@ export default class ConfirmTransactionBase extends Component {
} }
rows={[ rows={[
renderSimulationFailureWarning && simulationFailureWarning(), renderSimulationFailureWarning && simulationFailureWarning(),
!renderSimulationFailureWarning && !renderSimulationFailureWarning && !isMultiLayerFeeNetwork && (
!isMultiLayerFeeNetwork && <ConfirmGasDisplay
renderGasDetailsItem(), userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
),
!renderSimulationFailureWarning && isMultiLayerFeeNetwork && ( !renderSimulationFailureWarning && isMultiLayerFeeNetwork && (
<MultiLayerFeeMessage <MultiLayerFeeMessage
transaction={txData} transaction={txData}

View File

@ -23,7 +23,11 @@ setBackgroundConnection({
}); });
const baseStore = { const baseStore = {
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, send: {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
currentTransactionUUID: null,
draftTransactions: {},
},
DNS: domainInitialState, DNS: domainInitialState,
gas: { gas: {
customData: { limit: null, price: null }, customData: { limit: null, price: null },

View File

@ -1,11 +1,10 @@
import { TypographyVariant } from '../../../../../helpers/constants/design-system';
import { mapToTemplate } from '../../../../../components/app/flask/snap-ui-renderer'; import { mapToTemplate } from '../../../../../components/app/flask/snap-ui-renderer';
import { DelineatorType } from '../../../../../helpers/constants/flask'; import { DelineatorType } from '../../../../../helpers/constants/flask';
function getValues(pendingApproval, t, actions) { function getValues(pendingApproval, t, actions) {
const { const {
snapName, snapName,
requestData: { content, title, description, textAreaContent }, requestData: { content },
} = pendingApproval; } = pendingApproval;
return { return {
@ -25,49 +24,7 @@ function getValues(pendingApproval, t, actions) {
snapName, snapName,
}, },
// TODO: Replace with SnapUIRenderer when we don't need to inject the input manually. // TODO: Replace with SnapUIRenderer when we don't need to inject the input manually.
// TODO: Remove ternary once snap_confirm has been removed. children: mapToTemplate(content),
children: content
? mapToTemplate(content)
: [
{
element: 'Typography',
key: 'title',
children: title,
props: {
variant: TypographyVariant.H3,
fontWeight: 'bold',
boxProps: {
marginBottom: 4,
},
},
},
...(description
? [
{
element: 'Typography',
key: 'subtitle',
children: description,
props: {
variant: TypographyVariant.H6,
boxProps: {
marginBottom: 4,
},
},
},
]
: []),
...(textAreaContent
? [
{
element: 'Copyable',
key: 'snap-dialog-content-text',
props: {
text: textAreaContent,
},
},
]
: []),
],
}, },
}, },
], ],

View File

@ -0,0 +1,188 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CustodyAccountList renders accounts 1`] = `
<div>
<div
class="box box--padding-top-4 box--padding-right-7 box--padding-bottom-7 box--padding-left-7 box--flex-direction-row"
>
<div
class="box custody-account-list box--display-flex box--flex-direction-column box--width-full"
data-testid="custody-account-list"
>
<div
class="box custody-account-list__item box--display-flex box--flex-direction-row"
>
<div
class="box box--display-flex box--flex-direction-row box--align-items-flex-start"
data-testid="custody-account-list-item-radio-button"
>
<input
id="address-0"
name="selectedAccount"
type="checkbox"
value="0x1234567890123456789012345678901234567890"
/>
</div>
<div
class="box box--margin-left-2 box--display-flex box--flex-direction-column box--width-full"
>
<label
class="box mm-text mm-label mm-label--html-for custody-account-list__item__title mm-text--body-md mm-text--font-weight-bold box--margin-top-2 box--margin-left-2 box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default"
for="address-0"
>
<span
class="box mm-text custody-account-list__item__name mm-text--inherit box--padding-right-1 box--flex-direction-row box--color-text-default"
>
Test Account 1
</span>
</label>
<label
class="box mm-text mm-label mm-label--html-for mm-text--body-md mm-text--font-weight-bold box--margin-top-2 box--margin-right-3 box--display-flex box--flex-direction-row box--align-items-center box--color-text-default"
for="address-0"
>
<span
class="box mm-text custody-account-list__item mm-text--body-md box--display-flex box--flex-direction-row box--color-text-default"
>
<a
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
href="https://etherscan.io/address/0x1234567890123456789012345678901234567890"
rel="noopener noreferrer"
target="_blank"
>
<span
class="box mm-text mm-text--inherit box--flex-direction-row box--color-primary-default"
>
0x123...7890
<span
class="box mm-icon mm-icon--size-md box--margin-left-1 box--display-inline-block box--flex-direction-row box--color-primary-default"
style="mask-image: url('./images/icons/undefined.svg');"
/>
</span>
</a>
<div>
<div
aria-describedby="tippy-tooltip-1"
class=""
data-original-title="[copyToClipboard]"
data-tooltipped=""
style="display: inline; background-color: transparent;"
tabindex="0"
>
<button
class="custody-account-list__item__clipboard"
>
<span
class="box mm-icon mm-icon--size-md box--display-inline-block box--flex-direction-row box--color-icon-muted"
style="mask-image: url('./images/icons/undefined.svg');"
/>
</button>
</div>
</div>
</span>
</label>
<div
class="box box--display-flex box--flex-direction-row box--justify-content-space-between"
/>
</div>
</div>
<div
class="box custody-account-list__item box--display-flex box--flex-direction-row"
>
<div
class="box box--display-flex box--flex-direction-row box--align-items-flex-start"
data-testid="custody-account-list-item-radio-button"
>
<input
id="address-1"
name="selectedAccount"
type="checkbox"
value="0x0987654321098765432109876543210987654321"
/>
</div>
<div
class="box box--margin-left-2 box--display-flex box--flex-direction-column box--width-full"
>
<label
class="box mm-text mm-label mm-label--html-for custody-account-list__item__title mm-text--body-md mm-text--font-weight-bold box--margin-top-2 box--margin-left-2 box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default"
for="address-1"
>
<span
class="box mm-text custody-account-list__item__name mm-text--inherit box--padding-right-1 box--flex-direction-row box--color-text-default"
>
Test Account 2
</span>
</label>
<label
class="box mm-text mm-label mm-label--html-for mm-text--body-md mm-text--font-weight-bold box--margin-top-2 box--margin-right-3 box--display-flex box--flex-direction-row box--align-items-center box--color-text-default"
for="address-1"
>
<span
class="box mm-text custody-account-list__item mm-text--body-md box--display-flex box--flex-direction-row box--color-text-default"
>
<a
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
href="https://etherscan.io/address/0x0987654321098765432109876543210987654321"
rel="noopener noreferrer"
target="_blank"
>
<span
class="box mm-text mm-text--inherit box--flex-direction-row box--color-primary-default"
>
0x098...4321
<span
class="box mm-icon mm-icon--size-md box--margin-left-1 box--display-inline-block box--flex-direction-row box--color-primary-default"
style="mask-image: url('./images/icons/undefined.svg');"
/>
</span>
</a>
<div>
<div
aria-describedby="tippy-tooltip-2"
class=""
data-original-title="[copyToClipboard]"
data-tooltipped=""
style="display: inline; background-color: transparent;"
tabindex="0"
>
<button
class="custody-account-list__item__clipboard"
>
<span
class="box mm-icon mm-icon--size-md box--display-inline-block box--flex-direction-row box--color-icon-muted"
style="mask-image: url('./images/icons/undefined.svg');"
/>
</button>
</div>
</div>
</span>
</label>
<div
class="box box--display-flex box--flex-direction-row box--justify-content-space-between"
/>
</div>
</div>
</div>
</div>
<div
class="box custody-account-list__buttons box--padding-top-5 box--padding-right-7 box--padding-bottom-7 box--padding-left-7 box--display-flex box--flex-direction-row box--justify-content-space-between box--width-full"
>
<button
class="button btn--rounded btn-default btn--large custody-account-list__button"
data-testid="custody-account-cancel-button"
role="button"
tabindex="0"
>
[cancel]
</button>
<button
class="button btn--rounded btn-primary btn--large custody-account-list__button"
data-testid="custody-account-connect-button"
disabled=""
role="button"
tabindex="0"
>
[connect]
</button>
</div>
</div>
`;

View File

@ -0,0 +1,218 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../../../../components/ui/button';
import CustodyLabels from '../../../../components/institutional/custody-labels';
import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/swaps';
import { CHAIN_IDS } from '../../../../../shared/constants/network';
import { shortenAddress } from '../../../../helpers/utils/util';
import Tooltip from '../../../../components/ui/tooltip';
import {
TextVariant,
JustifyContent,
BLOCK_SIZES,
DISPLAY,
IconColor,
} from '../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import Box from '../../../../components/ui/box';
import {
Text,
Label,
Icon,
IconName,
IconSize,
ButtonLink,
} from '../../../../components/component-library';
import { useCopyToClipboard } from '../../../../hooks/useCopyToClipboard';
const getButtonLinkHref = (account) => {
const url = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET];
return `${url}address/${account.address}`;
};
export default function CustodyAccountList({
rawList,
accounts,
onAccountChange,
selectedAccounts,
onCancel,
onAddAccounts,
custody,
}) {
const t = useI18nContext();
const [copied, handleCopy] = useCopyToClipboard();
const tooltipText = copied ? t('copiedExclamation') : t('copyToClipboard');
const disabled = Object.keys(selectedAccounts).length === 0;
return (
<>
<Box paddingTop={4} paddingRight={7} paddingBottom={7} paddingLeft={7}>
<Box
display={DISPLAY.FLEX}
flexDirection={['column']}
width={BLOCK_SIZES.FULL}
className="custody-account-list"
data-testid="custody-account-list"
>
{accounts.map((account, idx) => (
<Box
display={DISPLAY.FLEX}
className="custody-account-list__item"
key={account.address}
>
<Box
display={DISPLAY.FLEX}
alignItems={['flex-start']}
data-testid="custody-account-list-item-radio-button"
>
{!rawList && (
<input
type="checkbox"
name="selectedAccount"
id={`address-${idx}`}
value={account.address}
onChange={(e) =>
onAccountChange({
name: account.name,
address: e.target.value,
custodianDetails: account.custodianDetails,
labels: account.labels,
chainId: account.chainId,
})
}
checked={
selectedAccounts && selectedAccounts[account.address]
}
/>
)}
</Box>
<Box
display={DISPLAY.FLEX}
flexDirection={['column']}
marginLeft={2}
width={BLOCK_SIZES.FULL}
>
<Label
display={DISPLAY.FLEX}
justifyContent={JustifyContent.center}
marginTop={2}
marginLeft={2}
htmlFor={`address-${idx}`}
className="custody-account-list__item__title"
>
<Text
as="span"
variant={TextVariant.inherit}
size={TextVariant.bodySm}
paddingRight={1}
className="custody-account-list__item__name"
>
{account.name}
</Text>
</Label>
<Label
display={DISPLAY.FLEX}
size={TextVariant.bodySm}
marginTop={2}
marginRight={3}
htmlFor={`address-${idx}`}
>
<Text
as="span"
variant={TextVariant.bodyMd}
display={DISPLAY.FLEX}
className="custody-account-list__item"
>
<ButtonLink
href={getButtonLinkHref(account)}
target="_blank"
rel="noopener noreferrer"
>
{shortenAddress(account.address)}
<Icon
name={IconSize.EXPORT}
size={IconName.SM}
color={IconColor.primaryDefault}
marginLeft={1}
/>
</ButtonLink>
<Tooltip
position="bottom"
title={tooltipText}
style={{ backgroundColor: 'transparent' }}
>
<button
className="custody-account-list__item__clipboard"
onClick={() => handleCopy(account.address)}
>
<Icon
name={IconSize.COPY}
size={IconName.XS}
color={IconColor.iconMuted}
/>
</button>
</Tooltip>
</Text>
</Label>
<Box
display={DISPLAY.FLEX}
justifyContent={JustifyContent.spaceBetween}
>
{account.labels && (
<CustodyLabels
labels={account.labels}
index={idx.toString()}
hideNetwork
/>
)}
</Box>
</Box>
</Box>
))}
</Box>
</Box>
{!rawList && (
<Box
display={DISPLAY.FLEX}
width={BLOCK_SIZES.FULL}
justifyContent={JustifyContent.spaceBetween}
paddingTop={5}
paddingRight={7}
paddingBottom={7}
paddingLeft={7}
className="custody-account-list__buttons"
>
<Button
data-testid="custody-account-cancel-button"
type="default"
large
className="custody-account-list__button"
onClick={onCancel}
>
{t('cancel')}
</Button>
<Button
data-testid="custody-account-connect-button"
type="primary"
large
className="custody-account-list__button"
disabled={disabled}
onClick={() => onAddAccounts(custody)}
>
{t('connect')}
</Button>
</Box>
)}
</>
);
}
CustodyAccountList.propTypes = {
custody: PropTypes.string,
accounts: PropTypes.array.isRequired,
onAccountChange: PropTypes.func,
selectedAccounts: PropTypes.object,
onAddAccounts: PropTypes.func,
onCancel: PropTypes.func,
rawList: PropTypes.bool,
};

View File

@ -0,0 +1,34 @@
import React from 'react';
import CustodyAccountList from '.';
const testAccounts = [
{
address: '0x1234567890123456789012345678901234567890',
name: 'Test Account 1',
chainId: 1,
},
{
address: '0x0987654321098765432109876543210987654321',
name: 'Test Account 2',
chainId: 1,
},
];
export default {
title: 'Pages/Institutional/CustodyAccountList',
component: CustodyAccountList,
args: {
custody: 'Test',
accounts: testAccounts,
onAccountChange: () => undefined,
selectedAccounts: {},
onAddAccounts: () => undefined,
onCancel: () => undefined,
provider: 'Test',
rawList: false,
},
};
export const DefaultStory = (args) => <CustodyAccountList {...args} />;
DefaultStory.storyName = 'CustodyAccountList';

View File

@ -0,0 +1,109 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import CustodyAccountList from './account-list';
const testAccounts = [
{
address: '0x1234567890123456789012345678901234567890',
name: 'Test Account 1',
chainId: 1,
},
{
address: '0x0987654321098765432109876543210987654321',
name: 'Test Account 2',
chainId: 1,
},
];
describe('CustodyAccountList', () => {
const onAccountChangeMock = jest.fn();
const onCancelMock = jest.fn();
const onAddAccountsMock = jest.fn();
const selectedAccountsMock = {};
afterEach(() => {
jest.clearAllMocks();
});
it('renders accounts', () => {
const { container } = render(
<CustodyAccountList
accounts={testAccounts}
selectedAccounts={selectedAccountsMock}
onAccountChange={onAccountChangeMock}
onCancel={onCancelMock}
onAddAccounts={onAddAccountsMock}
custody="Test"
/>,
);
expect(container).toMatchSnapshot();
});
it('calls onAccountChange when an account is selected', () => {
render(
<CustodyAccountList
accounts={testAccounts}
selectedAccounts={selectedAccountsMock}
onAccountChange={onAccountChangeMock}
onCancel={onCancelMock}
onAddAccounts={onAddAccountsMock}
custody="Test"
/>,
);
const firstAccountCheckbox = screen.getAllByRole('checkbox')[0];
fireEvent.click(firstAccountCheckbox);
expect(onAccountChangeMock).toHaveBeenCalledTimes(1);
expect(onAccountChangeMock).toHaveBeenCalledWith({
name: 'Test Account 1',
address: '0x1234567890123456789012345678901234567890',
custodianDetails: undefined,
labels: undefined,
chainId: 1,
});
});
it('calls onCancel when the Cancel button is clicked', () => {
render(
<CustodyAccountList
accounts={testAccounts}
selectedAccounts={selectedAccountsMock}
onAccountChange={onAccountChangeMock}
onCancel={onCancelMock}
onAddAccounts={onAddAccountsMock}
custody="Test"
/>,
);
const cancelButton = screen.getByTestId('custody-account-cancel-button');
fireEvent.click(cancelButton);
expect(onCancelMock).toHaveBeenCalledTimes(1);
});
it('calls onAddAccounts when the Connect button is clicked', () => {
selectedAccountsMock['0x1234567890123456789012345678901234567890'] = true;
selectedAccountsMock['0x0987654321098765432109876543210987654321'] = true;
render(
<CustodyAccountList
accounts={testAccounts}
selectedAccounts={selectedAccountsMock}
onAccountChange={onAccountChangeMock}
onCancel={onCancelMock}
onAddAccounts={onAddAccountsMock}
custody="Test"
/>,
);
const addAccountsButton = screen.getByTestId(
'custody-account-connect-button',
);
fireEvent.click(addAccountsButton);
expect(onAddAccountsMock).toHaveBeenCalledTimes(1);
expect(onAddAccountsMock).toHaveBeenCalledWith('Test');
});
});

View File

@ -0,0 +1 @@
export { default } from './account-list';

View File

@ -0,0 +1,49 @@
.custody-account-list {
flex: 1;
max-height: 50vh;
overflow: auto;
&__item {
border-bottom: 1px solid #d2d8dd;
input {
margin: 12px 0 0 10px;
}
&__title {
flex-flow: row;
}
&__name {
display: block;
white-space: nowrap;
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
}
&__clipboard {
background-color: transparent;
}
}
&__item:first-child {
border-top: 1px solid #d2d8dd;
}
&__item:last-child {
border: 0;
}
&__item:hover {
background-color: rgba(0, 0, 0, 0.03);
}
&__buttons {
border-top: 1px solid #d2d8dd;
}
&__button:not(:last-child) {
margin-right: 16px;
}
}

View File

@ -304,6 +304,7 @@ export default class Home extends PureComponent {
iconName={ICON_NAMES.CLOSE} iconName={ICON_NAMES.CLOSE}
size={ICON_SIZES.SM} size={ICON_SIZES.SM}
ariaLabel={t('close')} ariaLabel={t('close')}
onClick={onAutoHide}
/> />
</Box> </Box>
} }

View File

@ -12,6 +12,9 @@
@import 'connected-accounts/index'; @import 'connected-accounts/index';
@import 'connected-sites/index'; @import 'connected-sites/index';
@import 'create-account/index'; @import 'create-account/index';
///: BEGIN:ONLY_INCLUDE_IN(mmi)
@import "create-account/institutional/connect-custody/index";
///: END:ONLY_INCLUDE_IN
@import 'error/index'; @import 'error/index';
@import 'send/gas-display/index'; @import 'send/gas-display/index';
@import 'home/index'; @import 'home/index';

View File

@ -1,13 +1,10 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import { useGasFeeContext } from '../../../contexts/gasFee'; import { useGasFeeContext } from '../../../contexts/gasFee';
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common';
import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display'; import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display';
import GasTiming from '../../../components/app/gas-timing';
import InfoTooltip from '../../../components/ui/info-tooltip';
import Typography from '../../../components/ui/typography'; import Typography from '../../../components/ui/typography';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import Box from '../../../components/ui/box'; import Box from '../../../components/ui/box';
@ -16,13 +13,11 @@ import {
DISPLAY, DISPLAY,
FLEX_DIRECTION, FLEX_DIRECTION,
BLOCK_SIZES, BLOCK_SIZES,
Color,
FONT_STYLE,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { TokenStandard } from '../../../../shared/constants/transaction'; import { TokenStandard } from '../../../../shared/constants/transaction';
import LoadingHeartBeat from '../../../components/ui/loading-heartbeat'; import LoadingHeartBeat from '../../../components/ui/loading-heartbeat';
import TransactionDetailItem from '../../../components/app/transaction-detail-item'; import TransactionDetailItem from '../../../components/app/transaction-detail-item';
import { ConfirmGasDisplay } from '../../../components/app/confirm-gas-display';
import { NETWORK_TO_NAME_MAP } from '../../../../shared/constants/network'; import { NETWORK_TO_NAME_MAP } from '../../../../shared/constants/network';
import TransactionDetail from '../../../components/app/transaction-detail'; import TransactionDetail from '../../../components/app/transaction-detail';
import ActionableMessage from '../../../components/ui/actionable-message'; import ActionableMessage from '../../../components/ui/actionable-message';
@ -31,7 +26,6 @@ import {
getPreferences, getPreferences,
getIsBuyableChain, getIsBuyableChain,
transactionFeeSelector, transactionFeeSelector,
getIsMainnet,
getIsTestnet, getIsTestnet,
getUseCurrencyRateCheck, getUseCurrencyRateCheck,
} from '../../../selectors'; } from '../../../selectors';
@ -43,7 +37,6 @@ import { showModal } from '../../../store/actions';
import { import {
addHexes, addHexes,
hexWEIToDecETH, hexWEIToDecETH,
hexWEIToDecGWEI,
} from '../../../../shared/modules/conversion.utils'; } from '../../../../shared/modules/conversion.utils';
import { import {
MetaMetricsEventCategory, MetaMetricsEventCategory,
@ -61,7 +54,6 @@ export default function GasDisplay({ gasError }) {
const { openBuyCryptoInPdapp } = useRamps(); const { openBuyCryptoInPdapp } = useRamps();
const currentProvider = useSelector(getProvider); const currentProvider = useSelector(getProvider);
const isMainnet = useSelector(getIsMainnet);
const isTestnet = useSelector(getIsTestnet); const isTestnet = useSelector(getIsTestnet);
const isBuyableChain = useSelector(getIsBuyableChain); const isBuyableChain = useSelector(getIsBuyableChain);
const draftTransaction = useSelector(getCurrentDraftTransaction); const draftTransaction = useSelector(getCurrentDraftTransaction);
@ -95,11 +87,9 @@ export default function GasDisplay({ gasError }) {
userFeeLevel: editingTransaction?.userFeeLevel, userFeeLevel: editingTransaction?.userFeeLevel,
}; };
const { const { hexMaximumTransactionFee, hexTransactionTotal } = useSelector(
hexMinimumTransactionFee, (state) => transactionFeeSelector(state, transactionData),
hexMaximumTransactionFee, );
hexTransactionTotal,
} = useSelector((state) => transactionFeeSelector(state, transactionData));
let title; let title;
if ( if (
@ -158,119 +148,13 @@ export default function GasDisplay({ gasError }) {
detailTotal = primaryTotalTextOverrideMaxAmount; detailTotal = primaryTotalTextOverrideMaxAmount;
maxAmount = primaryTotalTextOverrideMaxAmount; maxAmount = primaryTotalTextOverrideMaxAmount;
} }
return ( return (
<> <>
<Box className="gas-display"> <Box className="gas-display">
<TransactionDetail <TransactionDetail
userAcknowledgedGasMissing={false} userAcknowledgedGasMissing={false}
rows={[ rows={[
<TransactionDetailItem <ConfirmGasDisplay key="gas-display" />,
key="gas-item"
detailTitle={
<Box display={DISPLAY.FLEX}>
<Box marginRight={1}>{t('gas')}</Box>
<Typography
as="span"
marginTop={0}
color={Color.textMuted}
fontStyle={FONT_STYLE.ITALIC}
fontWeight={FONT_WEIGHT.NORMAL}
className="gas-display__title__estimate"
>
({t('transactionDetailGasInfoV2')})
</Typography>
<InfoTooltip
contentText={
<>
<Typography variant={TypographyVariant.H7}>
{t('transactionDetailGasTooltipIntro', [
isMainnet ? t('networkNameEthereum') : '',
])}
</Typography>
<Typography variant={TypographyVariant.H7}>
{t('transactionDetailGasTooltipExplanation')}
</Typography>
<Typography variant={TypographyVariant.H7}>
<a
href="https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172"
target="_blank"
rel="noopener noreferrer"
>
{t('transactionDetailGasTooltipConversion')}
</a>
</Typography>
</>
}
position="right"
/>
</Box>
}
detailTitleColor={Color.textDefault}
detailText={
showCurrencyRateCheck && (
<Box className="gas-display__currency-container">
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexMinimumTransactionFee}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</Box>
)
}
detailTotal={
<Box className="gas-display__currency-container">
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={hexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</Box>
}
subText={
<>
<Box
key="editGasSubTextFeeLabel"
display={DISPLAY.INLINE_FLEX}
className={classNames('gas-display__gas-fee-label', {
'gas-display__gas-fee-warning': estimateUsed === 'high',
})}
>
<LoadingHeartBeat estimateUsed={estimateUsed} />
<Box marginRight={1}>
<strong>
{estimateUsed === 'high' && '⚠ '}
{t('editGasSubTextFeeLabel')}
</strong>
</Box>
<Box
key="editGasSubTextFeeValue"
className="gas-display__currency-container"
>
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</Box>
</Box>
</>
}
subTitle={
<GasTiming
maxPriorityFeePerGas={hexWEIToDecGWEI(
draftTransaction.gas.maxPriorityFeePerGas,
)}
maxFeePerGas={hexWEIToDecGWEI(
draftTransaction.gas.maxFeePerGas,
)}
/>
}
/>,
(gasError || isInsufficientTokenError) && ( (gasError || isInsufficientTokenError) && (
<TransactionDetailItem <TransactionDetailItem
key="total-item" key="total-item"

View File

@ -35,7 +35,6 @@
height: 120px; height: 120px;
} }
&__currency-container,
&__total-amount, &__total-amount,
&__total-value { &__total-value {
position: relative; position: relative;

View File

@ -197,7 +197,7 @@ exports[`SendContent Component render should match snapshot 1`] = `
Gas Gas
</div> </div>
<span <span
class="box box--margin-bottom-1 box--flex-direction-row typography gas-display__title__estimate typography--p typography--weight-normal typography--style-italic typography--color-text-muted" class="gas-details-item-title__estimate"
> >
( (
estimated estimated
@ -236,7 +236,7 @@ exports[`SendContent Component render should match snapshot 1`] = `
class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default" class="box box--margin-top-1 box--margin-bottom-1 box--margin-left-1 box--flex-direction-row box--text-align-right typography typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
> >
<div <div
class="box gas-display__currency-container box--flex-direction-row" class="gas-details-item__currency-container"
> >
<div <div
class="currency-display-component" class="currency-display-component"
@ -274,7 +274,7 @@ exports[`SendContent Component render should match snapshot 1`] = `
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative" class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography transaction-detail-item__row-subText typography--h7 typography--weight-normal typography--style-normal typography--align-end typography--color-text-alternative"
> >
<div <div
class="box gas-display__gas-fee-label box--display-inline-flex box--flex-direction-row" class="box gas-details-item__gasfee-label box--display-inline-flex box--flex-direction-row"
> >
<div <div
class="box box--margin-right-1 box--flex-direction-row" class="box box--margin-right-1 box--flex-direction-row"
@ -284,7 +284,7 @@ exports[`SendContent Component render should match snapshot 1`] = `
</strong> </strong>
</div> </div>
<div <div
class="box gas-display__currency-container box--flex-direction-row" class="gas-details-item__currency-container"
> >
<div <div
class="currency-display-component" class="currency-display-component"

View File

@ -79,6 +79,9 @@ const state = {
confirmTransaction: { confirmTransaction: {
txData: {}, txData: {},
}, },
send: {
draftTransactions: {},
},
}; };
jest.mock('../../store/actions', () => ({ jest.mock('../../store/actions', () => ({

View File

@ -0,0 +1,64 @@
import { toChecksumAddress } from 'ethereumjs-util';
import { getSelectedIdentity, getAccountType, getProvider } from '../selectors';
export function getWaitForConfirmDeepLinkDialog(state) {
return state.metamask.waitForConfirmDeepLinkDialog;
}
export function getTransactionStatusMap(state) {
return state.metamask.custodyStatusMaps;
}
export function getCustodyAccountDetails(state) {
return state.metamask.custodyAccountDetails;
}
export function getCustodyAccountSupportedChains(state, address) {
return state.metamask.custodianSupportedChains
? state.metamask.custodianSupportedChains[toChecksumAddress(address)]
: [];
}
export function getMmiPortfolioEnabled(state) {
return state.metamask.mmiConfiguration?.portfolio?.enabled;
}
export function getMmiPortfolioUrl(state) {
return state.metamask.mmiConfiguration?.portfolio?.url;
}
export function getConfiguredCustodians(state) {
return state.metamask.mmiConfiguration?.custodians || [];
}
export function getCustodianIconForAddress(state, address) {
let custodianIcon;
const checksummedAddress = toChecksumAddress(address);
if (state.metamask.custodyAccountDetails?.[checksummedAddress]) {
const { custodianName } =
state.metamask.custodyAccountDetails[checksummedAddress];
custodianIcon = state.metamask.mmiConfiguration?.custodians?.find(
(custodian) => custodian.name === custodianName,
)?.iconUrl;
}
return custodianIcon;
}
export function getIsCustodianSupportedChain(state) {
const selectedIdentity = getSelectedIdentity(state);
const accountType = getAccountType(state);
const provider = getProvider(state);
const supportedChains =
accountType === 'custody'
? getCustodyAccountSupportedChains(state, selectedIdentity.address)
: null;
return supportedChains?.supportedChains
? supportedChains.supportedChains.includes(
Number(provider.chainId).toString(),
)
: true;
}

View File

@ -0,0 +1,151 @@
import { toChecksumAddress } from 'ethereumjs-util';
import {
getConfiguredCustodians,
getCustodianIconForAddress,
getCustodyAccountDetails,
getCustodyAccountSupportedChains,
getMmiPortfolioEnabled,
getMmiPortfolioUrl,
getTransactionStatusMap,
getWaitForConfirmDeepLinkDialog,
getIsCustodianSupportedChain,
} from './selectors';
describe('Institutional selectors', () => {
const state = {
metamask: {
provider: {
type: 'test',
chainId: '1',
},
identities: {
'0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275': {
name: 'Custody Account A',
address: '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275',
},
},
selectedAddress: '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275',
waitForConfirmDeepLinkDialog: '123',
keyrings: [
{
type: 'Custody',
accounts: ['0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275'],
},
],
custodyStatusMaps: '123',
custodyAccountDetails: {
'0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275': {
custodianName: 'saturn',
},
},
custodianSupportedChains: {
'0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275': {
supportedChains: ['1', '2'],
custodianName: 'saturn',
},
},
mmiConfiguration: {
portfolio: {
enabled: true,
url: 'https://dashboard.metamask-institutional.io',
},
custodians: [
{
type: 'saturn',
name: 'saturn',
apiUrl: 'https://saturn-custody.dev.metamask-institutional.io',
iconUrl: 'images/saturn.svg',
displayName: 'Saturn Custody',
production: true,
refreshTokenUrl: null,
isNoteToTraderSupported: false,
version: 1,
},
],
},
},
};
describe('getWaitForConfirmDeepLinkDialog', () => {
it('extracts a state property', () => {
const result = getWaitForConfirmDeepLinkDialog(state);
expect(result).toStrictEqual(state.metamask.waitForConfirmDeepLinkDialog);
});
});
describe('getCustodyAccountDetails', () => {
it('extracts a state property', () => {
const result = getCustodyAccountDetails(state);
expect(result).toStrictEqual(state.metamask.custodyAccountDetails);
});
});
describe('getTransactionStatusMap', () => {
it('extracts a state property', () => {
const result = getTransactionStatusMap(state);
expect(result).toStrictEqual(state.metamask.custodyStatusMaps);
});
});
describe('getCustodianSupportedChains', () => {
it('extracts a state property', () => {
const result = getCustodyAccountSupportedChains(
state,
'0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275',
);
expect(result).toStrictEqual(
state.metamask.custodianSupportedChains[
toChecksumAddress('0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275')
],
);
});
});
describe('getMmiPortfolioEnabled', () => {
it('extracts a state property', () => {
const result = getMmiPortfolioEnabled(state);
expect(result).toStrictEqual(
state.metamask.mmiConfiguration.portfolio.enabled,
);
});
});
describe('getMmiPortfolioUrl', () => {
it('extracts a state property', () => {
const result = getMmiPortfolioUrl(state);
expect(result).toStrictEqual(
state.metamask.mmiConfiguration.portfolio.url,
);
});
});
describe('getConfiguredCustodians', () => {
it('extracts a state property', () => {
const result = getConfiguredCustodians(state);
expect(result).toStrictEqual(state.metamask.mmiConfiguration.custodians);
});
});
describe('getCustodianIconForAddress', () => {
it('extracts a state property', () => {
const result = getCustodianIconForAddress(
state,
'0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275',
);
expect(result).toStrictEqual(
state.metamask.mmiConfiguration.custodians[0].iconUrl,
);
});
});
describe('getIsCustodianSupportedChain', () => {
it('extracts a state property', () => {
const result = getIsCustodianSupportedChain(
state,
'0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275',
);
expect(result).toStrictEqual(true);
});
});
});

View File

@ -228,6 +228,12 @@ export function getAccountType(state) {
const currentKeyring = getCurrentKeyring(state); const currentKeyring = getCurrentKeyring(state);
const type = currentKeyring && currentKeyring.type; const type = currentKeyring && currentKeyring.type;
///: BEGIN:ONLY_INCLUDE_IN(mmi)
if (type.startsWith('Custody')) {
return 'custody';
}
///: END:ONLY_INCLUDE_IN
switch (type) { switch (type) {
case KeyringType.trezor: case KeyringType.trezor:
case KeyringType.ledger: case KeyringType.ledger: