Skip to content

Commit

Permalink
feat: create useSignatureEvent fragment hook and fix expected updateE…
Browse files Browse the repository at this point in the history
…ventFragment type (#27043)
  • Loading branch information
digiwand authored Sep 19, 2024
1 parent 88664bb commit 0c3e391
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 45 deletions.
22 changes: 13 additions & 9 deletions app/scripts/controllers/metametrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ export default class MetaMetricsController {
* Updates an event fragment in state
*
* @param {string} id - The fragment id to update
* @param {MetaMetricsEventFragment} payload - Fragment settings and
* @param {Partial<MetaMetricsEventFragment>} payload - Fragment settings and
* properties to initiate the fragment with.
*/
updateEventFragment(id, payload) {
Expand All @@ -356,19 +356,23 @@ export default class MetaMetricsController {
});
}

/**
* @typedef {object} MetaMetricsFinalizeEventFragmentOptions
* @property {boolean} [abandoned = false] - if true track the failure
* event instead of the success event
* @property {MetaMetricsContext.page} [page] - page the final event
* occurred on. This will override whatever is set on the fragment
* @property {MetaMetricsContext.referrer} [referrer] - Dapp that
* originated the fragment. This is for fallback only, the fragment referrer
* property will take precedence.
*/

/**
* Finalizes a fragment, tracking either a success event or failure Event
* and then removes the fragment from state.
*
* @param {string} id - UUID of the event fragment to be closed
* @param {object} options
* @param {boolean} [options.abandoned] - if true track the failure
* event instead of the success event
* @param {MetaMetricsContext.page} [options.page] - page the final event
* occurred on. This will override whatever is set on the fragment
* @param {MetaMetricsContext.referrer} [options.referrer] - Dapp that
* originated the fragment. This is for fallback only, the fragment referrer
* property will take precedence.
* @param {MetaMetricsFinalizeEventFragmentOptions} options
*/
finalizeEventFragment(id, { abandoned = false, page, referrer } = {}) {
const fragment = this.store.getState().fragments[id];
Expand Down
48 changes: 32 additions & 16 deletions app/scripts/lib/createRPCMethodTrackingMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ let globalRateLimitCount = 0;
*
* @param {MetaMetricsController} metaMetricsController
* @param {OriginalRequest} req
* @param {object} properties
* @param {Partial<MetaMetricsEventFragment>} fragmentPayload
*/
function createSignatureFragment(metaMetricsController, req, properties) {
function createSignatureFragment(metaMetricsController, req, fragmentPayload) {
metaMetricsController.createEventFragment({
category: MetaMetricsEventCategory.InpageProvider,
initialEvent: MetaMetricsEventName.SignatureRequested,
Expand All @@ -144,7 +144,7 @@ function createSignatureFragment(metaMetricsController, req, properties) {
referrer: {
url: req.origin,
},
properties,
...fragmentPayload,
});
}

Expand All @@ -153,23 +153,28 @@ function createSignatureFragment(metaMetricsController, req, properties) {
*
* @param {MetaMetricsController} metaMetricsController
* @param {OriginalRequest} req
* @param {object} options
* @param {boolean} options.abandoned
* @param {object} options.properties
* @param {MetaMetricsFinalizeEventFragmentOptions} finalizeEventOptions
* @param {Partial<MetaMetricsEventFragment>} fragmentPayload
*/
function finalizeSignatureFragment(
metaMetricsController,
req,
{ abandoned, properties },
finalizeEventOptions,
fragmentPayload,
) {
const signatureUniqueId = generateSignatureUniqueId(req.id);

metaMetricsController.updateEventFragment(signatureUniqueId, {
properties,
});
metaMetricsController.finalizeEventFragment(signatureUniqueId, {
abandoned,
});
if (fragmentPayload) {
metaMetricsController.updateEventFragment(
signatureUniqueId,
fragmentPayload,
);
}

metaMetricsController.finalizeEventFragment(
signatureUniqueId,
finalizeEventOptions,
);
}

/**
Expand Down Expand Up @@ -368,7 +373,9 @@ export default function createRPCMethodTrackingMiddleware({
}

if (event === MetaMetricsEventName.SignatureRequested) {
createSignatureFragment(metaMetricsController, req, eventProperties);
createSignatureFragment(metaMetricsController, req, {
properties: eventProperties,
});
} else {
metaMetricsController.trackEvent({
event,
Expand Down Expand Up @@ -430,10 +437,19 @@ export default function createRPCMethodTrackingMiddleware({
};

if (signatureUniqueId) {
finalizeSignatureFragment(metaMetricsController, req, {
const finalizeOptions = {
abandoned: event === eventType.REJECTED,
};
const fragmentPayload = {
properties,
});
};

finalizeSignatureFragment(
metaMetricsController,
req,
finalizeOptions,
fragmentPayload,
);
} else {
metaMetricsController.trackEvent({
event,
Expand Down
4 changes: 2 additions & 2 deletions shared/constants/metametrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export type MetaMetricsEventFragment = {
/**
* The event name to fire when the fragment is closed in an affirmative action.
*/
successEvent?: string;
successEvent: string;
/**
* The event name to fire when the fragment is closed with a rejection.
*/
Expand All @@ -169,7 +169,7 @@ export type MetaMetricsEventFragment = {
/**
* The event category to use for both the success and failure events.
*/
category?: string;
category: string;
/**
* Should this fragment be persisted in state and progressed after the
* extension is locked and unlocked.
Expand Down
22 changes: 15 additions & 7 deletions test/data/confirmations/typed_sign.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TransactionType } from '@metamask/transaction-controller';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { SignatureRequestType } from '../../../ui/pages/confirmations/types/confirm';

Expand All @@ -10,14 +11,15 @@ export const unapprovedTypedSignMsgV1 = {
},
status: 'unapproved',
time: 1710505271872,
type: 'eth_signTypedData',
type: TransactionType.signTypedData,
securityProviderResponse: null,
msgParams: {
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
data: [
{ type: 'string', name: 'Message', value: 'Hi, Alice!' },
{ type: 'uint32', name: 'A number', value: '1337' },
],
requestId: 11,
signatureMethod: 'eth_signTypedData',
version: 'V1',
origin: 'https://metamask.github.io',
Expand Down Expand Up @@ -65,12 +67,13 @@ export const unapprovedTypedSignMsgV3 = {
},
status: 'unapproved',
time: 1710249542175,
type: 'eth_signTypedData',
type: TransactionType.signTypedData,
securityProviderResponse: null,
msgParams: {
data: JSON.stringify(rawMessageV3),
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V3',
requestId: 12,
signatureMethod: 'eth_signTypedData_v3',
origin: 'https://metamask.github.io',
},
Expand Down Expand Up @@ -129,12 +132,13 @@ export const unapprovedTypedSignMsgV4 = {
status: 'unapproved',
time: new Date().getTime(),
chainid: '0x5',
type: 'eth_signTypedData',
type: TransactionType.signTypedData,
securityProviderResponse: null,
msgParams: {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
data: JSON.stringify(rawMessageV4),
origin: 'https://metamask.github.io',
requestId: 123456789,
signatureMethod: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4,
},
} as SignatureRequestType;
Expand All @@ -148,11 +152,12 @@ export const orderSignatureMsg = {
},
status: 'unapproved',
time: 1722011224974,
type: 'eth_signTypedData',
type: TransactionType.signTypedData,
msgParams: {
data: '{"types":{"Order":[{"type":"uint8","name":"direction"},{"type":"address","name":"maker"},{"type":"address","name":"taker"},{"type":"uint256","name":"expiry"},{"type":"uint256","name":"nonce"},{"type":"address","name":"erc20Token"},{"type":"uint256","name":"erc20TokenAmount"},{"type":"Fee[]","name":"fees"},{"type":"address","name":"erc721Token"},{"type":"uint256","name":"erc721TokenId"},{"type":"Property[]","name":"erc721TokenProperties"}],"Fee":[{"type":"address","name":"recipient"},{"type":"uint256","name":"amount"},{"type":"bytes","name":"feeData"}],"Property":[{"type":"address","name":"propertyValidator"},{"type":"bytes","name":"propertyData"}],"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}]},"domain":{"name":"ZeroEx","version":"1.0.0","chainId":"0x1","verifyingContract":"0xdef1c0ded9bec7f1a1670819833240f027b25eff"},"primaryType":"Order","message":{"direction":"0","maker":"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc","taker":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","expiry":"2524604400","nonce":"100131415900000000000000000000000000000083840314483690155566137712510085002484","erc20Token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","erc20TokenAmount":"42000000000000","fees":[],"erc721Token":"0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e","erc721TokenId":"2516","erc721TokenProperties":[]}}',
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V4',
requestId: 13,
signatureMethod: 'eth_signTypedData_v4',
origin: 'https://metamask.github.io',
},
Expand All @@ -167,11 +172,12 @@ export const permitSignatureMsg = {
},
status: 'unapproved',
time: 1716826404122,
type: 'eth_signTypedData',
type: TransactionType.signTypedData,
msgParams: {
data: '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"MyToken","version":"1","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","chainId":1},"message":{"owner":"0x935e73edb9ff52e23bac7f7e043a1ecd06d05477","spender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","value":3000,"nonce":0,"deadline":50000000000}}',
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V4',
requestId: 14,
signatureMethod: 'eth_signTypedData_v4',
origin: 'https://metamask.github.io',
},
Expand All @@ -186,11 +192,12 @@ export const permitBatchSignatureMsg = {
},
status: 'unapproved',
time: 1716826404122,
type: 'eth_signTypedData',
type: TransactionType.signTypedData,
msgParams: {
data: '{"types":{"PermitBatch":[{"name":"details","type":"PermitDetails[]"},{"name":"spender","type":"address"},{"name":"sigDeadline","type":"uint256"}],"PermitDetails":[{"name":"token","type":"address"},{"name":"amount","type":"uint160"},{"name":"expiration","type":"uint48"},{"name":"nonce","type":"uint48"}],"EIP712Domain":[{"name":"name","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}]},"domain":{"name":"Permit2","chainId":"1","verifyingContract":"0x000000000022d473030f116ddee9f6b43ac78ba3"},"primaryType":"PermitBatch","message":{"details":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"1461501637330902918203684832716283019655932542975","expiration":"1722887542","nonce":"5"},{"token":"0xb0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"2461501637330902918203684832716283019655932542975","expiration":"1722887642","nonce":"6"}],"spender":"0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad","sigDeadline":"1720297342"}}',
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V4',
requestId: 15,
signatureMethod: 'eth_signTypedData_v4',
origin: 'https://metamask.github.io',
},
Expand All @@ -205,11 +212,12 @@ export const permitSingleSignatureMsg = {
},
status: 'unapproved',
time: 1716826404122,
type: 'eth_signTypedData',
type: TransactionType.signTypedData,
msgParams: {
data: '{"types":{"PermitSingle":[{"name":"details","type":"PermitDetails"},{"name":"spender","type":"address"},{"name":"sigDeadline","type":"uint256"}],"PermitDetails":[{"name":"token","type":"address"},{"name":"amount","type":"uint160"},{"name":"expiration","type":"uint48"},{"name":"nonce","type":"uint48"}],"EIP712Domain":[{"name":"name","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}]},"domain":{"name":"Permit2","chainId":"1","verifyingContract":"0x000000000022d473030f116ddee9f6b43ac78ba3"},"primaryType":"PermitSingle","message":{"details":{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"1461501637330902918203684832716283019655932542975","expiration":"1722887542","nonce":"5"},"spender":"0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad","sigDeadline":"1720297342"}}',
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V4',
requestId: 16,
signatureMethod: 'eth_signTypedData_v4',
origin: 'https://metamask.github.io',
},
Expand Down
12 changes: 3 additions & 9 deletions ui/pages/confirmations/hooks/useConfirmationAlertMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import { useCallback, useEffect, useState } from 'react';
import { validate as isUuid } from 'uuid';

import useAlerts from '../../../hooks/useAlerts';
import { updateEventFragment } from '../../../store/actions';
import { SignatureRequestType } from '../types/confirm';
import { isSignatureTransactionType } from '../utils';
import { Alert } from '../../../ducks/confirm-alerts/confirm-alerts';
import { useConfirmContext } from '../context/confirm';
import { generateSignatureUniqueId } from '../../../helpers/utils/metrics';
import { AlertsName } from './alerts/constants';
import { useSignatureEventFragment } from './useSignatureEventFragment';
import { useTransactionEventFragment } from './useTransactionEventFragment';

export type AlertMetricsProperties = {
Expand Down Expand Up @@ -48,6 +46,7 @@ export function useConfirmationAlertMetrics() {
const { currentConfirmation } = useConfirmContext();
const ownerId = currentConfirmation?.id ?? '';
const { alerts, isAlertConfirmed } = useAlerts(ownerId);
const { updateSignatureEventFragment } = useSignatureEventFragment();
const { updateTransactionEventFragment } = useTransactionEventFragment();

const [metricsProperties, setMetricsProperties] =
Expand Down Expand Up @@ -116,12 +115,7 @@ export function useConfirmationAlertMetrics() {
}

if (isSignatureTransactionType(currentConfirmation)) {
const requestId = (currentConfirmation as SignatureRequestType).msgParams
?.requestId as number;
const fragmentUniqueId = generateSignatureUniqueId(requestId);
updateEventFragment(fragmentUniqueId, {
properties,
});
updateSignatureEventFragment({ properties });
} else {
updateTransactionEventFragment({ properties }, ownerId);
}
Expand Down
67 changes: 67 additions & 0 deletions ui/pages/confirmations/hooks/useSignatureEventFragment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { getMockTypedSignConfirmStateForRequest } from '../../../../test/data/confirmations/helper';
import { renderHookWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers';
import { orderSignatureMsg } from '../../../../test/data/confirmations/typed_sign';
import { generateSignatureUniqueId } from '../../../helpers/utils/metrics';
import { updateEventFragment } from '../../../store/actions';
import { SignatureRequestType } from '../types/confirm';
import { useSignatureEventFragment } from './useSignatureEventFragment';

const renderUseSignatureEventFragment = (signature: SignatureRequestType) => {
const mockState = getMockTypedSignConfirmStateForRequest(signature);

return renderHookWithConfirmContextProvider(
() => useSignatureEventFragment(),
mockState,
);
};

jest.mock('../../../store/actions', () => ({
updateEventFragment: jest.fn(),
}));

describe('useSignatureEventFragment', () => {
afterEach(jest.clearAllMocks);

describe('updateSignatureEventFragment', () => {
it('should call updateEventFragment to update the signature event fragment', () => {
const mockUpdateProps = {
event_name1: 'test_event 1',
event_name2: 'test_event 2',
};
const expectedFragmentId = generateSignatureUniqueId(
orderSignatureMsg.msgParams.requestId,
);

const { result } = renderUseSignatureEventFragment(orderSignatureMsg);
const { updateSignatureEventFragment } = result.current;

updateSignatureEventFragment(mockUpdateProps);

expect(updateEventFragment).toHaveBeenCalledWith(
expectedFragmentId,
mockUpdateProps,
);
});

it('should not call updateEventFragment if no signature requestId was found', () => {
const mockSignatureWithoutRequestId = {
...orderSignatureMsg,
msgParams: {
data: orderSignatureMsg.msgParams.data,
from: orderSignatureMsg.msgParams.from,
version: orderSignatureMsg.msgParams.version,
signatureMethod: orderSignatureMsg.msgParams.signatureMethod,
origin: orderSignatureMsg.msgParams.origin,
},
};
const { result } = renderUseSignatureEventFragment(
mockSignatureWithoutRequestId,
);
const { updateSignatureEventFragment } = result.current;

updateSignatureEventFragment();

expect(updateEventFragment).not.toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 0c3e391

Please sign in to comment.