Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use account snap by default in user operations #3844

Merged
merged 79 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
35c89ab
Create initial draft of user operation controller
matthewwalsh0 Nov 29, 2023
d97e14f
Add validation to addUserOperation and account responses
matthewwalsh0 Nov 29, 2023
5f3e6d3
Create initial unit tests for controller class
matthewwalsh0 Dec 1, 2023
7a9276f
Add validation unit tests
matthewwalsh0 Dec 1, 2023
a607ebb
Add unit tests to verify validation called
matthewwalsh0 Dec 1, 2023
8769b7d
Remove unnecessary dependencies
matthewwalsh0 Dec 1, 2023
f0e8dcf
Add additional JSDoc
matthewwalsh0 Dec 1, 2023
76fc8ce
Update yarn lock
matthewwalsh0 Dec 1, 2023
05e1143
Allow 0x in optional hex values
matthewwalsh0 Dec 1, 2023
2d40c4d
Remove additional dependencies
matthewwalsh0 Dec 1, 2023
568b758
Merge branch 'main' into feat/user-operation-controller
matthewwalsh0 Dec 1, 2023
f4ddaa2
Merge branch 'main' into feat/user-operation-controller
matthewwalsh0 Dec 1, 2023
ecb1d7d
Merge branch 'main' into feat/user-operation-controller
matthewwalsh0 Dec 5, 2023
de2c7c6
Remove unnecessary casting
matthewwalsh0 Dec 5, 2023
b0e2532
Add PendingUserOperationTracker
matthewwalsh0 Dec 5, 2023
9ca8ec7
Merge branch 'main' into feat/user-operation-controller
matthewwalsh0 Dec 6, 2023
d98ce77
Add transactionHash promise in return value
matthewwalsh0 Dec 6, 2023
618a726
Merge branch 'feat/user-operation-controller' into feat/monitor-pendi…
matthewwalsh0 Dec 6, 2023
d9fb9d1
Update yarn lock
matthewwalsh0 Dec 6, 2023
9f26b10
Use typed event emitter
matthewwalsh0 Dec 6, 2023
d141367
Request approval by default
matthewwalsh0 Dec 10, 2023
e66431e
Add transaction type and origin
matthewwalsh0 Dec 11, 2023
b43436e
Add unit tests for transaction utils
matthewwalsh0 Dec 11, 2023
d62f8ec
Add unit tests for addUserOperationFromTransaction and approvals
matthewwalsh0 Dec 12, 2023
13cbbd7
Merge branch 'main' into feat/user-operations-from-transactions
matthewwalsh0 Dec 12, 2023
02e4118
Support regeneration of user operation after approval
matthewwalsh0 Dec 12, 2023
a035cb0
Fix linting
matthewwalsh0 Dec 12, 2023
190abae
Add additional JSDoc
matthewwalsh0 Dec 12, 2023
beca241
Add validation unit tests
matthewwalsh0 Dec 13, 2023
5c35314
Merge branch 'main' into feat/user-operations-from-transactions
matthewwalsh0 Dec 13, 2023
7a4e086
Merge branch 'main' into feat/user-operations-from-transactions
matthewwalsh0 Dec 14, 2023
a456654
Remove usages of type any
matthewwalsh0 Dec 14, 2023
7f09450
Move gas and gas fee logic to utils
matthewwalsh0 Dec 15, 2023
0b03139
Use gas fee estimates callback
matthewwalsh0 Dec 16, 2023
388ac92
Remove unnecessary casting
matthewwalsh0 Dec 16, 2023
d57afba
Set user fee level
matthewwalsh0 Dec 16, 2023
a5e0cbb
Use block polling
matthewwalsh0 Dec 16, 2023
5b403bb
Add unit tests for gas fee utils
matthewwalsh0 Dec 16, 2023
5f89ceb
Add unit tests for gas utils
matthewwalsh0 Dec 16, 2023
bc4dd1b
Merge branch 'main' into feat/user-operations-from-transactions
matthewwalsh0 Dec 18, 2023
003ac2b
Merge branch 'feat/user-operations-from-transactions' into feat/estim…
matthewwalsh0 Dec 19, 2023
5a2bb32
Merge branch 'main' into feat/estimate-user-operation-gas-fees
matthewwalsh0 Dec 19, 2023
5871265
Fix linting
matthewwalsh0 Dec 19, 2023
3ccf308
Merge branch 'main' into feat/estimate-user-operation-gas-fees
matthewwalsh0 Dec 19, 2023
73bb263
Fix case
matthewwalsh0 Dec 19, 2023
c8c35ef
Remove unnecessary changes
matthewwalsh0 Dec 19, 2023
83a3184
Add swap and type request options
matthewwalsh0 Jan 3, 2024
2486439
Fix destinationTokenDecimals type
matthewwalsh0 Jan 3, 2024
a70c83d
Add comments and return type
matthewwalsh0 Jan 8, 2024
76c4650
Merge branch 'main' into feat/estimate-user-operation-gas-fees
matthewwalsh0 Jan 8, 2024
12112e4
Update yarn lock
matthewwalsh0 Jan 8, 2024
a12a6e5
Update gas estimation payload
matthewwalsh0 Jan 9, 2024
8c2e89c
Merge branch 'feat/estimate-user-operation-gas-fees' into feat/user-o…
matthewwalsh0 Jan 9, 2024
353ee83
Merge branch 'main' into feat/user-operation-swap-support
matthewwalsh0 Jan 9, 2024
2aa62a6
feat: use `EthKeyring` interface
danroc Dec 1, 2023
1d25f30
feat: add UserOperation methods
danroc Dec 1, 2023
fefdf35
chore: format `package.json`
danroc Dec 1, 2023
72f92dc
fix: hardcode supported account methods in tests
danroc Dec 4, 2023
ca36ea0
chore: enable lcov for test coverage
danroc Dec 4, 2023
9d3b1bd
fix: ignore Snaps dependencies errors
danroc Dec 4, 2023
d79576c
test: add UserOperation tests
danroc Dec 4, 2023
54e8e29
chore: update @metamask/keyring-api version to 2.0.0
danroc Dec 8, 2023
a6cf95a
test: add `MockErc4337Keyring` import
danroc Dec 8, 2023
487a3f6
chore: update `keyring-api` in package.json files
danroc Dec 8, 2023
0442d7d
chore: update `yarn.lock`
danroc Jan 9, 2024
4a6d0d5
chore: remove duplicate dependency
danroc Jan 9, 2024
f6cfaa1
chore: remove unused directive
danroc Jan 9, 2024
3162e15
chore: add temporary resolution
danroc Jan 9, 2024
d0f68fe
Merge branch 'main' into feat/user-operation-swap-support
matthewwalsh0 Jan 10, 2024
228ccac
chore: put ignore error back and await promise
danroc Jan 12, 2024
fcc27eb
Merge branch 'main' into feat/user-operation-swap-support
matthewwalsh0 Jan 12, 2024
5a9b005
Merge branch 'feature/erc-4337-support' into feat/snap-smart-contract…
matthewwalsh0 Jan 12, 2024
cdb90e1
Add SnapSmartContractAccount
matthewwalsh0 Jan 12, 2024
9f6b5d5
Delete user operation metadata if rejected
matthewwalsh0 Jan 12, 2024
5a2a64f
Merge branch 'main' into feat/snap-smart-contract-account
matthewwalsh0 Jan 22, 2024
f46a5d4
Merge branch 'main' into feat/snap-smart-contract-account
matthewwalsh0 Jan 25, 2024
577bfdc
Remove unnecessary changes
matthewwalsh0 Jan 25, 2024
06d1fcd
Merge branch 'main' into feat/snap-smart-contract-account
matthewwalsh0 Jan 26, 2024
52b5717
Merge branch 'main' into feat/snap-smart-contract-account
matthewwalsh0 Jan 29, 2024
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 packages/user-operation-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
"@metamask/controller-utils": "^8.0.2",
"@metamask/eth-query": "^4.0.0",
"@metamask/gas-fee-controller": "^13.0.0",
"@metamask/keyring-controller": "^12.2.0",
"@metamask/network-controller": "^17.2.0",
"@metamask/polling-controller": "^5.0.0",
"@metamask/rpc-errors": "^6.1.0",
"@metamask/transaction-controller": "^21.0.0",
"@metamask/utils": "^8.3.0",
"ethereumjs-util": "^7.0.10",
Expand All @@ -60,6 +62,7 @@
"peerDependencies": {
"@metamask/approval-controller": "^5.1.2",
"@metamask/gas-fee-controller": "^13.0.0",
"@metamask/keyring-controller": "^12.2.0",
"@metamask/network-controller": "^17.2.0",
"@metamask/transaction-controller": "^21.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApprovalType } from '@metamask/controller-utils';
import { errorCodes } from '@metamask/rpc-errors';
import {
determineTransactionType,
TransactionType,
Expand All @@ -9,6 +10,7 @@ import { EventEmitter } from 'stream';
import { ADDRESS_ZERO, EMPTY_BYTES, VALUE_ZERO } from './constants';
import * as BundlerHelper from './helpers/Bundler';
import * as PendingUserOperationTrackerHelper from './helpers/PendingUserOperationTracker';
import { SnapSmartContractAccount } from './helpers/SnapSmartContractAccount';
import type { UserOperationMetadata } from './types';
import {
UserOperationStatus,
Expand Down Expand Up @@ -39,6 +41,7 @@ jest.mock('./utils/gas-fees');
jest.mock('./utils/validation');
jest.mock('./helpers/Bundler');
jest.mock('./helpers/PendingUserOperationTracker');
jest.mock('./helpers/SnapSmartContractAccount');

const CHAIN_ID_MOCK = '0x5';
const USER_OPERATION_HASH_MOCK = '0x123';
Expand Down Expand Up @@ -78,15 +81,16 @@ const SIGN_USER_OPERATION_RESPONSE_MOCK: SignUserOperationResponse = {
signature: '0xB',
};

const ADD_USER_OPERATION_REQUEST_MOCK = {
const ADD_USER_OPERATION_REQUEST_MOCK: AddUserOperationRequest = {
data: '0x1',
from: '0x12',
to: '0x2',
value: '0x3',
maxFeePerGas: '0x4',
maxPriorityFeePerGas: '0x5',
};

const ADD_USER_OPERATION_OPTIONS_MOCK = {
const ADD_USER_OPERATION_OPTIONS_MOCK: AddUserOperationOptions = {
networkClientId: NETWORK_CLIENT_ID_MOCK,
origin: ORIGIN_MOCK,
};
Expand Down Expand Up @@ -257,10 +261,10 @@ describe('UserOperationController', () => {
updateGasFeesMock
.mockImplementationOnce(async ({ metadata }) => {
metadata.userOperation.maxFeePerGas =
ADD_USER_OPERATION_REQUEST_MOCK.maxFeePerGas;
ADD_USER_OPERATION_REQUEST_MOCK.maxFeePerGas as string;

metadata.userOperation.maxPriorityFeePerGas =
ADD_USER_OPERATION_REQUEST_MOCK.maxPriorityFeePerGas;
ADD_USER_OPERATION_REQUEST_MOCK.maxPriorityFeePerGas as string;
})
.mockImplementationOnce(async ({ metadata }) => {
metadata.userOperation.maxFeePerGas = '0x6';
Expand Down Expand Up @@ -589,6 +593,26 @@ describe('UserOperationController', () => {
);
});

it('deletes user operation if rejected', async () => {
const controller = new UserOperationController(optionsMock);

const error = new Error(ERROR_MESSAGE_MOCK);
(error as unknown as Record<string, unknown>).code =
errorCodes.provider.userRejectedRequest;

approvalControllerAddRequestMock.mockClear();
approvalControllerAddRequestMock.mockRejectedValue(error);

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

await expect(hash()).rejects.toThrow(ERROR_MESSAGE_MOCK);

expect(Object.keys(controller.state.userOperations)).toHaveLength(0);
});

// eslint-disable-next-line jest/expect-expect
it('does not throw if hash function not invoked', async () => {
const controller = new UserOperationController(optionsMock);
Expand Down Expand Up @@ -782,6 +806,24 @@ describe('UserOperationController', () => {
expect(resultCallbackSuccessMock).not.toHaveBeenCalled();
});

it('uses snap smart contract account if no smart contract account provided', async () => {
const prepareMock = jest.spyOn(
SnapSmartContractAccount.prototype,
'prepareUserOperation',
);

const controller = new UserOperationController(optionsMock);

await addUserOperation(controller, ADD_USER_OPERATION_REQUEST_MOCK, {
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount: undefined,
});

await flushPromises();

expect(prepareMock).toHaveBeenCalledTimes(1);
});

describe('if approval request resolved with updated transaction', () => {
it('updates gas fees without regeneration if paymaster data not set', async () => {
const controller = new UserOperationController(optionsMock);
Expand Down Expand Up @@ -1078,27 +1120,25 @@ describe('UserOperationController', () => {
});
});

if (method === 'addUserOperation') {
it('validates arguments', async () => {
const controller = new UserOperationController(optionsMock);
it('validates arguments', async () => {
const controller = new UserOperationController(optionsMock);

await addUserOperation(controller, ADD_USER_OPERATION_REQUEST_MOCK, {
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
});
await addUserOperation(controller, ADD_USER_OPERATION_REQUEST_MOCK, {
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
});

expect(validateAddUserOperationRequestMock).toHaveBeenCalledTimes(1);
expect(validateAddUserOperationRequestMock).toHaveBeenCalledWith(
ADD_USER_OPERATION_REQUEST_MOCK,
);
expect(validateAddUserOperationRequestMock).toHaveBeenCalledTimes(1);
expect(validateAddUserOperationRequestMock).toHaveBeenCalledWith(
ADD_USER_OPERATION_REQUEST_MOCK,
);

expect(validateAddUserOperationOptionsMock).toHaveBeenCalledTimes(1);
expect(validateAddUserOperationOptionsMock).toHaveBeenCalledWith({
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
});
expect(validateAddUserOperationOptionsMock).toHaveBeenCalledTimes(1);
expect(validateAddUserOperationOptionsMock).toHaveBeenCalledWith({
...ADD_USER_OPERATION_OPTIONS_MOCK,
smartContractAccount,
});
}
});

if (method === 'addUserOperationFromTransaction') {
it('sets data as undefined if empty string', async () => {
Expand Down
80 changes: 61 additions & 19 deletions packages/user-operation-controller/src/UserOperationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ import { BaseController } from '@metamask/base-controller';
import { ApprovalType } from '@metamask/controller-utils';
import EthQuery from '@metamask/eth-query';
import type { GasFeeState } from '@metamask/gas-fee-controller';
import type {
KeyringControllerPrepareUserOperationAction,
KeyringControllerPatchUserOperationAction,
KeyringControllerSignUserOperationAction,
} from '@metamask/keyring-controller';
import type {
NetworkControllerGetNetworkClientByIdAction,
Provider,
} from '@metamask/network-controller';
import { errorCodes } from '@metamask/rpc-errors';
import {
determineTransactionType,
type TransactionMeta,
Expand All @@ -27,6 +33,7 @@ import { v1 as random } from 'uuid';
import { ADDRESS_ZERO, EMPTY_BYTES, VALUE_ZERO } from './constants';
import { Bundler } from './helpers/Bundler';
import { PendingUserOperationTracker } from './helpers/PendingUserOperationTracker';
import { SnapSmartContractAccount } from './helpers/SnapSmartContractAccount';
import { projectLogger as log } from './logger';
import type {
SmartContractAccount,
Expand Down Expand Up @@ -95,7 +102,10 @@ export type UserOperationStateChange = {
export type UserOperationControllerActions =
| GetUserOperationState
| NetworkControllerGetNetworkClientByIdAction
| AddApprovalRequest;
| AddApprovalRequest
| KeyringControllerPrepareUserOperationAction
| KeyringControllerPatchUserOperationAction
| KeyringControllerSignUserOperationAction;

export type UserOperationControllerEvents = UserOperationStateChange;

Expand All @@ -117,6 +127,7 @@ export type UserOperationControllerOptions = {

export type AddUserOperationRequest = {
data?: string;
from: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
to?: string;
Expand All @@ -138,7 +149,7 @@ export type AddUserOperationOptions = {
networkClientId: string;
origin: string;
requireApproval?: boolean;
smartContractAccount: SmartContractAccount;
smartContractAccount?: SmartContractAccount;
swaps?: AddUserOperationSwapOptions;
type?: TransactionType;
};
Expand All @@ -157,7 +168,9 @@ export type AddUserOperationResponse = {
type UserOperationCache = {
chainId: string;
metadata: UserOperationMetadata;
options: AddUserOperationOptions;
options: AddUserOperationOptions & {
smartContractAccount: SmartContractAccount;
};
provider: Provider;
request: AddUserOperationRequest;
transaction?: TransactionParams;
Expand Down Expand Up @@ -228,7 +241,9 @@ export class UserOperationController extends BaseController<
* @param options.networkClientId - ID of the network client used to query the chain.
* @param options.origin - Origin of the user operation, such as the hostname of a dApp.
* @param options.requireApproval - Whether to require user approval before submitting the user operation. Defaults to true.
* @param options.smartContractAccount - Smart contract abstraction to provide the contract specific values such as call data and nonce.
* @param options.smartContractAccount - Smart contract abstraction to provide the contract specific values such as call data and nonce. Defaults to the current snap account.
* @param options.swaps - Swap metadata to record with the user operation.
* @param options.type - Type of the transaction.
*/
async addUserOperation(
request: AddUserOperationRequest,
Expand All @@ -248,7 +263,7 @@ export class UserOperationController extends BaseController<
* @param options.networkClientId - ID of the network client used to query the chain.
* @param options.origin - Origin of the user operation, such as the hostname of a dApp.
* @param options.requireApproval - Whether to require user approval before submitting the user operation. Defaults to true.
* @param options.smartContractAccount - Smart contract abstraction to provide the contract specific values such as call data and nonce.
* @param options.smartContractAccount - Smart contract abstraction to provide the contract specific values such as call data and nonce. Defaults to the current snap account.
* @param options.swaps - Swap metadata to record with the user operation.
* @param options.type - Type of the transaction.
*/
Expand All @@ -258,18 +273,21 @@ export class UserOperationController extends BaseController<
): Promise<AddUserOperationResponse> {
validateAddUserOperationOptions(options);

const { data, maxFeePerGas, maxPriorityFeePerGas, to, value } = transaction;
const { data, from, maxFeePerGas, maxPriorityFeePerGas, to, value } =
transaction;

return await this.#addUserOperation(
{
data: data === '' ? undefined : data,
maxFeePerGas,
maxPriorityFeePerGas,
to,
value,
},
{ ...options, transaction },
);
const request: AddUserOperationRequest = {
data: data === '' ? undefined : data,
from,
maxFeePerGas,
maxPriorityFeePerGas,
to,
value,
};

validateAddUserOperationRequest(request);

return await this.#addUserOperation(request, { ...options, transaction });
}

startPollingByNetworkClientId(networkClientId: string): string {
Expand All @@ -284,7 +302,14 @@ export class UserOperationController extends BaseController<
): Promise<AddUserOperationResponse> {
log('Adding user operation', { request, options });

const { networkClientId, origin, transaction, swaps } = options;
const {
networkClientId,
origin,
smartContractAccount: requestSmartContractAccount,
swaps,
transaction,
} = options;

const { chainId, provider } = await this.#getProvider(networkClientId);

const metadata = await this.#createMetadata(
Expand All @@ -294,10 +319,14 @@ export class UserOperationController extends BaseController<
swaps,
);

const smartContractAccount =
requestSmartContractAccount ??
new SnapSmartContractAccount(this.messagingSystem);

const cache: UserOperationCache = {
chainId,
metadata,
options,
options: { ...options, smartContractAccount },
provider,
request,
transaction,
Expand Down Expand Up @@ -435,7 +464,7 @@ export class UserOperationController extends BaseController<
const { chainId, metadata, options, provider, request, transaction } =
cache;

const { data, to, value } = request;
const { data, from, to, value } = request;
const { id, transactionParams, userOperation } = metadata;
const { smartContractAccount } = options;

Expand All @@ -462,6 +491,7 @@ export class UserOperationController extends BaseController<
const response = await smartContractAccount.prepareUserOperation({
chainId,
data,
from,
to,
value,
});
Expand Down Expand Up @@ -591,6 +621,12 @@ export class UserOperationController extends BaseController<
metadata.status = UserOperationStatus.Failed;

this.#updateMetadata(metadata);

if (
String(rawError.code) === String(errorCodes.provider.userRejectedRequest)
) {
this.#deleteMetadata(id);
}
}

#createEmptyUserOperation(transaction?: TransactionParams): UserOperation {
Expand Down Expand Up @@ -619,6 +655,12 @@ export class UserOperationController extends BaseController<
this.#updateTransaction(metadata);
}

#deleteMetadata(id: string) {
this.update((state) => {
delete state.userOperations[id];
});
}

#updateTransaction(metadata: UserOperationMetadata) {
if (!metadata.transactionParams) {
return;
Expand Down
Loading
Loading