Skip to content

Commit

Permalink
Support swaps using user operations (#3749)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
matthewwalsh0 authored Jan 12, 2024
1 parent 1c4a902 commit f8437e9
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 35 deletions.
2 changes: 1 addition & 1 deletion packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ type TransactionMetaBase = {
/**
* The decimals of the token being received of swap transaction.
*/
destinationTokenDecimals?: string;
destinationTokenDecimals?: number;

/**
* The symbol of the token being received with swap.
Expand Down
129 changes: 124 additions & 5 deletions packages/user-operation-controller/src/UserOperationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@metamask/transaction-controller';
import { EventEmitter } from 'stream';

import { ADDRESS_ZERO, EMPTY_BYTES, ENTRYPOINT, VALUE_ZERO } from './constants';
import { ADDRESS_ZERO, EMPTY_BYTES, VALUE_ZERO } from './constants';
import * as BundlerHelper from './helpers/Bundler';
import * as PendingUserOperationTrackerHelper from './helpers/PendingUserOperationTracker';
import type { UserOperationMetadata } from './types';
Expand Down Expand Up @@ -47,6 +47,7 @@ const ERROR_CODE_MOCK = '1234';
const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId';
const TRANSACTION_HASH_MOCK = '0x456';
const ORIGIN_MOCK = 'test.com';
const ENTRYPOINT_MOCK = '0x789';

const USER_OPERATION_METADATA_MOCK = {
chainId: CHAIN_ID_MOCK,
Expand Down Expand Up @@ -161,6 +162,7 @@ describe('UserOperationController', () => {
const updateGasFeesMock = jest.mocked(updateGasFees);

const optionsMock = {
entrypoint: ENTRYPOINT_MOCK,
getGasFeeEstimates,
messenger,
};
Expand Down Expand Up @@ -336,7 +338,29 @@ describe('UserOperationController', () => {
verificationGasLimit:
PREPARE_USER_OPERATION_RESPONSE_MOCK.gas?.verificationGasLimit,
},
ENTRYPOINT,
ENTRYPOINT_MOCK,
);
});

it('emits added event', async () => {
const controller = new UserOperationController(optionsMock);

const listener = jest.fn();
controller.hub.on('user-operation-added', listener);

const { id, hash } = await addUserOperation(
controller,
ADD_USER_OPERATION_REQUEST_MOCK,
{ ...ADD_USER_OPERATION_OPTIONS_MOCK, smartContractAccount },
);

await hash();

expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({
id,
}),
);
});

Expand All @@ -346,7 +370,10 @@ describe('UserOperationController', () => {
const { id } = await addUserOperation(
controller,
ADD_USER_OPERATION_REQUEST_MOCK,
{ ...ADD_USER_OPERATION_OPTIONS_MOCK, smartContractAccount },
{
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
},
);

expect(Object.keys(controller.state.userOperations)).toHaveLength(1);
Expand All @@ -362,6 +389,7 @@ describe('UserOperationController', () => {
id,
origin: ORIGIN_MOCK,
status: UserOperationStatus.Unapproved,
swapsMetadata: null,
time: expect.any(Number),
transactionHash: null,
userOperation: expect.objectContaining({
Expand All @@ -379,6 +407,75 @@ describe('UserOperationController', () => {
);
});

it('includes swap metadata in metadata', async () => {
const controller = new UserOperationController(optionsMock);

const { id } = await addUserOperation(
controller,
ADD_USER_OPERATION_REQUEST_MOCK,
{
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
swaps: {
approvalTxId: 'testTxId',
destinationTokenAddress: '0x1',
destinationTokenDecimals: 3,
destinationTokenSymbol: 'TEST',
estimatedBaseFee: '0x2',
sourceTokenSymbol: 'ETH',
swapMetaData: { test: 'value' },
swapTokenValue: '0x3',
},
},
);

expect(Object.keys(controller.state.userOperations)).toHaveLength(1);
expect(controller.state.userOperations[id]).toStrictEqual(
expect.objectContaining({
swapsMetadata: {
approvalTxId: 'testTxId',
destinationTokenAddress: '0x1',
destinationTokenDecimals: 3,
destinationTokenSymbol: 'TEST',
estimatedBaseFee: '0x2',
sourceTokenSymbol: 'ETH',
swapMetaData: { test: 'value' },
swapTokenValue: '0x3',
},
}),
);
});

it('defaults missing swap metadata to null', async () => {
const controller = new UserOperationController(optionsMock);

const { id } = await addUserOperation(
controller,
ADD_USER_OPERATION_REQUEST_MOCK,
{
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
swaps: {},
},
);

expect(Object.keys(controller.state.userOperations)).toHaveLength(1);
expect(controller.state.userOperations[id]).toStrictEqual(
expect.objectContaining({
swapsMetadata: {
approvalTxId: null,
destinationTokenAddress: null,
destinationTokenDecimals: null,
destinationTokenSymbol: null,
estimatedBaseFee: null,
sourceTokenSymbol: null,
swapMetaData: null,
swapTokenValue: null,
},
}),
);
});

it('updates metadata in state after submission', async () => {
const controller = new UserOperationController(optionsMock);

Expand Down Expand Up @@ -457,7 +554,7 @@ describe('UserOperationController', () => {
initCode: EMPTY_BYTES,
paymasterAndData: EMPTY_BYTES,
}),
ENTRYPOINT,
ENTRYPOINT_MOCK,
);
});

Expand Down Expand Up @@ -1028,7 +1125,29 @@ describe('UserOperationController', () => {
);
});

it('sets transaction type in metadata', async () => {
it('uses transaction type from request options', async () => {
const controller = new UserOperationController(optionsMock);

const { id } = await addUserOperation(
controller,
ADD_USER_OPERATION_REQUEST_MOCK,
{
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
type: TransactionType.swap,
},
);

await flushPromises();

expect(controller.state.userOperations[id].transactionType).toBe(
TransactionType.swap,
);

expect(determineTransactionTypeMock).toHaveBeenCalledTimes(0);
});

it('determines transaction type if not set', async () => {
const controller = new UserOperationController(optionsMock);

const { id } = await addUserOperation(
Expand Down
Loading

0 comments on commit f8437e9

Please sign in to comment.