Skip to content

Commit 3913167

Browse files
feat: add gas fee tokens to transaction metadata (#5524)
## Explanation Retrieve the available gas fee tokens from the simulation API when adding a transaction, and save them in the transaction metadata. Specifically: - Add `gasFeeTokens` to `TransactionMetadata`. - Add `selectedGasFeeToken` to `TransactionMetadata`. - Add additional request and response properties to types in `utils/simulation-api.ts`. - Update `utils/simulation.ts` to parse the gas fee tokens from the response. ## References Fixes [#4458](MetaMask/MetaMask-planning#4458) ## Changelog See `CHANGELOG.md`. ## 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 - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent 85894b8 commit 3913167

File tree

8 files changed

+534
-67
lines changed

8 files changed

+534
-67
lines changed

packages/transaction-controller/CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add `gasFeeTokens` to `TransactionMeta` ([#5524](https://github.com/MetaMask/core/pull/5524))
13+
- Add `GasFeeToken` type.
14+
- Add `selectedGasFeeToken` to `TransactionMeta`.
15+
- Add `updateSelectedGasFeeToken` method.
1216
- Support security validation of transaction batches ([#5526](https://github.com/MetaMask/core/pull/5526))
1317
- Add `ValidateSecurityRequest` type.
1418
- Add optional `securityAlertId` to `SecurityAlertResponse`.
@@ -69,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6973
### Added
7074

7175
- Add additional metadata for batch metrics ([#5488](https://github.com/MetaMask/core/pull/5488))
72-
- Add `delegationAddress` to `TransactionMetadata`.
76+
- Add `delegationAddress` to `TransactionMeta`.
7377
- Add `NestedTransactionMetadata` type containing `BatchTransactionParams` and `type`.
7478
- Add optional `type` to `TransactionBatchSingleRequest`.
7579
- Verify EIP-7702 contract address using signatures ([#5472](https://github.com/MetaMask/core/pull/5472))
@@ -80,8 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8084

8185
- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency to `^26.1.0` ([#5481](https://github.com/MetaMask/core/pull/5481))
8286
- **BREAKING:** Add additional metadata for batch metrics ([#5488](https://github.com/MetaMask/core/pull/5488))
83-
- Change `error` in `TransactionMetadata` to optional for all statuses.
84-
- Change `nestedTransactions` in `TransactionMetadata` to array of `NestedTransactionMetadata`.
87+
- Change `error` in `TransactionMeta` to optional for all statuses.
88+
- Change `nestedTransactions` in `TransactionMeta` to array of `NestedTransactionMetadata`.
8589
- Throw if `addTransactionBatch` called with external origin and size limit exceeded ([#5489](https://github.com/MetaMask/core/pull/5489))
8690
- Verify EIP-7702 contract address using signatures ([#5472](https://github.com/MetaMask/core/pull/5472))
8791
- Use new `contracts` property from feature flags instead of `contractAddresses`.

packages/transaction-controller/src/TransactionController.test.ts

Lines changed: 150 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ import type {
6262
TransactionParams,
6363
TransactionHistoryEntry,
6464
TransactionError,
65-
SimulationData,
6665
GasFeeFlow,
6766
GasFeeFlowResponse,
6867
SubmitHistoryEntry,
6968
InternalAccount,
7069
PublishHook,
70+
GasFeeToken,
7171
} from './types';
7272
import {
7373
GasFeeEstimateType,
@@ -86,6 +86,7 @@ import {
8686
getTransactionLayer1GasFee,
8787
updateTransactionLayer1GasFee,
8888
} from './utils/layer1-gas-fee-flow';
89+
import type { GetSimulationDataResult } from './utils/simulation';
8990
import { getSimulationData } from './utils/simulation';
9091
import {
9192
updatePostTransactionBalance,
@@ -448,24 +449,40 @@ const TRANSACTION_META_2_MOCK = {
448449
},
449450
} as TransactionMeta;
450451

451-
const SIMULATION_DATA_MOCK: SimulationData = {
452-
nativeBalanceChange: {
453-
previousBalance: '0x0',
454-
newBalance: '0x1',
455-
difference: '0x1',
456-
isDecrease: false,
457-
},
458-
tokenBalanceChanges: [
459-
{
460-
address: '0x123',
461-
standard: SimulationTokenStandard.erc721,
462-
id: '0x456',
463-
previousBalance: '0x1',
464-
newBalance: '0x3',
465-
difference: '0x2',
452+
const SIMULATION_DATA_RESULT_MOCK: GetSimulationDataResult = {
453+
gasFeeTokens: [],
454+
simulationData: {
455+
nativeBalanceChange: {
456+
previousBalance: '0x0',
457+
newBalance: '0x1',
458+
difference: '0x1',
466459
isDecrease: false,
467460
},
468-
],
461+
tokenBalanceChanges: [
462+
{
463+
address: '0x123',
464+
standard: SimulationTokenStandard.erc721,
465+
id: '0x456',
466+
previousBalance: '0x1',
467+
newBalance: '0x3',
468+
difference: '0x2',
469+
isDecrease: false,
470+
},
471+
],
472+
},
473+
};
474+
475+
const GAS_FEE_TOKEN_MOCK: GasFeeToken = {
476+
amount: '0x1',
477+
balance: '0x2',
478+
decimals: 18,
479+
gas: '0x3',
480+
maxFeePerGas: '0x4',
481+
maxPriorityFeePerGas: '0x5',
482+
rateWei: '0x6',
483+
recipient: '0x7',
484+
symbol: 'ETH',
485+
tokenAddress: '0x8',
469486
};
470487

471488
const GAS_FEE_ESTIMATES_MOCK: GasFeeFlowResponse = {
@@ -1990,7 +2007,9 @@ describe('TransactionController', () => {
19902007

19912008
describe('updates simulation data', () => {
19922009
it('by default', async () => {
1993-
getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK);
2010+
getSimulationDataMock.mockResolvedValueOnce(
2011+
SIMULATION_DATA_RESULT_MOCK,
2012+
);
19942013

19952014
const { controller } = setupController();
19962015

@@ -2021,12 +2040,14 @@ describe('TransactionController', () => {
20212040
);
20222041

20232042
expect(controller.state.transactions[0].simulationData).toStrictEqual(
2024-
SIMULATION_DATA_MOCK,
2043+
SIMULATION_DATA_RESULT_MOCK.simulationData,
20252044
);
20262045
});
20272046

20282047
it('with error if simulation disabled', async () => {
2029-
getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK);
2048+
getSimulationDataMock.mockResolvedValueOnce(
2049+
SIMULATION_DATA_RESULT_MOCK,
2050+
);
20302051

20312052
const { controller } = setupController({
20322053
options: { isSimulationEnabled: () => false },
@@ -2053,7 +2074,9 @@ describe('TransactionController', () => {
20532074
});
20542075

20552076
it('unless approval not required', async () => {
2056-
getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK);
2077+
getSimulationDataMock.mockResolvedValueOnce(
2078+
SIMULATION_DATA_RESULT_MOCK,
2079+
);
20572080

20582081
const { controller } = setupController();
20592082

@@ -2070,6 +2093,57 @@ describe('TransactionController', () => {
20702093
});
20712094
});
20722095

2096+
describe('updates gas fee tokens', () => {
2097+
it('by default', async () => {
2098+
getSimulationDataMock.mockResolvedValueOnce({
2099+
gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
2100+
simulationData: {
2101+
tokenBalanceChanges: [],
2102+
},
2103+
});
2104+
2105+
const { controller } = setupController();
2106+
2107+
await controller.addTransaction(
2108+
{
2109+
from: ACCOUNT_MOCK,
2110+
to: ACCOUNT_MOCK,
2111+
},
2112+
{
2113+
networkClientId: NETWORK_CLIENT_ID_MOCK,
2114+
},
2115+
);
2116+
2117+
await flushPromises();
2118+
2119+
expect(controller.state.transactions[0].gasFeeTokens).toStrictEqual([
2120+
GAS_FEE_TOKEN_MOCK,
2121+
]);
2122+
});
2123+
2124+
it('unless approval not required', async () => {
2125+
getSimulationDataMock.mockResolvedValueOnce({
2126+
gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
2127+
simulationData: {
2128+
tokenBalanceChanges: [],
2129+
},
2130+
});
2131+
2132+
const { controller } = setupController();
2133+
2134+
await controller.addTransaction(
2135+
{
2136+
from: ACCOUNT_MOCK,
2137+
to: ACCOUNT_MOCK,
2138+
},
2139+
{ requireApproval: false, networkClientId: NETWORK_CLIENT_ID_MOCK },
2140+
);
2141+
2142+
expect(getSimulationDataMock).toHaveBeenCalledTimes(0);
2143+
expect(controller.state.transactions[0].gasFeeTokens).toBeUndefined();
2144+
});
2145+
});
2146+
20732147
describe('on approve', () => {
20742148
it('submits transaction', async () => {
20752149
const { controller, messenger } = setupController({
@@ -6575,4 +6649,59 @@ describe('TransactionController', () => {
65756649
);
65766650
});
65776651
});
6652+
6653+
describe('updateSelectedGasFeeToken', () => {
6654+
it('updates selected gas fee token in state', () => {
6655+
const { controller } = setupController({
6656+
options: {
6657+
state: {
6658+
transactions: [
6659+
{
6660+
...TRANSACTION_META_MOCK,
6661+
gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
6662+
},
6663+
],
6664+
},
6665+
},
6666+
});
6667+
6668+
controller.updateSelectedGasFeeToken(
6669+
TRANSACTION_META_MOCK.id,
6670+
GAS_FEE_TOKEN_MOCK.tokenAddress,
6671+
);
6672+
6673+
expect(controller.state.transactions[0].selectedGasFeeToken).toBe(
6674+
GAS_FEE_TOKEN_MOCK.tokenAddress,
6675+
);
6676+
});
6677+
6678+
it('throws if transaction does not exist', () => {
6679+
const { controller } = setupController();
6680+
6681+
expect(() =>
6682+
controller.updateSelectedGasFeeToken(
6683+
TRANSACTION_META_MOCK.id,
6684+
GAS_FEE_TOKEN_MOCK.tokenAddress,
6685+
),
6686+
).toThrow(
6687+
`Cannot update transaction as ID not found - ${TRANSACTION_META_MOCK.id}`,
6688+
);
6689+
});
6690+
6691+
it('throws if no matching gas fee token', () => {
6692+
const { controller } = setupController({
6693+
options: {
6694+
state: {
6695+
transactions: [
6696+
{ ...TRANSACTION_META_MOCK, gasFeeTokens: [GAS_FEE_TOKEN_MOCK] },
6697+
],
6698+
},
6699+
},
6700+
});
6701+
6702+
expect(() =>
6703+
controller.updateSelectedGasFeeToken(TRANSACTION_META_MOCK.id, '0x123'),
6704+
).toThrow('No matching gas fee token found');
6705+
});
6706+
});
65786707
});

packages/transaction-controller/src/TransactionController.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import type {
100100
BatchTransactionParams,
101101
PublishHook,
102102
PublishBatchHook,
103+
GasFeeToken,
103104
} from './types';
104105
import {
105106
TransactionEnvelopeType,
@@ -2509,6 +2510,32 @@ export class TransactionController extends BaseController<
25092510
);
25102511
}
25112512

2513+
/**
2514+
* Update the selected gas fee token for a transaction.
2515+
*
2516+
* @param transactionId - The ID of the transaction to update.
2517+
* @param contractAddress - The contract address of the selected gas fee token.
2518+
*/
2519+
updateSelectedGasFeeToken(
2520+
transactionId: string,
2521+
contractAddress: Hex | undefined,
2522+
) {
2523+
this.#updateTransactionInternal({ transactionId }, (transactionMeta) => {
2524+
const hasMatchingGasFeeToken = transactionMeta.gasFeeTokens?.some(
2525+
(token) =>
2526+
token.tokenAddress.toLowerCase() === contractAddress?.toLowerCase(),
2527+
);
2528+
2529+
if (contractAddress && !hasMatchingGasFeeToken) {
2530+
throw new Error(
2531+
`No matching gas fee token found with address - ${contractAddress}`,
2532+
);
2533+
}
2534+
2535+
transactionMeta.selectedGasFeeToken = contractAddress;
2536+
});
2537+
}
2538+
25122539
private addMetadata(transactionMeta: TransactionMeta) {
25132540
validateTxParams(transactionMeta.txParams);
25142541
this.update((state) => {
@@ -3906,8 +3933,10 @@ export class TransactionController extends BaseController<
39063933
tokenBalanceChanges: [],
39073934
};
39083935

3936+
let gasFeeTokens: GasFeeToken[] = [];
3937+
39093938
if (this.#isSimulationEnabled()) {
3910-
simulationData = await this.#trace(
3939+
const result = await this.#trace(
39113940
{ name: 'Simulate', parentContext: traceContext },
39123941
() =>
39133942
getSimulationData(
@@ -3924,6 +3953,9 @@ export class TransactionController extends BaseController<
39243953
),
39253954
);
39263955

3956+
gasFeeTokens = result?.gasFeeTokens;
3957+
simulationData = result?.simulationData;
3958+
39273959
if (
39283960
blockTime &&
39293961
prevSimulationData &&
@@ -3956,6 +3988,7 @@ export class TransactionController extends BaseController<
39563988
skipResimulateCheck: Boolean(blockTime),
39573989
},
39583990
(txMeta) => {
3991+
txMeta.gasFeeTokens = gasFeeTokens;
39593992
txMeta.simulationData = simulationData;
39603993
},
39613994
);

packages/transaction-controller/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type {
3939
FeeMarketGasFeeEstimateForLevel,
4040
FeeMarketGasFeeEstimates,
4141
GasFeeEstimates,
42+
GasFeeToken,
4243
GasPriceGasFeeEstimates,
4344
GasPriceValue,
4445
InferTransactionTypeResult,

0 commit comments

Comments
 (0)