Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 src/SmartTransactionsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@

controller.timeoutHandle = setTimeout(() => ({}));

controller.poll(1000);

Check warning on line 450 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 450 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

expect(updateSmartTransactionsSpy).toHaveBeenCalled();
});
Expand Down Expand Up @@ -2222,6 +2222,9 @@
getFeatureFlags: jest.fn(),
updateTransaction: jest.fn(),
...options,
getRemoteFeatureFlags:
options?.getRemoteFeatureFlags ??
(() => ({ transactionsTxHashInAnalytics: false })),
});

function triggerNetworStateChange(state: NetworkState) {
Expand Down Expand Up @@ -2262,7 +2265,7 @@
triggerNetworStateChange,
});
} finally {
controller.stop();

Check warning on line 2268 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 2268 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
controller.stopAllPolling();
}
}
12 changes: 10 additions & 2 deletions src/SmartTransactionsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@
getMetaMetricsProps: () => Promise<MetaMetricsProps>;
getFeatureFlags: () => FeatureFlags;
updateTransaction: (transaction: TransactionMeta, note: string) => void;
getRemoteFeatureFlags: () => { transactionsTxHashInAnalytics?: boolean };
};

export type SmartTransactionsControllerPollingInput = {
Expand Down Expand Up @@ -245,6 +246,8 @@

#updateTransaction: SmartTransactionsControllerOptions['updateTransaction'];

#getRemoteFeatureFlags: () => { transactionsTxHashInAnalytics?: boolean };

/* istanbul ignore next */
async #fetch(request: string, options?: RequestInit) {
const fetchOptions = {
Expand Down Expand Up @@ -272,6 +275,7 @@
getMetaMetricsProps,
getFeatureFlags,
updateTransaction,
getRemoteFeatureFlags,
}: SmartTransactionsControllerOptions) {
super({
name: controllerName,
Expand Down Expand Up @@ -319,6 +323,7 @@
`${controllerName}:stateChange`,
(currentState) => this.checkPoll(currentState),
);
this.#getRemoteFeatureFlags = getRemoteFeatureFlags;
}

async _executePoll({
Expand Down Expand Up @@ -348,9 +353,9 @@
isSmartTransactionPending,
);
if (!this.timeoutHandle && pendingTransactions?.length > 0) {
this.poll();

Check warning on line 356 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 356 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
} else if (this.timeoutHandle && pendingTransactions?.length === 0) {
this.stop();

Check warning on line 358 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 358 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
}

Expand All @@ -375,7 +380,7 @@
}

this.timeoutHandle = setInterval(() => {
safelyExecute(async () => this.updateSmartTransactions());

Check warning on line 383 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 383 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}, this.#interval);
await safelyExecute(async () => this.updateSmartTransactions());
}
Expand Down Expand Up @@ -411,6 +416,7 @@
properties: getSmartTransactionMetricsProperties(updatedSmartTransaction),
sensitiveProperties: getSmartTransactionMetricsSensitiveProperties(
updatedSmartTransaction,
this.#getRemoteFeatureFlags,
),
});
}
Expand Down Expand Up @@ -442,7 +448,7 @@
ethQuery = new EthQuery(provider);
}

this.#createOrUpdateSmartTransaction(smartTransaction, {

Check warning on line 451 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 451 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
chainId,
ethQuery,
});
Expand Down Expand Up @@ -721,7 +727,7 @@
: originalTxMeta;

if (this.#doesTransactionNeedConfirmation(txHash)) {
this.#confirmExternalTransaction(

Check warning on line 730 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 730 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
// TODO: Replace 'as' assertion with correct typing for `txMeta`
txMeta as TransactionMeta,
transactionReceipt,
Expand All @@ -733,8 +739,10 @@
event: MetaMetricsEventName.StxConfirmed,
category: MetaMetricsEventCategory.Transactions,
properties: getSmartTransactionMetricsProperties(smartTransaction),
sensitiveProperties:
getSmartTransactionMetricsSensitiveProperties(smartTransaction),
sensitiveProperties: getSmartTransactionMetricsSensitiveProperties(
smartTransaction,
this.#getRemoteFeatureFlags,
),
});
this.#updateSmartTransaction(
{ ...smartTransaction, confirmed: true },
Expand Down
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('default export', () => {
getFeatureFlags: jest.fn(),
updateTransaction: jest.fn(),
clientId: ClientId.Extension,
getRemoteFeatureFlags: () => ({ transactionsTxHashInAnalytics: false }),
});
expect(controller).toBeInstanceOf(SmartTransactionsController);
jest.clearAllTimers();
Expand Down
57 changes: 57 additions & 0 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,24 +356,81 @@ describe('src/utils.js', () => {
});

describe('getTxHash', () => {
/**
* Core functionality test - verifies that the function correctly generates
* a deterministic hash from a valid signed transaction. This is the primary
* use case for transaction tracking in MetaMetrics.
*/
it('returns a transaction hash from a signed transaction', () => {
const expectedTxHash =
'0x0302b75dfb9fd9eb34056af031efcaee2a8cbd799ea054a85966165cd82a7356';
const txHash = utils.getTxHash(createSignedTransaction());
expect(txHash).toBe(expectedTxHash);
});

/**
* Safety check - ensures the function handles empty input gracefully
* instead of throwing errors. Important for defensive programming and
* preventing crashes when tracking transaction hashes.
*/
it('returns an empty string if there is no signed transaction', () => {
const expectedTxHash = '';
const txHash = utils.getTxHash('');
expect(txHash).toBe(expectedTxHash);
});

/**
* Error handling test - verifies that malformed transaction data
* results in appropriate error messaging rather than silent failures
* or incorrect hash generation.
*/
it('throws an error with an incorrect signed transaction', () => {
expect(() => {
utils.getTxHash('0x0302b75dfb9fd9eb34056af0');
}).toThrow('kzg instance required to instantiate blob tx');
});

/**
* Format handling test - ensures the function works correctly with both
* prefixed (0x) and unprefixed hex strings. This is crucial as transaction
* data might come in either format from different parts of the system.
*/
it('handles transaction hex without 0x prefix', () => {
const withPrefix = createSignedTransaction();
const withoutPrefix = createSignedTransaction().slice(2);
expect(utils.getTxHash(withPrefix)).toBe(utils.getTxHash(withoutPrefix));
});

/**
* Consistency test - verifies that the function generates the same hash
* regardless of hex string casing. This ensures reliable transaction
* tracking even if the hex representation varies across the system.
*/
it('maintains consistent hash with different string representations', () => {
const original = createSignedTransaction();
// Convert to upper/lower case but preserve the 0x prefix
const upperCase = `0x${original.slice(2).toUpperCase()}`;
const lowerCase = `0x${original.slice(2).toLowerCase()}`;
const hash = utils.getTxHash(original);

expect(utils.getTxHash(upperCase)).toBe(hash);
expect(utils.getTxHash(lowerCase)).toBe(hash);
});

/**
* Case insensitivity test - verifies that the function handles both uppercase
* and lowercase hex characters correctly by normalizing the input. This is
* important because hex strings might be represented in different cases
* across the system, but should still produce the same hash.
*/
it('maintains consistent hash regardless of hex string case', () => {
const original = createSignedTransaction();
// Convert the middle portion to uppercase, keeping 0x prefix lowercase
const mixedCase = `0x${original.slice(2).toUpperCase()}`;
const hash = utils.getTxHash(original);

expect(utils.getTxHash(mixedCase)).toBe(hash);
});
});

describe('getReturnTxHashAsap', () => {
Expand Down
41 changes: 35 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TransactionFactory } from '@ethereumjs/tx';
import { bytesToHex } from '@ethereumjs/util';
import { bytesToHex, hexToBytes } from '@ethereumjs/util';
import { hexlify } from '@ethersproject/bytes';
import type { TransactionMeta } from '@metamask/transaction-controller';
import { TransactionStatus } from '@metamask/transaction-controller';
Expand Down Expand Up @@ -224,13 +224,20 @@
return hexlify(Number(nonceInDec) + 1);
};

export const getTxHash = (signedTxHex: any) => {
export const getTxHash = (signedTxHex: string) => {
if (!signedTxHex) {
return '';
}
// First normalize the entire string to lowercase
const normalizedInput = signedTxHex.toLowerCase();
// Then remove 0x prefix if it exists
const hexWithoutPrefix = normalizedInput.startsWith('0x')
? normalizedInput.slice(2)
: normalizedInput;
// Add single 0x prefix
const prefixedHex = `0x${hexWithoutPrefix}`;
const txHashBytes = TransactionFactory.fromSerializedData(
// eslint-disable-next-line no-restricted-globals
Buffer.from(signedTxHex.slice(2), 'hex'),
hexToBytes(prefixedHex),
).hash();
return bytesToHex(txHashBytes);
};
Expand All @@ -256,19 +263,41 @@
};
};

type SmartTransactionSensitiveProperties = {
token_from_symbol?: string;
token_to_symbol?: string;
account_hardware_type?: string;
account_type?: string;
device_model?: string;
transaction_hash?: string;
};

export const getSmartTransactionMetricsSensitiveProperties = (
smartTransaction: SmartTransaction,
) => {
getRemoteFeatureFlags?: () => { transactionsTxHashInAnalytics?: boolean },
): SmartTransactionSensitiveProperties => {
if (!smartTransaction) {
return {};
}
return {

const sensitiveProperties: SmartTransactionSensitiveProperties = {
token_from_symbol: smartTransaction.sourceTokenSymbol,
token_to_symbol: smartTransaction.destinationTokenSymbol,
account_hardware_type: smartTransaction.accountHardwareType,
account_type: smartTransaction.accountType,
device_model: smartTransaction.deviceModel,
};

// Add transaction hash if feature flag is enabled and user has opted in
if (
getRemoteFeatureFlags?.()?.transactionsTxHashInAnalytics &&
smartTransaction.statusMetadata?.minedHash
) {
sensitiveProperties.transaction_hash =
smartTransaction.statusMetadata.minedHash;
}

return sensitiveProperties;
};

export const getReturnTxHashAsap = (
Expand Down Expand Up @@ -335,8 +364,8 @@
status: TransactionStatus.failed,
error: {
name: 'SmartTransactionFailed',
message: `Smart transaction failed with status: ${status}`,

Check warning on line 367 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Invalid type "string | undefined" of template literal expression

Check warning on line 367 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Invalid type "string | undefined" of template literal expression
},
};
updateTransaction(updatedTransaction, `Smart transaction status: ${status}`);

Check warning on line 370 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Invalid type "string | undefined" of template literal expression

Check warning on line 370 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Invalid type "string | undefined" of template literal expression
};
Loading