From 7b064462662c09b6029fab79e6c01048889cd6f6 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Tue, 5 Nov 2024 15:20:48 +0100 Subject: [PATCH 01/17] Add firstTimeInteraction to transactionMeta --- .../src/TransactionController.test.ts | 14 +++ .../src/TransactionController.ts | 80 ++++++++++++++ packages/transaction-controller/src/errors.ts | 10 ++ packages/transaction-controller/src/types.ts | 5 + .../utils/first-time-interaction-api.test.ts | 102 ++++++++++++++++++ .../src/utils/first-time-interaction-api.ts | 75 +++++++++++++ .../src/utils/validation.ts | 27 +++++ 7 files changed, 313 insertions(+) create mode 100644 packages/transaction-controller/src/utils/first-time-interaction-api.test.ts create mode 100644 packages/transaction-controller/src/utils/first-time-interaction-api.ts diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 05bfd86bf4..71ada2b59a 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -80,6 +80,7 @@ import { TransactionType, WalletDevice, } from './types'; +import { getFirstTimeInteraction } from './utils/first-time-interaction-api'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; import { getGasFeeFlow } from './utils/gas-flow'; @@ -110,6 +111,7 @@ jest.mock('./helpers/GasFeePoller'); jest.mock('./helpers/IncomingTransactionHelper'); jest.mock('./helpers/MultichainTrackingHelper'); jest.mock('./helpers/PendingTransactionTracker'); +jest.mock('./utils/first-time-interaction-api'); jest.mock('./utils/gas'); jest.mock('./utils/gas-fees'); jest.mock('./utils/gas-flow'); @@ -487,6 +489,7 @@ describe('TransactionController', () => { getTransactionLayer1GasFee, ); const getGasFeeFlowMock = jest.mocked(getGasFeeFlow); + const getFirstTimeInteractionMock = jest.mocked(getFirstTimeInteraction); const shouldResimulateMock = jest.mocked(shouldResimulate); let mockEthQuery: EthQuery; @@ -870,6 +873,10 @@ describe('TransactionController', () => { updateSwapsTransactionMock.mockImplementation( (transactionMeta) => transactionMeta, ); + + getFirstTimeInteractionMock.mockResolvedValue({ + isFirstTimeInteraction: undefined, + }); }); describe('constructor', () => { @@ -1378,6 +1385,10 @@ describe('TransactionController', () => { it('adds unapproved transaction to state', async () => { const { controller } = setupController(); + getFirstTimeInteractionMock.mockResolvedValueOnce({ + isFirstTimeInteraction: true, + }); + const mockDeviceConfirmedOn = WalletDevice.OTHER; const mockOrigin = 'origin'; const mockSecurityAlertResponse = { @@ -1412,6 +1423,8 @@ describe('TransactionController', () => { }, ); + await flushPromises(); + const transactionMeta = controller.state.transactions[0]; expect(updateSwapsTransactionMock).toHaveBeenCalledTimes(1); @@ -1426,6 +1439,7 @@ describe('TransactionController', () => { expect(controller.state.transactions[0].sendFlowHistory).toStrictEqual( mockSendFlowHistory, ); + expect(controller.state.transactions[0].firstTimeInteraction).toBe(true); }); describe('networkClientId exists in the MultichainTrackingHelper', () => { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 64fb2d3064..cbff37cae8 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -90,6 +90,11 @@ import { SimulationErrorCode, } from './types'; import { validateConfirmedExternalTransaction } from './utils/external-transactions'; +import type { FirstTimeInteractionResponse } from './utils/first-time-interaction-api'; +import { + type FirstTimeInteractionRequest, + getFirstTimeInteraction, +} from './utils/first-time-interaction-api'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; import { getGasFeeFlow } from './utils/gas-flow'; @@ -123,6 +128,7 @@ import { normalizeGasFeeValues, } from './utils/utils'; import { + validateFirstTimeInteraction, validateTransactionOrigin, validateTxParams, } from './utils/validation'; @@ -1149,6 +1155,13 @@ export class TransactionController extends BaseController< log('Error while updating simulation data', error); throw error; }); + + this.#updateFirstInteraction(addedTransactionMeta, { + traceContext, + }).catch((error) => { + log('Error while updating first interaction', error); + throw error; + }); } else { log('Skipping simulation as approval not required'); } @@ -3614,6 +3627,73 @@ export class TransactionController extends BaseController< return transactionMeta; } + async #updateFirstInteraction( + transactionMeta: TransactionMeta, + { + traceContext, + }: { + traceContext?: TraceContext; + } = {}, + ) { + const { + chainId, + id: transactionId, + txParams: { to, from }, + } = transactionMeta; + + const request: FirstTimeInteractionRequest = { + chainId, + to, + from, + }; + + let firstTimeInteractionResponse: FirstTimeInteractionResponse; + + try { + validateFirstTimeInteraction(request); + + firstTimeInteractionResponse = await this.#trace( + { name: 'FirstTimeInteraction', parentContext: traceContext }, + () => getFirstTimeInteraction(request), + ); + } catch (error) { + log('Error during first interaction check', error); + firstTimeInteractionResponse = { + isFirstTimeInteraction: undefined, + }; + } + + const finalTransactionMeta = this.getTransaction(transactionId); + + /* istanbul ignore if */ + if (!finalTransactionMeta) { + log( + 'Cannot update first time interaction as transaction not found', + transactionId, + firstTimeInteractionResponse, + ); + + return; + } + + this.#updateTransactionInternal( + { + transactionId, + note: 'TransactionController#updateFirstInteraction - Update first time interaction', + }, + (txMeta) => { + txMeta.firstTimeInteraction = + firstTimeInteractionResponse.isFirstTimeInteraction; + }, + ); + + log( + 'Updated first time interaction', + transactionId, + firstTimeInteractionResponse, + ); + } + async #updateSimulationData( transactionMeta: TransactionMeta, { diff --git a/packages/transaction-controller/src/errors.ts b/packages/transaction-controller/src/errors.ts index 6695ffb0ec..53f79810aa 100644 --- a/packages/transaction-controller/src/errors.ts +++ b/packages/transaction-controller/src/errors.ts @@ -12,6 +12,16 @@ export class SimulationError extends Error { } } +export class FirstTimeInteractionError extends Error { + code?: string | number; + + constructor(message?: string, code?: string | number) { + super(message ?? 'Error checking first time interaction'); + + this.code = code; + } +} + export class SimulationChainNotSupportedError extends SimulationError { constructor(chainId: Hex) { super( diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index f70679a6bc..d4a2585380 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -167,6 +167,11 @@ type TransactionMetaBase = { */ firstRetryBlockNumber?: string; + /** + * Whether the transaction is the first time interaction. + */ + firstTimeInteraction?: boolean; + /** Alternate EIP-1559 gas fee estimates for multiple priority levels. */ gasFeeEstimates?: GasFeeEstimates; diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts new file mode 100644 index 0000000000..d2094771fc --- /dev/null +++ b/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts @@ -0,0 +1,102 @@ +import { FirstTimeInteractionError } from '../errors'; +import type { + FirstTimeInteractionRequest, + FirstTimeInteractionResponse, +} from './first-time-interaction-api'; +import { getFirstTimeInteraction } from './first-time-interaction-api'; + +describe('FirstTimeInteraction API Utils', () => { + let fetchMock: jest.MockedFunction; + + /** + * Mock a JSON response from fetch. + * @param jsonResponse - The response body to return. + */ + function mockFetchResponse(jsonResponse: unknown) { + fetchMock.mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue(jsonResponse), + } as unknown as Response); + } + + beforeEach(() => { + fetchMock = jest.spyOn(global, 'fetch') as jest.MockedFunction< + typeof fetch + >; + fetchMock.mockClear(); + }); + + describe('getFirstTimeInteraction', () => { + const requestMock: FirstTimeInteractionRequest = { + chainId: '0x1', + from: '0xFromAddress', + to: '0xToAddress', + }; + + it('returns isFirstTimeInteraction as true when count is 0', async () => { + mockFetchResponse({ count: 0 }); + + const response: FirstTimeInteractionResponse = + await getFirstTimeInteraction(requestMock); + + expect(response.isFirstTimeInteraction).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('returns isFirstTimeInteraction as true when count is undefined', async () => { + mockFetchResponse({}); + + const response: FirstTimeInteractionResponse = + await getFirstTimeInteraction(requestMock); + + expect(response.isFirstTimeInteraction).toBe(true); + }); + + it('returns isFirstTimeInteraction as false when count is greater than 0', async () => { + mockFetchResponse({ count: 5 }); + + const response: FirstTimeInteractionResponse = + await getFirstTimeInteraction(requestMock); + + expect(response.isFirstTimeInteraction).toBe(false); + }); + + it('throws FirstTimeInteractionError when API returns an error other than FAILED_TO_PARSE_MESSAGE', async () => { + const errorResponse = { + error: { message: 'Some other error', code: 500 }, + }; + mockFetchResponse(errorResponse); + + await expect(getFirstTimeInteraction(requestMock)).rejects.toThrow( + FirstTimeInteractionError, + ); + }); + + it('returns isFirstTimeInteraction as true when API returns FAILED_TO_PARSE_MESSAGE', async () => { + const errorResponse = { + error: { + message: 'Failed to parse account address relationship.', + code: 400, + }, + }; + mockFetchResponse(errorResponse); + + const response: FirstTimeInteractionResponse = + await getFirstTimeInteraction(requestMock); + + expect(response.isFirstTimeInteraction).toBe(true); + }); + + it('sends request to correct URL', async () => { + mockFetchResponse({ count: 1 }); + + const { chainId, from, to } = requestMock; + + await getFirstTimeInteraction(requestMock); + + // The values are not undefined + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const expectedUrl = `https://accounts.api.cx.metamask.io//v1/networks/${chainId}/accounts/${from}/relationships/${to}`; + expect(fetchMock).toHaveBeenCalledWith(expectedUrl, { method: 'GET' }); + }); + }); +}); diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.ts new file mode 100644 index 0000000000..71f901b84c --- /dev/null +++ b/packages/transaction-controller/src/utils/first-time-interaction-api.ts @@ -0,0 +1,75 @@ +import { createModuleLogger, type Hex } from '@metamask/utils'; + +import { FirstTimeInteractionError } from '../errors'; +import { projectLogger } from '../logger'; + +const BASE_URL = 'https://accounts.api.cx.metamask.io/'; +const FAILED_TO_PARSE_MESSAGE = 'Failed to parse account address relationship.'; + +const log = createModuleLogger(projectLogger, 'first-time-interaction-api'); + +export type FirstTimeInteractionRequest = { + /** Chain ID of the transaction. */ + chainId: Hex; + + /** Recipient of the transaction. */ + to?: string; + + /** Sender of the transaction. */ + from: string; +}; + +export type FirstTimeInteractionResponse = { + isFirstTimeInteraction?: boolean; +}; + +/** + * Get the first time interaction count for an account. + * @param request - The request to get the first time interaction count for. + * @returns The first time interaction count for the account. + */ +export async function getFirstTimeInteraction( + request: FirstTimeInteractionRequest, +): Promise { + const url = await getFirstTimeInteractionUrl(request); + + log('Sending request', url, request); + + const response = await fetch(url, { + method: 'GET', + }); + + const responseJson = await response.json(); + + log('Received response', responseJson); + + if (responseJson.error) { + const { message, code } = responseJson.error; + + if (message === FAILED_TO_PARSE_MESSAGE) { + return { isFirstTimeInteraction: true }; + } + + throw new FirstTimeInteractionError(message, code); + } + + return { + isFirstTimeInteraction: + responseJson?.count === 0 || responseJson?.count === undefined, + }; +} + +/** + * Get the URL for the first time interaction API. + * @param request - The request to get the URL for. + * @returns The URL for the first time interaction API. + */ +async function getFirstTimeInteractionUrl( + request: FirstTimeInteractionRequest, +): Promise { + const { chainId, from, to } = request; + + // The values are not undefined because they are validated in the controller + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${BASE_URL}/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; +} diff --git a/packages/transaction-controller/src/utils/validation.ts b/packages/transaction-controller/src/utils/validation.ts index 689243b3eb..a2ddccf3b3 100644 --- a/packages/transaction-controller/src/utils/validation.ts +++ b/packages/transaction-controller/src/utils/validation.ts @@ -5,6 +5,7 @@ import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { isStrictHexString } from '@metamask/utils'; import { TransactionEnvelopeType, type TransactionParams } from '../types'; +import type { FirstTimeInteractionRequest } from './first-time-interaction-api'; import { isEIP1559Transaction } from './utils'; type GasFieldsToValidate = 'gasPrice' | 'maxFeePerGas' | 'maxPriorityFeePerGas'; @@ -66,6 +67,20 @@ export function validateTxParams( validateGasFeeParams(txParams); } +/** + * Validates the request for the first time interaction API. + * + * @param request - The request to validate. + */ +export function validateFirstTimeInteraction( + request: FirstTimeInteractionRequest, +) { + const { chainId, from, to } = request; + validateParamTo(to); + validateParamFrom(from); + validateParamChainId(chainId); +} + /** * Validates the `type` property, ensuring that if it is specified, it is a valid transaction envelope type. * @@ -184,6 +199,18 @@ function validateParamFrom(from: string) { } } +/** + * Validates the recipient address in a transaction's parameters. + * + * @param to - The to property to validate. + * @throws Throws an error if the recipient address is invalid. + */ +function validateParamTo(to?: string) { + if (!to || typeof to !== 'string') { + throw rpcErrors.invalidParams(`Invalid "to" address`); + } +} + /** * Validates input data for transactions. * From 35c9c85f515daeb4fdebb3707dad3db69d398c8e Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Tue, 5 Nov 2024 15:27:35 +0100 Subject: [PATCH 02/17] Fix url --- .../src/utils/first-time-interaction-api.test.ts | 2 +- .../src/utils/first-time-interaction-api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts index d2094771fc..0913d1aea1 100644 --- a/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts +++ b/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts @@ -95,7 +95,7 @@ describe('FirstTimeInteraction API Utils', () => { // The values are not undefined // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const expectedUrl = `https://accounts.api.cx.metamask.io//v1/networks/${chainId}/accounts/${from}/relationships/${to}`; + const expectedUrl = `https://primitives.api.cx.metamask.io/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; expect(fetchMock).toHaveBeenCalledWith(expectedUrl, { method: 'GET' }); }); }); diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.ts index 71f901b84c..d1d229f14b 100644 --- a/packages/transaction-controller/src/utils/first-time-interaction-api.ts +++ b/packages/transaction-controller/src/utils/first-time-interaction-api.ts @@ -3,7 +3,7 @@ import { createModuleLogger, type Hex } from '@metamask/utils'; import { FirstTimeInteractionError } from '../errors'; import { projectLogger } from '../logger'; -const BASE_URL = 'https://accounts.api.cx.metamask.io/'; +const BASE_URL = 'https://primitives.api.cx.metamask.io'; const FAILED_TO_PARSE_MESSAGE = 'Failed to parse account address relationship.'; const log = createModuleLogger(projectLogger, 'first-time-interaction-api'); From 70f789615269e238f60e883f990330a46bb615c1 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Tue, 5 Nov 2024 15:33:09 +0100 Subject: [PATCH 03/17] Update coverage thresholds --- packages/transaction-controller/jest.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index d84ee83366..1011dfca5a 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.74, - functions: 97.51, - lines: 98.34, - statements: 98.35, + branches: 93.46, + functions: 97.31, + lines: 98.25, + statements: 98.25, }, }, From 38ff6ddaf64246e3407a09dae7ce41833861cf1b Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Wed, 13 Nov 2024 10:08:16 +0100 Subject: [PATCH 04/17] Change url --- .../src/utils/first-time-interaction-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.ts index d1d229f14b..183e05bc06 100644 --- a/packages/transaction-controller/src/utils/first-time-interaction-api.ts +++ b/packages/transaction-controller/src/utils/first-time-interaction-api.ts @@ -3,7 +3,7 @@ import { createModuleLogger, type Hex } from '@metamask/utils'; import { FirstTimeInteractionError } from '../errors'; import { projectLogger } from '../logger'; -const BASE_URL = 'https://primitives.api.cx.metamask.io'; +const BASE_URL = 'https://accounts.api.cx.metamask.io'; const FAILED_TO_PARSE_MESSAGE = 'Failed to parse account address relationship.'; const log = createModuleLogger(projectLogger, 'first-time-interaction-api'); From 3f52b8148c2d2dd1a5668a297139ee68f152c59a Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Wed, 13 Nov 2024 10:17:10 +0100 Subject: [PATCH 05/17] Fix test --- .../src/utils/first-time-interaction-api.test.ts | 7 +++++-- .../src/utils/first-time-interaction-api.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts index 0913d1aea1..0de1104e0c 100644 --- a/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts +++ b/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts @@ -3,7 +3,10 @@ import type { FirstTimeInteractionRequest, FirstTimeInteractionResponse, } from './first-time-interaction-api'; -import { getFirstTimeInteraction } from './first-time-interaction-api'; +import { + BASE_URL, + getFirstTimeInteraction, +} from './first-time-interaction-api'; describe('FirstTimeInteraction API Utils', () => { let fetchMock: jest.MockedFunction; @@ -95,7 +98,7 @@ describe('FirstTimeInteraction API Utils', () => { // The values are not undefined // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const expectedUrl = `https://primitives.api.cx.metamask.io/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; + const expectedUrl = `${BASE_URL}/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; expect(fetchMock).toHaveBeenCalledWith(expectedUrl, { method: 'GET' }); }); }); diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.ts index 183e05bc06..3450871311 100644 --- a/packages/transaction-controller/src/utils/first-time-interaction-api.ts +++ b/packages/transaction-controller/src/utils/first-time-interaction-api.ts @@ -3,7 +3,7 @@ import { createModuleLogger, type Hex } from '@metamask/utils'; import { FirstTimeInteractionError } from '../errors'; import { projectLogger } from '../logger'; -const BASE_URL = 'https://accounts.api.cx.metamask.io'; +export const BASE_URL = 'https://accounts.api.cx.metamask.io'; const FAILED_TO_PARSE_MESSAGE = 'Failed to parse account address relationship.'; const log = createModuleLogger(projectLogger, 'first-time-interaction-api'); From 104521169af5621cc2b38b71ad4e2dd19ed7d158 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 14 Nov 2024 13:03:34 +0100 Subject: [PATCH 06/17] Adjust suggestions --- .../src/TransactionController.test.ts | 28 ++-- .../src/TransactionController.ts | 58 ++++---- .../src/api/accounts-api.test.ts | 119 +++++++++++++++++ .../src/api/accounts-api.ts | 126 ++++++++++++++++++ packages/transaction-controller/src/types.ts | 5 + .../utils/first-time-interaction-api.test.ts | 105 --------------- .../src/utils/first-time-interaction-api.ts | 75 ----------- .../src/utils/validation.ts | 10 +- 8 files changed, 297 insertions(+), 229 deletions(-) create mode 100644 packages/transaction-controller/src/api/accounts-api.test.ts create mode 100644 packages/transaction-controller/src/api/accounts-api.ts delete mode 100644 packages/transaction-controller/src/utils/first-time-interaction-api.test.ts delete mode 100644 packages/transaction-controller/src/utils/first-time-interaction-api.ts diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 71ada2b59a..de91164e47 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -46,6 +46,7 @@ import { buildCustomNetworkClientConfiguration, buildMockGetNetworkClientById, } from '../../network-controller/tests/helpers'; +import { getAccountAddressRelationship } from './api/accounts-api'; import { CHAIN_IDS } from './constants'; import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow'; import { LineaGasFeeFlow } from './gas-flows/LineaGasFeeFlow'; @@ -80,7 +81,6 @@ import { TransactionType, WalletDevice, } from './types'; -import { getFirstTimeInteraction } from './utils/first-time-interaction-api'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; import { getGasFeeFlow } from './utils/gas-flow'; @@ -104,6 +104,7 @@ const MOCK_V1_UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'; const TRANSACTION_HASH_MOCK = '0x123456'; jest.mock('@metamask/eth-query'); +jest.mock('./api/accounts-api'); jest.mock('./gas-flows/DefaultGasFeeFlow'); jest.mock('./gas-flows/LineaGasFeeFlow'); jest.mock('./gas-flows/TestGasFeeFlow'); @@ -111,7 +112,6 @@ jest.mock('./helpers/GasFeePoller'); jest.mock('./helpers/IncomingTransactionHelper'); jest.mock('./helpers/MultichainTrackingHelper'); jest.mock('./helpers/PendingTransactionTracker'); -jest.mock('./utils/first-time-interaction-api'); jest.mock('./utils/gas'); jest.mock('./utils/gas-fees'); jest.mock('./utils/gas-flow'); @@ -489,8 +489,10 @@ describe('TransactionController', () => { getTransactionLayer1GasFee, ); const getGasFeeFlowMock = jest.mocked(getGasFeeFlow); - const getFirstTimeInteractionMock = jest.mocked(getFirstTimeInteraction); const shouldResimulateMock = jest.mocked(shouldResimulate); + const getAccountAddressRelationshipMock = jest.mocked( + getAccountAddressRelationship, + ); let mockEthQuery: EthQuery; let getNonceLockSpy: jest.Mock; @@ -874,8 +876,9 @@ describe('TransactionController', () => { (transactionMeta) => transactionMeta, ); - getFirstTimeInteractionMock.mockResolvedValue({ + getAccountAddressRelationshipMock.mockResolvedValue({ isFirstTimeInteraction: undefined, + isFirstTimeInteractionDisabled: false, }); }); @@ -1385,8 +1388,9 @@ describe('TransactionController', () => { it('adds unapproved transaction to state', async () => { const { controller } = setupController(); - getFirstTimeInteractionMock.mockResolvedValueOnce({ + getAccountAddressRelationshipMock.mockResolvedValueOnce({ isFirstTimeInteraction: true, + isFirstTimeInteractionDisabled: false, }); const mockDeviceConfirmedOn = WalletDevice.OTHER; @@ -2225,7 +2229,7 @@ describe('TransactionController', () => { const mockActionId = 'mockActionId'; - const { result, transactionMeta } = await controller.addTransaction( + const { result } = await controller.addTransaction( { from: ACCOUNT_MOCK, to: ACCOUNT_MOCK, @@ -2245,10 +2249,14 @@ describe('TransactionController', () => { await finishedPromise; expect(rejectedEventListener).toHaveBeenCalledTimes(1); - expect(rejectedEventListener).toHaveBeenCalledWith({ - transactionMeta: { ...transactionMeta, status: 'rejected' }, - actionId: mockActionId, - }); + expect(rejectedEventListener).toHaveBeenCalledWith( + expect.objectContaining({ + transactionMeta: expect.objectContaining({ + status: 'rejected', + }), + actionId: mockActionId, + }), + ); }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index cbff37cae8..ce7e9d0f58 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -45,13 +45,17 @@ import type { import { NonceTracker } from '@metamask/nonce-tracker'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; -import { add0x } from '@metamask/utils'; +import { add0x, hexToNumber } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import { MethodRegistry } from 'eth-method-registry'; import { EventEmitter } from 'events'; import { cloneDeep, mapValues, merge, pickBy, sortBy } from 'lodash'; import { v1 as random } from 'uuid'; +import { + getAccountAddressRelationship, + type GetAccountAddressRelationshipRequest, +} from './api/accounts-api'; import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow'; import { LineaGasFeeFlow } from './gas-flows/LineaGasFeeFlow'; import { OptimismLayer1GasFeeFlow } from './gas-flows/OptimismLayer1GasFeeFlow'; @@ -90,11 +94,6 @@ import { SimulationErrorCode, } from './types'; import { validateConfirmedExternalTransaction } from './utils/external-transactions'; -import type { FirstTimeInteractionResponse } from './utils/first-time-interaction-api'; -import { - type FirstTimeInteractionRequest, - getFirstTimeInteraction, -} from './utils/first-time-interaction-api'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; import { getGasFeeFlow } from './utils/gas-flow'; @@ -128,7 +127,7 @@ import { normalizeGasFeeValues, } from './utils/utils'; import { - validateFirstTimeInteraction, + validateAccountAddressRelationshipRequest, validateTransactionOrigin, validateTxParams, } from './utils/validation'; @@ -1156,11 +1155,10 @@ export class TransactionController extends BaseController< throw error; }); - this.#updateFirstInteraction(addedTransactionMeta, { + this.#updateFirstInteractionProperties(addedTransactionMeta, { traceContext, }).catch((error) => { - log('Error while updating first interaction', error); - throw error; + log('Error while updating first interaction properties', error); }); } else { log('Skipping simulation as approval not required'); @@ -3627,7 +3625,7 @@ export class TransactionController extends BaseController< return transactionMeta; } - async #updateFirstInteraction( + async #updateFirstInteractionProperties( transactionMeta: TransactionMeta, { traceContext, @@ -3641,36 +3639,27 @@ export class TransactionController extends BaseController< txParams: { to, from }, } = transactionMeta; - const request: FirstTimeInteractionRequest = { - chainId, - to, + const request: GetAccountAddressRelationshipRequest = { + chainId: hexToNumber(chainId), + to: to as string, // This is validated in validateAccountAddressRelationshipRequest from, }; - let firstTimeInteractionResponse: FirstTimeInteractionResponse; - - try { - validateFirstTimeInteraction(request); + validateAccountAddressRelationshipRequest(request); - firstTimeInteractionResponse = await this.#trace( - { name: 'FirstTimeInteraction', parentContext: traceContext }, - () => getFirstTimeInteraction(request), - ); - } catch (error) { - log('Error during first interaction check', error); - firstTimeInteractionResponse = { - isFirstTimeInteraction: undefined, - }; - } + const accountAddressRelationshipResponse = await this.#trace( + { name: 'Account Address Relationship', parentContext: traceContext }, + () => getAccountAddressRelationship(request), + ); const finalTransactionMeta = this.getTransaction(transactionId); /* istanbul ignore if */ if (!finalTransactionMeta) { log( - 'Cannot update first time interaction as transaction not found', + 'Cannot update first time interaction properties as transaction not found', transactionId, - firstTimeInteractionResponse, + accountAddressRelationshipResponse, ); return; @@ -3683,14 +3672,17 @@ export class TransactionController extends BaseController< }, (txMeta) => { txMeta.firstTimeInteraction = - firstTimeInteractionResponse.isFirstTimeInteraction; + accountAddressRelationshipResponse.isFirstTimeInteraction; + + txMeta.isFirstTimeInteractionDisabled = + accountAddressRelationshipResponse.isFirstTimeInteractionDisabled; }, ); log( - 'Updated first time interaction', + 'Updated first time interaction properties', transactionId, - firstTimeInteractionResponse, + accountAddressRelationshipResponse, ); } diff --git a/packages/transaction-controller/src/api/accounts-api.test.ts b/packages/transaction-controller/src/api/accounts-api.test.ts new file mode 100644 index 0000000000..9326ffa7a2 --- /dev/null +++ b/packages/transaction-controller/src/api/accounts-api.test.ts @@ -0,0 +1,119 @@ +import { getAccountAddressRelationship } from './accounts-api'; +import type { GetAccountAddressRelationshipRequest } from './accounts-api'; + +describe('Accounts API', () => { + let fetchMock: jest.MockedFunction; + /** + * Mock a JSON response from fetch. + * @param jsonResponse - The response body to return. + * @param status - The status code to return. + */ + function mockFetchResponse(jsonResponse: unknown, status = 200) { + fetchMock.mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue(jsonResponse), + status, + } as unknown as Response); + } + + beforeEach(() => { + fetchMock = jest.spyOn(global, 'fetch') as jest.MockedFunction< + typeof fetch + >; + }); + + describe('getAccountAddressRelationship', () => { + const CHAIN_ID_SUPPORTED = 1; + const CHAIN_ID_UNSUPPORTED = 999; + const FROM_ADDRESS = '0xSender'; + const TO_ADDRESS = '0xRecipient'; + + const REQUEST_MOCK: GetAccountAddressRelationshipRequest = { + chainId: CHAIN_ID_SUPPORTED, + from: FROM_ADDRESS, + to: TO_ADDRESS, + }; + + const RESPONSE_ERROR_MOCK = { + error: 'Some error', + }; + + const EXISTING_RELATIONSHIP_RESPONSE_MOCK = { + count: 1, + }; + + const NO_COUNT_RESPONSE_MOCK = {}; + + describe('returns isFirstTimeInteraction as true', () => { + it('for 204 responses', async () => { + mockFetchResponse({}, 204); + + const result = await getAccountAddressRelationship(REQUEST_MOCK); + + expect(result).toStrictEqual({ + isFirstTimeInteraction: true, + isFirstTimeInteractionDisabled: false, + }); + }); + + it('when there is no existing relationship', async () => { + mockFetchResponse({ count: 0 }); + + const result = await getAccountAddressRelationship(REQUEST_MOCK); + + expect(result).toStrictEqual({ + isFirstTimeInteraction: true, + isFirstTimeInteractionDisabled: false, + }); + }); + }); + + it('returns isFirstTimeInteraction as false for existing relationship', async () => { + mockFetchResponse(EXISTING_RELATIONSHIP_RESPONSE_MOCK); + + const result = await getAccountAddressRelationship(REQUEST_MOCK); + + expect(result).toStrictEqual({ + isFirstTimeInteraction: false, + isFirstTimeInteractionDisabled: false, + }); + }); + + describe('returns isFirstTimeInteractionDisabled as true', () => { + it('for unsupported chains', async () => { + const request = { + chainId: CHAIN_ID_UNSUPPORTED, + from: FROM_ADDRESS, + to: TO_ADDRESS, + }; + const result = await getAccountAddressRelationship(request); + + expect(result).toStrictEqual({ + isFirstTimeInteraction: undefined, + isFirstTimeInteractionDisabled: true, + }); + }); + + it('if no count property in response', async () => { + mockFetchResponse(NO_COUNT_RESPONSE_MOCK); + + const result = await getAccountAddressRelationship(REQUEST_MOCK); + + expect(result).toStrictEqual({ + isFirstTimeInteraction: undefined, + isFirstTimeInteractionDisabled: true, + }); + }); + + it('on error', async () => { + mockFetchResponse(RESPONSE_ERROR_MOCK); + + const result = await getAccountAddressRelationship(REQUEST_MOCK); + + expect(result).toStrictEqual({ + isFirstTimeInteraction: undefined, + isFirstTimeInteractionDisabled: true, + }); + }); + }); + }); +}); diff --git a/packages/transaction-controller/src/api/accounts-api.ts b/packages/transaction-controller/src/api/accounts-api.ts new file mode 100644 index 0000000000..b6b06061b1 --- /dev/null +++ b/packages/transaction-controller/src/api/accounts-api.ts @@ -0,0 +1,126 @@ +import { createModuleLogger } from '@metamask/utils'; + +import { projectLogger } from '../logger'; + +const SUPPORTED_CHAIN_IDS_FOR_RELATIONSHIP_API = [ + 1, // Ethereum Mainnet + 10, // Optimism + 56, // BSC + 137, // Polygon + 8453, // Base + 42161, // Arbitrum + 59144, // Linea + 534352, // Scroll +]; + +export type AccountAddressRelationshipResponse = { + chainId: number; + count: number; + data: { + hash: string; + timestamp: string; + chainId: number; + blockNumber: string; + blockHash: string; + gas: number; + gasUsed: number; + gasPrice: string; + effectiveGasPrice: number; + nonce: number; + cumulativeGasUsed: number; + methodId: string; + value: string; + to: string; + from: string; + }; + txHash: string; +}; + +export type AccountAddressRelationshipResult = + AccountAddressRelationshipResponse & { + error?: string; + }; + +export type GetAccountAddressRelationshipRequest = { + /** Chain ID of account relationship to check. */ + chainId: number; + + /** Recipient of the transaction. */ + to: string; + + /** Sender of the transaction. */ + from: string; +}; + +export type GetAccountFirstTimeInteractionResponse = { + isFirstTimeInteraction: boolean | undefined; + isFirstTimeInteractionDisabled: boolean; +}; + +const BASE_URL = `https://accounts.api.cx.metamask.io/v1/accounts/`; + +const log = createModuleLogger(projectLogger, 'accounts-api'); + +/** + * Fetch account address relationship from the accounts API. + * @param request - The request object. + * @returns The response object. + */ +export async function getAccountAddressRelationship( + request: GetAccountAddressRelationshipRequest, +): Promise { + const { chainId, from, to } = request; + + if (!SUPPORTED_CHAIN_IDS_FOR_RELATIONSHIP_API.includes(chainId)) { + log('Unsupported chain ID for account relationship API', chainId); + return { + isFirstTimeInteraction: undefined, + isFirstTimeInteractionDisabled: true, + }; + } + + const url = `${BASE_URL}/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; + + log('Getting account address relationship', { request, url }); + + const response = await fetch(url); + + // The accounts API returns a 204 if the relationship does not exist + if (response.status === 204) { + log( + 'No content for account address relationship, marking as first interaction', + ); + return { + isFirstTimeInteraction: true, + isFirstTimeInteractionDisabled: false, + }; + } + + const responseJson: AccountAddressRelationshipResult = await response.json(); + + log('Retrieved account address relationship', responseJson); + + if (responseJson.error) { + // The accounts API returns an error we ignore the relationship feature + log('Error fetching account address relationship', responseJson.error); + return { + isFirstTimeInteraction: undefined, + isFirstTimeInteractionDisabled: true, + }; + } + + const { count } = responseJson; + + if (count === undefined) { + // The accounts API returns no count hence we will ignore the relationship feature + return { + isFirstTimeInteraction: undefined, + isFirstTimeInteractionDisabled: true, + }; + } + + return { + isFirstTimeInteraction: count === 0, + isFirstTimeInteractionDisabled: false, + }; +} diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index d4a2585380..ab8f516007 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -172,6 +172,11 @@ type TransactionMetaBase = { */ firstTimeInteraction?: boolean; + /** + * Whether the first time interaction is disabled. + */ + isFirstTimeInteractionDisabled?: boolean; + /** Alternate EIP-1559 gas fee estimates for multiple priority levels. */ gasFeeEstimates?: GasFeeEstimates; diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts deleted file mode 100644 index 0de1104e0c..0000000000 --- a/packages/transaction-controller/src/utils/first-time-interaction-api.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { FirstTimeInteractionError } from '../errors'; -import type { - FirstTimeInteractionRequest, - FirstTimeInteractionResponse, -} from './first-time-interaction-api'; -import { - BASE_URL, - getFirstTimeInteraction, -} from './first-time-interaction-api'; - -describe('FirstTimeInteraction API Utils', () => { - let fetchMock: jest.MockedFunction; - - /** - * Mock a JSON response from fetch. - * @param jsonResponse - The response body to return. - */ - function mockFetchResponse(jsonResponse: unknown) { - fetchMock.mockResolvedValueOnce({ - json: jest.fn().mockResolvedValue(jsonResponse), - } as unknown as Response); - } - - beforeEach(() => { - fetchMock = jest.spyOn(global, 'fetch') as jest.MockedFunction< - typeof fetch - >; - fetchMock.mockClear(); - }); - - describe('getFirstTimeInteraction', () => { - const requestMock: FirstTimeInteractionRequest = { - chainId: '0x1', - from: '0xFromAddress', - to: '0xToAddress', - }; - - it('returns isFirstTimeInteraction as true when count is 0', async () => { - mockFetchResponse({ count: 0 }); - - const response: FirstTimeInteractionResponse = - await getFirstTimeInteraction(requestMock); - - expect(response.isFirstTimeInteraction).toBe(true); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it('returns isFirstTimeInteraction as true when count is undefined', async () => { - mockFetchResponse({}); - - const response: FirstTimeInteractionResponse = - await getFirstTimeInteraction(requestMock); - - expect(response.isFirstTimeInteraction).toBe(true); - }); - - it('returns isFirstTimeInteraction as false when count is greater than 0', async () => { - mockFetchResponse({ count: 5 }); - - const response: FirstTimeInteractionResponse = - await getFirstTimeInteraction(requestMock); - - expect(response.isFirstTimeInteraction).toBe(false); - }); - - it('throws FirstTimeInteractionError when API returns an error other than FAILED_TO_PARSE_MESSAGE', async () => { - const errorResponse = { - error: { message: 'Some other error', code: 500 }, - }; - mockFetchResponse(errorResponse); - - await expect(getFirstTimeInteraction(requestMock)).rejects.toThrow( - FirstTimeInteractionError, - ); - }); - - it('returns isFirstTimeInteraction as true when API returns FAILED_TO_PARSE_MESSAGE', async () => { - const errorResponse = { - error: { - message: 'Failed to parse account address relationship.', - code: 400, - }, - }; - mockFetchResponse(errorResponse); - - const response: FirstTimeInteractionResponse = - await getFirstTimeInteraction(requestMock); - - expect(response.isFirstTimeInteraction).toBe(true); - }); - - it('sends request to correct URL', async () => { - mockFetchResponse({ count: 1 }); - - const { chainId, from, to } = requestMock; - - await getFirstTimeInteraction(requestMock); - - // The values are not undefined - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const expectedUrl = `${BASE_URL}/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; - expect(fetchMock).toHaveBeenCalledWith(expectedUrl, { method: 'GET' }); - }); - }); -}); diff --git a/packages/transaction-controller/src/utils/first-time-interaction-api.ts b/packages/transaction-controller/src/utils/first-time-interaction-api.ts deleted file mode 100644 index 3450871311..0000000000 --- a/packages/transaction-controller/src/utils/first-time-interaction-api.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createModuleLogger, type Hex } from '@metamask/utils'; - -import { FirstTimeInteractionError } from '../errors'; -import { projectLogger } from '../logger'; - -export const BASE_URL = 'https://accounts.api.cx.metamask.io'; -const FAILED_TO_PARSE_MESSAGE = 'Failed to parse account address relationship.'; - -const log = createModuleLogger(projectLogger, 'first-time-interaction-api'); - -export type FirstTimeInteractionRequest = { - /** Chain ID of the transaction. */ - chainId: Hex; - - /** Recipient of the transaction. */ - to?: string; - - /** Sender of the transaction. */ - from: string; -}; - -export type FirstTimeInteractionResponse = { - isFirstTimeInteraction?: boolean; -}; - -/** - * Get the first time interaction count for an account. - * @param request - The request to get the first time interaction count for. - * @returns The first time interaction count for the account. - */ -export async function getFirstTimeInteraction( - request: FirstTimeInteractionRequest, -): Promise { - const url = await getFirstTimeInteractionUrl(request); - - log('Sending request', url, request); - - const response = await fetch(url, { - method: 'GET', - }); - - const responseJson = await response.json(); - - log('Received response', responseJson); - - if (responseJson.error) { - const { message, code } = responseJson.error; - - if (message === FAILED_TO_PARSE_MESSAGE) { - return { isFirstTimeInteraction: true }; - } - - throw new FirstTimeInteractionError(message, code); - } - - return { - isFirstTimeInteraction: - responseJson?.count === 0 || responseJson?.count === undefined, - }; -} - -/** - * Get the URL for the first time interaction API. - * @param request - The request to get the URL for. - * @returns The URL for the first time interaction API. - */ -async function getFirstTimeInteractionUrl( - request: FirstTimeInteractionRequest, -): Promise { - const { chainId, from, to } = request; - - // The values are not undefined because they are validated in the controller - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `${BASE_URL}/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; -} diff --git a/packages/transaction-controller/src/utils/validation.ts b/packages/transaction-controller/src/utils/validation.ts index a2ddccf3b3..aa6636a7aa 100644 --- a/packages/transaction-controller/src/utils/validation.ts +++ b/packages/transaction-controller/src/utils/validation.ts @@ -4,8 +4,8 @@ import { abiERC20 } from '@metamask/metamask-eth-abis'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { isStrictHexString } from '@metamask/utils'; +import type { GetAccountAddressRelationshipRequest } from '../api/accounts-api'; import { TransactionEnvelopeType, type TransactionParams } from '../types'; -import type { FirstTimeInteractionRequest } from './first-time-interaction-api'; import { isEIP1559Transaction } from './utils'; type GasFieldsToValidate = 'gasPrice' | 'maxFeePerGas' | 'maxPriorityFeePerGas'; @@ -72,13 +72,11 @@ export function validateTxParams( * * @param request - The request to validate. */ -export function validateFirstTimeInteraction( - request: FirstTimeInteractionRequest, +export function validateAccountAddressRelationshipRequest( + request: GetAccountAddressRelationshipRequest, ) { - const { chainId, from, to } = request; + const { to } = request; validateParamTo(to); - validateParamFrom(from); - validateParamChainId(chainId); } /** From e04c0621169089fc68805cdb77027178220d754b Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 14 Nov 2024 15:28:44 +0100 Subject: [PATCH 07/17] Add existing transaction check --- .../src/TransactionController.test.ts | 36 +++++++++++++- .../src/TransactionController.ts | 49 ++++++++++++------- packages/transaction-controller/src/types.ts | 2 +- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index de91164e47..420425d2d2 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1443,7 +1443,41 @@ describe('TransactionController', () => { expect(controller.state.transactions[0].sendFlowHistory).toStrictEqual( mockSendFlowHistory, ); - expect(controller.state.transactions[0].firstTimeInteraction).toBe(true); + expect(controller.state.transactions[0].isFirstTimeInteraction).toBe( + true, + ); + }); + + it('does not check account address relationship if a transaction with the same from, to, and chainId exists', async () => { + const { controller, messenger } = setupController({ + options: { + state: { + transactions: [ + { + id: '1', + chainId: MOCK_NETWORK.chainId, + status: TransactionStatus.confirmed as const, + time: 123456789, + txParams: { + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }, + isFirstTimeInteraction: false, // Ensure this is set + }, + ], + }, + }, + }); + + // Add second transaction with the same from, to, and chainId + await controller.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }); + + await flushPromises(); + + expect(controller.state.transactions[1].isFirstTimeInteraction).toBe(false); }); describe('networkClientId exists in the MultichainTrackingHelper', () => { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ce7e9d0f58..2193b4e274 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1095,15 +1095,17 @@ export class TransactionController extends BaseController< dappSuggestedGasFees, deviceConfirmedOn, id: random(), + isFirstTimeInteraction: false, + isFirstTimeInteractionDisabled: false, + networkClientId, origin, securityAlertResponse, status: TransactionStatus.unapproved as const, time: Date.now(), txParams, + type: transactionType, userEditedGasLimit: false, verifiedOnBlockchain: false, - type: transactionType, - networkClientId, }; await this.#trace( @@ -1161,7 +1163,9 @@ export class TransactionController extends BaseController< log('Error while updating first interaction properties', error); }); } else { - log('Skipping simulation as approval not required'); + log( + 'Skipping simulation & first interaction update as approval not required', + ); } this.messagingSystem.publish( @@ -3647,11 +3651,26 @@ export class TransactionController extends BaseController< validateAccountAddressRelationshipRequest(request); - const accountAddressRelationshipResponse = await this.#trace( - { name: 'Account Address Relationship', parentContext: traceContext }, - () => getAccountAddressRelationship(request), + const existingTransaction = this.state.transactions.find( + (tx) => + tx.chainId === chainId && + tx.txParams.from === from && + tx.txParams.to === to && + tx.id !== transactionId, ); + // Check if there is an existing transaction with the same from, to, and chainId + // else we continue to check the account address relationship from API + if (existingTransaction) { + return; + } + + const { isFirstTimeInteractionDisabled, isFirstTimeInteraction } = + await this.#trace( + { name: 'Account Address Relationship', parentContext: traceContext }, + () => getAccountAddressRelationship(request), + ); + const finalTransactionMeta = this.getTransaction(transactionId); /* istanbul ignore if */ @@ -3659,9 +3678,7 @@ export class TransactionController extends BaseController< log( 'Cannot update first time interaction properties as transaction not found', transactionId, - accountAddressRelationshipResponse, ); - return; } @@ -3671,19 +3688,15 @@ export class TransactionController extends BaseController< note: 'TransactionController#updateFirstInteraction - Update first time interaction', }, (txMeta) => { - txMeta.firstTimeInteraction = - accountAddressRelationshipResponse.isFirstTimeInteraction; - - txMeta.isFirstTimeInteractionDisabled = - accountAddressRelationshipResponse.isFirstTimeInteractionDisabled; + txMeta.isFirstTimeInteraction = isFirstTimeInteraction; + txMeta.isFirstTimeInteractionDisabled = isFirstTimeInteractionDisabled; }, ); - log( - 'Updated first time interaction properties', - transactionId, - accountAddressRelationshipResponse, - ); + log('Updated first time interaction properties', transactionId, { + isFirstTimeInteractionDisabled, + isFirstTimeInteraction, + }); } async #updateSimulationData( diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index ab8f516007..3b1906e091 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -170,7 +170,7 @@ type TransactionMetaBase = { /** * Whether the transaction is the first time interaction. */ - firstTimeInteraction?: boolean; + isFirstTimeInteraction?: boolean; /** * Whether the first time interaction is disabled. From 6458f04980002a20ab2cadb4ceabfe45d1d4773a Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 15 Nov 2024 11:44:14 +0100 Subject: [PATCH 08/17] Fix lint --- .../src/TransactionController.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 420425d2d2..670c11a552 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1449,7 +1449,7 @@ describe('TransactionController', () => { }); it('does not check account address relationship if a transaction with the same from, to, and chainId exists', async () => { - const { controller, messenger } = setupController({ + const { controller } = setupController({ options: { state: { transactions: [ @@ -1468,7 +1468,7 @@ describe('TransactionController', () => { }, }, }); - + // Add second transaction with the same from, to, and chainId await controller.addTransaction({ from: ACCOUNT_MOCK, @@ -1476,8 +1476,10 @@ describe('TransactionController', () => { }); await flushPromises(); - - expect(controller.state.transactions[1].isFirstTimeInteraction).toBe(false); + + expect(controller.state.transactions[1].isFirstTimeInteraction).toBe( + false, + ); }); describe('networkClientId exists in the MultichainTrackingHelper', () => { From 849fa54cd7950d678eada3af6c440e08cfb5d6b2 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 15 Nov 2024 11:48:55 +0100 Subject: [PATCH 09/17] Update coverage --- packages/transaction-controller/jest.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 1011dfca5a..719873579e 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.46, + branches: 93.38, functions: 97.31, - lines: 98.25, - statements: 98.25, + lines: 98.26, + statements: 98.27, }, }, From e8e736817e4d193b2f6e99dd44f1fcf5a3c2d12f Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 15 Nov 2024 12:02:51 +0100 Subject: [PATCH 10/17] Fix tests --- .../transaction-controller/src/TransactionController.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 670c11a552..576cf9648e 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1566,6 +1566,8 @@ describe('TransactionController', () => { dappSuggestedGasFees: undefined, deviceConfirmedOn: undefined, id: expect.any(String), + isFirstTimeInteraction: false, + isFirstTimeInteractionDisabled: false, networkClientId: MOCK_NETWORK.state.selectedNetworkClientId, origin: undefined, securityAlertResponse: undefined, From 342cd27b656dd0f3196a31dc99af0c3919379cca Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Tue, 19 Nov 2024 10:41:01 +0100 Subject: [PATCH 11/17] Add isFirstTimeInteractionEnabled --- .../src/TransactionController.test.ts | 15 +++++++++++++++ .../src/TransactionController.ts | 11 +++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 576cf9648e..68525aff7c 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1482,6 +1482,21 @@ describe('TransactionController', () => { ); }); + it('does not update first time interaction properties if disabled', async () => { + const { controller } = setupController({ + options: { isFirstTimeInteractionEnabled: () => false }, + }); + + await controller.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }); + + await flushPromises(); + + expect(getAccountAddressRelationshipMock).not.toHaveBeenCalled(); + }); + describe('networkClientId exists in the MultichainTrackingHelper', () => { it('adds unapproved transaction to state when using networkClientId', async () => { const { controller } = setupController({ diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 2193b4e274..c73180e2a4 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -302,6 +302,7 @@ export type TransactionControllerOptions = { etherscanApiKeysByChainId?: Record; }; isMultichainEnabled: boolean; + isFirstTimeInteractionEnabled?: () => boolean; isSimulationEnabled?: () => boolean; messenger: TransactionControllerMessenger; onNetworkStateChange: (listener: (state: NetworkState) => void) => void; @@ -643,6 +644,8 @@ export class TransactionController extends BaseController< #transactionHistoryLimit: number; + #isFirstTimeInteractionEnabled: () => boolean; + #isSimulationEnabled: () => boolean; #testGasFeeFlows: boolean; @@ -761,6 +764,7 @@ export class TransactionController extends BaseController< * @param options.getSavedGasFees - Gets the saved gas fee config. * @param options.incomingTransactions - Configuration options for incoming transaction support. * @param options.isMultichainEnabled - Enable multichain support. + * @param options.isFirstTimeInteractionEnabled - Whether first time interaction checks are enabled. * @param options.isSimulationEnabled - Whether new transactions will be automatically simulated. * @param options.messenger - The controller messenger. * @param options.onNetworkStateChange - Allows subscribing to network controller state changes. @@ -789,6 +793,7 @@ export class TransactionController extends BaseController< getSavedGasFees, incomingTransactions = {}, isMultichainEnabled = false, + isFirstTimeInteractionEnabled, isSimulationEnabled, messenger, onNetworkStateChange, @@ -817,6 +822,8 @@ export class TransactionController extends BaseController< this.isSendFlowHistoryDisabled = disableSendFlowHistory ?? false; this.isHistoryDisabled = disableHistory ?? false; this.isSwapsDisabled = disableSwaps ?? false; + this.#isFirstTimeInteractionEnabled = + isFirstTimeInteractionEnabled ?? (() => true); this.#isSimulationEnabled = isSimulationEnabled ?? (() => true); // @ts-expect-error the type in eth-method-registry is inappropriate and should be changed this.registry = new MethodRegistry({ provider }); @@ -3637,6 +3644,10 @@ export class TransactionController extends BaseController< traceContext?: TraceContext; } = {}, ) { + if (!this.#isFirstTimeInteractionEnabled()) { + return; + } + const { chainId, id: transactionId, From 188c262b3ac47cd6b96be656971244b3c7d8300a Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Tue, 19 Nov 2024 10:42:29 +0100 Subject: [PATCH 12/17] Update coverage --- packages/transaction-controller/jest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 719873579e..d300faee75 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -17,8 +17,8 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.38, - functions: 97.31, + branches: 93.4, + functions: 97.32, lines: 98.26, statements: 98.27, }, From a64e3fd33a2054c7904dbdcba1231109a87a4d67 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 21 Nov 2024 11:46:32 +0100 Subject: [PATCH 13/17] Fix suggestions --- .../src/TransactionController.test.ts | 9 +-- .../src/TransactionController.ts | 64 ++++++++++--------- .../src/api/accounts-api.test.ts | 56 +++++----------- .../src/api/accounts-api.ts | 62 ++++++------------ packages/transaction-controller/src/types.ts | 5 -- .../src/utils/validation.ts | 15 +---- 6 files changed, 73 insertions(+), 138 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 68525aff7c..64999524d8 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -877,8 +877,7 @@ describe('TransactionController', () => { ); getAccountAddressRelationshipMock.mockResolvedValue({ - isFirstTimeInteraction: undefined, - isFirstTimeInteractionDisabled: false, + count: 1, }); }); @@ -1389,8 +1388,7 @@ describe('TransactionController', () => { const { controller } = setupController(); getAccountAddressRelationshipMock.mockResolvedValueOnce({ - isFirstTimeInteraction: true, - isFirstTimeInteractionDisabled: false, + count: 0, }); const mockDeviceConfirmedOn = WalletDevice.OTHER; @@ -1581,8 +1579,7 @@ describe('TransactionController', () => { dappSuggestedGasFees: undefined, deviceConfirmedOn: undefined, id: expect.any(String), - isFirstTimeInteraction: false, - isFirstTimeInteractionDisabled: false, + isFirstTimeInteraction: undefined, networkClientId: MOCK_NETWORK.state.selectedNetworkClientId, origin: undefined, securityAlertResponse: undefined, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index c73180e2a4..2c426821ac 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -127,7 +127,7 @@ import { normalizeGasFeeValues, } from './utils/utils'; import { - validateAccountAddressRelationshipRequest, + validateParamTo, validateTransactionOrigin, validateTxParams, } from './utils/validation'; @@ -1102,8 +1102,7 @@ export class TransactionController extends BaseController< dappSuggestedGasFees, deviceConfirmedOn, id: random(), - isFirstTimeInteraction: false, - isFirstTimeInteractionDisabled: false, + isFirstTimeInteraction: undefined, networkClientId, origin, securityAlertResponse, @@ -3656,11 +3655,11 @@ export class TransactionController extends BaseController< const request: GetAccountAddressRelationshipRequest = { chainId: hexToNumber(chainId), - to: to as string, // This is validated in validateAccountAddressRelationshipRequest + to: to as string, from, }; - validateAccountAddressRelationshipRequest(request); + validateParamTo(to); const existingTransaction = this.state.transactions.find( (tx) => @@ -3676,38 +3675,45 @@ export class TransactionController extends BaseController< return; } - const { isFirstTimeInteractionDisabled, isFirstTimeInteraction } = - await this.#trace( + try { + const { count } = await this.#trace( { name: 'Account Address Relationship', parentContext: traceContext }, () => getAccountAddressRelationship(request), ); - const finalTransactionMeta = this.getTransaction(transactionId); + const isFirstTimeInteraction = + count === undefined ? undefined : count === 0; - /* istanbul ignore if */ - if (!finalTransactionMeta) { + const finalTransactionMeta = this.getTransaction(transactionId); + + /* istanbul ignore if */ + if (!finalTransactionMeta) { + log( + 'Cannot update first time interaction as transaction not found', + transactionId, + ); + return; + } + + this.#updateTransactionInternal( + { + transactionId, + note: 'TransactionController#updateFirstInteraction - Update first time interaction', + }, + (txMeta) => { + txMeta.isFirstTimeInteraction = isFirstTimeInteraction; + }, + ); + + log('Updated first time interaction', transactionId, { + isFirstTimeInteraction, + }); + } catch (error) { log( - 'Cannot update first time interaction properties as transaction not found', - transactionId, + 'Error fetching account address relationship, skipping first time interaction update', + error, ); - return; } - - this.#updateTransactionInternal( - { - transactionId, - note: 'TransactionController#updateFirstInteraction - Update first time interaction', - }, - (txMeta) => { - txMeta.isFirstTimeInteraction = isFirstTimeInteraction; - txMeta.isFirstTimeInteractionDisabled = isFirstTimeInteractionDisabled; - }, - ); - - log('Updated first time interaction properties', transactionId, { - isFirstTimeInteractionDisabled, - isFirstTimeInteraction, - }); } async #updateSimulationData( diff --git a/packages/transaction-controller/src/api/accounts-api.test.ts b/packages/transaction-controller/src/api/accounts-api.test.ts index 9326ffa7a2..72ac59bae8 100644 --- a/packages/transaction-controller/src/api/accounts-api.test.ts +++ b/packages/transaction-controller/src/api/accounts-api.test.ts @@ -1,3 +1,4 @@ +import { FirstTimeInteractionError } from '../errors'; import { getAccountAddressRelationship } from './accounts-api'; import type { GetAccountAddressRelationshipRequest } from './accounts-api'; @@ -33,25 +34,18 @@ describe('Accounts API', () => { to: TO_ADDRESS, }; - const RESPONSE_ERROR_MOCK = { - error: 'Some error', - }; - const EXISTING_RELATIONSHIP_RESPONSE_MOCK = { count: 1, }; - const NO_COUNT_RESPONSE_MOCK = {}; - - describe('returns isFirstTimeInteraction as true', () => { + describe('returns API response', () => { it('for 204 responses', async () => { mockFetchResponse({}, 204); const result = await getAccountAddressRelationship(REQUEST_MOCK); expect(result).toStrictEqual({ - isFirstTimeInteraction: true, - isFirstTimeInteractionDisabled: false, + count: 0, }); }); @@ -61,58 +55,40 @@ describe('Accounts API', () => { const result = await getAccountAddressRelationship(REQUEST_MOCK); expect(result).toStrictEqual({ - isFirstTimeInteraction: true, - isFirstTimeInteractionDisabled: false, + count: 0, }); }); }); - it('returns isFirstTimeInteraction as false for existing relationship', async () => { + it('returns correct response for existing relationship', async () => { mockFetchResponse(EXISTING_RELATIONSHIP_RESPONSE_MOCK); const result = await getAccountAddressRelationship(REQUEST_MOCK); - expect(result).toStrictEqual({ - isFirstTimeInteraction: false, - isFirstTimeInteractionDisabled: false, - }); + expect(result).toStrictEqual(EXISTING_RELATIONSHIP_RESPONSE_MOCK); }); - describe('returns isFirstTimeInteractionDisabled as true', () => { + describe('throws FirstTimeInteractionError', () => { it('for unsupported chains', async () => { const request = { chainId: CHAIN_ID_UNSUPPORTED, from: FROM_ADDRESS, to: TO_ADDRESS, }; - const result = await getAccountAddressRelationship(request); - expect(result).toStrictEqual({ - isFirstTimeInteraction: undefined, - isFirstTimeInteractionDisabled: true, - }); + await expect(getAccountAddressRelationship(request)).rejects.toThrow( + FirstTimeInteractionError, + ); }); - it('if no count property in response', async () => { - mockFetchResponse(NO_COUNT_RESPONSE_MOCK); - - const result = await getAccountAddressRelationship(REQUEST_MOCK); - - expect(result).toStrictEqual({ - isFirstTimeInteraction: undefined, - isFirstTimeInteractionDisabled: true, + it('on error response', async () => { + mockFetchResponse({ + error: { code: 'error_code', message: 'Some error' }, }); - }); - - it('on error', async () => { - mockFetchResponse(RESPONSE_ERROR_MOCK); - const result = await getAccountAddressRelationship(REQUEST_MOCK); - - expect(result).toStrictEqual({ - isFirstTimeInteraction: undefined, - isFirstTimeInteractionDisabled: true, - }); + await expect( + getAccountAddressRelationship(REQUEST_MOCK), + ).rejects.toThrow(FirstTimeInteractionError); }); }); }); diff --git a/packages/transaction-controller/src/api/accounts-api.ts b/packages/transaction-controller/src/api/accounts-api.ts index b6b06061b1..4d3f1ec4cb 100644 --- a/packages/transaction-controller/src/api/accounts-api.ts +++ b/packages/transaction-controller/src/api/accounts-api.ts @@ -1,5 +1,6 @@ import { createModuleLogger } from '@metamask/utils'; +import { FirstTimeInteractionError } from '../errors'; import { projectLogger } from '../logger'; const SUPPORTED_CHAIN_IDS_FOR_RELATIONSHIP_API = [ @@ -14,9 +15,9 @@ const SUPPORTED_CHAIN_IDS_FOR_RELATIONSHIP_API = [ ]; export type AccountAddressRelationshipResponse = { - chainId: number; - count: number; - data: { + chainId?: number; + count?: number; + data?: { hash: string; timestamp: string; chainId: number; @@ -33,12 +34,15 @@ export type AccountAddressRelationshipResponse = { to: string; from: string; }; - txHash: string; + txHash?: string; }; export type AccountAddressRelationshipResult = AccountAddressRelationshipResponse & { - error?: string; + error?: { + code: string; + message: string; + }; }; export type GetAccountAddressRelationshipRequest = { @@ -52,11 +56,6 @@ export type GetAccountAddressRelationshipRequest = { from: string; }; -export type GetAccountFirstTimeInteractionResponse = { - isFirstTimeInteraction: boolean | undefined; - isFirstTimeInteractionDisabled: boolean; -}; - const BASE_URL = `https://accounts.api.cx.metamask.io/v1/accounts/`; const log = createModuleLogger(projectLogger, 'accounts-api'); @@ -64,19 +63,16 @@ const log = createModuleLogger(projectLogger, 'accounts-api'); /** * Fetch account address relationship from the accounts API. * @param request - The request object. - * @returns The response object. + * @returns The raw response object from the API. */ export async function getAccountAddressRelationship( request: GetAccountAddressRelationshipRequest, -): Promise { +): Promise { const { chainId, from, to } = request; if (!SUPPORTED_CHAIN_IDS_FOR_RELATIONSHIP_API.includes(chainId)) { log('Unsupported chain ID for account relationship API', chainId); - return { - isFirstTimeInteraction: undefined, - isFirstTimeInteractionDisabled: true, - }; + throw new FirstTimeInteractionError('Unsupported chain ID'); } const url = `${BASE_URL}/v1/networks/${chainId}/accounts/${from}/relationships/${to}`; @@ -85,15 +81,10 @@ export async function getAccountAddressRelationship( const response = await fetch(url); - // The accounts API returns a 204 if the relationship does not exist if (response.status === 204) { - log( - 'No content for account address relationship, marking as first interaction', - ); - return { - isFirstTimeInteraction: true, - isFirstTimeInteractionDisabled: false, - }; + // The accounts API returns a 204 status code when there are no transactions with empty body + // imitating a count of 0 + return { count: 0 }; } const responseJson: AccountAddressRelationshipResult = await response.json(); @@ -101,26 +92,9 @@ export async function getAccountAddressRelationship( log('Retrieved account address relationship', responseJson); if (responseJson.error) { - // The accounts API returns an error we ignore the relationship feature - log('Error fetching account address relationship', responseJson.error); - return { - isFirstTimeInteraction: undefined, - isFirstTimeInteractionDisabled: true, - }; - } - - const { count } = responseJson; - - if (count === undefined) { - // The accounts API returns no count hence we will ignore the relationship feature - return { - isFirstTimeInteraction: undefined, - isFirstTimeInteractionDisabled: true, - }; + const { code, message } = responseJson.error; + throw new FirstTimeInteractionError(message, code); } - return { - isFirstTimeInteraction: count === 0, - isFirstTimeInteractionDisabled: false, - }; + return responseJson; } diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 3b1906e091..ce7ea5b8ea 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -172,11 +172,6 @@ type TransactionMetaBase = { */ isFirstTimeInteraction?: boolean; - /** - * Whether the first time interaction is disabled. - */ - isFirstTimeInteractionDisabled?: boolean; - /** Alternate EIP-1559 gas fee estimates for multiple priority levels. */ gasFeeEstimates?: GasFeeEstimates; diff --git a/packages/transaction-controller/src/utils/validation.ts b/packages/transaction-controller/src/utils/validation.ts index aa6636a7aa..3e725483fe 100644 --- a/packages/transaction-controller/src/utils/validation.ts +++ b/packages/transaction-controller/src/utils/validation.ts @@ -4,7 +4,6 @@ import { abiERC20 } from '@metamask/metamask-eth-abis'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { isStrictHexString } from '@metamask/utils'; -import type { GetAccountAddressRelationshipRequest } from '../api/accounts-api'; import { TransactionEnvelopeType, type TransactionParams } from '../types'; import { isEIP1559Transaction } from './utils'; @@ -67,18 +66,6 @@ export function validateTxParams( validateGasFeeParams(txParams); } -/** - * Validates the request for the first time interaction API. - * - * @param request - The request to validate. - */ -export function validateAccountAddressRelationshipRequest( - request: GetAccountAddressRelationshipRequest, -) { - const { to } = request; - validateParamTo(to); -} - /** * Validates the `type` property, ensuring that if it is specified, it is a valid transaction envelope type. * @@ -203,7 +190,7 @@ function validateParamFrom(from: string) { * @param to - The to property to validate. * @throws Throws an error if the recipient address is invalid. */ -function validateParamTo(to?: string) { +export function validateParamTo(to?: string) { if (!to || typeof to !== 'string') { throw rpcErrors.invalidParams(`Invalid "to" address`); } From be3c24f8016fd05f31895237288f8edb51cbde39 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 21 Nov 2024 11:56:02 +0100 Subject: [PATCH 14/17] Update coverage --- packages/transaction-controller/jest.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 7867d905e7..7b87e61b12 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.92, + branches: 93.57, functions: 97.56, - lines: 98.39, - statements: 98.4, + lines: 98.38, + statements: 98.39, }, }, From 3fc4b3fb0ffe74a938e051552a89f1c23042832d Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 21 Nov 2024 12:20:42 +0100 Subject: [PATCH 15/17] Update naming --- packages/transaction-controller/src/TransactionController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 2c426821ac..5c0c0fc58d 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1163,7 +1163,7 @@ export class TransactionController extends BaseController< throw error; }); - this.#updateFirstInteractionProperties(addedTransactionMeta, { + this.#updateFirstTimeInteraction(addedTransactionMeta, { traceContext, }).catch((error) => { log('Error while updating first interaction properties', error); @@ -3635,7 +3635,7 @@ export class TransactionController extends BaseController< return transactionMeta; } - async #updateFirstInteractionProperties( + async #updateFirstTimeInteraction( transactionMeta: TransactionMeta, { traceContext, From dc7114cbf279d40137d08b2252085d7824a5a5ad Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 21 Nov 2024 12:41:44 +0100 Subject: [PATCH 16/17] Fix url --- packages/transaction-controller/src/api/accounts-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/api/accounts-api.ts b/packages/transaction-controller/src/api/accounts-api.ts index 4d3f1ec4cb..874be88595 100644 --- a/packages/transaction-controller/src/api/accounts-api.ts +++ b/packages/transaction-controller/src/api/accounts-api.ts @@ -56,7 +56,7 @@ export type GetAccountAddressRelationshipRequest = { from: string; }; -const BASE_URL = `https://accounts.api.cx.metamask.io/v1/accounts/`; +const BASE_URL = `https://accounts.api.cx.metamask.io/v1/accounts`; const log = createModuleLogger(projectLogger, 'accounts-api'); From 608079df8a31762e70301c85c7b2c2ab5bdb1445 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 21 Nov 2024 12:52:33 +0100 Subject: [PATCH 17/17] Update url --- packages/transaction-controller/src/api/accounts-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/api/accounts-api.ts b/packages/transaction-controller/src/api/accounts-api.ts index 874be88595..8364aaddda 100644 --- a/packages/transaction-controller/src/api/accounts-api.ts +++ b/packages/transaction-controller/src/api/accounts-api.ts @@ -56,7 +56,7 @@ export type GetAccountAddressRelationshipRequest = { from: string; }; -const BASE_URL = `https://accounts.api.cx.metamask.io/v1/accounts`; +const BASE_URL = `https://accounts.api.cx.metamask.io`; const log = createModuleLogger(projectLogger, 'accounts-api');