Skip to content

Commit 44e6bc6

Browse files
committed
Add validateSecurity callback
1 parent bed0c3a commit 44e6bc6

File tree

4 files changed

+197
-4
lines changed

4 files changed

+197
-4
lines changed

packages/transaction-controller/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export type {
6868
TransactionMeta,
6969
TransactionParams,
7070
TransactionReceipt,
71+
ValidateSecurityRequest,
7172
} from './types';
7273
export {
7374
GasFeeEstimateLevel,

packages/transaction-controller/src/types.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,10 +1138,11 @@ export type TransactionError = {
11381138
* Type for security alert response from transaction validator.
11391139
*/
11401140
export type SecurityAlertResponse = {
1141-
reason: string;
11421141
features?: string[];
1143-
result_type: string;
11441142
providerRequestsCount?: Record<string, number>;
1143+
reason?: string;
1144+
result_type?: string;
1145+
securityAlertId?: string;
11451146
};
11461147

11471148
/** Alternate priority levels for which values are provided in gas fee estimates. */
@@ -1513,6 +1514,9 @@ export type TransactionBatchRequest = {
15131514
/** Whether an approval request should be created to require confirmation from the user. */
15141515
requireApproval?: boolean;
15151516

1517+
/** Security alert ID to persist on the transaction. */
1518+
securityAlertId?: string;
1519+
15161520
/** Transactions to be submitted as part of the batch. */
15171521
transactions: TransactionBatchSingleRequest[];
15181522

@@ -1521,6 +1525,17 @@ export type TransactionBatchRequest = {
15211525
* Defaults to false.
15221526
*/
15231527
useHook?: boolean;
1528+
1529+
/**
1530+
* Callback to trigger security validation in the client.
1531+
*
1532+
* @param request - The JSON-RPC request to validate.
1533+
* @param chainId - The chain ID of the transaction batch.
1534+
*/
1535+
validateSecurity?: (
1536+
request: ValidateSecurityRequest,
1537+
chainId: Hex,
1538+
) => Promise<void>;
15241539
};
15251540

15261541
/**
@@ -1595,3 +1610,17 @@ export type PublishBatchHook = (
15951610
/** Data required to call the hook. */
15961611
request: PublishBatchHookRequest,
15971612
) => Promise<PublishBatchHookResult>;
1613+
1614+
/**
1615+
* Request to validate security of a transaction in the client.
1616+
*/
1617+
export type ValidateSecurityRequest = {
1618+
/** JSON-RPC method to validate. */
1619+
method: string;
1620+
1621+
/** Parameters of the JSON-RPC method to validate. */
1622+
params: unknown[];
1623+
1624+
/** Optional EIP-7702 delegation to mock for the transaction sender. */
1625+
delegationMock?: Hex;
1626+
};

packages/transaction-controller/src/utils/batch.test.ts

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const TRANSACTION_HASH_2_MOCK = '0x456';
5252
const TRANSACTION_SIGNATURE_MOCK = '0xabc';
5353
const TRANSACTION_SIGNATURE_2_MOCK = '0xdef';
5454
const ERROR_MESSAGE_MOCK = 'Test error';
55+
const SECURITY_ALERT_ID_MOCK = '123-456';
5556

5657
const TRANSACTION_META_MOCK = {
5758
id: BATCH_ID_CUSTOM_MOCK,
@@ -61,7 +62,7 @@ const TRANSACTION_META_MOCK = {
6162
data: DATA_MOCK,
6263
value: VALUE_MOCK,
6364
},
64-
} as TransactionMeta;
65+
} as unknown as TransactionMeta;
6566

6667
describe('Batch Utils', () => {
6768
const doesChainSupportEIP7702Mock = jest.mocked(doesChainSupportEIP7702);
@@ -100,11 +101,13 @@ describe('Batch Utils', () => {
100101
jest.resetAllMocks();
101102
addTransactionMock = jest.fn();
102103
getChainIdMock = jest.fn();
104+
updateTransactionMock = jest.fn();
103105

104106
determineTransactionTypeMock.mockResolvedValue({
105107
type: TransactionType.simpleSend,
106108
});
107-
updateTransactionMock = jest.fn();
109+
110+
getChainIdMock.mockReturnValue(CHAIN_ID_MOCK);
108111

109112
request = {
110113
addTransaction: addTransactionMock,
@@ -408,6 +411,138 @@ describe('Batch Utils', () => {
408411
);
409412
});
410413

414+
it('adds security alert ID to transaction', async () => {
415+
doesChainSupportEIP7702Mock.mockReturnValueOnce(true);
416+
417+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
418+
delegationAddress: undefined,
419+
isSupported: true,
420+
});
421+
422+
addTransactionMock.mockResolvedValueOnce({
423+
transactionMeta: TRANSACTION_META_MOCK,
424+
result: Promise.resolve(''),
425+
});
426+
427+
generateEIP7702BatchTransactionMock.mockReturnValueOnce({
428+
to: TO_MOCK,
429+
data: DATA_MOCK,
430+
value: VALUE_MOCK,
431+
});
432+
433+
request.request.securityAlertId = SECURITY_ALERT_ID_MOCK;
434+
435+
await addTransactionBatch(request);
436+
437+
expect(addTransactionMock).toHaveBeenCalledTimes(1);
438+
expect(addTransactionMock).toHaveBeenCalledWith(
439+
expect.any(Object),
440+
expect.objectContaining({
441+
securityAlertResponse: {
442+
securityAlertId: SECURITY_ALERT_ID_MOCK,
443+
},
444+
}),
445+
);
446+
});
447+
448+
describe('validates security', () => {
449+
it('using transaction params', async () => {
450+
doesChainSupportEIP7702Mock.mockReturnValueOnce(true);
451+
452+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
453+
delegationAddress: undefined,
454+
isSupported: true,
455+
});
456+
457+
addTransactionMock.mockResolvedValueOnce({
458+
transactionMeta: TRANSACTION_META_MOCK,
459+
result: Promise.resolve(''),
460+
});
461+
462+
generateEIP7702BatchTransactionMock.mockReturnValueOnce({
463+
to: TO_MOCK,
464+
data: DATA_MOCK,
465+
value: VALUE_MOCK,
466+
});
467+
468+
const validateSecurityMock = jest.fn();
469+
validateSecurityMock.mockResolvedValueOnce({});
470+
471+
request.request.validateSecurity = validateSecurityMock;
472+
473+
await addTransactionBatch(request);
474+
475+
expect(validateSecurityMock).toHaveBeenCalledTimes(1);
476+
expect(validateSecurityMock).toHaveBeenCalledWith(
477+
{
478+
delegationMock: undefined,
479+
method: 'eth_sendTransaction',
480+
params: [
481+
{
482+
authorizationList: undefined,
483+
data: DATA_MOCK,
484+
from: FROM_MOCK,
485+
to: TO_MOCK,
486+
type: TransactionEnvelopeType.feeMarket,
487+
value: VALUE_MOCK,
488+
},
489+
],
490+
},
491+
CHAIN_ID_MOCK,
492+
);
493+
});
494+
495+
it('using delegation mock if not upgraded', async () => {
496+
doesChainSupportEIP7702Mock.mockReturnValueOnce(true);
497+
498+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
499+
delegationAddress: undefined,
500+
isSupported: false,
501+
});
502+
503+
addTransactionMock.mockResolvedValueOnce({
504+
transactionMeta: TRANSACTION_META_MOCK,
505+
result: Promise.resolve(''),
506+
});
507+
508+
generateEIP7702BatchTransactionMock.mockReturnValueOnce({
509+
to: TO_MOCK,
510+
data: DATA_MOCK,
511+
value: VALUE_MOCK,
512+
});
513+
514+
getEIP7702UpgradeContractAddressMock.mockReturnValue(
515+
CONTRACT_ADDRESS_MOCK,
516+
);
517+
518+
const validateSecurityMock = jest.fn();
519+
validateSecurityMock.mockResolvedValueOnce({});
520+
521+
request.request.validateSecurity = validateSecurityMock;
522+
523+
await addTransactionBatch(request);
524+
525+
expect(validateSecurityMock).toHaveBeenCalledTimes(1);
526+
expect(validateSecurityMock).toHaveBeenCalledWith(
527+
{
528+
delegationMock: CONTRACT_ADDRESS_MOCK,
529+
method: 'eth_sendTransaction',
530+
params: [
531+
{
532+
authorizationList: undefined,
533+
data: DATA_MOCK,
534+
from: FROM_MOCK,
535+
to: TO_MOCK,
536+
type: TransactionEnvelopeType.feeMarket,
537+
value: VALUE_MOCK,
538+
},
539+
],
540+
},
541+
CHAIN_ID_MOCK,
542+
);
543+
});
544+
});
545+
411546
describe('with publish batch hook', () => {
412547
it('adds each nested transaction', async () => {
413548
const publishBatchHook = jest.fn();

packages/transaction-controller/src/utils/batch.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type {
3131
PublishBatchHookTransaction,
3232
PublishHook,
3333
TransactionBatchRequest,
34+
ValidateSecurityRequest,
3435
} from '../types';
3536
import {
3637
TransactionEnvelopeType,
@@ -95,8 +96,10 @@ export async function addTransactionBatch(
9596
from,
9697
networkClientId,
9798
requireApproval,
99+
securityAlertId,
98100
transactions,
99101
useHook,
102+
validateSecurity,
100103
} = userRequest;
101104

102105
log('Adding', userRequest);
@@ -161,15 +164,40 @@ export async function addTransactionBatch(
161164
txParams.authorizationList = [{ address: upgradeContractAddress }];
162165
}
163166

167+
if (validateSecurity) {
168+
const securityRequest: ValidateSecurityRequest = {
169+
method: 'eth_sendTransaction',
170+
params: [
171+
{
172+
...txParams,
173+
authorizationList: undefined,
174+
type: TransactionEnvelopeType.feeMarket,
175+
},
176+
],
177+
delegationMock: txParams.authorizationList?.[0]?.address,
178+
};
179+
180+
log('Security request', securityRequest);
181+
182+
validateSecurity(securityRequest, chainId).catch((error) => {
183+
log('Security validation failed', error);
184+
});
185+
}
186+
164187
log('Adding batch transaction', txParams, networkClientId);
165188

166189
const batchId = batchIdOverride ?? generateBatchId();
167190

191+
const securityAlertResponse = securityAlertId
192+
? { securityAlertId }
193+
: undefined;
194+
168195
const { result } = await addTransaction(txParams, {
169196
batchId,
170197
nestedTransactions,
171198
networkClientId,
172199
requireApproval,
200+
securityAlertResponse,
173201
type: TransactionType.batch,
174202
});
175203

0 commit comments

Comments
 (0)