diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 9b1b04c60..0d9df7907 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `details` field to `Transaction` type ([#445](https://github.com/MetaMask/accounts/pull/445)) + - Add `SecurityAlertResponse` enum with values: `benign`, `warning`, `malicious` + - Add optional `origin` field (string) to track transaction request source + - Add optional `securityAlertResponse` field for Security Alert API responses + ## [21.5.0] ### Added diff --git a/packages/keyring-api/src/api/transaction.test-d.ts b/packages/keyring-api/src/api/transaction.test-d.ts index 0b5d8e2e2..7312cf36a 100644 --- a/packages/keyring-api/src/api/transaction.test-d.ts +++ b/packages/keyring-api/src/api/transaction.test-d.ts @@ -240,3 +240,102 @@ expectNotAssignable({ }, ], }); + +// Transaction with full details (valid) +expectAssignable({ + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'eip155:1', + status: 'submitted', + type: 'send', + account: '5cd17616-ea18-4d72-974f-6dbaa3c56d15', + from: [], + to: [], + fees: [], + events: [], + details: { + origin: 'https://dapp.test', + securityAlertResponse: 'Benign', + }, +}); + +// Transaction with empty details object (valid) +expectAssignable({ + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'eip155:1', + status: 'submitted', + type: 'send', + account: '5cd17616-ea18-4d72-974f-6dbaa3c56d15', + from: [], + to: [], + fees: [], + events: [], + details: {}, +}); + +// Transaction with only origin in details (valid) +expectAssignable({ + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'eip155:1', + status: 'submitted', + type: 'send', + account: '5cd17616-ea18-4d72-974f-6dbaa3c56d15', + from: [], + to: [], + fees: [], + events: [], + details: { + origin: 'metamask', + }, +}); + +// Transaction with only securityAlertResponse in details (valid) +expectAssignable({ + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'eip155:1', + status: 'submitted', + type: 'send', + account: '5cd17616-ea18-4d72-974f-6dbaa3c56d15', + from: [], + to: [], + fees: [], + events: [], + details: { + securityAlertResponse: 'Warning', + }, +}); + +// Transaction with undefined details (invalid - exactOptional doesn't allow undefined) +expectNotAssignable({ + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'eip155:1', + status: 'submitted', + type: 'send', + account: '5cd17616-ea18-4d72-974f-6dbaa3c56d15', + from: [], + to: [], + fees: [], + events: [], + details: undefined, +}); + +// Transaction with invalid securityAlertResponse (invalid - must be 'benign', 'warning', or 'malicious') +expectNotAssignable({ + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'eip155:1', + status: 'submitted', + type: 'send', + account: '5cd17616-ea18-4d72-974f-6dbaa3c56d15', + from: [], + to: [], + fees: [], + events: [], + details: { + securityAlertResponse: 'Invalid', + }, +}); diff --git a/packages/keyring-api/src/api/transaction.test.ts b/packages/keyring-api/src/api/transaction.test.ts new file mode 100644 index 000000000..356bb6675 --- /dev/null +++ b/packages/keyring-api/src/api/transaction.test.ts @@ -0,0 +1,86 @@ +import { is } from '@metamask/superstruct'; + +import { TransactionStruct } from './transaction'; + +describe('TransactionStruct', () => { + const baseTransaction = { + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + chain: 'eip155:1', + account: '5cd17616-ea18-4d72-974f-6dbaa3c56d15', + status: 'confirmed', + timestamp: 1716367781, + type: 'send', + from: [], + to: [], + fees: [], + events: [], + }; + + describe('details field', () => { + it.each([ + // Without details field + { transaction: baseTransaction, expected: true }, + // With empty details + { transaction: { ...baseTransaction, details: {} }, expected: true }, + // With only origin + { + transaction: { + ...baseTransaction, + details: { origin: 'https://dapp.test' }, + }, + expected: true, + }, + // With only securityAlertResponse + { + transaction: { + ...baseTransaction, + details: { securityAlertResponse: 'Benign' }, + }, + expected: true, + }, + // With both fields + { + transaction: { + ...baseTransaction, + details: { origin: 'metamask', securityAlertResponse: 'Warning' }, + }, + expected: true, + }, + // All valid securityAlertResponse values + { + transaction: { + ...baseTransaction, + details: { securityAlertResponse: 'Benign' }, + }, + expected: true, + }, + { + transaction: { + ...baseTransaction, + details: { securityAlertResponse: 'Warning' }, + }, + expected: true, + }, + { + transaction: { + ...baseTransaction, + details: { securityAlertResponse: 'Malicious' }, + }, + expected: true, + }, + // Invalid securityAlertResponse + { + transaction: { + ...baseTransaction, + details: { securityAlertResponse: 'Invalid' }, + }, + expected: false, + }, + ])( + 'returns $expected for is($transaction, TransactionStruct)', + ({ transaction, expected }) => { + expect(is(transaction, TransactionStruct)).toBe(expected); + }, + ); + }); +}); diff --git a/packages/keyring-api/src/api/transaction.ts b/packages/keyring-api/src/api/transaction.ts index 3d3f38566..de87bc640 100644 --- a/packages/keyring-api/src/api/transaction.ts +++ b/packages/keyring-api/src/api/transaction.ts @@ -1,5 +1,5 @@ import type { InferEquals } from '@metamask/keyring-utils'; -import { object, UuidStruct } from '@metamask/keyring-utils'; +import { exactOptional, object, UuidStruct } from '@metamask/keyring-utils'; import type { Infer } from '@metamask/superstruct'; import { array, enums, nullable, number, string } from '@metamask/superstruct'; @@ -164,6 +164,7 @@ export enum TransactionType { * Represents a stake withdrawal transaction. */ StakeWithdraw = 'stake:withdraw', + /** * The transaction type is unknown. It's not possible to determine the * transaction type based on the information available. @@ -171,6 +172,67 @@ export enum TransactionType { Unknown = 'unknown', } +/** + * Security alert response values from the Security Alert API. + */ +export enum SecurityAlertResponse { + /** + * The transaction is considered safe with no detected security issues. + */ + Benign = 'Benign', + + /** + * The transaction has potential security concerns that warrant user attention. + */ + Warning = 'Warning', + + /** + * The transaction has been identified as malicious and should be avoided. + */ + Malicious = 'Malicious', +} + +/** + * This struct represents additional transaction details. + * + * @example + * ```ts + * { + * origin: 'https://dapp.example.com', + * securityAlertResponse: 'Benign', + * } + * ``` + * + * @example + * ```ts + * { + * origin: 'metamask', + * securityAlertResponse: 'Warning', + * } + * ``` + */ +export const TransactionDetailsStruct = object({ + /** + * Origin of the original transaction request. + * + * This can be either 'metamask' for internally initiated transactions, or a URL + * (e.g., 'https://dapp.example.com') for dapp-initiated transactions. + */ + origin: exactOptional(string()), + + /** + * Response from the Security Alert API indicating the security assessment of the + * transaction. + */ + securityAlertResponse: exactOptional( + enums([ + `${SecurityAlertResponse.Benign}`, + `${SecurityAlertResponse.Warning}`, + `${SecurityAlertResponse.Malicious}`, + ]), + ), +}); + /** * This struct represents a transaction event. */ @@ -318,8 +380,24 @@ export const TransactionStruct = object({ * all transactions. */ events: array(TransactionEventStruct), + + /** + * Additional transaction details {@see TransactionDetailsStruct}. + * + * Contains contextual information about the transaction such as its origin and + * security assessment. This field is optional and may not be present for all + * transactions. + */ + details: exactOptional(TransactionDetailsStruct), }); +/** + * Transaction details object. + * + * See {@link TransactionDetailsStruct}. + */ +export type TransactionDetails = Infer; + /** * Transaction object. *