diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 1d72b367a7f..179c29428c6 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `gasFeeTokens` to `TransactionMeta` ([#5524](https://github.com/MetaMask/core/pull/5524)) + - Add `GasFeeToken` type. + - Add `selectedGasFeeToken` to `TransactionMeta`. + - Add `updateSelectedGasFeeToken` method. - Support security validation of transaction batches ([#5526](https://github.com/MetaMask/core/pull/5526)) - Add `ValidateSecurityRequest` type. - Add optional `securityAlertId` to `SecurityAlertResponse`. @@ -69,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add additional metadata for batch metrics ([#5488](https://github.com/MetaMask/core/pull/5488)) - - Add `delegationAddress` to `TransactionMetadata`. + - Add `delegationAddress` to `TransactionMeta`. - Add `NestedTransactionMetadata` type containing `BatchTransactionParams` and `type`. - Add optional `type` to `TransactionBatchSingleRequest`. - 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 - **BREAKING:** Bump `@metamask/accounts-controller` peer dependency to `^26.1.0` ([#5481](https://github.com/MetaMask/core/pull/5481)) - **BREAKING:** Add additional metadata for batch metrics ([#5488](https://github.com/MetaMask/core/pull/5488)) - - Change `error` in `TransactionMetadata` to optional for all statuses. - - Change `nestedTransactions` in `TransactionMetadata` to array of `NestedTransactionMetadata`. + - Change `error` in `TransactionMeta` to optional for all statuses. + - Change `nestedTransactions` in `TransactionMeta` to array of `NestedTransactionMetadata`. - Throw if `addTransactionBatch` called with external origin and size limit exceeded ([#5489](https://github.com/MetaMask/core/pull/5489)) - Verify EIP-7702 contract address using signatures ([#5472](https://github.com/MetaMask/core/pull/5472)) - Use new `contracts` property from feature flags instead of `contractAddresses`. diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 1ed29e176bc..e9835abad72 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -62,12 +62,12 @@ import type { TransactionParams, TransactionHistoryEntry, TransactionError, - SimulationData, GasFeeFlow, GasFeeFlowResponse, SubmitHistoryEntry, InternalAccount, PublishHook, + GasFeeToken, } from './types'; import { GasFeeEstimateType, @@ -86,6 +86,7 @@ import { getTransactionLayer1GasFee, updateTransactionLayer1GasFee, } from './utils/layer1-gas-fee-flow'; +import type { GetSimulationDataResult } from './utils/simulation'; import { getSimulationData } from './utils/simulation'; import { updatePostTransactionBalance, @@ -448,24 +449,40 @@ 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', +const SIMULATION_DATA_RESULT_MOCK: GetSimulationDataResult = { + gasFeeTokens: [], + 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, + }, + ], + }, +}; + +const GAS_FEE_TOKEN_MOCK: GasFeeToken = { + amount: '0x1', + balance: '0x2', + decimals: 18, + gas: '0x3', + maxFeePerGas: '0x4', + maxPriorityFeePerGas: '0x5', + rateWei: '0x6', + recipient: '0x7', + symbol: 'ETH', + tokenAddress: '0x8', }; const GAS_FEE_ESTIMATES_MOCK: GasFeeFlowResponse = { @@ -1990,7 +2007,9 @@ describe('TransactionController', () => { describe('updates simulation data', () => { it('by default', async () => { - getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK); + getSimulationDataMock.mockResolvedValueOnce( + SIMULATION_DATA_RESULT_MOCK, + ); const { controller } = setupController(); @@ -2021,12 +2040,14 @@ describe('TransactionController', () => { ); expect(controller.state.transactions[0].simulationData).toStrictEqual( - SIMULATION_DATA_MOCK, + SIMULATION_DATA_RESULT_MOCK.simulationData, ); }); it('with error if simulation disabled', async () => { - getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK); + getSimulationDataMock.mockResolvedValueOnce( + SIMULATION_DATA_RESULT_MOCK, + ); const { controller } = setupController({ options: { isSimulationEnabled: () => false }, @@ -2053,7 +2074,9 @@ describe('TransactionController', () => { }); it('unless approval not required', async () => { - getSimulationDataMock.mockResolvedValueOnce(SIMULATION_DATA_MOCK); + getSimulationDataMock.mockResolvedValueOnce( + SIMULATION_DATA_RESULT_MOCK, + ); const { controller } = setupController(); @@ -2070,6 +2093,57 @@ describe('TransactionController', () => { }); }); + describe('updates gas fee tokens', () => { + it('by default', async () => { + getSimulationDataMock.mockResolvedValueOnce({ + gasFeeTokens: [GAS_FEE_TOKEN_MOCK], + simulationData: { + tokenBalanceChanges: [], + }, + }); + + const { controller } = setupController(); + + await controller.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }, + { + networkClientId: NETWORK_CLIENT_ID_MOCK, + }, + ); + + await flushPromises(); + + expect(controller.state.transactions[0].gasFeeTokens).toStrictEqual([ + GAS_FEE_TOKEN_MOCK, + ]); + }); + + it('unless approval not required', async () => { + getSimulationDataMock.mockResolvedValueOnce({ + gasFeeTokens: [GAS_FEE_TOKEN_MOCK], + simulationData: { + tokenBalanceChanges: [], + }, + }); + + const { controller } = setupController(); + + await controller.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }, + { requireApproval: false, networkClientId: NETWORK_CLIENT_ID_MOCK }, + ); + + expect(getSimulationDataMock).toHaveBeenCalledTimes(0); + expect(controller.state.transactions[0].gasFeeTokens).toBeUndefined(); + }); + }); + describe('on approve', () => { it('submits transaction', async () => { const { controller, messenger } = setupController({ @@ -6575,4 +6649,59 @@ describe('TransactionController', () => { ); }); }); + + describe('updateSelectedGasFeeToken', () => { + it('updates selected gas fee token in state', () => { + const { controller } = setupController({ + options: { + state: { + transactions: [ + { + ...TRANSACTION_META_MOCK, + gasFeeTokens: [GAS_FEE_TOKEN_MOCK], + }, + ], + }, + }, + }); + + controller.updateSelectedGasFeeToken( + TRANSACTION_META_MOCK.id, + GAS_FEE_TOKEN_MOCK.tokenAddress, + ); + + expect(controller.state.transactions[0].selectedGasFeeToken).toBe( + GAS_FEE_TOKEN_MOCK.tokenAddress, + ); + }); + + it('throws if transaction does not exist', () => { + const { controller } = setupController(); + + expect(() => + controller.updateSelectedGasFeeToken( + TRANSACTION_META_MOCK.id, + GAS_FEE_TOKEN_MOCK.tokenAddress, + ), + ).toThrow( + `Cannot update transaction as ID not found - ${TRANSACTION_META_MOCK.id}`, + ); + }); + + it('throws if no matching gas fee token', () => { + const { controller } = setupController({ + options: { + state: { + transactions: [ + { ...TRANSACTION_META_MOCK, gasFeeTokens: [GAS_FEE_TOKEN_MOCK] }, + ], + }, + }, + }); + + expect(() => + controller.updateSelectedGasFeeToken(TRANSACTION_META_MOCK.id, '0x123'), + ).toThrow('No matching gas fee token found'); + }); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 5b081afe49c..7e5149724ae 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -100,6 +100,7 @@ import type { BatchTransactionParams, PublishHook, PublishBatchHook, + GasFeeToken, } from './types'; import { TransactionEnvelopeType, @@ -2509,6 +2510,32 @@ export class TransactionController extends BaseController< ); } + /** + * Update the selected gas fee token for a transaction. + * + * @param transactionId - The ID of the transaction to update. + * @param contractAddress - The contract address of the selected gas fee token. + */ + updateSelectedGasFeeToken( + transactionId: string, + contractAddress: Hex | undefined, + ) { + this.#updateTransactionInternal({ transactionId }, (transactionMeta) => { + const hasMatchingGasFeeToken = transactionMeta.gasFeeTokens?.some( + (token) => + token.tokenAddress.toLowerCase() === contractAddress?.toLowerCase(), + ); + + if (contractAddress && !hasMatchingGasFeeToken) { + throw new Error( + `No matching gas fee token found with address - ${contractAddress}`, + ); + } + + transactionMeta.selectedGasFeeToken = contractAddress; + }); + } + private addMetadata(transactionMeta: TransactionMeta) { validateTxParams(transactionMeta.txParams); this.update((state) => { @@ -3906,8 +3933,10 @@ export class TransactionController extends BaseController< tokenBalanceChanges: [], }; + let gasFeeTokens: GasFeeToken[] = []; + if (this.#isSimulationEnabled()) { - simulationData = await this.#trace( + const result = await this.#trace( { name: 'Simulate', parentContext: traceContext }, () => getSimulationData( @@ -3924,6 +3953,9 @@ export class TransactionController extends BaseController< ), ); + gasFeeTokens = result?.gasFeeTokens; + simulationData = result?.simulationData; + if ( blockTime && prevSimulationData && @@ -3956,6 +3988,7 @@ export class TransactionController extends BaseController< skipResimulateCheck: Boolean(blockTime), }, (txMeta) => { + txMeta.gasFeeTokens = gasFeeTokens; txMeta.simulationData = simulationData; }, ); diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index 6925ded3382..6536e329306 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -39,6 +39,7 @@ export type { FeeMarketGasFeeEstimateForLevel, FeeMarketGasFeeEstimates, GasFeeEstimates, + GasFeeToken, GasPriceGasFeeEstimates, GasPriceValue, InferTransactionTypeResult, diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 04a98a9b006..946e80eb973 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -184,6 +184,9 @@ export type TransactionMeta = { */ firstRetryBlockNumber?: string; + /** Available tokens that can be used to pay for gas. */ + gasFeeTokens?: GasFeeToken[]; + /** * Whether the transaction is active. */ @@ -347,6 +350,12 @@ export type TransactionMeta = { // eslint-disable-next-line @typescript-eslint/no-explicit-any securityProviderResponse?: Record; + /** + * The token address of the selected gas fee token. + * Corresponds to the `gasFeeTokens` property. + */ + selectedGasFeeToken?: Hex; + /** * An array of entries that describe the user's journey through the send flow. * This is purely attached to state logs for troubleshooting and support. @@ -1624,3 +1633,36 @@ export type ValidateSecurityRequest = { /** Optional EIP-7702 delegation to mock for the transaction sender. */ delegationMock?: Hex; }; + +/** Data required to pay for transaction gas using an ERC-20 token. */ +export type GasFeeToken = { + /** Amount needed for the gas fee. */ + amount: Hex; + + /** Current token balance of the sender. */ + balance: Hex; + + /** Decimals of the token. */ + decimals: number; + + /** The corresponding gas limit this token fee would equal. */ + gas: Hex; + + /** The corresponding maxFeePerGas this token fee would equal. */ + maxFeePerGas: Hex; + + /** The corresponding maxPriorityFeePerGas this token fee would equal. */ + maxPriorityFeePerGas: Hex; + + /** Conversion rate of 1 token to native WEI. */ + rateWei: Hex; + + /** Account address to send the token to. */ + recipient: Hex; + + /** Symbol of the token. */ + symbol: string; + + /** Address of the token contract. */ + tokenAddress: Hex; +}; diff --git a/packages/transaction-controller/src/utils/simulation-api.ts b/packages/transaction-controller/src/utils/simulation-api.ts index 7134c4069be..caede8a19a6 100644 --- a/packages/transaction-controller/src/utils/simulation-api.ts +++ b/packages/transaction-controller/src/utils/simulation-api.ts @@ -61,6 +61,17 @@ export type SimulationRequest = { }; }; + /** + * Whether to include available token fees. + */ + suggestFees?: { + /* Whether to include the native transfer if available. */ + withTransfer?: boolean; + + /* Whether to include the gas fee of the token transfer. */ + withFeeTransfer?: boolean; + }; + /** * Whether to include call traces in the response. * Defaults to false. @@ -114,6 +125,32 @@ export type SimulationResponseStateDiff = { }; }; +export type SimulationResponseTokenFee = { + /** Token data independent of current transaction. */ + token: { + /** Address of the token contract. */ + address: Hex; + + /** Decimals of the token. */ + decimals: number; + + /** Symbol of the token. */ + symbol: string; + }; + + /** Amount of tokens needed to pay for gas. */ + balanceNeededToken: Hex; + + /** Current token balance of sender. */ + currentBalanceToken: Hex; + + /** Account address that token should be transferred to. */ + feeRecipient: Hex; + + /** Conversation rate of 1 token to native WEI. */ + rateWei: Hex; +}; + /** Response from the simulation API for a single transaction. */ export type SimulationResponseTransaction = { /** Hierarchy of call data including nested calls and logs. */ @@ -122,6 +159,21 @@ export type SimulationResponseTransaction = { /** An error message indicating the transaction could not be simulated. */ error?: string; + /** Recommended gas fees for the transaction. */ + fees?: { + /** Gas limit for the fee level. */ + gas: Hex; + + /** Maximum fee per gas for the fee level. */ + maxFeePerGas: Hex; + + /** Maximum priority fee per gas for the fee level. */ + maxPriorityFeePerGas: Hex; + + /** Token fee data for the fee level. */ + tokenFees: SimulationResponseTokenFee[]; + }[]; + /** The total gas used by the transaction. */ gasUsed?: Hex; diff --git a/packages/transaction-controller/src/utils/simulation.test.ts b/packages/transaction-controller/src/utils/simulation.test.ts index 1eb9ebed1aa..58cf3d6532d 100644 --- a/packages/transaction-controller/src/utils/simulation.test.ts +++ b/packages/transaction-controller/src/utils/simulation.test.ts @@ -274,9 +274,9 @@ describe('Simulation Utils', () => { createNativeBalanceResponse(previousBalance, newBalance), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: { difference: DIFFERENCE_MOCK, isDecrease, @@ -293,9 +293,9 @@ describe('Simulation Utils', () => { createNativeBalanceResponse(BALANCE_1_MOCK, BALANCE_1_MOCK), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [], }); @@ -403,12 +403,12 @@ describe('Simulation Utils', () => { createBalanceOfResponse(previousBalances, newBalances), ); - const simulationData = await getSimulationData({ + const result = await getSimulationData({ chainId: '0x1', from, }); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -453,9 +453,9 @@ describe('Simulation Utils', () => { ), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -509,9 +509,9 @@ describe('Simulation Utils', () => { createBalanceOfResponse([BALANCE_2_MOCK], [BALANCE_1_MOCK]), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -553,9 +553,9 @@ describe('Simulation Utils', () => { ), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -614,7 +614,7 @@ describe('Simulation Utils', () => { ), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); expect(simulateTransactionsMock).toHaveBeenCalledTimes(2); @@ -648,7 +648,7 @@ describe('Simulation Utils', () => { ], }, ); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -684,9 +684,9 @@ describe('Simulation Utils', () => { createBalanceOfResponse([BALANCE_1_MOCK], [BALANCE_2_MOCK]), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [], }); @@ -708,9 +708,9 @@ describe('Simulation Utils', () => { createEventResponseMock([createLogMock(CONTRACT_ADDRESS_1_MOCK)]), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [], }); @@ -729,9 +729,9 @@ describe('Simulation Utils', () => { createBalanceOfResponse([BALANCE_1_MOCK], [BALANCE_1_MOCK]), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [], }); @@ -746,9 +746,9 @@ describe('Simulation Utils', () => { createBalanceOfResponse([BALANCE_1_MOCK], [BALANCE_2_MOCK]), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -797,9 +797,9 @@ describe('Simulation Utils', () => { ], }); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -823,7 +823,9 @@ describe('Simulation Utils', () => { message: ERROR_MESSAGE_MOCK, }); - expect(await getSimulationData(REQUEST_MOCK)).toStrictEqual({ + const result = await getSimulationData(REQUEST_MOCK); + + expect(result.simulationData).toStrictEqual({ error: { code: ERROR_CODE_MOCK, message: ERROR_MESSAGE_MOCK, @@ -837,7 +839,9 @@ describe('Simulation Utils', () => { code: ERROR_CODE_MOCK, }); - expect(await getSimulationData(REQUEST_MOCK)).toStrictEqual({ + const result = await getSimulationData(REQUEST_MOCK); + + expect(result.simulationData).toStrictEqual({ error: { code: ERROR_CODE_MOCK, message: undefined, @@ -855,9 +859,9 @@ describe('Simulation Utils', () => { ) .mockResolvedValueOnce(createBalanceOfResponse([], [])); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ error: { code: SimulationErrorCode.InvalidResponse, message: new SimulationInvalidResponseError().message, @@ -876,9 +880,9 @@ describe('Simulation Utils', () => { ], }); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ error: { code: SimulationErrorCode.Reverted, message: new SimulationRevertedError().message, @@ -897,9 +901,9 @@ describe('Simulation Utils', () => { ], }); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ error: { code: undefined, message: 'test 1 2 3', @@ -914,9 +918,9 @@ describe('Simulation Utils', () => { message: 'test insufficient funds for gas test', }); - const simulationData = await getSimulationData(REQUEST_MOCK); + const result = await getSimulationData(REQUEST_MOCK); - expect(simulationData).toStrictEqual({ + expect(result.simulationData).toStrictEqual({ error: { code: SimulationErrorCode.Reverted, message: new SimulationRevertedError().message, @@ -925,5 +929,157 @@ describe('Simulation Utils', () => { }); }); }); + + describe('returns gas fee tokens', () => { + it('using token fee data', async () => { + simulateTransactionsMock.mockResolvedValueOnce({ + transactions: [ + { + fees: [ + { + gas: '0x1', + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + tokenFees: [ + { + token: { + address: CONTRACT_ADDRESS_1_MOCK, + decimals: 3, + symbol: 'TEST1', + }, + balanceNeededToken: '0x4', + currentBalanceToken: '0x5', + feeRecipient: '0x6', + rateWei: '0x7', + }, + { + token: { + address: CONTRACT_ADDRESS_2_MOCK, + decimals: 4, + symbol: 'TEST2', + }, + balanceNeededToken: '0x8', + currentBalanceToken: '0x9', + feeRecipient: '0xa', + rateWei: '0xb', + }, + ], + }, + ], + return: '0x', + }, + ], + }); + + const result = await getSimulationData(REQUEST_MOCK); + + expect(result.gasFeeTokens).toStrictEqual([ + { + amount: '0x4', + balance: '0x5', + decimals: 3, + gas: '0x1', + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + rateWei: '0x7', + recipient: '0x6', + symbol: 'TEST1', + tokenAddress: CONTRACT_ADDRESS_1_MOCK, + }, + { + amount: '0x8', + balance: '0x9', + decimals: 4, + gas: '0x1', + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + rateWei: '0xb', + recipient: '0xa', + symbol: 'TEST2', + tokenAddress: CONTRACT_ADDRESS_2_MOCK, + }, + ]); + }); + + it('using first fee level', async () => { + simulateTransactionsMock.mockResolvedValueOnce({ + transactions: [ + { + fees: [ + { + gas: '0x1', + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + tokenFees: [ + { + token: { + address: CONTRACT_ADDRESS_1_MOCK, + decimals: 3, + symbol: 'TEST1', + }, + balanceNeededToken: '0x4', + currentBalanceToken: '0x5', + feeRecipient: '0x6', + rateWei: '0x7', + }, + ], + }, + { + gas: '0x8', + maxFeePerGas: '0x9', + maxPriorityFeePerGas: '0xa', + tokenFees: [ + { + token: { + address: CONTRACT_ADDRESS_2_MOCK, + decimals: 4, + symbol: 'TEST2', + }, + balanceNeededToken: '0xb', + currentBalanceToken: '0xc', + feeRecipient: '0xd', + rateWei: '0xe', + }, + ], + }, + ], + return: '0x', + }, + ], + }); + + const result = await getSimulationData(REQUEST_MOCK); + + expect(result.gasFeeTokens).toStrictEqual([ + { + amount: '0x4', + balance: '0x5', + decimals: 3, + gas: '0x1', + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + rateWei: '0x7', + recipient: '0x6', + symbol: 'TEST1', + tokenAddress: CONTRACT_ADDRESS_1_MOCK, + }, + ]); + }); + + it('as empty if missing data', async () => { + simulateTransactionsMock.mockResolvedValueOnce({ + transactions: [ + { + fees: [], + return: '0x', + }, + ], + }); + + const result = await getSimulationData(REQUEST_MOCK); + + expect(result.gasFeeTokens).toStrictEqual([]); + }); + }); }); }); diff --git a/packages/transaction-controller/src/utils/simulation.ts b/packages/transaction-controller/src/utils/simulation.ts index 6ad99c0029f..bfba1dbdc57 100644 --- a/packages/transaction-controller/src/utils/simulation.ts +++ b/packages/transaction-controller/src/utils/simulation.ts @@ -27,6 +27,7 @@ import type { SimulationData, SimulationTokenBalanceChange, SimulationToken, + GasFeeToken, } from '../types'; import { SimulationTokenStandard } from '../types'; @@ -48,6 +49,11 @@ export type GetSimulationDataRequest = { value?: Hex; }; +export type GetSimulationDataResult = { + gasFeeTokens: GasFeeToken[]; + simulationData: SimulationData; +}; + type ParsedEvent = { contractAddress: Hex; tokenStandard: SimulationTokenStandard; @@ -113,7 +119,7 @@ type BalanceTransactionMap = Map; export async function getSimulationData( request: GetSimulationDataRequest, options: GetSimulationDataOptions = {}, -): Promise { +): Promise { const { chainId, from, to, value, data } = request; const { blockTime } = options; @@ -125,12 +131,14 @@ export async function getSimulationData( { data, from, - maxFeePerGas: '0x0', - maxPriorityFeePerGas: '0x0', to, value, }, ], + suggestFees: { + withTransfer: true, + withFeeTransfer: true, + }, withCallTrace: true, withLogs: true, ...(blockTime && { @@ -157,10 +165,23 @@ export async function getSimulationData( options, ); - return { + const simulationData = { nativeBalanceChange, tokenBalanceChanges, }; + + let gasFeeTokens: GasFeeToken[] = []; + + try { + gasFeeTokens = getGasFeeTokens(response); + } catch (error) { + log('Failed to parse gas fee tokens', error, response); + } + + return { + gasFeeTokens, + simulationData, + }; } catch (error) { log('Failed to get simulation data', error, request); @@ -177,10 +198,13 @@ export async function getSimulationData( const { code, message } = simulationError; return { - tokenBalanceChanges: [], - error: { - code, - message, + gasFeeTokens: [], + simulationData: { + tokenBalanceChanges: [], + error: { + code, + message, + }, }, }; } @@ -686,3 +710,29 @@ function getContractInterfaces(): Map { }), ); } + +/** + * Extract gas fee tokens from a simulation response. + * + * @param response - The simulation response. + * @returns An array of gas fee tokens. + */ +function getGasFeeTokens(response: SimulationResponse): GasFeeToken[] { + const feeLevel = response.transactions?.[0] + ?.fees?.[0] as Required['fees'][0]; + + const tokenFees = feeLevel?.tokenFees ?? []; + + return tokenFees.map((tokenFee) => ({ + amount: tokenFee.balanceNeededToken, + balance: tokenFee.currentBalanceToken, + decimals: tokenFee.token.decimals, + gas: feeLevel.gas, + maxFeePerGas: feeLevel.maxFeePerGas, + maxPriorityFeePerGas: feeLevel.maxPriorityFeePerGas, + rateWei: tokenFee.rateWei, + recipient: tokenFee.feeRecipient, + symbol: tokenFee.token.symbol, + tokenAddress: tokenFee.token.address, + })); +}