diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 1f4c01245b5..2c2bd580536 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -5,6 +5,7 @@ import { NetworksTicker, toHex, BUILT_IN_NETWORKS, + ORIGIN_METAMASK, } from '@metamask/controller-utils'; import type { BlockTracker, @@ -22,7 +23,7 @@ import type { TransactionConfig, } from './TransactionController'; import { TransactionController } from './TransactionController'; -import type { TransactionMeta } from './types'; +import type { TransactionMeta, DappSuggestedGasFees } from './types'; import { WalletDevice, TransactionStatus } from './types'; import { ESTIMATE_GAS_ERROR } from './utils'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; @@ -734,6 +735,54 @@ describe('TransactionController', () => { ); }); + describe('adds dappSuggestedGasFees to transaction', () => { + it.each([ + ['origin is MM', ORIGIN_METAMASK], + ['origin is not defined', undefined], + ['no fee information is given', 'MockDappOrigin'], + ])('as undefined if %s', async (_testName, origin) => { + const controller = newController(); + await controller.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }, + { + origin, + }, + ); + expect( + controller.state.transactions[0]?.dappSuggestedGasFees, + ).toBeUndefined(); + }); + + it.each([ + ['gasPrice'], + ['maxFeePerGas'], + ['maxPriorityFeePerGas'], + ['gas'], + ])('if %s is defined', async (gasPropName) => { + const controller = newController(); + const mockDappOrigin = 'MockDappOrigin'; + const mockGasValue = '0x1'; + await controller.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + [gasPropName]: mockGasValue, + }, + { + origin: mockDappOrigin, + }, + ); + expect( + controller.state.transactions[0]?.dappSuggestedGasFees?.[ + gasPropName as keyof DappSuggestedGasFees + ], + ).toBe(mockGasValue); + }); + }); + it.each([ ['mainnet', MOCK_MAINNET_NETWORK], ['custom network', MOCK_CUSTOM_NETWORK], diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index f2f4bf387a7..44f86951740 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -41,7 +41,12 @@ import { v1 as random } from 'uuid'; import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource'; import { IncomingTransactionHelper } from './IncomingTransactionHelper'; -import type { Transaction, TransactionMeta, WalletDevice } from './types'; +import type { + Transaction, + TransactionMeta, + WalletDevice, + DappSuggestedGasFees, +} from './types'; import { TransactionStatus } from './types'; import { getAndFormatTransactionsForNonceTracker, @@ -399,6 +404,11 @@ export class TransactionController extends BaseController< transaction = normalizeTransaction(transaction); validateTransaction(transaction); + const dappSuggestedGasFees = this.generateDappSuggestedGasFees( + transaction, + origin, + ); + const transactionMeta: TransactionMeta = { id: random(), networkID: networkId ?? undefined, @@ -409,6 +419,7 @@ export class TransactionController extends BaseController< transaction, deviceConfirmedOn, verifiedOnBlockchain: false, + dappSuggestedGasFees, }; try { @@ -1364,6 +1375,44 @@ export class TransactionController extends BaseController< this.update({ lastFetchedBlockNumbers }); this.hub.emit('incomingTransactionBlock', blockNumber); } + + private generateDappSuggestedGasFees( + transaction: Transaction, + origin?: string, + ): DappSuggestedGasFees | undefined { + if (!origin || origin === ORIGIN_METAMASK) { + return undefined; + } + + const { gasPrice, maxFeePerGas, maxPriorityFeePerGas, gas } = transaction; + + if ( + gasPrice === undefined && + maxFeePerGas === undefined && + maxPriorityFeePerGas === undefined && + gas === undefined + ) { + return undefined; + } + + const dappSuggestedGasFees: DappSuggestedGasFees = {}; + + if (gasPrice !== undefined) { + dappSuggestedGasFees.gasPrice = gasPrice; + } else if ( + maxFeePerGas !== undefined || + maxPriorityFeePerGas !== undefined + ) { + dappSuggestedGasFees.maxFeePerGas = maxFeePerGas; + dappSuggestedGasFees.maxPriorityFeePerGas = maxPriorityFeePerGas; + } + + if (gas !== undefined) { + dappSuggestedGasFees.gas = gas; + } + + return dappSuggestedGasFees; + } } export default TransactionController; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 8b7eb253856..98c51bf9461 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1,23 +1,7 @@ import type { Hex } from '@metamask/utils'; /** - * @type TransactionMeta - * - * TransactionMeta representation - * @property baseFeePerGas - Base fee of the block as a hex value, introduced in EIP-1559. - * @property error - Synthesized error information for failed transactions. - * @property id - Generated UUID associated with this transaction. - * @property networkID - Network code as per EIP-155 for this transaction. - * @property origin - Origin this transaction was sent from. - * @property deviceConfirmedOn - string to indicate what device the transaction was confirmed. - * @property rawTransaction - Hex representation of the underlying transaction. - * @property status - String status of this transaction. - * @property time - Timestamp associated with this transaction. - * @property toSmartContract - Whether transaction recipient is a smart contract. - * @property transaction - Underlying Transaction object. - * @property txReceipt - Transaction receipt. - * @property transactionHash - Hash of a successful transaction. - * @property blockNumber - Number of the block where the transaction has been included. + * Representation of transaction metadata. */ export type TransactionMeta = | ({ @@ -25,27 +9,98 @@ export type TransactionMeta = } & TransactionMetaBase) | ({ status: TransactionStatus.failed; error: Error } & TransactionMetaBase); +/** + * Information about a single transaction such as status and block number. + */ type TransactionMetaBase = { + /** + * Base fee of the block as a hex value, introduced in EIP-1559. + */ baseFeePerGas?: Hex; + + /** + * Number of the block where the transaction has been included. + */ blockNumber?: string; + + /** + * Network code as per EIP-155 for this transaction. + */ chainId?: Hex; + + /** + * Gas values provided by the dApp. + */ + dappSuggestedGasFees?: DappSuggestedGasFees; + + /** + * String to indicate what device the transaction was confirmed on. + */ deviceConfirmedOn?: WalletDevice; + + /** + * Generated UUID associated with this transaction. + */ id: string; + + /** + * Whether the transaction is a transfer. + */ isTransfer?: boolean; + + /** + * Network code as per EIP-155 for this transaction. + */ networkID?: string; + + /** + * Origin this transaction was sent from. + */ origin?: string; + + /** + * Hex representation of the underlying transaction. + */ rawTransaction?: string; + + /** + * Timestamp associated with this transaction. + */ time: number; + + /** + * Whether transaction recipient is a smart contract. + */ toSmartContract?: boolean; + + /** + * Underlying Transaction object. + */ transaction: Transaction; + + /** + * Hash of a successful transaction. + */ transactionHash?: string; + + /** + * Additional transfer information. + */ transferInformation?: { contractAddress: string; decimals: number; symbol: string; }; - verifiedOnBlockchain?: boolean; + + /** + * Transaction receipt. + */ txReceipt?: TransactionReceipt; + + /** + * Whether the transaction is verified on the blockchain. + */ + verifiedOnBlockchain?: boolean; }; /** @@ -74,33 +129,73 @@ export enum WalletDevice { } /** - * @type Transaction - * - * Transaction representation - * @property chainId - Network ID as per EIP-155 - * @property data - Data to pass with this transaction - * @property from - Address to send this transaction from - * @property gas - Gas to send with this transaction - * @property gasPrice - Price of gas with this transaction - * @property gasUsed - Gas used in the transaction - * @property nonce - Unique number to prevent replay attacks - * @property to - Address to send this transaction to - * @property value - Value associated with this transaction + * Standard data concerning a transaction to be processed by the blockchain. */ export interface Transaction { + /** + * Network ID as per EIP-155. + */ chainId?: Hex; + + /** + * Data to pass with this transaction. + */ data?: string; + + /** + * Error message for gas estimation failure. + */ + estimateGasError?: string; + + /** + * Estimated base fee for this transaction. + */ + estimatedBaseFee?: string; + + /** + * Address to send this transaction from. + */ from: string; + + /** + * Gas to send with this transaction. + */ gas?: string; + + /** + * Price of gas with this transaction. + */ gasPrice?: string; + + /** + * Gas used in the transaction. + */ gasUsed?: string; + + /** + * Maximum fee per gas for this transaction. + */ + maxFeePerGas?: string; + + /** + * Maximum priority fee per gas for this transaction. + */ + maxPriorityFeePerGas?: string; + + /** + * Unique number to prevent replay attacks. + */ nonce?: string; + + /** + * Address to send this transaction to. + */ to?: string; + + /** + * Value associated with this transaction. + */ value?: string; - maxFeePerGas?: string; - maxPriorityFeePerGas?: string; - estimatedBaseFee?: string; - estimateGasError?: string; } /** @@ -208,3 +303,13 @@ export interface RemoteTransactionSource { request: RemoteTransactionSourceRequest, ) => Promise; } + +/** + * Gas values initially suggested by the dApp. + */ +export interface DappSuggestedGasFees { + gas?: string; + gasPrice?: string; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; +}