diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 5ba155be96..44614ecab7 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -740,11 +740,6 @@ "count": 3 } }, - "packages/bridge-status-controller/src/utils/transaction.ts": { - "no-restricted-syntax": { - "count": 4 - } - }, "packages/chain-agnostic-permission/src/caip25Permission.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 11 diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 2b732e4b5d..b43a397546 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Refactor tx submission into strategies to reduce quote-specific branching in the controller, and to de-duplicate shared logic between `submitTx` and `submitIntent`. Each strategy yields payloads that the controller uses to update history, poll, and publish metrics ([#8257](https://github.com/MetaMask/core/pull/8257)) - Bump `@metamask/bridge-controller` from `^72.0.4` to `^73.0.1` ([#8850](https://github.com/MetaMask/core/pull/8850), [#8866](https://github.com/MetaMask/core/pull/8866)) - Remove unnecessary type assertions for bridge quotes ([#8805](https://github.com/MetaMask/core/pull/8805)) diff --git a/packages/bridge-status-controller/jest.config.js b/packages/bridge-status-controller/jest.config.js index b1e1437631..121fa72f48 100644 --- a/packages/bridge-status-controller/jest.config.js +++ b/packages/bridge-status-controller/jest.config.js @@ -16,7 +16,12 @@ module.exports = merge(baseConfig, { coverageProvider: 'v8', - coveragePathIgnorePatterns: ['.*/index\\.ts', '.*-method-action-types\\.ts'], + coveragePathIgnorePatterns: [ + ...baseConfig.coveragePathIgnorePatterns, + '.*/strategy/types\\.ts$', + '.*/index\\.ts', + '.*-method-action-types\\.ts', + ], // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index 06e65352d1..58f1e2c95c 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -14,7 +14,7 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, - "location": undefined, + "location": "Main View", "originalTransactionId": "bridgeTxMetaId1", "pricingData": { "amountSent": "1.234", @@ -269,7 +269,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, - "location": undefined, + "location": "Main View", "originalTransactionId": "bridgeTxMetaId1", "pricingData": { "amountSent": "1.234", @@ -4002,6 +4002,7 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti "requireApproval": false, "transactions": [ { + "assetsFiatValues": undefined, "params": { "data": "0xapprovalData", "from": "0xaccount1", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index 667b70b142..e72961514a 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -371,9 +371,7 @@ describe('BridgeStatusController (intent swaps)', () => { "chainId": "0x1", "hash": undefined, "id": "intentDisplayTxId1", - "isIntentTx": true, "networkClientId": "network-client-id-1", - "orderUid": "order-uid-approve-1", "status": "submitted", "time": 1773879217428, "txParams": { diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index e03bf27b4e..384aeb978a 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -17,6 +17,7 @@ import { FeatureId, getQuotesReceivedProperties, UnifiedSwapBridgeEventName, + MetaMetricsSwapsEventSource, } from '@metamask/bridge-controller'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { @@ -56,6 +57,7 @@ import type { } from './types'; import * as bridgeStatusUtils from './utils/bridge-status'; import * as historyUtils from './utils/history'; +import * as metricsUtils from './utils/metrics'; import * as transactionUtils from './utils/transaction'; type AllBridgeStatusControllerActions = @@ -332,6 +334,7 @@ const getMockStartPollingForBridgeTxStatusArgs = ({ initialDestAssetBalance: undefined, targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', isStxEnabled, + location: MetaMetricsSwapsEventSource.MainView, }); const MockTxHistory = { @@ -432,7 +435,7 @@ const MockTxHistory = { completionTime: undefined, attempts, featureId, - location: undefined, + location: MetaMetricsSwapsEventSource.MainView, }, }), getUnknown: ({ @@ -547,7 +550,7 @@ const MockTxHistory = { isStxEnabled: true, hasApprovalTx: false, attempts: undefined, - location: undefined, + location: MetaMetricsSwapsEventSource.MainView, }, }), }; @@ -5100,6 +5103,81 @@ describe('BridgeStatusController', () => { expect(messengerCallSpy.mock.lastCall).toMatchSnapshot(); }); + it('should use txMeta properties if history item does not exist', () => { + const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); + + const transactionMeta = { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: 'bridgeTxMetaId1', + }; + const getEVMTxPropertiesFromTransactionMetaSpy = jest + .spyOn(metricsUtils, 'getEVMTxPropertiesFromTransactionMeta') + .mockImplementationOnce(() => { + bridgeStatusController.wipeBridgeStatus({ + address: 'otherAccount', + ignoreNetwork: true, + }); + return metricsUtils.getEVMTxPropertiesFromTransactionMeta( + transactionMeta, + ); + }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta, + }, + ); + + expect(getEVMTxPropertiesFromTransactionMetaSpy).toHaveBeenCalledTimes( + 2, + ); + expect(bridgeStatusController.state.txHistory).toStrictEqual({}); + expect(messengerCallSpy.mock.lastCall).toMatchInlineSnapshot(` + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Failed", + { + "account_hardware_type": null, + "action_type": "swapbridge-v1", + "actual_time_minutes": 0, + "chain_id_destination": "eip155:42161", + "chain_id_source": "eip155:42161", + "custom_slippage": false, + "error_message": "Transaction failed. tx-error", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "location": "Main View", + "price_impact": 0, + "provider": "", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": [], + "source_transaction": "FAILED", + "stx_enabled": false, + "swap_type": "crosschain", + "token_address_destination": "eip155:42161/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + "token_symbol_destination": "", + "token_symbol_source": "", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 0, + "usd_quoted_return": 0, + }, + ] + `); + }); + it('should include ab_tests and active_ab_tests from history in tracked event properties', () => { const abTestsTxMetaId = 'bridgeTxMetaIdAbTests'; mockMessenger.call( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 3265f7c6e3..d8ac782b40 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -2,25 +2,20 @@ import type { StateMetadata } from '@metamask/base-controller'; import type { QuoteMetadata, RequiredEventContextFromClient, - TxData, QuoteResponse, Trade, - TronTradeData, + FeatureId, } from '@metamask/bridge-controller'; import { - formatChainIdToHex, isNonEvmChainId, StatusTypes, getAccountHardwareType, UnifiedSwapBridgeEventName, isCrossChain, - isTronChainId, - isEvmTxData, MetricsActionType, MetaMetricsSwapsEventSource, - isBitcoinTrade, - isTronTrade, PollingStatus, + formatChainIdToHex, } from '@metamask/bridge-controller'; import type { TraceCallback } from '@metamask/controller-utils'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; @@ -41,11 +36,13 @@ import { MAX_ATTEMPTS, REFRESH_INTERVAL_MS, } from './constants'; +import executeSubmitStrategy from './strategy'; +import { SubmitStep } from './strategy/types'; +import type { SubmitStrategyParams } from './strategy/types'; import type { BridgeStatusControllerState, StartPollingForBridgeTxStatusArgsSerialized, FetchFunction, - SolanaTransactionMeta, BridgeHistoryItem, } from './types'; import type { BridgeStatusControllerMessenger } from './types'; @@ -66,13 +63,6 @@ import { shouldPollHistoryItem, getMatchingHistoryEntryForApprovalTxMeta, } from './utils/history'; -import { - getIntentFromQuote, - IntentSubmissionParams, - mapIntentOrderStatusToTransactionStatus, - postSubmitOrder, -} from './utils/intent-api'; -import { signTypedMessage } from './utils/keyring'; import { getFinalizedTxProperties, getPriceImpactFromQuote, @@ -84,23 +74,11 @@ import { getPreConfirmationPropertiesFromQuote, getPollingStatusUpdatedProperties, } from './utils/metrics'; +import { getSelectedChainId } from './utils/network'; +import { getTraceParams } from './utils/trace'; import { - getNetworkClientIdByChainId, - getSelectedChainId, -} from './utils/network'; -import { handleNonEvmTx } from './utils/snaps'; -import { getApprovalTraceParams, getTraceParams } from './utils/trace'; -import { - getAddTransactionBatchParams, - handleApprovalDelay, - handleMobileHardwareWalletDelay, - generateActionId, - waitForTxConfirmation, getTransactionMetaById, - addTransactionBatch, - addSyntheticTransaction, getTransactions, - submitEvmTransaction, checkIsDelegatedAccount, isCrossChainTx, } from './utils/transaction'; @@ -538,31 +516,32 @@ export class BridgeStatusController extends StaticIntervalPollingController - ): string => { - const { historyKey, txHistoryItem } = getInitialHistoryItem(...args); + ): void => { + const txHistoryItem = getInitialHistoryItem(...args); this.update((state) => { - // Use actionId as key for pre-submission, or txMeta.id for post-submission state.txHistory[historyKey] = txHistoryItem; }); - return historyKey; }; /** * Rekeys a history item from actionId to txMeta.id after successful submission. * Also updates txMetaId and srcTxHash which weren't available pre-submission. * - * @param actionId - The actionId used as the temporary key for the history item + * @param oldKey - The temporary key to use for the history item, usually the actionId + * @param newKey - The new key to use, typcally the txmeta.id * @param txMeta - The transaction meta from the successful submission * @param txMeta.id - The transaction meta id to use as the new key * @param txMeta.hash - The transaction hash to set on the history item */ readonly #rekeyHistoryItem = ( - actionId: string, + oldKey: string, + newKey: string, txMeta: { id: string; hash?: string }, ): void => { this.update((state) => { - rekeyHistoryItemInState(state, actionId, txMeta); + rekeyHistoryItemInState(state, oldKey, newKey, txMeta); }); }; @@ -602,7 +581,8 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, - isBridgeTx: boolean, - srcChainId: QuoteResponse['quote']['srcChainId'], - approval?: TxData | TronTradeData, - resetApproval?: TxData, - requireApproval?: boolean, - ): Promise => { - if (approval && isEvmTxData(approval)) { - const approveTx = async (): Promise => { - if (resetApproval) { - await submitEvmTransaction({ - messenger: this.messenger, - transactionType: TransactionType.bridgeApproval, - trade: resetApproval, - }); - } + readonly #executeSubmitStrategy = async ( + params: SubmitStrategyParams, + sharedHistoryItemProperties: { + startTime: number; + location: MetaMetricsSwapsEventSource; + abTests?: Record; + activeAbTests?: { key: string; value: string }[]; + tokenSecurityTypeDestination?: string | null; + }, + ): Promise => { + let tradeTxMeta!: TransactionMeta; + + const steps = executeSubmitStrategy(params); + + // Each submission strategy determines when to execute step, which means these actions can happen in any order + for await (const { type, payload } of steps) { + try { + switch (type) { + case SubmitStep.RekeyHistoryItem: + this.#rekeyHistoryItem( + payload.oldHistoryKey, + payload.newHistoryKey, + payload.tradeMeta, + ); + break; - const approvalTxMeta = await submitEvmTransaction({ - messenger: this.messenger, - transactionType: isBridgeTx - ? TransactionType.bridgeApproval - : TransactionType.swapApproval, - trade: approval, - requireApproval, - }); + case SubmitStep.SetTradeMeta: + tradeTxMeta = payload.tradeMeta; + break; - await handleApprovalDelay(srcChainId); - return approvalTxMeta; - }; + case SubmitStep.AddHistoryItem: + this.#addTxToHistory(payload.historyKey, { + ...payload, + ...sharedHistoryItemProperties, + quoteResponse: params.quoteResponse, + accountAddress: params.selectedAccount.address, + isStxEnabled: params.isStxEnabled, + slippagePercentage: 0, // TODO include slippage provided by quote if using dynamic slippage, or slippage from quote request + }); + break; - return await this.#trace( - getApprovalTraceParams(quoteResponse, false), - approveTx, - ); - } + case SubmitStep.StartPolling: + this.#startPollingForTxId(payload.historyKey); + break; - return undefined; - }; + case SubmitStep.PublishCompletedEvent: + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Completed, + payload.historyKey, + ); + break; - // TODO simplify and make more readable - /** - * Submits batched EVM transactions to the TransactionController - * - * @param args - The parameters for the transaction - * @param args.isBridgeTx - Whether the transaction is a bridge transaction - * @param args.trade - The trade data to confirm - * @param args.approval - The approval data to confirm - * @param args.resetApproval - The ethereum:USDT reset approval data to confirm - * @param args.quoteResponse - The quote response - * @param args.requireApproval - Whether to require approval for the transaction - * @returns The approvalMeta and tradeMeta for the batched transaction - */ - readonly #handleEvmTransactionBatch = async ( - args: Omit< - Parameters[0], - 'messenger' | 'estimateGasFeeFn' - >, - ): Promise<{ - approvalMeta?: TransactionMeta; - tradeMeta: TransactionMeta; - }> => { - const transactionParams = await getAddTransactionBatchParams({ - messenger: this.messenger, - ...args, - }); + /* c8 ignore start */ + default: + throw new Error(`Unknown submit step type: ${String(type)}`); + /* c8 ignore end */ + } + } catch (error) { + console.error( + 'Failed to add to bridge history and start polling.', + error, + ); + } + } - return await addTransactionBatch( - this.messenger, - this.#addTransactionBatchFn, - transactionParams, - ); + return tradeTxMeta; }; /** @@ -1053,29 +1027,29 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, - isStxEnabledOnClient: boolean, + isStxEnabled: boolean, quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived], location: MetaMetricsSwapsEventSource = MetaMetricsSwapsEventSource.MainView, abTests?: Record, activeAbTests?: { key: string; value: string }[], tokenSecurityTypeDestination?: string | null, - ): Promise> => { - stopPollingForQuotes( - this.messenger, - quoteResponse.featureId, - quotesReceivedContext, - ); + ): Promise => { + const { featureId, quote } = quoteResponse; + const startTime = Date.now(); + + stopPollingForQuotes(this.messenger, featureId, quotesReceivedContext); const selectedAccount = getAccountByAddress(this.messenger, accountAddress); if (!selectedAccount) { @@ -1083,11 +1057,19 @@ export class BridgeStatusController extends StaticIntervalPollingController; - let approvalTxId: string | undefined; - let isDelegatedAccount = false; - const startTime = Date.now(); - - const isBridgeTx = isCrossChain( - quoteResponse.quote.srcChainId, - quoteResponse.quote.destChainId, - ); - const isTronTx = isTronChainId(quoteResponse.quote.srcChainId); - try { // Emit Submitted event after submit button is clicked - !quoteResponse.featureId && - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.Submitted, - undefined, - preConfirmationProperties, - ); - // Submit non-EVM tx (Solana, BTC, Tron) - if (isNonEvmChainId(quoteResponse.quote.srcChainId)) { - // Handle non-EVM approval if present (e.g., Tron token approvals) - if (quoteResponse.approval && isTronTrade(quoteResponse.approval)) { - const approvalTxMeta = await this.#trace( - getApprovalTraceParams(quoteResponse, false), - async () => { - return quoteResponse.approval && - isTronTrade(quoteResponse.approval) - ? await handleNonEvmTx( - this.messenger, - quoteResponse.approval, - quoteResponse, - selectedAccount, - ) - : /* c8 ignore start */ - undefined; - /* c8 ignore end */ - }, - ); - - approvalTxId = approvalTxMeta?.id; - - // Add delay after approval similar to EVM flow - await handleApprovalDelay(quoteResponse.quote.srcChainId); - } - - txMeta = await this.#trace( - getTraceParams(quoteResponse, false), - async () => { - if ( - !( - isTronTrade(quoteResponse.trade) || - isBitcoinTrade(quoteResponse.trade) || - typeof quoteResponse.trade === 'string' - ) - ) { - throw new Error( - 'Failed to submit cross-chain swap transaction: trade is not a non-EVM transaction', - ); - } - return await handleNonEvmTx( - this.messenger, - quoteResponse.trade, - quoteResponse, - selectedAccount, - ); - }, - ); - } else { - // Submit EVM tx - // For hardware wallets on Mobile, this is fixes an issue where the Ledger does not get prompted for the 2nd approval - // Extension does not have this issue - const requireApproval = - this.#clientId === BridgeClientId.MOBILE && - accountHardwareType !== null; - - // Handle smart transactions if enabled - txMeta = await this.#trace( - getTraceParams(quoteResponse, isStxEnabledOnClient), - async () => { - if (!isEvmTxData(quoteResponse.trade)) { - throw new Error( - 'Failed to submit cross-chain swap transaction: trade is not an EVM transaction', - ); - } - // Check if the account is an EIP-7702 delegated account - // Delegated accounts only allow 1 in-flight tx, so approve + swap - // must be batched into a single transaction - const hexChainId = formatChainIdToHex( - quoteResponse.quote.srcChainId, - ); - isDelegatedAccount = await checkIsDelegatedAccount( - this.messenger, - quoteResponse.trade.from as `0x`, - [hexChainId], - ); - - if ( - isStxEnabledOnClient || - quoteResponse.quote.gasIncluded7702 || - isDelegatedAccount - ) { - const { tradeMeta, approvalMeta } = - await this.#handleEvmTransactionBatch({ - isBridgeTx, - resetApproval: quoteResponse.resetApproval, - approval: - quoteResponse.approval && - isEvmTxData(quoteResponse.approval) - ? quoteResponse.approval - : undefined, - trade: quoteResponse.trade, - quoteResponse, - requireApproval, - isDelegatedAccount, - }); - - approvalTxId = approvalMeta?.id; - return tradeMeta; - } - // Set approval time and id if an approval tx is needed - const approvalTxMeta = await this.#handleApprovalTx( - quoteResponse, - isBridgeTx, - quoteResponse.quote.srcChainId, - quoteResponse.approval && isEvmTxData(quoteResponse.approval) - ? quoteResponse.approval - : undefined, - quoteResponse.resetApproval, - requireApproval, - ); - - approvalTxId = approvalTxMeta?.id; - - // Hardware-wallet delay first (Ledger second-prompt spacing), then wait for - // on-chain approval confirmation so swap gas estimation runs after allowance is set. - if (requireApproval && approvalTxMeta) { - await handleMobileHardwareWalletDelay(requireApproval); - await waitForTxConfirmation(this.messenger, approvalTxMeta.id); - } else { - await handleMobileHardwareWalletDelay(requireApproval); - } - - // Generate actionId for pre-submission history (non-batch EVM only) - const actionId = generateActionId().toString(); - - // Add pre-submission history keyed by actionId - // This ensures we have quote data available if transaction fails during submission - const historyKey = this.#addTxToHistory({ - accountAddress: selectedAccount.address, - quoteResponse, - slippagePercentage: 0, - isStxEnabled: isStxEnabledOnClient, - startTime, - approvalTxId, - location, - abTests, - activeAbTests, - actionId, - tokenSecurityTypeDestination, - }); + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Submitted, + undefined, + preConfirmationProperties, + featureId, + ); - // Pass txFee when gasIncluded is true to use the quote's gas fees - // instead of re-estimating (which would fail for max native token swaps) - const tradeTxMeta = await submitEvmTransaction({ - messenger: this.messenger, - transactionType: isBridgeTx - ? TransactionType.bridge - : TransactionType.swap, - trade: quoteResponse.trade, - requireApproval, - txFee: quoteResponse.quote.gasIncluded - ? quoteResponse.quote.feeData.txFee - : undefined, - actionId, - }); + /** + * Check if the account is an EIP-7702 delegated account. + * Delegated accounts only allow 1 in-flight tx, so approve + swap + * must be batched into a single transaction + */ + const isDelegatedAccount = isNonEvmChainId(quote.srcChainId) + ? false + : await checkIsDelegatedAccount( + this.messenger, + selectedAccount.address as Hex, + [formatChainIdToHex(quote.srcChainId)], + ); - // On success, rekey from actionId to txMeta.id and update srcTxHash - this.#rekeyHistoryItem(historyKey, tradeTxMeta); + const strategyParams: SubmitStrategyParams = { + messenger: this.messenger, + quoteResponse, + isStxEnabled, + isBridgeTx, + isDelegatedAccount, + selectedAccount, + requireApproval, + clientId: this.#clientId, + bridgeApiBaseUrl: this.#config.customBridgeApiBaseUrl, + addTransactionBatchFn: this.#addTransactionBatchFn, + fetchFn: this.#fetchFn, + traceFn: this.#trace, + }; - return tradeTxMeta; - }, - ); - } + return await this.#trace( + getTraceParams(quoteResponse, isStxEnabled), + async () => + await this.#executeSubmitStrategy(strategyParams, { + startTime, + location, + abTests, + activeAbTests, + tokenSecurityTypeDestination, + }), + ); } catch (error) { - !quoteResponse.featureId && - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.Failed, - undefined, - { - error_message: (error as Error)?.message, - ...preConfirmationProperties, - }, - ); + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Failed, + undefined, + { + error_message: (error as Error)?.message, + ...preConfirmationProperties, + }, + featureId, + ); throw error; } - - try { - // For non-batch EVM transactions, history was already added/rekeyed above - // Only add history here for non-EVM and batch EVM transactions - const isNonBatchEvm = - !isNonEvmChainId(quoteResponse.quote.srcChainId) && - !isStxEnabledOnClient && - !quoteResponse.quote.gasIncluded7702 && - !isDelegatedAccount; - - let historyKey: string = txMeta.id; - if (!isNonBatchEvm) { - // Add swap or bridge tx to history - historyKey = this.#addTxToHistory({ - accountAddress: selectedAccount.address, - bridgeTxMeta: txMeta, // Only the id and hash fields are used by the BridgeStatusController - quoteResponse, - slippagePercentage: 0, // TODO include slippage provided by quote if using dynamic slippage, or slippage from quote request - isStxEnabled: isStxEnabledOnClient, - startTime, - approvalTxId, - location, - abTests, - activeAbTests, - tokenSecurityTypeDestination, - }); - } - - if (isNonEvmChainId(quoteResponse.quote.srcChainId)) { - // Start polling for bridge tx status - this.#startPollingForTxId(historyKey); - // Track non-EVM Swap completed event - if (!(isBridgeTx || isTronTx)) { - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.Completed, - historyKey, - ); - } - } - } catch { - // Ignore errors here, we don't want to crash the app if this fails and tx submission succeeds - } - return txMeta; }; /** @@ -1346,8 +1149,11 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata; @@ -1355,6 +1161,8 @@ export class BridgeStatusController extends StaticIntervalPollingController; activeAbTests?: { key: string; value: string }[]; + isStxEnabled?: boolean; + quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived]; tokenSecurityTypeDestination?: string | null; }): Promise> => { const { @@ -1363,179 +1171,22 @@ export class BridgeStatusController extends StaticIntervalPollingController { // Track polling status updated event const historyItem = this.state.txHistory[historyKey]; - if (historyItem && !historyItem.featureId) { - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.PollingStatusUpdated, - historyKey, - getPollingStatusUpdatedProperties( - this.messenger, - pollingStatus, - historyItem, - ), - ); - } + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.PollingStatusUpdated, + historyKey, + getPollingStatusUpdatedProperties( + this.messenger, + pollingStatus, + historyItem, + ), + ); }; /** @@ -1563,6 +1212,8 @@ export class BridgeStatusController extends StaticIntervalPollingController[EventName], + featureIdOverride?: FeatureId, ): void => { + const historyItem: BridgeHistoryItem | undefined = txMetaId + ? this.state.txHistory[txMetaId] + : undefined; + const featureId = featureIdOverride ?? historyItem?.featureId; + + const shouldSkipMetrics = + // Skip tracking all other events when featureId is set (i.e. PERPS) + featureId && + // Always publish StatusValidationFailed event, regardless of featureId + eventName !== UnifiedSwapBridgeEventName.StatusValidationFailed; + if (shouldSkipMetrics) { + return; + } + // Legacy/new metrics fields are intentionally kept independent during migration. const historyAbTests = txMetaId ? this.state.txHistory?.[txMetaId]?.abTests @@ -1590,13 +1256,14 @@ export class BridgeStatusController extends StaticIntervalPollingController 0 && { ab_tests: resolvedAbTests, @@ -1608,18 +1275,6 @@ export class BridgeStatusController extends StaticIntervalPollingController, +): AsyncGenerator { + const { + requireApproval, + quoteResponse, + messenger, + isBridgeTx, + addTransactionBatchFn, + isDelegatedAccount, + } = args; + + const tradeData: Parameters< + typeof getAddTransactionBatchParams + >[0]['tradeData'] = []; + + const approvalTxType = isBridgeTx + ? TransactionType.bridgeApproval + : TransactionType.swapApproval; + + if (quoteResponse.resetApproval) { + tradeData.push({ + tx: quoteResponse.resetApproval, + type: approvalTxType, + }); + } + if (quoteResponse.approval) { + tradeData.push({ + tx: quoteResponse.approval, + type: approvalTxType, + }); + } + if (quoteResponse.trade) { + tradeData.push({ + tx: quoteResponse.trade, + type: isBridgeTx ? TransactionType.bridge : TransactionType.swap, + assetsFiatValues: { + sending: quoteResponse.sentAmount?.valueInCurrency?.toString(), + receiving: quoteResponse.toTokenAmount?.valueInCurrency?.toString(), + }, + }); + } + + const transactionParams = await getAddTransactionBatchParams({ + messenger, + tradeData, + quote: quoteResponse.quote, + requireApproval, + isDelegatedAccount, + }); + + const { approvalMeta, tradeMeta } = await addTransactionBatch( + messenger, + addTransactionBatchFn, + transactionParams, + ); + + yield { + type: SubmitStep.SetTradeMeta, + payload: { tradeMeta }, + }; + + yield { + type: SubmitStep.AddHistoryItem, + payload: { + historyKey: tradeMeta.id, + approvalTxId: approvalMeta?.id, + bridgeTxMeta: { + id: tradeMeta.id, + hash: tradeMeta.hash, + batchId: tradeMeta.batchId, + }, + }, + }; +} diff --git a/packages/bridge-status-controller/src/strategy/evm-strategy.ts b/packages/bridge-status-controller/src/strategy/evm-strategy.ts new file mode 100644 index 0000000000..27313109fb --- /dev/null +++ b/packages/bridge-status-controller/src/strategy/evm-strategy.ts @@ -0,0 +1,156 @@ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { isEvmTxData } from '@metamask/bridge-controller'; +import type { TxData } from '@metamask/bridge-controller'; +import { TransactionType } from '@metamask/transaction-controller'; + +import { getApprovalTraceParams } from '../utils/trace'; +import { + generateActionId, + handleApprovalDelay, + handleMobileHardwareWalletDelay, + submitEvmTransaction, + waitForTxConfirmation, +} from '../utils/transaction'; +import { SubmitStep } from './types'; +import type { SubmitStrategyParams, SubmitStepResult } from './types'; + +/** + * Submits a single trade and returns the txMetaId + * + * @param args - The parameters for the transaction + * @param args.messenger - The messenger + * @param args.requireApproval - Whether to require approval for the transaction + * @param transactionType - The type of transaction to submit + * @param trade - The tx to submit + * @param submitParams - Optional parameters to pass to the submitEvmTransaction function + * @returns The txMeta of the transaction + */ +const handleSingleTx = async ( + { messenger, requireApproval }: SubmitStrategyParams, + transactionType: TransactionType, + trade: TxData, + submitParams: Partial[0]> = {}, +) => + await submitEvmTransaction({ + messenger, + trade, + transactionType, + requireApproval, + ...submitParams, + }); + +/** + * Submits the approval and resetApproval transactions through the TransactionController. + * If there is a resetApproval, it will be submitted first. + * But only the approval's txMetaId will be returned. + * + * @param args - The parameters for the submission flow + * + * @returns The approvalTxId of the approval transaction + */ +const approve = async (args: SubmitStrategyParams) => { + const { quoteResponse, isBridgeTx } = args; + const { approval, resetApproval } = quoteResponse; + if (!approval || !isEvmTxData(approval)) { + return undefined; + } + + const transactionType = isBridgeTx + ? TransactionType.bridgeApproval + : TransactionType.swapApproval; + + if (resetApproval) { + await handleSingleTx(args, transactionType, resetApproval); + } + + if (approval) { + const approvalTxMeta = await handleSingleTx( + args, + transactionType, + approval, + ); + return approvalTxMeta?.id; + } +}; + +export const handleEvmApprovals = async (args: SubmitStrategyParams) => + await args.traceFn( + getApprovalTraceParams(args.quoteResponse, args.isStxEnabled), + async () => await approve(args), + ); + +/** + * Sequentially submits EVM resetApproval, approval and trade transactions through the TransactionController. + * + * @param args - The parameters for the transaction + * @yields Data for updating the BridgeStatusController + */ +export async function* submitEvmHandler( + args: SubmitStrategyParams, +): AsyncGenerator { + const { quoteResponse, requireApproval, isBridgeTx } = args; + + // Submit resetApproval and approval transactions if present + const approvalTxId = await handleEvmApprovals(args); + + // Delay after approval + if (approvalTxId) { + await handleApprovalDelay(quoteResponse.quote.srcChainId); + } + // Hardware-wallet delay first (Ledger second-prompt spacing), then wait for + // on-chain approval confirmation so swap gas estimation runs after allowance is set. + await handleMobileHardwareWalletDelay(requireApproval); + if (requireApproval && approvalTxId) { + await waitForTxConfirmation(args.messenger, approvalTxId); + } + + // Generate trade actionId for pre-submission history + const actionId = generateActionId(); + + // Add pre-submission history keyed by actionId + // This ensures we have quote data available if transaction fails during submission + yield { + type: SubmitStep.AddHistoryItem, + payload: { + historyKey: actionId, + approvalTxId, + actionId, + }, + }; + + const transactionType = isBridgeTx + ? TransactionType.bridge + : TransactionType.swap; + const tradeMeta = await handleSingleTx( + args, + transactionType, + quoteResponse.trade, + { + // TODO figure out if this is needed + // Pass txFee when gasIncluded is true to use the quote's gas fees + // instead of re-estimating (which would fail for max native token swaps) + txFee: quoteResponse.quote.gasIncluded + ? quoteResponse.quote.feeData.txFee + : undefined, + actionId, + }, + ); + + // Use the tradeMeta's id as history key + yield { + type: SubmitStep.RekeyHistoryItem, + payload: { + oldHistoryKey: actionId, + newHistoryKey: tradeMeta.id, + tradeMeta, + }, + }; + + yield { + type: SubmitStep.SetTradeMeta, + payload: { + tradeMeta, + }, + }; +} diff --git a/packages/bridge-status-controller/src/strategy/index.ts b/packages/bridge-status-controller/src/strategy/index.ts new file mode 100644 index 0000000000..93083e4d8a --- /dev/null +++ b/packages/bridge-status-controller/src/strategy/index.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import { + BitcoinTradeData, + ChainId, + isBitcoinTrade, + isEvmTxData, + isNonEvmChainId, + isTronTrade, + Trade, + TronTradeData, + TxData, +} from '@metamask/bridge-controller'; + +import { submitBatchHandler } from './batch-strategy'; +import { submitEvmHandler as defaultSubmitHandler } from './evm-strategy'; +import { submitIntentHandler } from './intent-strategy'; +import { submitNonEvmHandler } from './non-evm-strategy'; +import type { SubmitStrategyParams, SubmitStepResult } from './types'; + +const validateParams = < + TxDataType extends BitcoinTradeData | TronTradeData | string | TxData, +>( + params: SubmitStrategyParams, +): params is SubmitStrategyParams => { + const txs = [ + params.quoteResponse.trade, + params.quoteResponse.approval, + params.quoteResponse.resetApproval, + ].filter((tx): tx is TxDataType => tx !== undefined); + + switch (params.quoteResponse.quote.srcChainId) { + case ChainId.SOLANA: + return txs.every((tx) => typeof tx === 'string'); + case ChainId.BTC: + return txs.every(isBitcoinTrade); + case ChainId.TRON: + return txs.every(isTronTrade); + default: + return txs.every(isEvmTxData); + } +}; + +/** + * Selects the appropriate submit strategy based on the quote parameters then executes it + * + * @param params - The parameters for the transaction + * @returns An async generator that yields results from each step of the submit flow. The yielded + * results are used to update the BridgeStatusController state and emit events. + */ +const executeSubmitStrategy = ( + params: SubmitStrategyParams, +): AsyncGenerator => { + const { quoteResponse, isStxEnabled, isDelegatedAccount } = params; + + // Non-EVM transactions + if (isNonEvmChainId(quoteResponse.quote.srcChainId)) { + if (!validateParams(params)) { + throw new Error( + 'Failed to submit cross-chain swap transaction: trade is not a non-EVM transaction', + ); + } + return submitNonEvmHandler(params); + } + + // EVM transactions + if (!validateParams(params)) { + throw new Error( + 'Failed to submit cross-chain swap transaction: trade is not an EVM transaction', + ); + } + + // Intent transactions + if (quoteResponse.quote.intent) { + return submitIntentHandler(params); + } + + // Batched transactions + const shouldBatchTxs = + isStxEnabled || quoteResponse.quote.gasIncluded7702 || isDelegatedAccount; + if (shouldBatchTxs) { + return submitBatchHandler(params); + } + + // Non-stx/gasless EVM transactions + return defaultSubmitHandler(params); +}; + +export default executeSubmitStrategy; diff --git a/packages/bridge-status-controller/src/strategy/intent-strategy.ts b/packages/bridge-status-controller/src/strategy/intent-strategy.ts new file mode 100644 index 0000000000..b11078ba3c --- /dev/null +++ b/packages/bridge-status-controller/src/strategy/intent-strategy.ts @@ -0,0 +1,201 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { + formatChainIdToHex, + isEvmTxData, + TxData, +} from '@metamask/bridge-controller'; +import { TransactionType } from '@metamask/transaction-controller'; + +import { getJwt } from '../utils/authentication'; +import { + getIntentFromQuote, + mapIntentOrderStatusToTransactionStatus, + postSubmitOrder, +} from '../utils/intent-api'; +import { signTypedMessage } from '../utils/keyring'; +import { getNetworkClientIdByChainId } from '../utils/network'; +import { + addSyntheticTransaction, + waitForTxConfirmation, +} from '../utils/transaction'; +import { handleEvmApprovals } from './evm-strategy'; +import { SubmitStrategyParams, SubmitStepResult, SubmitStep } from './types'; + +/** + * Submits a synthetic EVM transaction to the TransactionController in order to display the intent order's + * status in theclients, before the actual transaction is finalized on chain. The resulting transaction + * is only available locally and is not submitted to the chain. + * + * @param orderUid - The order uid of the intent transaction + * @param args - The parameters for the transaction + * @returns The tradeMeta for the synthetic transaction + */ +const handleSyntheticTx = async ( + orderUid: string, + args: SubmitStrategyParams, +) => { + const { quoteResponse, messenger, isBridgeTx, selectedAccount } = args; + const { + quote: { srcChainId }, + } = quoteResponse; + + // Determine transaction type: swap for same-chain, bridge for cross-chain + const transactionType = isBridgeTx + ? /* c8 ignore start */ + TransactionType.bridge + : /* c8 ignore end */ + TransactionType.swap; + + const networkClientId = getNetworkClientIdByChainId(messenger, srcChainId); + + // This is a synthetic transaction whose purpose is to be able + // to track the order status via the history + if (!isEvmTxData(quoteResponse.trade)) { + throw new Error('Failed to submit intent: trade is not an EVM transaction'); + } + const intent = getIntentFromQuote(quoteResponse); + // This is a synthetic transaction whose purpose is to be able + // to track the order status via the history + /** + * @deprecated use trade data from quote response instead + */ + const intentTransactionParams = { + chainId: formatChainIdToHex(srcChainId), + from: selectedAccount.address, + to: + intent.settlementContract ?? '0x9008D19f58AAbd9eD0D60971565AA8510560ab41', // Default settlement contract + data: `0x${orderUid?.slice(-8)}`, // Use last 8 chars of orderUid to make each transaction unique + value: '0x0', + gas: '0x5208', // Minimal gas for display purposes + gasPrice: '0x3b9aca00', // 1 Gwei - will be converted to EIP-1559 fees if network supports it + }; + + const initialTxMeta = await addSyntheticTransaction( + messenger, + intentTransactionParams, + { + requireApproval: false, + networkClientId, + type: transactionType, + }, + ); + return initialTxMeta; +}; + +/** + * Submits batched EVM transactions to the TransactionController + * + * @param args - The parameters for the transaction + * @param args.quoteResponse - The quote response + * @param args.messenger - The messenger + * @param args.selectedAccount - The selected account + * @param args.traceFn - The trace function + * @param args.isBridgeTx - Whether the transaction is a bridge transaction + * @returns The approvalTxId and tradeMeta for the non-EVM transaction + */ +const handleSubmitIntent = async (args: SubmitStrategyParams) => { + const { + quoteResponse, + messenger, + selectedAccount, + clientId, + fetchFn, + bridgeApiBaseUrl, + } = args; + const { srcChainId, requestId } = quoteResponse.quote; + + const intent = getIntentFromQuote(quoteResponse); + const signature = await signTypedMessage({ + messenger, + accountAddress: selectedAccount.address, + typedData: intent.typedData, + }); + + const { id: orderUid, status } = await postSubmitOrder({ + params: { + srcChainId, + quoteId: requestId, + signature, + order: intent.order, + userAddress: selectedAccount.address, + aggregatorId: intent.protocol, + }, + clientId, + jwt: await getJwt(messenger), + fetchFn, + bridgeApiBaseUrl, + }); + + return { + orderUid, + orderStatus: status, + }; +}; + +/** + * Submits an approval tx to the TransactionController, + * posts an intent order to the bridge-api, + * and creates a synthetic transaction in the TransactionController + * + * @param args - The parameters for the transaction + * @param args.quoteResponse - The quote response + * @param args.messenger - The messenger + * @param args.selectedAccount - The selected account + * @param args.traceFn - The trace function + * @param args.isBridgeTx - Whether the transaction is a bridge transaction + * @yields The approvalTxId and tradeMeta for the intent transaction + */ +export async function* submitIntentHandler( + args: SubmitStrategyParams, +): AsyncGenerator { + // TODO handle STX/batch approvals + const approvalTxId = await handleEvmApprovals(args); + approvalTxId && (await waitForTxConfirmation(args.messenger, approvalTxId)); + + // TODO add to history after approval tx is confirmed + + // Submit the intent order to the bridge-api + const { orderUid, orderStatus } = await handleSubmitIntent(args); + + // Initialize a transaction in the TransactionController + const syntheticTxMeta = await handleSyntheticTx(orderUid, { + ...args, + requireApproval: false, + isStxEnabled: false, + }); + + // Use synthetic transaction metadata + translated intent order status as the tradeMeta + yield { + type: SubmitStep.SetTradeMeta, + payload: { + tradeMeta: { + ...syntheticTxMeta, + // Map intent order status to TransactionController status + status: mapIntentOrderStatusToTransactionStatus(orderStatus), + }, + }, + }; + + // Update txHistory with synthetic txMeta and order id + yield { + type: SubmitStep.AddHistoryItem, + payload: { + // Use orderId as the history key for intent transactions + historyKey: orderUid, + bridgeTxMeta: { + id: syntheticTxMeta?.id, + }, + approvalTxId, + // Keep original txId for TransactionController updates + originalTransactionId: syntheticTxMeta?.id, + }, + }; + + // Start polling using the orderId as the history key + yield { + type: SubmitStep.StartPolling, + payload: { + historyKey: orderUid, + }, + }; +} diff --git a/packages/bridge-status-controller/src/strategy/non-evm-strategy.ts b/packages/bridge-status-controller/src/strategy/non-evm-strategy.ts new file mode 100644 index 0000000000..44accdd7e2 --- /dev/null +++ b/packages/bridge-status-controller/src/strategy/non-evm-strategy.ts @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { isTronChainId } from '@metamask/bridge-controller'; +import type { + BitcoinTradeData, + TronTradeData, + TxData, +} from '@metamask/bridge-controller'; + +import { handleNonEvmTx } from '../utils/snaps'; +import { getApprovalTraceParams } from '../utils/trace'; +import { handleApprovalDelay } from '../utils/transaction'; +import { SubmitStep } from './types'; +import type { SubmitStrategyParams, SubmitStepResult } from './types'; + +/** + * Submits the approval transaction for a non-EVM transaction if present + * + * @param args - The parameters for the transaction + * @returns The tx id of the approval transaction + */ +const handleTronApproval = async ( + args: SubmitStrategyParams< + TronTradeData | BitcoinTradeData | string | TxData + >, +) => { + const { quoteResponse, traceFn } = args; + + const approvalTxId = await traceFn( + getApprovalTraceParams(quoteResponse, false), + async () => { + if (quoteResponse.approval) { + const txMeta = await handleNonEvmTx( + args.messenger, + quoteResponse.approval, + quoteResponse, + args.selectedAccount, + ); + return txMeta.id; + } + return undefined; + }, + ); + + if (approvalTxId) { + // Add delay after approval similar to EVM flow + await handleApprovalDelay(quoteResponse.quote.srcChainId); + return approvalTxId; + } + return undefined; +}; + +/** + * Submits Solana, Bitcoin, or Tron transactions to the snap controller + * + * @param args - The parameters for the transaction + * @param args.quoteResponse - The quote response + * @param args.messenger - The messenger + * @param args.selectedAccount - The selected account + * @param args.traceFn - The trace function + * @param args.isBridgeTx - Whether the transaction is a bridge transaction + * @yields The approvalTxId and tradeMeta for the non-EVM transaction + */ +export async function* submitNonEvmHandler( + args: SubmitStrategyParams< + BitcoinTradeData | TronTradeData | string | TxData + >, +): AsyncGenerator { + const { quoteResponse, isBridgeTx } = args; + + const approvalTxId = await handleTronApproval(args); + + // TODO bridge-status should update history with actionId if approvalTxId is present + + const tradeMeta = await handleNonEvmTx( + args.messenger, + quoteResponse.trade, + quoteResponse, + args.selectedAccount, + ); + + yield { + type: SubmitStep.SetTradeMeta, + payload: { tradeMeta }, + }; + + yield { + type: SubmitStep.AddHistoryItem, + payload: { + historyKey: tradeMeta.id, + approvalTxId, + bridgeTxMeta: { + id: tradeMeta.id, + hash: tradeMeta.hash, + }, + }, + }; + + yield { + type: SubmitStep.StartPolling, + payload: { + historyKey: tradeMeta.id, + }, + }; + + if (!isTronChainId(quoteResponse.quote.srcChainId) && !isBridgeTx) { + yield { + type: SubmitStep.PublishCompletedEvent, + payload: { + historyKey: tradeMeta.id, + }, + }; + } +} diff --git a/packages/bridge-status-controller/src/strategy/types.ts b/packages/bridge-status-controller/src/strategy/types.ts new file mode 100644 index 0000000000..f895b7c7de --- /dev/null +++ b/packages/bridge-status-controller/src/strategy/types.ts @@ -0,0 +1,92 @@ +import type { AccountsControllerState } from '@metamask/accounts-controller'; +import type { + BridgeClientId, + QuoteMetadata, + QuoteResponse, + Trade, + TxData, +} from '@metamask/bridge-controller'; +import type { TraceCallback } from '@metamask/controller-utils'; +import type { + TransactionController, + TransactionMeta, +} from '@metamask/transaction-controller'; + +import type { + BridgeStatusControllerMessenger, + FetchFunction, + StartPollingForBridgeTxStatusArgs, +} from '../types'; + +export enum SubmitStep { + AddHistoryItem = 'addHistoryItem', + RekeyHistoryItem = 'rekeyHistoryItem', + StartPolling = 'startPolling', + PublishCompletedEvent = 'publishCompletedEvent', + SetTradeMeta = 'setTradeMeta', +} + +/** + * Any possible result returned by steps in a submission strategy. These can be returned in any order. + */ +export type SubmitStepResult = + | { + type: SubmitStep.AddHistoryItem; + payload: Pick< + StartPollingForBridgeTxStatusArgs, + 'approvalTxId' | 'bridgeTxMeta' | 'originalTransactionId' | 'actionId' + > & { + historyKey: string; + }; + } + | { + type: SubmitStep.RekeyHistoryItem; + payload: { + /** Usually the actionId of the preceeding `approval` transaction */ + oldHistoryKey: string; + /** Usually the txMeta.id of the `trade` transaction */ + newHistoryKey: string; + /** The {@link TransactionMeta} for the `trade` transaction after it has been submitted successfully */ + tradeMeta: TransactionMeta; + }; + } + | { + type: SubmitStep.StartPolling; + payload: { + /** The `txHistory` key of the transaction to start polling for */ + historyKey: string; + }; + } + | { + type: SubmitStep.PublishCompletedEvent; + payload: { + /** The `txHistory` key of the transaction that has been submitted successfully */ + historyKey: string; + }; + } + | { + type: SubmitStep.SetTradeMeta; + /** The {@link TransactionMeta} for the transaction that has been submitted successfully */ + payload: { + tradeMeta: TransactionMeta; + }; + }; + +/** + * The parameters for the submission flow + */ +export type SubmitStrategyParams = { + addTransactionBatchFn: TransactionController['addTransactionBatch']; + isBridgeTx: boolean; + isDelegatedAccount: boolean; + isStxEnabled: boolean; + messenger: BridgeStatusControllerMessenger; + quoteResponse: QuoteResponse & QuoteMetadata; + requireApproval: boolean; + selectedAccount: AccountsControllerState['internalAccounts']['accounts'][string]; + traceFn: TraceCallback; + // Used for intent transactions + fetchFn: FetchFunction; + clientId: BridgeClientId; + bridgeApiBaseUrl: string; +}; diff --git a/packages/bridge-status-controller/src/types.ts b/packages/bridge-status-controller/src/types.ts index 05222f513d..6b01432a4b 100644 --- a/packages/bridge-status-controller/src/types.ts +++ b/packages/bridge-status-controller/src/types.ts @@ -239,7 +239,7 @@ export type StartPollingForBridgeTxStatusArgs = { targetContractAddress?: BridgeHistoryItem['targetContractAddress']; approvalTxId?: BridgeHistoryItem['approvalTxId']; isStxEnabled?: BridgeHistoryItem['isStxEnabled']; - location?: BridgeHistoryItem['location']; + location: MetaMetricsSwapsEventSource; // Legacy field for `ab_tests` metrics payload. abTests?: BridgeHistoryItem['abTests']; // New field for `active_ab_tests` metrics payload. diff --git a/packages/bridge-status-controller/src/utils/gas.test.ts b/packages/bridge-status-controller/src/utils/gas.test.ts index 5967defba9..fe1cb9f3bf 100644 --- a/packages/bridge-status-controller/src/utils/gas.test.ts +++ b/packages/bridge-status-controller/src/utils/gas.test.ts @@ -155,20 +155,8 @@ describe('gas calculation utils', () => { value: '0x1', }; - it('should return empty object if gas fields should be skipped (skipGasFields is true)', async () => { - const result = await calculateGasFees( - true, - null as never, - mockTrade, - 'mainnet', - '0x1', - ); - expect(result).toStrictEqual({}); - }); - it('should txFee when provided', async () => { const result = await calculateGasFees( - false, null as never, mockTrade, 'mainnet', @@ -211,7 +199,6 @@ describe('gas calculation utils', () => { }, }); const result = await calculateGasFees( - false, { call: mockCall } as never, { ...mockTrade, gasLimit }, 'mainnet', diff --git a/packages/bridge-status-controller/src/utils/history.test.ts b/packages/bridge-status-controller/src/utils/history.test.ts index d22c4a2423..04f522c706 100644 --- a/packages/bridge-status-controller/src/utils/history.test.ts +++ b/packages/bridge-status-controller/src/utils/history.test.ts @@ -25,7 +25,7 @@ describe('History Utils', () => { it('returns false when history item missing', () => { const state = makeState(); - const result = rekeyHistoryItemInState(state, 'missing', { + const result = rekeyHistoryItemInState(state, 'missing', 'tx1', { id: 'tx1', hash: '0xhash', }); @@ -52,7 +52,7 @@ describe('History Utils', () => { }, }); - const result = rekeyHistoryItemInState(state, 'action1', { + const result = rekeyHistoryItemInState(state, 'action1', 'tx1', { id: 'tx1', hash: '0xnew', }); @@ -82,7 +82,9 @@ describe('History Utils', () => { }, }); - const result = rekeyHistoryItemInState(state, 'action1', { id: 'tx1' }); + const result = rekeyHistoryItemInState(state, 'action1', 'tx1', { + id: 'tx1', + }); expect(result).toBe(true); expect(state.txHistory.tx1.status.srcChain.txHash).toBe('0xold'); @@ -126,7 +128,7 @@ describe('History Utils', () => { } as unknown as StartPollingForBridgeTxStatusArgsSerialized; it('omits tokenSecurityTypeDestination when not provided', () => { - const { txHistoryItem } = getInitialHistoryItem(baseArgs); + const txHistoryItem = getInitialHistoryItem(baseArgs); expect( Object.prototype.hasOwnProperty.call( txHistoryItem, @@ -136,7 +138,7 @@ describe('History Utils', () => { }); it('persists a non-null tokenSecurityTypeDestination', () => { - const { txHistoryItem } = getInitialHistoryItem({ + const txHistoryItem = getInitialHistoryItem({ ...baseArgs, tokenSecurityTypeDestination: 'Malicious', }); @@ -144,7 +146,7 @@ describe('History Utils', () => { }); it('persists a null tokenSecurityTypeDestination', () => { - const { txHistoryItem } = getInitialHistoryItem({ + const txHistoryItem = getInitialHistoryItem({ ...baseArgs, tokenSecurityTypeDestination: null, }); diff --git a/packages/bridge-status-controller/src/utils/history.ts b/packages/bridge-status-controller/src/utils/history.ts index 0f15aef511..af227514fa 100644 --- a/packages/bridge-status-controller/src/utils/history.ts +++ b/packages/bridge-status-controller/src/utils/history.ts @@ -14,29 +14,40 @@ import type { } from '../types'; import { getMaxPendingHistoryItemAgeMs } from './feature-flags'; +const updateHistoryItem = ( + oldHistoryItem: BridgeHistoryItem, + txMeta: { id: string; hash?: string }, +): Partial => { + return { + ...oldHistoryItem, + txMetaId: txMeta.id, + originalTransactionId: oldHistoryItem.originalTransactionId ?? txMeta.id, + status: { + ...oldHistoryItem.status, + srcChain: { + ...oldHistoryItem.status.srcChain, + txHash: txMeta.hash ?? oldHistoryItem.status.srcChain?.txHash, + }, + }, + }; +}; + export const rekeyHistoryItemInState = ( state: BridgeStatusControllerState, - actionId: string, + oldKey: string, + newKey: string, txMeta: { id: string; hash?: string }, ): boolean => { - const historyItem = state.txHistory[actionId]; + const historyItem = state.txHistory[oldKey]; if (!historyItem) { return false; } - state.txHistory[txMeta.id] = { + state.txHistory[newKey] = { ...historyItem, - txMetaId: txMeta.id, - originalTransactionId: historyItem.originalTransactionId ?? txMeta.id, - status: { - ...historyItem.status, - srcChain: { - ...historyItem.status.srcChain, - txHash: txMeta.hash ?? historyItem.status.srcChain?.txHash, - }, - }, + ...updateHistoryItem(historyItem, txMeta), }; - delete state.txHistory[actionId]; + delete state.txHistory[oldKey]; return true; }; @@ -95,6 +106,7 @@ export const getMatchingHistoryEntryForApprovalTxMeta = ( * Determines the key to use for storing a bridge history item. * Uses actionId for pre-submission tracking, or bridgeTxMetaId for post-submission. * + * @deprecated specify an explicit history key instead * @param actionId - The action ID used for pre-submission tracking * @param bridgeTxMetaId - The transaction meta ID from bridgeTxMeta * @param syntheticTransactionId - The transactionId of the intent's placeholder transaction @@ -117,10 +129,7 @@ export function getHistoryKey( export const getInitialHistoryItem = ( args: StartPollingForBridgeTxStatusArgsSerialized, -): { - historyKey: string; - txHistoryItem: BridgeHistoryItem; -} => { +): BridgeHistoryItem => { const { bridgeTxMeta, quoteResponse, @@ -138,14 +147,6 @@ export const getInitialHistoryItem = ( actionId, tokenSecurityTypeDestination, } = args; - // Determine the key for this history item: - // - For pre-submission (non-batch EVM): use actionId - // - For post-submission or other cases: use bridgeTxMeta.id - const historyKey = getHistoryKey( - actionId, - bridgeTxMeta?.id, - originalTransactionId, - ); // Write all non-status fields to state so we can reference the quote in Activity list without the Bridge API // We know it's in progress but not the exact status yet @@ -195,7 +196,7 @@ export const getInitialHistoryItem = ( }), }; - return { historyKey, txHistoryItem }; + return txHistoryItem; }; export const shouldPollHistoryItem = ( diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index 5d7e7a8253..3f8c6fd6f7 100644 --- a/packages/bridge-status-controller/src/utils/metrics.ts +++ b/packages/bridge-status-controller/src/utils/metrics.ts @@ -182,7 +182,7 @@ export const getPriceImpactFromQuote = ( * The quote is used to populate event properties before confirmation * * @param quoteResponse - The quote response - * @param isStxEnabledOnClient - Whether smart transactions are enabled on the client, for example the getSmartTransactionsEnabled selector value from the extension + * @param isStxEnabled - Whether smart transactions are enabled on the client, for example the getSmartTransactionsEnabled selector value from the extension * @param accountHardwareType - The hardware wallet type used to submit the tx, or null if not a hardware wallet * @param location - The entry point from which the user initiated the swap or bridge (e.g. Main View, Token View, Trending Explore) * @param abTests - Legacy A/B test context for `ab_tests` (backward compatibility) @@ -192,9 +192,9 @@ export const getPriceImpactFromQuote = ( */ export const getPreConfirmationPropertiesFromQuote = ( quoteResponse: QuoteResponse & Partial, - isStxEnabledOnClient: boolean, + isStxEnabled: boolean, accountHardwareType: AccountHardwareType, - location: MetaMetricsSwapsEventSource = MetaMetricsSwapsEventSource.MainView, + location?: MetaMetricsSwapsEventSource, abTests?: Record, activeAbTests?: { key: string; value: string }[], tokenSecurityTypeDestination?: string | null, @@ -217,7 +217,7 @@ export const getPreConfirmationPropertiesFromQuote = ( quoteResponse.quote.destChainId, ), usd_amount_source: Number(quoteResponse.sentAmount?.usd ?? 0), - stx_enabled: isStxEnabledOnClient, + stx_enabled: isStxEnabled, action_type: MetricsActionType.SWAPBRIDGE_V1, custom_slippage: false, // TODO detect whether the user changed the default slippage location, diff --git a/packages/bridge-status-controller/src/utils/trace.ts b/packages/bridge-status-controller/src/utils/trace.ts index 1bc7587819..72098788d1 100644 --- a/packages/bridge-status-controller/src/utils/trace.ts +++ b/packages/bridge-status-controller/src/utils/trace.ts @@ -9,7 +9,7 @@ import { TraceName } from '../constants'; export const getTraceParams = ( quoteResponse: QuoteResponse, - isStxEnabledOnClient: boolean, + isStxEnabled: boolean, ) => { return { name: isCrossChain( @@ -20,14 +20,14 @@ export const getTraceParams = ( : TraceName.SwapTransactionCompleted, data: { srcChainId: formatChainIdToCaip(quoteResponse.quote.srcChainId), - stxEnabled: isStxEnabledOnClient, + stxEnabled: isStxEnabled, }, }; }; export const getApprovalTraceParams = ( quoteResponse: QuoteResponse, - isStxEnabledOnClient: boolean, + isStxEnabled: boolean, ) => { return { name: isCrossChain( @@ -38,7 +38,7 @@ export const getApprovalTraceParams = ( : TraceName.SwapTransactionApprovalCompleted, data: { srcChainId: formatChainIdToCaip(quoteResponse.quote.srcChainId), - stxEnabled: isStxEnabledOnClient, + stxEnabled: isStxEnabled, }, }; }; diff --git a/packages/bridge-status-controller/src/utils/transaction.test.ts b/packages/bridge-status-controller/src/utils/transaction.test.ts index 34b5baadcc..daf730137c 100644 --- a/packages/bridge-status-controller/src/utils/transaction.test.ts +++ b/packages/bridge-status-controller/src/utils/transaction.test.ts @@ -1665,7 +1665,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }); describe('toBatchTxParams', () => { - it('should return params without gas if skipGasFields is true', () => { + it('should return params without gas if gasFees are undefined', () => { const mockTrade = { chainId: 1, gasLimit: 1231, @@ -1674,7 +1674,7 @@ describe('Bridge Status Controller Transaction Utils', () => { from: '0x1', value: '0x1', }; - const result = toBatchTxParams(true, mockTrade as TxData, {}); + const result = toBatchTxParams(mockTrade as TxData); expect(result).toStrictEqual({ data: '0x1', from: '0x1', @@ -1701,8 +1701,7 @@ describe('Bridge Status Controller Transaction Utils', () => { includeApproval?: boolean; includeResetApproval?: boolean; } = {}, - ): QuoteResponse & - QuoteMetadata & { approval?: TxData; resetApproval?: TxData } => + ): QuoteResponse & QuoteMetadata => ({ quote: { bridgeId: 'bridge1', @@ -1745,6 +1744,7 @@ describe('Bridge Status Controller Transaction Utils', () => { to: '0xTokenContract', data: '0xapprovalData', from: '0xUserAddress', + chainId: ChainId.ETH, }, }), ...(overrides.includeResetApproval && { @@ -1752,6 +1752,7 @@ describe('Bridge Status Controller Transaction Utils', () => { to: '0xTokenContract', data: '0xresetData', from: '0xUserAddress', + chainId: ChainId.ETH, }, }), sentAmount: { @@ -1817,11 +1818,19 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: true, - trade: mockQuoteResponse.trade, - approval: mockQuoteResponse.approval, + tradeData: [ + { + tx: mockQuoteResponse.approval as TxData, + type: TransactionType.bridgeApproval, + }, + { + tx: mockQuoteResponse.trade, + type: TransactionType.bridge, + }, + ], + isDelegatedAccount: false, }); expect(result.disable7702).toBe(false); @@ -1839,10 +1848,15 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: false, - trade: mockQuoteResponse.trade, + tradeData: [ + { + tx: mockQuoteResponse.trade, + type: TransactionType.swap, + }, + ], + isDelegatedAccount: false, }); expect(result.disable7702).toBe(true); @@ -1859,11 +1873,19 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: false, - trade: mockQuoteResponse.trade, - approval: mockQuoteResponse.approval, + tradeData: [ + { + tx: mockQuoteResponse.approval as TxData, + type: TransactionType.swapApproval, + }, + { + tx: mockQuoteResponse.trade, + type: TransactionType.swap, + }, + ], + isDelegatedAccount: false, }); expect(result.transactions).toHaveLength(2); @@ -1877,11 +1899,19 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: false, - trade: mockQuoteResponse.trade, - resetApproval: mockQuoteResponse.resetApproval, + tradeData: [ + { + tx: mockQuoteResponse.resetApproval as TxData, + type: TransactionType.swapApproval, + }, + { + tx: mockQuoteResponse.trade, + type: TransactionType.swap, + }, + ], + isDelegatedAccount: false, }); expect(result.transactions).toHaveLength(2); @@ -1897,11 +1927,19 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: true, - trade: mockQuoteResponse.trade, - resetApproval: mockQuoteResponse.resetApproval, + tradeData: [ + { + tx: mockQuoteResponse.resetApproval as TxData, + type: TransactionType.bridgeApproval, + }, + { + tx: mockQuoteResponse.trade, + type: TransactionType.bridge, + }, + ], + isDelegatedAccount: false, }); expect(result.disable7702).toBe(true); @@ -1919,10 +1957,15 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: false, - trade: mockQuoteResponse.trade, + tradeData: [ + { + tx: mockQuoteResponse.trade, + type: TransactionType.swap, + }, + ], + isDelegatedAccount: false, }); expect(result.isGasFeeIncluded).toBe(false); @@ -1935,10 +1978,15 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: false, - trade: mockQuoteResponse.trade, + tradeData: [ + { + tx: mockQuoteResponse.trade, + type: TransactionType.swap, + }, + ], + isDelegatedAccount: false, }); expect(result.isGasFeeIncluded).toBe(true); @@ -1951,10 +1999,15 @@ describe('Bridge Status Controller Transaction Utils', () => { }); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: false, - trade: mockQuoteResponse.trade, + tradeData: [ + { + tx: mockQuoteResponse.trade, + type: TransactionType.swap, + }, + ], + isDelegatedAccount: false, }); expect(result.isGasFeeIncluded).toBe(false); @@ -1977,10 +2030,14 @@ describe('Bridge Status Controller Transaction Utils', () => { const callSpy = jest.spyOn(mockMessenger, 'call'); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessenger, - isBridgeTx: true, - trade: mockQuoteResponse.trade, + tradeData: [ + { + tx: mockQuoteResponse.trade as TxData, + type: TransactionType.bridge, + }, + ], isDelegatedAccount: true, }); @@ -2033,10 +2090,14 @@ describe('Bridge Status Controller Transaction Utils', () => { const callSpy = jest.spyOn(mockMessagingSystem, 'call'); const result = await getAddTransactionBatchParams({ - quoteResponse: mockQuoteResponse, + quote: mockQuoteResponse.quote, messenger: mockMessagingSystem, - isBridgeTx: true, - trade: mockQuoteResponse.trade, + tradeData: [ + { + tx: mockQuoteResponse.trade as TxData, + type: TransactionType.bridge, + }, + ], isDelegatedAccount: true, }); diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index bb8663727b..aabaf1f2ef 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -1,14 +1,11 @@ +/* eslint-disable no-restricted-syntax */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { ChainId, formatChainIdToHex, BRIDGE_PREFERRED_GAS_ESTIMATE, } from '@metamask/bridge-controller'; -import type { - QuoteMetadata, - QuoteResponse, - TxData, -} from '@metamask/bridge-controller'; +import type { Quote, QuoteResponse, TxData } from '@metamask/bridge-controller'; import { toHex } from '@metamask/controller-utils'; import { TransactionStatus, @@ -98,16 +95,12 @@ export const getTxGasEstimates = async ( }; export const calculateGasFees = async ( - skipGasFields: boolean, messenger: BridgeStatusControllerMessenger, { chainId: _, gasLimit, ...trade }: TxData, networkClientId: string, chainId: Hex, txFee?: { maxFeePerGas: string; maxPriorityFeePerGas: string }, ) => { - if (skipGasFields) { - return {}; - } if (txFee) { return { ...txFee, gas: gasLimit?.toString() }; } @@ -324,13 +317,12 @@ export const waitForTxConfirmation = async ( }; export const toBatchTxParams = ( - skipGasFields: boolean, { chainId, gasLimit, ...trade }: TxData, - { - maxFeePerGas, - maxPriorityFeePerGas, - gas, - }: { maxFeePerGas?: string; maxPriorityFeePerGas?: string; gas?: string }, + gasFees?: { + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; + gas?: string; + }, ): BatchTransactionParams => { const params = { ...trade, @@ -338,10 +330,11 @@ export const toBatchTxParams = ( to: trade.to, value: trade.value, }; - if (skipGasFields) { + if (!gasFees) { return params; } + const { maxFeePerGas, maxPriorityFeePerGas, gas } = gasFees; return { ...params, gas: toHex(gas ?? 0), @@ -350,122 +343,98 @@ export const toBatchTxParams = ( }; }; +const getGaslessParams = ({ + quote: { + feeData: { txFee }, + gasIncluded, + gasIncluded7702, + gasSponsored, + }, + isDelegatedAccount = false, +}: { + quote: Quote; + isDelegatedAccount: boolean; +}) => ({ + // Gas fields should be omitted only when gas is sponsored via 7702 + skipGasFields: gasIncluded7702, + disable7702: + // Enable 7702 batching when the quote includes gasless 7702 support, + gasIncluded7702 + ? false + : // or when the account is already delegated (to avoid the in-flight transaction limit for delegated accounts) + !isDelegatedAccount || + // For gasless transactions with STX/sendBundle we keep disabling 7702. + gasIncluded, + txFee: gasIncluded || gasIncluded7702 ? txFee : undefined, + isGasFeeIncluded: Boolean(gasIncluded7702), + isGasFeeSponsored: Boolean(gasSponsored), +}); + export const getAddTransactionBatchParams = async ({ messenger, - isBridgeTx, - approval, - resetApproval, - trade, - quoteResponse: { - quote: { - feeData: { txFee }, - gasIncluded, - gasIncluded7702, - gasSponsored, - }, - sentAmount, - toTokenAmount, - }, + tradeData, requireApproval = false, - isDelegatedAccount = false, + ...gasIncludedArgs }: { messenger: BridgeStatusControllerMessenger; - isBridgeTx: boolean; - trade: TxData; - quoteResponse: Omit & - Partial; - approval?: TxData; - resetApproval?: TxData; + tradeData: { + tx: TxData; + type: TransactionType; + assetsFiatValues?: { sending?: string; receiving?: string }; + }[]; requireApproval?: boolean; - isDelegatedAccount?: boolean; -}) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isGasless = gasIncluded || gasIncluded7702; - const selectedAccount = getAccountByAddress(messenger, trade.from); +} & Parameters[0]): Promise< + Parameters[0] +> => { + const { + isGasFeeIncluded, + isGasFeeSponsored, + disable7702, + skipGasFields, + txFee, + } = getGaslessParams(gasIncludedArgs); + + const trade = tradeData[0]?.tx; + const selectedAccount = getAccountByAddress(messenger, trade?.from); if (!selectedAccount) { throw new Error( 'Failed to submit cross-chain swap batch transaction: unknown account in trade data', ); } + const hexChainId = formatChainIdToHex(trade.chainId); const networkClientId = getNetworkClientIdByChainId(messenger, hexChainId); - // Gas fields should be omitted only when gas is sponsored via 7702 - const skipGasFields = gasIncluded7702 === true; - // Enable 7702 batching when the quote includes gasless 7702 support, - // or when the account is already delegated (to avoid the in-flight - // transaction limit for delegated accounts) - let disable7702 = !skipGasFields && !isDelegatedAccount; - - // For gasless transactions with STX/sendBundle we keep disabling 7702. - if (gasIncluded && !gasIncluded7702) { - disable7702 = true; - } + const transactions: TransactionBatchSingleRequest[] = await Promise.all( + tradeData.map(async ({ type, assetsFiatValues, tx }) => { + const gasFees = skipGasFields + ? undefined + : await calculateGasFees( + messenger, + tx, + networkClientId, + hexChainId, + txFee, + ); + return { + type, + params: toBatchTxParams(tx, gasFees), + assetsFiatValues, + }; + }), + ).then((txs) => txs); - const transactions: TransactionBatchSingleRequest[] = []; - if (resetApproval) { - const gasFees = await calculateGasFees( - skipGasFields, - messenger, - resetApproval, - networkClientId, - hexChainId, - isGasless ? txFee : undefined, - ); - transactions.push({ - type: isBridgeTx - ? TransactionType.bridgeApproval - : TransactionType.swapApproval, - params: toBatchTxParams(skipGasFields, resetApproval, gasFees), - }); - } - if (approval) { - const gasFees = await calculateGasFees( - skipGasFields, - messenger, - approval, - networkClientId, - hexChainId, - isGasless ? txFee : undefined, - ); - transactions.push({ - type: isBridgeTx - ? TransactionType.bridgeApproval - : TransactionType.swapApproval, - params: toBatchTxParams(skipGasFields, approval, gasFees), - }); - } - const gasFees = await calculateGasFees( - skipGasFields, - messenger, - trade, - networkClientId, - hexChainId, - isGasless ? txFee : undefined, - ); - transactions.push({ - type: isBridgeTx ? TransactionType.bridge : TransactionType.swap, - params: toBatchTxParams(skipGasFields, trade, gasFees), - assetsFiatValues: { - sending: sentAmount?.valueInCurrency?.toString(), - receiving: toTokenAmount?.valueInCurrency?.toString(), - }, - }); - const transactionParams: Parameters< - TransactionController['addTransactionBatch'] - >[0] = { + return { disable7702, - isGasFeeIncluded: Boolean(gasIncluded7702), - isGasFeeSponsored: Boolean(gasSponsored), + isGasFeeIncluded, + isGasFeeSponsored, networkClientId, requireApproval, origin: 'metamask', - from: trade.from, + from: selectedAccount.address as Hex, isInternal: true, transactions, }; - - return transactionParams; }; export const findAndUpdateTransactionsInBatch = ({