Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ui/pages/confirmations/components/confirm/info/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import TokenTransferInfo from './token-transfer/token-transfer';
import TypedSignV1Info from './typed-sign-v1/typed-sign-v1';
import TypedSignInfo from './typed-sign/typed-sign';
import { useTrustSignalMetrics } from '../../../hooks/useTrustSignalMetrics';

Check failure on line 16 in ui/pages/confirmations/components/confirm/info/info.tsx

View workflow job for this annotation

GitHub Actions / test-lint / Test lint

`../../../hooks/useTrustSignalMetrics` import should occur before import of `./approve/approve`

const Info = () => {
const { currentConfirmation } = useConfirmContext();
Expand All @@ -21,6 +22,8 @@
useSmartTransactionFeatureFlags();
useTransactionFocusEffect();

useTrustSignalMetrics();

const ConfirmationInfoComponentMap = useMemo(
() => ({
[TransactionType.batch]: () => BaseTransactionInfo,
Expand Down
165 changes: 165 additions & 0 deletions ui/pages/confirmations/hooks/useTrustSignalMetrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { TransactionMeta } from '@metamask/transaction-controller';
import { act } from '@testing-library/react-hooks';

Check failure on line 2 in ui/pages/confirmations/hooks/useTrustSignalMetrics.test.ts

View workflow job for this annotation

GitHub Actions / test-lint / Test lint

'act' is defined but never used

import { useTrustSignalMetrics } from './useTrustSignalMetrics';

Check failure on line 4 in ui/pages/confirmations/hooks/useTrustSignalMetrics.test.ts

View workflow job for this annotation

GitHub Actions / test-lint / Test lint

`./useTrustSignalMetrics` import should occur after import of `../../../../app/scripts/lib/trust-signals/types`
import { renderHookWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers';
import {
getMockConfirmStateForTransaction,
getMockTypedSignConfirmStateForRequest,
} from '../../../../test/data/confirmations/helper';
import { genUnapprovedContractInteractionConfirmation } from '../../../../test/data/confirmations/contract-interaction';
import { unapprovedTypedSignMsgV4 } from '../../../../test/data/confirmations/typed_sign';
import { ResultType } from '../../../../app/scripts/lib/trust-signals/types';

Check failure on line 12 in ui/pages/confirmations/hooks/useTrustSignalMetrics.test.ts

View workflow job for this annotation

GitHub Actions / test-lint / Test lint

Unexpected path "../../../../app/scripts/lib/trust-signals/types" imported in restricted zone. Should not import from background in UI, use shared directory instead
import * as useTransactionEventFragmentHook from './useTransactionEventFragment';
import * as useSignatureEventFragmentHook from './useSignatureEventFragment';

jest.mock('./useTransactionEventFragment');
jest.mock('./useSignatureEventFragment');

const mockUpdateTransactionEventFragment = jest.fn();
const mockUpdateSignatureEventFragment = jest.fn();

const OWNER_ID_MOCK = '123';
const TARGET_ADDRESS_MOCK = '0x88aa6343307ec9a652ccddda3646e62b2f1a5125'; // This is the default "to" address from genUnapprovedContractInteractionConfirmation
const SIGNATURE_VERIFYING_CONTRACT_MOCK =
'0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'; // verifyingContract from unapprovedTypedSignMsgV3
const SECURITY_ALERT_RESPONSE_MOCK = {
result_type: ResultType.Malicious,

Check failure on line 27 in ui/pages/confirmations/hooks/useTrustSignalMetrics.test.ts

View workflow job for this annotation

GitHub Actions / test-lint / Test lint

Object Literal Property name `result_type` must match one of the following formats: camelCase, PascalCase, UPPER_CASE
label: 'Malicious',
reason: 'This address is associated with fraudulent activities',
};

const contractInteraction = genUnapprovedContractInteractionConfirmation({
chainId: '0x5',
});
const TX_STATE_MOCK_NO_ALERT = getMockConfirmStateForTransaction(
{ ...contractInteraction, id: OWNER_ID_MOCK } as TransactionMeta,
{
metamask: {},
},
);
const TX_STATE_MOCK = getMockConfirmStateForTransaction(
{ ...contractInteraction, id: OWNER_ID_MOCK } as TransactionMeta,
{
metamask: {
addressSecurityAlertResponses: {
[TARGET_ADDRESS_MOCK.toLowerCase()]: SECURITY_ALERT_RESPONSE_MOCK,
},
},
},
);

const signatureRequest = { ...unapprovedTypedSignMsgV4, id: OWNER_ID_MOCK };
const SIGNATURE_STATE_MOCK = getMockTypedSignConfirmStateForRequest(
signatureRequest,
{
metamask: {
addressSecurityAlertResponses: {
[SIGNATURE_VERIFYING_CONTRACT_MOCK.toLowerCase()]:
SECURITY_ALERT_RESPONSE_MOCK,
},
},
},
);
const SIGNATURE_STATE_MOCK_NO_ALERT = getMockTypedSignConfirmStateForRequest(
signatureRequest,
{
metamask: {},
},
);

beforeEach(() => {
jest.clearAllMocks();
(
useTransactionEventFragmentHook.useTransactionEventFragment as jest.Mock
).mockReturnValue({
updateTransactionEventFragment: mockUpdateTransactionEventFragment,
});
(
useSignatureEventFragmentHook.useSignatureEventFragment as jest.Mock
).mockReturnValue({
updateSignatureEventFragment: mockUpdateSignatureEventFragment,
});
});

describe('useTrustSignalMetrics', () => {
describe('transactions', () => {
it('does not update event fragments when no security alert response', () => {
renderHookWithConfirmContextProvider(
() => useTrustSignalMetrics(),
TX_STATE_MOCK_NO_ALERT,
);

expect(mockUpdateTransactionEventFragment).not.toHaveBeenCalled();
expect(mockUpdateSignatureEventFragment).not.toHaveBeenCalled();
});

it('updates event fragments with trust signal metrics when security alert response exists', () => {
renderHookWithConfirmContextProvider(
() => useTrustSignalMetrics(),
TX_STATE_MOCK,
);

const expectedProperties = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_alert_response: ResultType.Malicious,
};

const expectedAnonymousProperties = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_label: 'Malicious',
};

expect(mockUpdateTransactionEventFragment).toHaveBeenCalledWith(
{ properties: expectedProperties },
OWNER_ID_MOCK,
);
expect(mockUpdateTransactionEventFragment).toHaveBeenCalledWith(
{ sensitiveProperties: expectedAnonymousProperties },
OWNER_ID_MOCK,
);
expect(mockUpdateSignatureEventFragment).not.toHaveBeenCalled();
});
});

describe('signatures', () => {
it('does not update event fragments when no security alert response', () => {
renderHookWithConfirmContextProvider(
() => useTrustSignalMetrics(),
SIGNATURE_STATE_MOCK_NO_ALERT,
);

expect(mockUpdateSignatureEventFragment).not.toHaveBeenCalled();
expect(mockUpdateTransactionEventFragment).not.toHaveBeenCalled();
});

it('updates event fragments with trust signal metrics when security alert response exists', () => {
const expectedProperties = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_alert_response: ResultType.Malicious,
};

const expectedAnonymousProperties = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_label: 'Malicious',
};

renderHookWithConfirmContextProvider(
() => useTrustSignalMetrics(),
SIGNATURE_STATE_MOCK,
);

expect(mockUpdateSignatureEventFragment).toHaveBeenCalledWith({
properties: expectedProperties,
});
expect(mockUpdateSignatureEventFragment).toHaveBeenCalledWith({
sensitiveProperties: expectedAnonymousProperties,
});
expect(mockUpdateTransactionEventFragment).not.toHaveBeenCalled();
});
});
});
116 changes: 116 additions & 0 deletions ui/pages/confirmations/hooks/useTrustSignalMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { TransactionMeta } from '@metamask/transaction-controller';

import { getAddressSecurityAlertResponse } from '../../../selectors';
import { useConfirmContext } from '../context/confirm';
import { isSignatureTransactionType } from '../utils';
import type { Confirmation, SignatureRequestType } from '../types/confirm';
import { ResultType } from '../../../../app/scripts/lib/trust-signals/types';

Check failure on line 9 in ui/pages/confirmations/hooks/useTrustSignalMetrics.ts

View workflow job for this annotation

GitHub Actions / test-lint / Test lint

Unexpected path "../../../../app/scripts/lib/trust-signals/types" imported in restricted zone. Should not import from background in UI, use shared directory instead
import { useTransactionEventFragment } from './useTransactionEventFragment';
import { useSignatureEventFragment } from './useSignatureEventFragment';

export type TrustSignalMetricsProperties = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_alert_response?: ResultType;
};

export type TrustSignalMetricsAnonProperties = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_label?: string;
};

// For transactions, this is the 'to' address. For signatures, this is the verifying contract.
function getTargetAddress(confirmation: Confirmation): string | null {
if (!isSignatureTransactionType(confirmation)) {
const txMeta = confirmation as TransactionMeta;
return txMeta.txParams.to ?? null;
}
try {
const data = (confirmation as SignatureRequestType)?.msgParams?.data;
const parsedData = typeof data === 'string' ? JSON.parse(data) : data;
if (parsedData?.domain?.verifyingContract) {
return parsedData.domain.verifyingContract;
}
} catch (e) {
// do nothing
}
return null;
}

export function useTrustSignalMetrics() {
const { currentConfirmation } = useConfirmContext();
const { updateSignatureEventFragment } = useSignatureEventFragment();
const { updateTransactionEventFragment } = useTransactionEventFragment();

const addressToCheck = useMemo(
() => getTargetAddress(currentConfirmation),
[currentConfirmation],
);

const addressSecurityAlertResponse = useSelector((state) =>
addressToCheck
? getAddressSecurityAlertResponse(state, addressToCheck)
: undefined,
);

const { properties, anonymousProperties } = useMemo((): {
properties: TrustSignalMetricsProperties;
anonymousProperties: TrustSignalMetricsAnonProperties;
} => {
if (!addressSecurityAlertResponse) {
return { properties: {}, anonymousProperties: {} };
}

return {
properties: {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_alert_response: addressSecurityAlertResponse.result_type,
},
anonymousProperties: {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
address_label: addressSecurityAlertResponse.label || undefined,
},
};
}, [addressToCheck, addressSecurityAlertResponse]);

const updateTrustSignalMetrics = useCallback(() => {
if (!addressSecurityAlertResponse || !currentConfirmation) {
return;
}

const ownerId = currentConfirmation?.id ?? '';

if (isSignatureTransactionType(currentConfirmation)) {
updateSignatureEventFragment({ properties });
if (anonymousProperties.address_label) {
updateSignatureEventFragment({
sensitiveProperties: anonymousProperties,
});
}
} else {
updateTransactionEventFragment({ properties }, ownerId);
if (anonymousProperties.address_label) {
updateTransactionEventFragment(
{ sensitiveProperties: anonymousProperties },
ownerId,
);
}
}
}, [
addressSecurityAlertResponse,
currentConfirmation,
properties,
anonymousProperties,
updateSignatureEventFragment,
updateTransactionEventFragment,
]);

useEffect(() => {
updateTrustSignalMetrics();
}, [updateTrustSignalMetrics]);
}
Loading