Skip to content

Commit

Permalink
Simulate transactions (#4020)
Browse files Browse the repository at this point in the history
Simulate transactions added to the `TransactionController`.
Add `isSimulationEnabled` constructor option to dynamically disable.
  • Loading branch information
matthewwalsh0 authored Mar 7, 2024
1 parent 9c9ec73 commit 4b01dc3
Show file tree
Hide file tree
Showing 10 changed files with 1,441 additions and 6 deletions.
8 changes: 4 additions & 4 deletions packages/transaction-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 91.79,
functions: 98.37,
lines: 98.83,
statements: 98.84,
branches: 94.06,
functions: 98.48,
lines: 98.87,
statements: 98.88,
},
},

Expand Down
1 change: 1 addition & 0 deletions packages/transaction-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@ethereumjs/tx": "^4.2.0",
"@ethereumjs/util": "^8.1.0",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@metamask/approval-controller": "^5.1.3",
"@metamask/base-controller": "^4.1.1",
"@metamask/controller-utils": "^8.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,17 @@ import type {
TransactionParams,
TransactionHistoryEntry,
TransactionError,
SimulationData,
} from './types';
import {
SimulationTokenStandard,
TransactionStatus,
TransactionType,
WalletDevice,
} from './types';
import { TransactionStatus, TransactionType, WalletDevice } from './types';
import { addGasBuffer, estimateGas, updateGas } from './utils/gas';
import { updateGasFees } from './utils/gas-fees';
import { getSimulationData } from './utils/simulation';
import {
updatePostTransactionBalance,
updateSwapsTransaction,
Expand Down Expand Up @@ -84,6 +91,7 @@ jest.mock('./helpers/PendingTransactionTracker');
jest.mock('./utils/gas');
jest.mock('./utils/gas-fees');
jest.mock('./utils/swaps');
jest.mock('./utils/simulation');

jest.mock('uuid');

Expand Down Expand Up @@ -428,6 +436,26 @@ const TRANSACTION_META_2_MOCK = {
},
} as TransactionMeta;

const SIMULATION_DATA_MOCK: SimulationData = {
nativeBalanceChange: {
previousBalance: '0x0',
newBalance: '0x1',
difference: '0x1',
isDecrease: false,
},
tokenBalanceChanges: [
{
address: '0x123',
standard: SimulationTokenStandard.erc721,
id: '0x456',
previousBalance: '0x1',
newBalance: '0x3',
difference: '0x2',
isDecrease: false,
},
],
};

describe('TransactionController', () => {
const uuidModuleMock = jest.mocked(uuidModule);
const EthQueryMock = jest.mocked(EthQuery);
Expand All @@ -442,6 +470,7 @@ describe('TransactionController', () => {
const defaultGasFeeFlowClassMock = jest.mocked(DefaultGasFeeFlow);
const lineaGasFeeFlowClassMock = jest.mocked(LineaGasFeeFlow);
const gasFeePollerClassMock = jest.mocked(GasFeePoller);
const getSimulationDataMock = jest.mocked(getSimulationData);

let mockEthQuery: EthQuery;
let getNonceLockSpy: jest.Mock;
Expand Down Expand Up @@ -1346,6 +1375,7 @@ describe('TransactionController', () => {
origin: undefined,
securityAlertResponse: undefined,
sendFlowHistory: expect.any(Array),
simulationData: undefined,
status: TransactionStatus.unapproved as const,
time: expect.any(Number),
txParams: expect.anything(),
Expand Down Expand Up @@ -1653,6 +1683,46 @@ describe('TransactionController', () => {
});
});

it('updates simulation data by default', async () => {
getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK);

const { controller } = setupController();

await controller.addTransaction({
from: ACCOUNT_MOCK,
to: ACCOUNT_MOCK,
});

expect(getSimulationDataMock).toHaveBeenCalledTimes(1);
expect(getSimulationDataMock).toHaveBeenCalledWith({
chainId: MOCK_NETWORK.state.providerConfig.chainId,
data: undefined,
from: ACCOUNT_MOCK,
to: ACCOUNT_MOCK,
value: '0x0',
});

expect(controller.state.transactions[0].simulationData).toStrictEqual(
SIMULATION_DATA_MOCK,
);
});

it('does not update simulation data if simulation disabled', async () => {
getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK);

const { controller } = setupController({
options: { isSimulationEnabled: () => false },
});

await controller.addTransaction({
from: ACCOUNT_MOCK,
to: ACCOUNT_MOCK,
});

expect(getSimulationDataMock).toHaveBeenCalledTimes(0);
expect(controller.state.transactions[0].simulationData).toBeUndefined();
});

describe('on approve', () => {
it('submits transaction', async () => {
const { controller, messenger } = setupController({
Expand Down
33 changes: 32 additions & 1 deletion packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
getAndFormatTransactionsForNonceTracker,
getNextNonce,
} from './utils/nonce';
import { getSimulationData } from './utils/simulation';
import {
updatePostTransactionBalance,
updateSwapsTransaction,
Expand Down Expand Up @@ -245,6 +246,7 @@ export type PendingTransactionOptions = {
* @property getSelectedAddress - Gets the address of the currently selected account.
* @property incomingTransactions - Configuration options for incoming transaction support.
* @property isMultichainEnabled - Enable multichain support.
* @property isSimulationEnabled - Whether new transactions will be automatically simulated.
* @property messenger - The controller messenger.
* @property onNetworkStateChange - Allows subscribing to network controller state changes.
* @property pendingTransactions - Configuration options for pending transaction support.
Expand Down Expand Up @@ -280,6 +282,7 @@ export type TransactionControllerOptions = {
getSelectedAddress: () => string;
incomingTransactions?: IncomingTransactionOptions;
isMultichainEnabled: boolean;
isSimulationEnabled?: () => boolean;
messenger: TransactionControllerMessenger;
onNetworkStateChange: (listener: (state: NetworkState) => void) => void;
pendingTransactions?: PendingTransactionOptions;
Expand Down Expand Up @@ -602,6 +605,8 @@ export class TransactionController extends BaseController<

#transactionHistoryLimit: number;

#isSimulationEnabled: () => boolean;

private readonly afterSign: (
transactionMeta: TransactionMeta,
signedTx: TypedTransaction,
Expand Down Expand Up @@ -697,6 +702,7 @@ export class TransactionController extends BaseController<
* @param options.getSelectedAddress - Gets the address of the currently selected account.
* @param options.incomingTransactions - Configuration options for incoming transaction support.
* @param options.isMultichainEnabled - Enable multichain support.
* @param options.isSimulationEnabled - Whether new transactions will be automatically simulated.
* @param options.messenger - The controller messenger.
* @param options.onNetworkStateChange - Allows subscribing to network controller state changes.
* @param options.pendingTransactions - Configuration options for pending transaction support.
Expand All @@ -723,6 +729,7 @@ export class TransactionController extends BaseController<
getSelectedAddress,
incomingTransactions = {},
isMultichainEnabled = false,
isSimulationEnabled,
messenger,
onNetworkStateChange,
pendingTransactions = {},
Expand All @@ -748,6 +755,7 @@ export class TransactionController extends BaseController<
this.isSendFlowHistoryDisabled = disableSendFlowHistory ?? false;
this.isHistoryDisabled = disableHistory ?? false;
this.isSwapsDisabled = disableSwaps ?? false;
this.#isSimulationEnabled = isSimulationEnabled ?? (() => true);
// @ts-expect-error the type in eth-method-registry is inappropriate and should be changed
this.registry = new MethodRegistry({ provider });
this.getSavedGasFees = getSavedGasFees ?? ((_chainId) => undefined);
Expand Down Expand Up @@ -1024,7 +1032,10 @@ export class TransactionController extends BaseController<
networkClientId,
};

await this.updateGasProperties(addedTransactionMeta);
await Promise.all([
this.updateGasProperties(addedTransactionMeta),
this.#simulateTransaction(addedTransactionMeta),
]);

// Checks if a transaction already exists with a given actionId
if (!existingTransactionMeta) {
Expand Down Expand Up @@ -3474,4 +3485,24 @@ export class TransactionController extends BaseController<
state.transactions[index] = transactionWithUpdatedHistory;
});
}

async #simulateTransaction(transactionMeta: TransactionMeta) {
if (!this.#isSimulationEnabled()) {
log('Skipping simulation as disabled');
return;
}

const { chainId, txParams } = transactionMeta;
const { from, to, value, data } = txParams;

transactionMeta.simulationData = await getSimulationData({
chainId,
from: from as Hex,
to: to as Hex,
value: value as Hex,
data: data as Hex,
});

log('Retrieved simulation data', transactionMeta.simulationData);
}
}
52 changes: 52 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ type TransactionMetaBase = {
*/
sendFlowHistory?: SendFlowHistoryEntry[];

/**
* Simulation data for the transaction used to predict its outcome.
*/
simulationData?: SimulationData;

/**
* If the gas estimation fails, an object containing error and block information.
*/
Expand Down Expand Up @@ -1017,3 +1022,50 @@ export type GasFeeFlow = {
*/
getGasFees: (request: GasFeeFlowRequest) => Promise<GasFeeFlowResponse>;
};

/** Simulation data concerning an update to a native or token balance. */
export type SimulationBalanceChange = {
/** The balance before the transaction. */
previousBalance: Hex;

/** The balance after the transaction. */
newBalance: Hex;

/** The difference in balance. */
difference: Hex;

/** Whether the balance is increasing or decreasing. */
isDecrease: boolean;
};

/** Token standards supported by simulation. */
export enum SimulationTokenStandard {
erc20 = 'erc20',
erc721 = 'erc721',
erc1155 = 'erc1155',
}

/** Simulation data concerning an updated token. */
export type SimulationToken = {
/** The token's contract address. */
address: Hex;

/** The standard of the token. */
standard: SimulationTokenStandard;

/** The ID of the token if supported by the standard. */
id?: Hex;
};

/** Simulation data concerning a change to the a token balance. */
export type SimulationTokenBalanceChange = SimulationToken &
SimulationBalanceChange;

/** Simulation data for a transaction. */
export type SimulationData = {
/** Data concerning a change to the user's native balance. */
nativeBalanceChange?: SimulationBalanceChange;

/** Data concerning a change to the user's token balances. */
tokenBalanceChanges: SimulationTokenBalanceChange[];
};
Loading

0 comments on commit 4b01dc3

Please sign in to comment.