Skip to content

Commit f8437e9

Browse files
Support swaps using user operations (#3749)
Add the type option to force a TransactionType such as swap or swapApproval. Add the swaps option to specify swaps specific metadata such as sourceTokenSymbol and swapTokenValue. Emit the user-operation-added event to allow the clients to check for a swap transaction type and notify other controllers.
1 parent 1c4a902 commit f8437e9

File tree

11 files changed

+364
-35
lines changed

11 files changed

+364
-35
lines changed

packages/transaction-controller/src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ type TransactionMetaBase = {
141141
/**
142142
* The decimals of the token being received of swap transaction.
143143
*/
144-
destinationTokenDecimals?: string;
144+
destinationTokenDecimals?: number;
145145

146146
/**
147147
* The symbol of the token being received with swap.

packages/user-operation-controller/src/UserOperationController.test.ts

+124-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from '@metamask/transaction-controller';
77
import { EventEmitter } from 'stream';
88

9-
import { ADDRESS_ZERO, EMPTY_BYTES, ENTRYPOINT, VALUE_ZERO } from './constants';
9+
import { ADDRESS_ZERO, EMPTY_BYTES, VALUE_ZERO } from './constants';
1010
import * as BundlerHelper from './helpers/Bundler';
1111
import * as PendingUserOperationTrackerHelper from './helpers/PendingUserOperationTracker';
1212
import type { UserOperationMetadata } from './types';
@@ -47,6 +47,7 @@ const ERROR_CODE_MOCK = '1234';
4747
const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId';
4848
const TRANSACTION_HASH_MOCK = '0x456';
4949
const ORIGIN_MOCK = 'test.com';
50+
const ENTRYPOINT_MOCK = '0x789';
5051

5152
const USER_OPERATION_METADATA_MOCK = {
5253
chainId: CHAIN_ID_MOCK,
@@ -161,6 +162,7 @@ describe('UserOperationController', () => {
161162
const updateGasFeesMock = jest.mocked(updateGasFees);
162163

163164
const optionsMock = {
165+
entrypoint: ENTRYPOINT_MOCK,
164166
getGasFeeEstimates,
165167
messenger,
166168
};
@@ -336,7 +338,29 @@ describe('UserOperationController', () => {
336338
verificationGasLimit:
337339
PREPARE_USER_OPERATION_RESPONSE_MOCK.gas?.verificationGasLimit,
338340
},
339-
ENTRYPOINT,
341+
ENTRYPOINT_MOCK,
342+
);
343+
});
344+
345+
it('emits added event', async () => {
346+
const controller = new UserOperationController(optionsMock);
347+
348+
const listener = jest.fn();
349+
controller.hub.on('user-operation-added', listener);
350+
351+
const { id, hash } = await addUserOperation(
352+
controller,
353+
ADD_USER_OPERATION_REQUEST_MOCK,
354+
{ ...ADD_USER_OPERATION_OPTIONS_MOCK, smartContractAccount },
355+
);
356+
357+
await hash();
358+
359+
expect(listener).toHaveBeenCalledTimes(1);
360+
expect(listener).toHaveBeenCalledWith(
361+
expect.objectContaining({
362+
id,
363+
}),
340364
);
341365
});
342366

@@ -346,7 +370,10 @@ describe('UserOperationController', () => {
346370
const { id } = await addUserOperation(
347371
controller,
348372
ADD_USER_OPERATION_REQUEST_MOCK,
349-
{ ...ADD_USER_OPERATION_OPTIONS_MOCK, smartContractAccount },
373+
{
374+
...ADD_USER_OPERATION_OPTIONS_MOCK,
375+
smartContractAccount,
376+
},
350377
);
351378

352379
expect(Object.keys(controller.state.userOperations)).toHaveLength(1);
@@ -362,6 +389,7 @@ describe('UserOperationController', () => {
362389
id,
363390
origin: ORIGIN_MOCK,
364391
status: UserOperationStatus.Unapproved,
392+
swapsMetadata: null,
365393
time: expect.any(Number),
366394
transactionHash: null,
367395
userOperation: expect.objectContaining({
@@ -379,6 +407,75 @@ describe('UserOperationController', () => {
379407
);
380408
});
381409

410+
it('includes swap metadata in metadata', async () => {
411+
const controller = new UserOperationController(optionsMock);
412+
413+
const { id } = await addUserOperation(
414+
controller,
415+
ADD_USER_OPERATION_REQUEST_MOCK,
416+
{
417+
...ADD_USER_OPERATION_OPTIONS_MOCK,
418+
smartContractAccount,
419+
swaps: {
420+
approvalTxId: 'testTxId',
421+
destinationTokenAddress: '0x1',
422+
destinationTokenDecimals: 3,
423+
destinationTokenSymbol: 'TEST',
424+
estimatedBaseFee: '0x2',
425+
sourceTokenSymbol: 'ETH',
426+
swapMetaData: { test: 'value' },
427+
swapTokenValue: '0x3',
428+
},
429+
},
430+
);
431+
432+
expect(Object.keys(controller.state.userOperations)).toHaveLength(1);
433+
expect(controller.state.userOperations[id]).toStrictEqual(
434+
expect.objectContaining({
435+
swapsMetadata: {
436+
approvalTxId: 'testTxId',
437+
destinationTokenAddress: '0x1',
438+
destinationTokenDecimals: 3,
439+
destinationTokenSymbol: 'TEST',
440+
estimatedBaseFee: '0x2',
441+
sourceTokenSymbol: 'ETH',
442+
swapMetaData: { test: 'value' },
443+
swapTokenValue: '0x3',
444+
},
445+
}),
446+
);
447+
});
448+
449+
it('defaults missing swap metadata to null', async () => {
450+
const controller = new UserOperationController(optionsMock);
451+
452+
const { id } = await addUserOperation(
453+
controller,
454+
ADD_USER_OPERATION_REQUEST_MOCK,
455+
{
456+
...ADD_USER_OPERATION_OPTIONS_MOCK,
457+
smartContractAccount,
458+
swaps: {},
459+
},
460+
);
461+
462+
expect(Object.keys(controller.state.userOperations)).toHaveLength(1);
463+
expect(controller.state.userOperations[id]).toStrictEqual(
464+
expect.objectContaining({
465+
swapsMetadata: {
466+
approvalTxId: null,
467+
destinationTokenAddress: null,
468+
destinationTokenDecimals: null,
469+
destinationTokenSymbol: null,
470+
estimatedBaseFee: null,
471+
sourceTokenSymbol: null,
472+
swapMetaData: null,
473+
swapTokenValue: null,
474+
},
475+
}),
476+
);
477+
});
478+
382479
it('updates metadata in state after submission', async () => {
383480
const controller = new UserOperationController(optionsMock);
384481

@@ -457,7 +554,7 @@ describe('UserOperationController', () => {
457554
initCode: EMPTY_BYTES,
458555
paymasterAndData: EMPTY_BYTES,
459556
}),
460-
ENTRYPOINT,
557+
ENTRYPOINT_MOCK,
461558
);
462559
});
463560

@@ -1028,7 +1125,29 @@ describe('UserOperationController', () => {
10281125
);
10291126
});
10301127

1031-
it('sets transaction type in metadata', async () => {
1128+
it('uses transaction type from request options', async () => {
1129+
const controller = new UserOperationController(optionsMock);
1130+
1131+
const { id } = await addUserOperation(
1132+
controller,
1133+
ADD_USER_OPERATION_REQUEST_MOCK,
1134+
{
1135+
...ADD_USER_OPERATION_OPTIONS_MOCK,
1136+
smartContractAccount,
1137+
type: TransactionType.swap,
1138+
},
1139+
);
1140+
1141+
await flushPromises();
1142+
1143+
expect(controller.state.userOperations[id].transactionType).toBe(
1144+
TransactionType.swap,
1145+
);
1146+
1147+
expect(determineTransactionTypeMock).toHaveBeenCalledTimes(0);
1148+
});
1149+
1150+
it('determines transaction type if not set', async () => {
10321151
const controller = new UserOperationController(optionsMock);
10331152

10341153
const { id } = await addUserOperation(

0 commit comments

Comments
 (0)