Skip to content

Commit

Permalink
Support Layer1GasFeeFlows and add layer1GasFee property to `Trans…
Browse files Browse the repository at this point in the history
…actionMeta` (#3944)

## Explanation

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

This PR implements support of `Layer1GasFeeFlows` and add `layer1GasFee`
property to `TransactionMeta`

## References

<!--
Are there any issues that this pull request is tied to? Are there other
links that reviewers should consult to understand these changes better?

For example:

* Fixes #12345
* Related to #67890
-->

* Fixes MetaMask/MetaMask-planning#2031

## Changelog

<!--
If you're making any consumer-facing changes, list those changes here as
if you were updating a changelog, using the template below as a guide.

(CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or
FIXED. For security-related issues, follow the Security Advisory
process.)

Please take care to name the exact pieces of the API you've added or
changed (e.g. types, interfaces, functions, or methods).

If there are any breaking changes, make sure to offer a solution for
consumers to follow once they upgrade to the changes.

Finally, if you're only making changes to development scripts or tests,
you may replace the template below with "None".
-->

### `@metamask/transaction-controller`

- **ADDED**: Support `Layer1GasFeeFlows` and add `layer1GasFee` property
to `TransactionMeta`

## Checklist

- [X] I've updated the test suite for new or updated code as appropriate
- [X] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [X] I've highlighted breaking changes using the "BREAKING" category
above as appropriate

---------

Co-authored-by: Matthew Walsh <[email protected]>
  • Loading branch information
OGPoyraz and matthewwalsh0 authored Mar 13, 2024
1 parent 14f625a commit d203db7
Show file tree
Hide file tree
Showing 9 changed files with 415 additions and 30 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: 94.2,
functions: 98.48,
lines: 98.91,
statements: 98.93,
branches: 94.23,
functions: 98.5,
lines: 98.93,
statements: 98.94,
},
},

Expand Down
27 changes: 27 additions & 0 deletions packages/transaction-controller/src/TransactionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
} from './types';
import { addGasBuffer, estimateGas, updateGas } from './utils/gas';
import { updateGasFees } from './utils/gas-fees';
import { updateTransactionLayer1GasFee } from './utils/layer1-gas-fee-flow';
import { getSimulationData } from './utils/simulation';
import {
updatePostTransactionBalance,
Expand Down Expand Up @@ -91,6 +92,7 @@ jest.mock('./helpers/PendingTransactionTracker');
jest.mock('./utils/gas');
jest.mock('./utils/gas-fees');
jest.mock('./utils/swaps');
jest.mock('./utils/layer1-gas-fee-flow');
jest.mock('./utils/simulation');

jest.mock('uuid');
Expand Down Expand Up @@ -5433,6 +5435,31 @@ describe('TransactionController', () => {
expect(updatedTransaction?.txParams).toStrictEqual(params);
});

it('updates transaction layer 1 gas fee updater', async () => {
const { controller } = setupController({
options: {
state: {
transactions: [transactionMeta],
},
},
});

const updatedTransaction = await controller.updateEditableParams(
transactionId,
params,
);

expect(updateTransactionLayer1GasFee).toHaveBeenCalledTimes(1);
expect(updateTransactionLayer1GasFee).toHaveBeenCalledWith(
expect.objectContaining({
transactionMeta: {
...updatedTransaction,
history: expect.any(Array),
},
}),
);
});

it('throws an error if no transaction metadata is found', async () => {
const { controller } = setupController();
await expect(
Expand Down
46 changes: 34 additions & 12 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { PendingTransactionTracker } from './helpers/PendingTransactionTracker';
import { projectLogger as log } from './logger';
import type {
DappSuggestedGasFees,
Layer1GasFeeFlow,
SavedGasFees,
SecurityProviderRequest,
SendFlowHistoryEntry,
Expand All @@ -80,6 +81,7 @@ import {
addInitialHistorySnapshot,
updateTransactionHistory,
} from './utils/history';
import { updateTransactionLayer1GasFee } from './utils/layer1-gas-fee-flow';
import {
getAndFormatTransactionsForNonceTracker,
getNextNonce,
Expand Down Expand Up @@ -591,6 +593,8 @@ export class TransactionController extends BaseController<
chainId?: string,
) => NonceTrackerTransaction[];

private readonly layer1GasFeeFlows: Layer1GasFeeFlow[];

readonly #incomingTransactionOptions: IncomingTransactionOptions;

private readonly incomingTransactionHelper: IncomingTransactionHelper;
Expand Down Expand Up @@ -844,6 +848,7 @@ export class TransactionController extends BaseController<
});

this.gasFeeFlows = this.#getGasFeeFlows();
this.layer1GasFeeFlows = this.#getLayer1GasFeeFlows();

const gasFeePoller = new GasFeePoller({
// Default gas fee polling is not yet supported by the clients
Expand All @@ -855,6 +860,7 @@ export class TransactionController extends BaseController<
}),
getGasFeeControllerEstimates: this.getGasFeeEstimates,
getTransactions: () => this.state.transactions,
layer1GasFeeFlows: this.layer1GasFeeFlows,
onStateChange: (listener) => {
this.messagingSystem.subscribe(
'TransactionController:stateChange',
Expand Down Expand Up @@ -1923,15 +1929,22 @@ export class TransactionController extends BaseController<
) as TransactionParams;

const updatedTransaction = merge({}, transactionMeta, editableParams);
const ethQuery = this.#multichainTrackingHelper.getEthQuery({
networkClientId: transactionMeta.networkClientId,
chainId: transactionMeta.chainId,
});
const { type } = await determineTransactionType(
updatedTransaction.txParams,
this.#multichainTrackingHelper.getEthQuery({
networkClientId: transactionMeta.networkClientId,
chainId: transactionMeta.chainId,
}),
ethQuery,
);
updatedTransaction.type = type;

await updateTransactionLayer1GasFee({
ethQuery,
layer1GasFeeFlows: this.layer1GasFeeFlows,
transactionMeta: updatedTransaction,
});

this.updateTransaction(
updatedTransaction,
`Update Editable Params for ${txId}`,
Expand Down Expand Up @@ -2320,27 +2333,32 @@ export class TransactionController extends BaseController<
).configuration.type === NetworkClientType.Custom
: this.getNetworkState().providerConfig.type === NetworkType.rpc;

const ethQuery = this.#multichainTrackingHelper.getEthQuery({
networkClientId,
chainId,
});

await updateGas({
ethQuery: this.#multichainTrackingHelper.getEthQuery({
networkClientId,
chainId,
}),
ethQuery,
chainId,
isCustomNetwork,
txMeta: transactionMeta,
});

await updateGasFees({
eip1559: isEIP1559Compatible,
ethQuery: this.#multichainTrackingHelper.getEthQuery({
networkClientId,
chainId,
}),
ethQuery,
gasFeeFlows: this.gasFeeFlows,
getGasFeeEstimates: this.getGasFeeEstimates,
getSavedGasFees: this.getSavedGasFees.bind(this),
txMeta: transactionMeta,
});

await updateTransactionLayer1GasFee({
ethQuery,
layer1GasFeeFlows: this.layer1GasFeeFlows,
transactionMeta,
});
}

private onBootCleanup() {
Expand Down Expand Up @@ -3459,6 +3477,10 @@ export class TransactionController extends BaseController<
return [new LineaGasFeeFlow(), new DefaultGasFeeFlow()];
}

#getLayer1GasFeeFlows(): Layer1GasFeeFlow[] {
return [];
}

#updateTransactionInternal(
transactionMeta: TransactionMeta,
{ note, skipHistory }: { note?: string; skipHistory?: boolean },
Expand Down
5 changes: 5 additions & 0 deletions packages/transaction-controller/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
export const CHAIN_IDS = {
MAINNET: '0x1',
GOERLI: '0x5',
BASE: '0x2105',
BASE_TESTNET: '0x14a33',
BSC: '0x38',
BSC_TESTNET: '0x61',
OPTIMISM: '0xa',
OPTIMISM_TESTNET: '0x1a4',
OPBNB: '0xcc',
OPBNB_TESTNET: '0x15eb',
OPTIMISM_SEPOLIA: '0xaa37dc',
POLYGON: '0x89',
POLYGON_TESTNET: '0x13881',
Expand Down
72 changes: 64 additions & 8 deletions packages/transaction-controller/src/helpers/GasFeePoller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import type EthQuery from '@metamask/eth-query';
import type { Hex } from '@metamask/utils';

import { flushPromises } from '../../../../tests/helpers';
import type { GasFeeFlowResponse } from '../types';
import type { GasFeeFlowResponse, Layer1GasFeeFlow } from '../types';
import {
TransactionStatus,
type GasFeeFlow,
type TransactionMeta,
} from '../types';
import { updateTransactionLayer1GasFee } from '../utils/layer1-gas-fee-flow';
import { GasFeePoller } from './GasFeePoller';

jest.mock('../utils/layer1-gas-fee-flow', () => ({
updateTransactionLayer1GasFee: jest.fn(),
}));

jest.useFakeTimers();

const CHAIN_ID_MOCK: Hex = '0x123';
Expand Down Expand Up @@ -38,6 +43,8 @@ const GAS_FEE_FLOW_RESPONSE_MOCK: GasFeeFlowResponse = {
},
};

const LAYER1_GAS_FEE_MOCK = '0x123';

/**
* Creates a mock GasFeeFlow.
* @returns The mock GasFeeFlow.
Expand All @@ -54,6 +61,21 @@ describe('GasFeePoller', () => {
let gasFeeFlowMock: jest.Mocked<GasFeeFlow>;
let triggerOnStateChange: () => void;
let getTransactionsMock: jest.MockedFunction<() => TransactionMeta[]>;
const updateTransactionLayer1GasFeeMock =
updateTransactionLayer1GasFee as jest.MockedFunction<
typeof updateTransactionLayer1GasFee
>;
// As we mock implementation of updateTransactionLayer1GasFee, it does not matter if we pass matching flow here
const layer1GasFeeFlowsMock: jest.Mocked<Layer1GasFeeFlow[]> = [];

const transactionLayer1GasFeeUpdater = ({
transactionMeta,
}: {
transactionMeta: TransactionMeta;
}) => {
transactionMeta.layer1GasFee = LAYER1_GAS_FEE_MOCK;
return Promise.resolve();
};

beforeEach(() => {
jest.resetAllMocks();
Expand All @@ -64,13 +86,18 @@ describe('GasFeePoller', () => {
gasFeeFlowMock.getGasFees.mockResolvedValue(GAS_FEE_FLOW_RESPONSE_MOCK);

getTransactionsMock = jest.fn();
getTransactionsMock.mockReturnValue([TRANSACTION_META_MOCK]);
getTransactionsMock.mockReturnValue([{ ...TRANSACTION_META_MOCK }]);

updateTransactionLayer1GasFeeMock.mockImplementation(
transactionLayer1GasFeeUpdater,
);

constructorOptions = {
gasFeeFlows: [gasFeeFlowMock],
getEthQuery: () => ({} as EthQuery),
getGasFeeControllerEstimates: jest.fn(),
getTransactions: getTransactionsMock,
layer1GasFeeFlows: layer1GasFeeFlowsMock,
onStateChange: (listener: () => void) => {
triggerOnStateChange = listener;
},
Expand All @@ -79,7 +106,18 @@ describe('GasFeePoller', () => {

describe('on state change', () => {
describe('if unapproved transaction', () => {
beforeEach(() => {
// to avoid side effect of the mock implementation
// otherwise argument assertion would fail because mock.calls[][] holds reference
updateTransactionLayer1GasFeeMock.mockResolvedValue(undefined);
});

it('emits updated event', async () => {
// skip into original implementation
updateTransactionLayer1GasFeeMock.mockImplementationOnce(
transactionLayer1GasFeeUpdater,
);

const listener = jest.fn();

const gasFeePoller = new GasFeePoller(constructorOptions);
Expand All @@ -88,22 +126,19 @@ describe('GasFeePoller', () => {
triggerOnStateChange();
await flushPromises();

expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenCalledWith({
...TRANSACTION_META_MOCK,
gasFeeEstimates: GAS_FEE_FLOW_RESPONSE_MOCK.estimates,
layer1GasFee: LAYER1_GAS_FEE_MOCK,
gasFeeEstimatesLoaded: true,
});
});

it('calls gas fee flow', async () => {
const listener = jest.fn();

const gasFeePoller = new GasFeePoller(constructorOptions);
gasFeePoller.hub.on('transaction-updated', listener);
new GasFeePoller(constructorOptions);

triggerOnStateChange();
await flushPromises();

expect(gasFeeFlowMock.getGasFees).toHaveBeenCalledTimes(1);
expect(gasFeeFlowMock.getGasFees).toHaveBeenCalledWith({
Expand All @@ -114,6 +149,19 @@ describe('GasFeePoller', () => {
});
});

it('calls layer1 gas fee updater', async () => {
new GasFeePoller(constructorOptions);

triggerOnStateChange();

expect(updateTransactionLayer1GasFeeMock).toHaveBeenCalledTimes(1);
expect(updateTransactionLayer1GasFeeMock).toHaveBeenCalledWith({
ethQuery: expect.any(Object),
layer1GasFeeFlows: layer1GasFeeFlowsMock,
transactionMeta: TRANSACTION_META_MOCK,
});
});

it('creates polling timeout', async () => {
new GasFeePoller(constructorOptions);

Expand Down Expand Up @@ -179,6 +227,9 @@ describe('GasFeePoller', () => {
const listener = jest.fn();

gasFeeFlowMock.matchesTransaction.mockReturnValue(false);
updateTransactionLayer1GasFeeMock.mockImplementation(() => {
return Promise.resolve();
});

getTransactionsMock.mockReturnValue([
{ ...TRANSACTION_META_MOCK, gasFeeEstimatesLoaded: true },
Expand All @@ -196,6 +247,11 @@ describe('GasFeePoller', () => {
it('gas fee flow throws and already loaded', async () => {
const listener = jest.fn();

// to make sure update will be called by gas fee flow
updateTransactionLayer1GasFeeMock.mockImplementation(() => {
return Promise.resolve();
});

gasFeeFlowMock.getGasFees.mockRejectedValue(new Error('TestError'));

getTransactionsMock.mockReturnValue([
Expand Down
Loading

0 comments on commit d203db7

Please sign in to comment.