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

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

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

View File

@ -2697,10 +2697,6 @@
"message": "Regelmäßige Transaktionen planen und ausführen.",
"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": {
"message": "Dialogfenster in MetaMask anzeigen.",
"description": "The description for the `snap_dialog` permission"

View File

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

View File

@ -2697,10 +2697,6 @@
"message": "Programar y ejecutar acciones periódicas.",
"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": {
"message": "Mostrar ventanas de diálogo en MetaMask.",
"description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Planifiez et exécutez des actions périodiques.",
"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": {
"message": "Afficher les boîtes de dialogue dans MetaMask.",
"description": "The description for the `snap_dialog` permission"

View File

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

View File

@ -2697,10 +2697,6 @@
"message": "Jadwalkan dan lakukan tindakan berkala.",
"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": {
"message": "Tampilkan jendela dialog di MetaMask.",
"description": "The description for the `snap_dialog` permission"

View File

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

View File

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

View File

@ -2697,10 +2697,6 @@
"message": "Agende e execute ações periódicas.",
"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": {
"message": "Exibir janelas de diálogo na MetaMask.",
"description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Планируйте и выполняйте периодические действия.",
"description": "The description for the `snap_cronjob` permission"
},
"permission_customConfirmation": {
"message": "Показать подтверждение в MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": {
"message": "Отображение диалоговых окон в MetaMask.",
"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.",
"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": {
"message": "Ipakita ang mga dialog window sa MetaMask.",
"description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "Periyodik eylemleri planla ve gerçekleştir.",
"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": {
"message": "MetaMask'te iletişim kutusu pencerelerini göster.",
"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ỳ.",
"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": {
"message": "Hiển thị cửa sổ hộp thoại trong MetaMask.",
"description": "The description for the `snap_dialog` permission"

View File

@ -2697,10 +2697,6 @@
"message": "规划并执行定期操作。",
"description": "The description for the `snap_cronjob` permission"
},
"permission_customConfirmation": {
"message": "在MetaMask中显示确认。",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": {
"message": "在 MetaMask 中显示对话框窗口。",
"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 () {
const clock = sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET);
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
@ -304,7 +304,7 @@ describe('DetectTokensController', function () {
it('should not check and add tokens while on unsupported networks', async function () {
sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.SEPOLIA);
await network.setProviderType(NETWORK_TYPES.SEPOLIA);
const tokenListMessengerSepolia = new ControllerMessenger().getRestricted({
name: 'TokenListController',
});
@ -337,7 +337,7 @@ describe('DetectTokensController', function () {
it('should skip adding tokens listed in ignoredTokens array', async function () {
sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET);
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
@ -388,7 +388,7 @@ describe('DetectTokensController', function () {
it('should check and add tokens while on supported networks', async function () {
sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET);
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
@ -483,7 +483,7 @@ describe('DetectTokensController', function () {
it('should not trigger detect new tokens when not unlocked', async function () {
const clock = sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET);
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
@ -504,7 +504,7 @@ describe('DetectTokensController', function () {
it('should not trigger detect new tokens when not open', async function () {
const clock = sandbox.useFakeTimers();
network.setProviderType(NETWORK_TYPES.MAINNET);
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,

View File

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

View File

@ -1,6 +1,6 @@
import { strict as assert } from 'assert';
import EventEmitter from 'events';
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import {
createSwappableProxy,
@ -379,6 +379,21 @@ function buildDefaultNetworkConfigurationsState(): NetworkConfigurations {
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
* 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.
*/
messenger: NetworkControllerMessenger;
/**
* Observable store containing the provider configuration.
*/
providerStore: ObservableStore<ProviderConfiguration>;
#messenger: NetworkControllerMessenger;
/**
* Observable store containing the provider configuration for the previously
@ -425,44 +435,23 @@ export class NetworkController extends EventEmitter {
*/
#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
* 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.
@ -482,51 +471,27 @@ export class NetworkController extends EventEmitter {
}: NetworkControllerOptions) {
super();
this.messenger = messenger;
this.#messenger = messenger;
// create stores
this.providerStore = new ObservableStore(
state.provider || buildDefaultProviderConfigState(),
);
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.store = new ObservableStore({
...buildDefaultState(),
...state,
});
this.#previousProviderConfig = this.store.getState().provider;
// provider and block tracker
this._provider = null;
this._blockTracker = null;
this.#provider = null;
this.#blockTracker = null;
// provider and block tracker proxies - because the network changes
this._providerProxy = null;
this._blockTrackerProxy = null;
this.#providerProxy = null;
this.#blockTrackerProxy = null;
if (!infuraProjectId || typeof infuraProjectId !== 'string') {
throw new Error('Invalid Infura project ID');
}
this._infuraProjectId = infuraProjectId;
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
this.#infuraProjectId = infuraProjectId;
this.#trackMetaMetricsEvent = trackMetaMetricsEvent;
}
/**
@ -535,7 +500,7 @@ export class NetworkController extends EventEmitter {
* In-progress requests will not be aborted.
*/
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.
*/
async initializeProvider(): Promise<void> {
const { type, rpcUrl, chainId } = this.providerStore.getState();
this._configureProvider({ type, rpcUrl, chainId });
const { type, rpcUrl, chainId } = this.store.getState().provider;
this.#configureProvider({ type, rpcUrl, chainId });
await this.lookupNetwork();
}
@ -555,8 +520,8 @@ export class NetworkController extends EventEmitter {
provider: SwappableProxy<SafeEventEmitterProvider> | null;
blockTracker: SwappableProxy<PollingBlockTracker> | null;
} {
const provider = this._providerProxy;
const blockTracker = this._blockTrackerProxy;
const provider = this.#providerProxy;
const blockTracker = this.#blockTrackerProxy;
return { provider, blockTracker };
}
@ -569,7 +534,7 @@ export class NetworkController extends EventEmitter {
* and false otherwise.
*/
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
// already prevents duplicate requests from taking place
if (EIPS[1559] !== undefined) {
@ -584,11 +549,15 @@ export class NetworkController extends EventEmitter {
return false;
}
const supportsEIP1559 = await this._determineEIP1559Compatibility(provider);
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
const supportsEIP1559 = await this.#determineEIP1559Compatibility(provider);
const { networkDetails } = this.store.getState();
this.store.updateState({
networkDetails: {
...networkDetails,
EIPS: {
...networkDetails.EIPS,
1559: supportsEIP1559,
},
},
});
return supportsEIP1559;
@ -606,7 +575,7 @@ export class NetworkController extends EventEmitter {
* blocking requests, or if the network is not Infura-supported.
*/
async lookupNetwork(): Promise<void> {
const { chainId, type } = this.providerStore.getState();
const { chainId, type } = this.store.getState().provider;
const { provider } = this.getProviderAndBlockTracker();
let networkChanged = false;
let networkId: NetworkIdState = null;
@ -624,9 +593,9 @@ export class NetworkController extends EventEmitter {
log.warn(
'NetworkController - lookupNetwork aborted due to missing chainId',
);
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
this.#resetNetworkId();
this.#resetNetworkStatus();
this.#resetNetworkDetails();
return;
}
@ -634,20 +603,20 @@ export class NetworkController extends EventEmitter {
const listener = () => {
networkChanged = true;
this.messenger.unsubscribe(
this.#messenger.unsubscribe(
NetworkControllerEventType.NetworkDidChange,
listener,
);
};
this.messenger.subscribe(
this.#messenger.subscribe(
NetworkControllerEventType.NetworkDidChange,
listener,
);
try {
const results = await Promise.all([
this._getNetworkId(provider),
this._determineEIP1559Compatibility(provider),
this.#getNetworkId(provider),
this.#determineEIP1559Compatibility(provider),
]);
const possibleNetworkId = results[0];
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.
return;
}
this.messenger.unsubscribe(
this.#messenger.unsubscribe(
NetworkControllerEventType.NetworkDidChange,
listener,
);
this.networkStatusStore.putState(networkStatus);
this.store.updateState({
networkStatus,
});
if (networkStatus === NetworkStatus.Available) {
this.networkIdStore.putState(networkId);
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
const { networkDetails } = this.store.getState();
this.store.updateState({
networkId,
networkDetails: {
...networkDetails,
EIPS: {
...networkDetails.EIPS,
1559: supportsEIP1559,
},
},
});
} else {
this._resetNetworkId();
this._resetNetworkDetails();
this.#resetNetworkId();
this.#resetNetworkDetails();
}
if (isInfura) {
if (networkStatus === NetworkStatus.Available) {
this.messenger.publish(NetworkControllerEventType.InfuraIsUnblocked);
this.#messenger.publish(NetworkControllerEventType.InfuraIsUnblocked);
} else if (networkStatus === NetworkStatus.Blocked) {
this.messenger.publish(NetworkControllerEventType.InfuraIsBlocked);
this.#messenger.publish(NetworkControllerEventType.InfuraIsBlocked);
}
} else {
// Always publish infuraIsUnblocked regardless of network status to
// prevent consumers from being stuck in a blocked state if they were
// previously connected to an Infura network that was blocked
this.messenger.publish(NetworkControllerEventType.InfuraIsUnblocked);
this.#messenger.publish(NetworkControllerEventType.InfuraIsUnblocked);
}
}
@ -731,7 +706,7 @@ export class NetworkController extends EventEmitter {
*/
setActiveNetwork(networkConfigurationId: NetworkConfigurationId): string {
const targetNetwork =
this.networkConfigurationsStore.getState()[networkConfigurationId];
this.store.getState().networkConfigurations[networkConfigurationId];
if (!targetNetwork) {
throw new Error(
@ -739,7 +714,7 @@ export class NetworkController extends EventEmitter {
);
}
this._setProviderConfig({
this.#setProviderConfig({
type: NETWORK_TYPES.RPC,
...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
* network.
*/
setProviderType(type: string): void {
async setProviderType(type: string) {
assert.notStrictEqual(
type,
NETWORK_TYPES.RPC,
@ -765,7 +740,7 @@ export class NetworkController extends EventEmitter {
`Unknown Infura provider type "${type}".`,
);
const network = BUILT_IN_INFURA_NETWORKS[type];
this._setProviderConfig({
await this.#setProviderConfig({
type,
rpcUrl: '',
chainId: network.chainId,
@ -778,8 +753,8 @@ export class NetworkController extends EventEmitter {
/**
* Re-initializes the provider and block tracker for the current network.
*/
resetConnection(): void {
this._setProviderConfig(this.providerStore.getState());
async resetConnection() {
await this.#setProviderConfig(this.store.getState().provider);
}
/**
@ -789,8 +764,10 @@ export class NetworkController extends EventEmitter {
*/
async rollbackToPreviousProvider() {
const config = this.#previousProviderConfig;
this.providerStore.putState(config);
await this._switchNetwork(config);
this.store.updateState({
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
* 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) => {
const ethQuery = new EthQuery(provider);
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
* an error.
*/
async _getNetworkId(provider: SafeEventEmitterProvider): Promise<string> {
async #getNetworkId(provider: SafeEventEmitterProvider): Promise<string> {
const ethQuery = new EthQuery(provider);
return await new Promise((resolve, reject) => {
ethQuery.sendAsync<never[], string>(
@ -842,22 +819,28 @@ export class NetworkController extends EventEmitter {
/**
* Clears the stored network ID.
*/
_resetNetworkId(): void {
this.networkIdStore.putState(buildDefaultNetworkIdState());
#resetNetworkId(): void {
this.store.updateState({
networkId: buildDefaultNetworkIdState(),
});
}
/**
* Resets network status to the default ("unknown").
*/
_resetNetworkStatus(): void {
this.networkStatusStore.putState(buildDefaultNetworkStatusState());
#resetNetworkStatus(): void {
this.store.updateState({
networkStatus: buildDefaultNetworkStatusState(),
});
}
/**
* Clears details previously stored for the network.
*/
_resetNetworkDetails(): void {
this.networkDetails.putState(buildDefaultNetworkDetailsState());
#resetNetworkDetails(): void {
this.store.updateState({
networkDetails: buildDefaultNetworkDetailsState(),
});
}
/**
@ -866,10 +849,10 @@ export class NetworkController extends EventEmitter {
*
* @param providerConfig - The provider configuration.
*/
async _setProviderConfig(providerConfig: ProviderConfiguration) {
this.#previousProviderConfig = this.providerStore.getState();
this.providerStore.putState(providerConfig);
await this._switchNetwork(providerConfig);
async #setProviderConfig(providerConfig: ProviderConfiguration) {
this.#previousProviderConfig = this.store.getState().provider;
this.store.updateState({ provider: 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
* and false otherwise.
*/
async _determineEIP1559Compatibility(
async #determineEIP1559Compatibility(
provider: SafeEventEmitterProvider,
): Promise<boolean> {
const latestBlock = await this._getLatestBlock(provider);
const latestBlock = await this.#getLatestBlock(provider);
return latestBlock?.baseFeePerGas !== undefined;
}
@ -900,13 +883,13 @@ export class NetworkController extends EventEmitter {
* @param providerConfig - The provider configuration object that specifies
* the new network.
*/
async _switchNetwork(providerConfig: ProviderConfiguration) {
this.messenger.publish(NetworkControllerEventType.NetworkWillChange);
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
this._configureProvider(providerConfig);
this.messenger.publish(NetworkControllerEventType.NetworkDidChange);
async #switchNetwork(providerConfig: ProviderConfiguration) {
this.#messenger.publish(NetworkControllerEventType.NetworkWillChange);
this.#resetNetworkId();
this.#resetNetworkStatus();
this.#resetNetworkDetails();
this.#configureProvider(providerConfig);
this.#messenger.publish(NetworkControllerEventType.NetworkDidChange);
await this.lookupNetwork();
}
@ -924,20 +907,20 @@ export class NetworkController extends EventEmitter {
* any 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);
if (isInfura) {
// infura type-based endpoints
this._configureInfuraProvider({
this.#configureInfuraProvider({
type,
infuraProjectId: this._infuraProjectId,
infuraProjectId: this.#infuraProjectId,
});
} else if (type === NETWORK_TYPES.RPC && rpcUrl) {
// url-based rpc endpoints
this._configureStandardProvider(rpcUrl, chainId);
this.#configureStandardProvider(rpcUrl, chainId);
} else {
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
* now-obsolete term we've retained for backward compatibility.)
*/
_configureInfuraProvider({
#configureInfuraProvider({
type,
infuraProjectId,
}: {
type: BuiltInInfuraNetwork;
infuraProjectId: NetworkControllerOptions['infuraProjectId'];
}): void {
log.info('NetworkController - configureInfuraProvider', type);
log.info('NetworkController - #configureInfuraProvider', type);
const { provider, blockTracker } = createNetworkClient({
network: type,
infuraProjectId,
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 chainId - The chain ID of the network (as per EIP-155).
*/
_configureStandardProvider(rpcUrl: string, chainId: ChainId): void {
log.info('NetworkController - configureStandardProvider', rpcUrl);
#configureStandardProvider(rpcUrl: string, chainId: ChainId): void {
log.info('NetworkController - #configureStandardProvider', rpcUrl);
const { provider, blockTracker } = createNetworkClient({
chainId,
rpcUrl,
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.blockTracker - The block tracker.
*/
_setProviderAndBlockTracker({
#setProviderAndBlockTracker({
provider,
blockTracker,
}: {
@ -1002,21 +985,21 @@ export class NetworkController extends EventEmitter {
blockTracker: PollingBlockTracker;
}): void {
// update or initialize proxies
if (this._providerProxy) {
this._providerProxy.setTarget(provider);
if (this.#providerProxy) {
this.#providerProxy.setTarget(provider);
} else {
this._providerProxy = createSwappableProxy(provider);
this.#providerProxy = createSwappableProxy(provider);
}
if (this._blockTrackerProxy) {
this._blockTrackerProxy.setTarget(blockTracker);
if (this.#blockTrackerProxy) {
this.#blockTrackerProxy.setTarget(blockTracker);
} else {
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, {
this.#blockTrackerProxy = createEventEmitterProxy(blockTracker, {
eventFilter: 'skipInternal',
});
}
// set new provider and blockTracker
this._provider = provider;
this._blockTracker = blockTracker;
this.#provider = provider;
this.#blockTracker = blockTracker;
}
/**
@ -1105,7 +1088,7 @@ export class NetworkController extends EventEmitter {
);
}
const networkConfigurations = this.networkConfigurationsStore.getState();
const { networkConfigurations } = this.store.getState();
const newNetworkConfiguration = {
rpcUrl,
chainId,
@ -1120,16 +1103,18 @@ export class NetworkController extends EventEmitter {
)?.id;
const newNetworkConfigurationId = oldNetworkConfigurationId || uuid();
this.networkConfigurationsStore.putState({
...networkConfigurations,
[newNetworkConfigurationId]: {
...newNetworkConfiguration,
id: newNetworkConfigurationId,
this.store.updateState({
networkConfigurations: {
...networkConfigurations,
[newNetworkConfigurationId]: {
...newNetworkConfiguration,
id: newNetworkConfigurationId,
},
},
});
if (!oldNetworkConfigurationId) {
this._trackMetaMetricsEvent({
this.#trackMetaMetricsEvent({
event: 'Custom Network Added',
category: MetaMetricsEventCategory.Network,
referrer: {
@ -1160,9 +1145,11 @@ export class NetworkController extends EventEmitter {
networkConfigurationId: NetworkConfigurationId,
): void {
const networkConfigurations = {
...this.networkConfigurationsStore.getState(),
...this.store.getState().networkConfigurations,
};
delete networkConfigurations[networkConfigurationId];
this.networkConfigurationsStore.putState(networkConfigurations);
this.store.updateState({
networkConfigurations,
});
}
}

View File

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

View File

@ -2,36 +2,13 @@ import { strict as assert } from 'assert';
import sinon from 'sinon';
import { ControllerMessenger } from '@metamask/base-controller';
import { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences';
import { NetworkController } from './network';
describe('preferences controller', function () {
let preferencesController;
let network;
let currentChainId;
let provider;
let tokenListController;
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({
name: 'TokenListController',
});
@ -43,14 +20,8 @@ describe('preferences controller', function () {
messenger: tokenListMessenger,
});
sandbox
.stub(network, '_getLatestBlock')
.callsFake(() => Promise.resolve({}));
preferencesController = new PreferencesController({
initLangCode: 'en_US',
network,
provider,
tokenListController,
onInfuraIsBlocked: sinon.spy(),
onInfuraIsUnblocked: sinon.spy(),

View File

@ -1,5 +1,7 @@
import { errorCodes } from 'eth-rpc-errors';
import { detectSIWE } from '@metamask/controller-utils';
import { isValidAddress } from 'ethereumjs-util';
import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app';
import { TransactionStatus } from '../../../shared/constants/transaction';
import { SECOND } from '../../../shared/constants/time';
@ -168,8 +170,17 @@ export default function createRPCMethodTrackingMiddleware({
if (event === MetaMetricsEventName.SignatureRequested) {
eventProperties.signature_type = method;
const data = req?.params?.[0];
const from = req?.params?.[1];
// In personal messages the first param is data while in typed messages second param is data
// 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 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.LINEA_TESTNET
) {
setProviderType(approvedRequestData.type);
await setProviderType(approvedRequestData.type);
} else {
await setActiveNetwork(approvedRequestData.id);
}

View File

@ -544,7 +544,7 @@ export default class MetamaskController extends EventEmitter {
messenger: currencyRateMessenger,
state: {
...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,
getNetworkStatus: () =>
this.networkController.store.getState().networkStatus,
onNetworkStateChange: (listener) =>
this.networkController.networkIdStore.subscribe(listener),
onNetworkStateChange: (listener) => {
let previousNetworkId =
this.networkController.store.getState().networkId;
this.networkController.store.subscribe((state) => {
if (previousNetworkId !== state.networkId) {
listener();
previousNetworkId = state.networkId;
}
});
},
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
preferencesStore: this.preferencesController.store,
@ -1166,6 +1174,9 @@ export default class MetamaskController extends EventEmitter {
preferencesController: this.preferencesController,
getState: this.getState.bind(this),
securityProviderRequest: this.securityProviderRequest.bind(this),
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.swapsController = new SwapsController({
@ -1524,12 +1535,6 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger,
'SnapController:getSnapState',
),
showConfirmation: (origin, confirmationData) =>
this.approvalController.addAndShowApprovalRequest({
origin,
type: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,
requestData: confirmationData,
}),
showDialog: (origin, type, content, placeholder) =>
this.approvalController.addAndShowApprovalRequest({
origin,
@ -3237,32 +3242,6 @@ export default class MetamaskController extends EventEmitter {
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
/**
@ -3868,7 +3847,6 @@ export default class MetamaskController extends EventEmitter {
///: BEGIN:ONLY_INCLUDE_IN(flask)
engine.push(
createSnapMethodMiddleware(subjectType === SubjectType.Snap, {
getAppKey: this.getAppKeyForSubject.bind(this, origin),
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
this.appStateController,
),

View File

@ -17,7 +17,7 @@ import {
TextVariant,
} from '../../../helpers/constants/design-system';
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 { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
@ -111,7 +111,7 @@ export default function ApproveContentCard({
(!isMultiLayerFeeNetwork &&
supportsEIP1559 &&
!renderSimulationFailureWarning ? (
<GasDetailsItem
<ConfirmGasDisplay
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 { 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 { useI18nContext } from '../../../hooks/useI18nContext';
@ -14,10 +19,20 @@ import LoadingHeartBeat from '../../ui/loading-heartbeat';
import GasTiming from '../gas-timing/gas-timing.component';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
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';
const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
const t = useI18nContext();
const draftTransaction = useSelector(getCurrentDraftTransaction);
const { transactionData } = useDraftTransactionGasValues();
const {
hexMinimumTransactionFee: draftHexMinimumTransactionFee,
hexMaximumTransactionFee: draftHexMaximumTransactionFee,
} = useSelector((state) => transactionFeeSelector(state, transactionData));
const {
estimateUsed,
hasSimulationError,
@ -41,7 +56,8 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
detailTitle={<GasDetailsItemTitle />}
detailTitleColor={TextColor.textDefault}
detailText={
useCurrencyRateCheck && (
useCurrencyRateCheck &&
Object.keys(draftTransaction).length === 0 && (
<div className="gas-details-item__currency-container">
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
@ -57,7 +73,7 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={hexMinimumTransactionFee}
value={hexMinimumTransactionFee || draftHexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
@ -86,7 +102,9 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
value={
hexMaximumTransactionFee || draftHexMaximumTransactionFee
}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
@ -95,8 +113,14 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
}
subTitle={
<GasTiming
maxPriorityFeePerGas={maxPriorityFeePerGas.toString()}
maxFeePerGas={maxFeePerGas.toString()}
maxPriorityFeePerGas={(
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
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`
- `FONT_WEIGHT.MEDIUM` = `medium` || `500`
- `FONT_WEIGHT.BOLD` = `bold` || `700`
- `FontWeight.Normal` = `normal` || `400`
- `FontWeight.Medium` = `medium` || `500`
- `FontWeight.Bold` = `bold` || `700`
<Canvas>
<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
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
</Text>
<Text fontWeight={FONT_WEIGHT.MEDIUM}>
<Text fontWeight={FontWeight.Medium}>
medium
</Text>
<Text fontWeight={FONT_WEIGHT.BOLD}>
<Text fontWeight={FontWeight.Bold}>
bold
</Text>
```
### 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`
- `FONT_STYLE.ITALIC`
- `FontStyle.Normal`
- `FontStyle.Italic`
<Canvas>
<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
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
</Text>
<Text fontStyle={FONT_STYLE.ITALIC}>
<Text fontStyle={FontStyle.Italic}>
bold
</Text>
```
### 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>
<Story id="components-componentlibrary-text--text-transform" />
@ -170,22 +170,22 @@ Use the `textTransform` prop and the `TEXT_TRANSFORM` object from `./ui/helpers/
```jsx
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
</Text>
<Text textAlign={TEXT_TRANSFORM.LOWERCASE}>
<Text textAlign={TextTransform.Lowercase}>
lowercase
</Text>
<Text textAlign={TEXT_TRANSFORM.CAPITALIZE}>
<Text textAlign={TextTransform.Capitalize}>
capitalize
</Text>
```
### 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>
<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
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
</Text>
<Text textAlign={TEXT_ALIGN.CENTER}>
<Text textAlign={TextAlign.Center}>
center
</Text>
<Text textAlign={TEXT_ALIGN.RIGHT}>
<Text textAlign={TextAlign.Right}>
right
</Text>
<Text textAlign={TEXT_ALIGN.JUSTIFY}>
<Text textAlign={TextAlign.Justify}>
justify
</Text>
<Text textAlign={TEXT_ALIGN.END}>
<Text textAlign={TextAlign.End}>
end
</Text>
```
### 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>
<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
import { Text } from '../../component-library';
import { OVERFLOW_WRAP } from '../../../helpers/constants/design-system';
import { OverflowWrap } from '../../../helpers/constants/design-system';
<div
style={{
@ -231,11 +231,11 @@ import { OVERFLOW_WRAP } from '../../../helpers/constants/design-system';
display: 'block',
}}
>
<Text overflowWrap={OVERFLOW_WRAP.NORMAL}>
{OVERFLOW_WRAP.NORMAL}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d
<Text overflowWrap={OverflowWrap.Normal}>
{OverflowWrap.Normal}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d
</Text>
<Text overflowWrap={OVERFLOW_WRAP.BREAK_WORD}>
{OVERFLOW_WRAP.BREAK_WORD}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d
<Text overflowWrap={OverflowWrap.BreakWord}>
{OverflowWrap.BreakWord}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d
</Text>
</div>;
```
@ -538,14 +538,14 @@ import { TextVariant } from '../../../helpers/constants/design-system';
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
// Before
<Typograpghy align={TEXT_ALIGN.CENTER}>Demo</Typograpghy>;
<Typography align={TEXT_ALIGN.CENTER}>Demo</Typography>;
// After
<Text textAlign={TEXT_ALIGN.CENTER}>Demo</Text>;
<Text textAlign={TextAlign.Center}>Demo</Text>;
```
### Box Props

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import React from 'react';
import type { BoxProps } from '../../ui/box/box.d';
import {
FONT_WEIGHT,
FONT_STYLE,
FontWeight,
FontStyle,
TextVariant,
TEXT_ALIGN,
TEXT_TRANSFORM,
OVERFLOW_WRAP,
TextAlign,
TextTransform,
OverflowWrap,
TextColor,
Color,
} from '../../../helpers/constants/design-system';
@ -70,35 +70,35 @@ export interface TextProps extends BoxProps {
*/
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
*/
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
*/
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
*/
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
*/
textAlign?: keyof typeof TEXT_ALIGN;
textAlign?: TextAlign;
/**
* 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)
*/
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
*/
overflowWrap?: keyof typeof OVERFLOW_WRAP;
overflowWrap?: OverflowWrap;
/**
* 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',
};
export enum TextAlign {
Left = 'left',
Center = 'center',
Right = 'right',
Justify = 'justify',
End = 'end',
Start = 'start',
}
export const TEXT_ALIGN = {
LEFT: 'left',
CENTER: 'center',
@ -306,24 +315,50 @@ export const TEXT_ALIGN = {
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 = {
UPPERCASE: 'uppercase',
LOWERCASE: 'lowercase',
CAPITALIZE: 'capitalize',
};
export enum FontWeight {
Bold = 'bold',
Medium = 'medium',
Normal = 'normal',
}
export const FONT_WEIGHT = {
BOLD: 'bold',
MEDIUM: 'medium',
NORMAL: 'normal',
};
export enum OverflowWrap {
BreakWord = 'break-word',
Anywhere = 'anywhere',
Normal = 'normal',
}
export const OVERFLOW_WRAP = {
BREAK_WORD: 'break-word',
ANYWHERE: 'anywhere',
NORMAL: 'normal',
};
export enum FontStyle {
Italic = 'italic',
Normal = 'normal',
}
export const FONT_STYLE = {
ITALIC: 'italic',
NORMAL: 'normal',

View File

@ -71,12 +71,6 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
weight: 2,
}),
///: 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 }) => ({
label: t('permission_dialog'),
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,
} from '../../../helpers/constants/design-system';
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 { TokenStandard } from '../../../../shared/constants/transaction';
import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network';
@ -34,6 +33,7 @@ import {
} from '../../../components/component-library/icon/deprecated';
import { ButtonIcon } from '../../../components/component-library/button-icon/deprecated';
import { Text } from '../../../components/component-library';
import { ConfirmGasDisplay } from '../../../components/app/confirm-gas-display';
export default class ConfirmApproveContent extends Component {
static contextTypes = {
@ -170,7 +170,7 @@ export default class ConfirmApproveContent extends Component {
!renderSimulationFailureWarning
) {
return (
<GasDetailsItem
<ConfirmGasDisplay
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
);

View File

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

View File

@ -28,18 +28,9 @@ import {
import { TransactionModalContextProvider } from '../../contexts/transaction-modal';
import TransactionDetail from '../../components/app/transaction-detail/transaction-detail.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 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 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 {
disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling,
@ -53,16 +44,13 @@ import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
import {
addHexes,
hexToDecimal,
hexWEIToDecGWEI,
} from '../../../shared/modules/conversion.utils';
import TransactionAlerts from '../../components/app/transaction-alerts';
import { ConfirmHexData } from '../../components/app/confirm-hexdata';
import { ConfirmData } from '../../components/app/confirm-data';
import { ConfirmTitle } from '../../components/app/confirm-title';
import { ConfirmSubTitle } from '../../components/app/confirm-subtitle';
const renderHeartBeatIfNotInTest = () =>
process.env.IN_TEST ? null : <LoadingHeartBeat />;
import { ConfirmGasDisplay } from '../../components/app/confirm-gas-display';
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
@ -136,7 +124,6 @@ export default class ConfirmTransactionBase extends Component {
maxFeePerGas: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string,
baseFeePerGas: PropTypes.string,
isMainnet: PropTypes.bool,
gasFeeIsCustom: PropTypes.bool,
showLedgerSteps: PropTypes.bool.isRequired,
nativeCurrency: PropTypes.string,
@ -319,11 +306,7 @@ export default class ConfirmTransactionBase extends Component {
txData,
useNativeCurrencyAsPrimaryCurrency,
primaryTotalTextOverrideMaxAmount,
maxFeePerGas,
maxPriorityFeePerGas,
isMainnet,
showLedgerSteps,
supportsEIP1559,
isMultiLayerFeeNetwork,
nativeCurrency,
isBuyableChain,
@ -439,128 +422,6 @@ export default class ConfirmTransactionBase extends Component {
</div>
) : 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 = () => (
<div className="confirm-page-container-content__error-container">
<SimulationErrorMessage
@ -594,9 +455,11 @@ export default class ConfirmTransactionBase extends Component {
}
rows={[
renderSimulationFailureWarning && simulationFailureWarning(),
!renderSimulationFailureWarning &&
!isMultiLayerFeeNetwork &&
renderGasDetailsItem(),
!renderSimulationFailureWarning && !isMultiLayerFeeNetwork && (
<ConfirmGasDisplay
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
),
!renderSimulationFailureWarning && isMultiLayerFeeNetwork && (
<MultiLayerFeeMessage
transaction={txData}

View File

@ -23,7 +23,11 @@ setBackgroundConnection({
});
const baseStore = {
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
send: {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
currentTransactionUUID: null,
draftTransactions: {},
},
DNS: domainInitialState,
gas: {
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 { DelineatorType } from '../../../../../helpers/constants/flask';
function getValues(pendingApproval, t, actions) {
const {
snapName,
requestData: { content, title, description, textAreaContent },
requestData: { content },
} = pendingApproval;
return {
@ -25,49 +24,7 @@ function getValues(pendingApproval, t, actions) {
snapName,
},
// TODO: Replace with SnapUIRenderer when we don't need to inject the input manually.
// TODO: Remove ternary once snap_confirm has been removed.
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,
},
},
]
: []),
],
children: mapToTemplate(content),
},
},
],

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}
size={ICON_SIZES.SM}
ariaLabel={t('close')}
onClick={onAutoHide}
/>
</Box>
}

View File

@ -12,6 +12,9 @@
@import 'connected-accounts/index';
@import 'connected-sites/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 'send/gas-display/index';
@import 'home/index';

View File

@ -1,13 +1,10 @@
import React, { useContext } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { I18nContext } from '../../../contexts/i18n';
import { useGasFeeContext } from '../../../contexts/gasFee';
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common';
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 Button from '../../../components/ui/button';
import Box from '../../../components/ui/box';
@ -16,13 +13,11 @@ import {
DISPLAY,
FLEX_DIRECTION,
BLOCK_SIZES,
Color,
FONT_STYLE,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
import { TokenStandard } from '../../../../shared/constants/transaction';
import LoadingHeartBeat from '../../../components/ui/loading-heartbeat';
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 TransactionDetail from '../../../components/app/transaction-detail';
import ActionableMessage from '../../../components/ui/actionable-message';
@ -31,7 +26,6 @@ import {
getPreferences,
getIsBuyableChain,
transactionFeeSelector,
getIsMainnet,
getIsTestnet,
getUseCurrencyRateCheck,
} from '../../../selectors';
@ -43,7 +37,6 @@ import { showModal } from '../../../store/actions';
import {
addHexes,
hexWEIToDecETH,
hexWEIToDecGWEI,
} from '../../../../shared/modules/conversion.utils';
import {
MetaMetricsEventCategory,
@ -61,7 +54,6 @@ export default function GasDisplay({ gasError }) {
const { openBuyCryptoInPdapp } = useRamps();
const currentProvider = useSelector(getProvider);
const isMainnet = useSelector(getIsMainnet);
const isTestnet = useSelector(getIsTestnet);
const isBuyableChain = useSelector(getIsBuyableChain);
const draftTransaction = useSelector(getCurrentDraftTransaction);
@ -95,11 +87,9 @@ export default function GasDisplay({ gasError }) {
userFeeLevel: editingTransaction?.userFeeLevel,
};
const {
hexMinimumTransactionFee,
hexMaximumTransactionFee,
hexTransactionTotal,
} = useSelector((state) => transactionFeeSelector(state, transactionData));
const { hexMaximumTransactionFee, hexTransactionTotal } = useSelector(
(state) => transactionFeeSelector(state, transactionData),
);
let title;
if (
@ -158,119 +148,13 @@ export default function GasDisplay({ gasError }) {
detailTotal = primaryTotalTextOverrideMaxAmount;
maxAmount = primaryTotalTextOverrideMaxAmount;
}
return (
<>
<Box className="gas-display">
<TransactionDetail
userAcknowledgedGasMissing={false}
rows={[
<TransactionDetailItem
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,
)}
/>
}
/>,
<ConfirmGasDisplay key="gas-display" />,
(gasError || isInsufficientTokenError) && (
<TransactionDetailItem
key="total-item"

View File

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

View File

@ -197,7 +197,7 @@ exports[`SendContent Component render should match snapshot 1`] = `
Gas
</div>
<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
@ -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"
>
<div
class="box gas-display__currency-container box--flex-direction-row"
class="gas-details-item__currency-container"
>
<div
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"
>
<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
class="box box--margin-right-1 box--flex-direction-row"
@ -284,7 +284,7 @@ exports[`SendContent Component render should match snapshot 1`] = `
</strong>
</div>
<div
class="box gas-display__currency-container box--flex-direction-row"
class="gas-details-item__currency-container"
>
<div
class="currency-display-component"

View File

@ -79,6 +79,9 @@ const state = {
confirmTransaction: {
txData: {},
},
send: {
draftTransactions: {},
},
};
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 type = currentKeyring && currentKeyring.type;
///: BEGIN:ONLY_INCLUDE_IN(mmi)
if (type.startsWith('Custody')) {
return 'custody';
}
///: END:ONLY_INCLUDE_IN
switch (type) {
case KeyringType.trezor:
case KeyringType.ledger: