From da8e0be0093a43c7ffd40e533d2e5c60a666a261 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Mon, 1 Sep 2025 20:10:36 +0200 Subject: [PATCH 01/85] feat: cow-swap --- packages/bridge-controller/src/index.ts | 2 + packages/bridge-controller/src/types.ts | 3 + .../bridge-controller/src/utils/validators.ts | 36 ++ .../src/bridge-status-controller.ts | 521 +++++++++++++++++- .../bridge-status-controller/src/types.ts | 12 + .../src/TransactionController.ts | 77 ++- .../src/helpers/PendingTransactionTracker.ts | 3 +- 7 files changed, 636 insertions(+), 18 deletions(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 019fdffdac7..25a7d0eef59 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -40,6 +40,8 @@ export type { QuoteResponse, FeeData, TxData, + Intent, + CowSwapOrderLike, BridgeControllerState, BridgeControllerAction, BridgeControllerActions, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 7cbca3d247a..79f7956b03f 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -30,6 +30,7 @@ import type { BridgeAssetSchema, ChainConfigurationSchema, FeeDataSchema, + IntentSchema, PlatformConfigSchema, ProtocolSchema, QuoteResponseSchema, @@ -255,6 +256,8 @@ export type FeeData = Infer; export type Quote = Infer; export type TxData = Infer; +export type Intent = Infer; +export type CowSwapOrderLike = Intent['order']; /** * This is the type for the quote response from the bridge-api * TxDataType can be overriden to be a string when the quote is non-evm diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index c4da78ff522..e725e184840 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -153,6 +153,41 @@ export const StepSchema = type({ const RefuelDataSchema = StepSchema; +// Allow digit strings for amounts/validTo for flexibility across providers +const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); + + +// Intent support (e.g., CoW Swap EIP-712 order signing) +const IntentProtocolSchema = enums(['cowswap']); + +export const CowSwapOrderSchema = type({ + // EIP-712 Order fields (subset required for signing/submission) + sellToken: HexAddressSchema, + buyToken: HexAddressSchema, + receiver: optional(HexAddressSchema), + validTo: DigitStringOrNumberSchema, + appData: string(), + appDataHash: HexStringSchema, + feeAmount: TruthyDigitStringSchema, + kind: enums(['sell', 'buy']), + partiallyFillable: boolean(), + // One of these is required by CoW depending on kind; we keep both optional here and rely on backend validation + sellAmount: optional(TruthyDigitStringSchema), + buyAmount: optional(TruthyDigitStringSchema), + // Optional owner/from for convenience when building domain/message + from: optional(HexAddressSchema), +}); + +export const IntentSchema = type({ + protocol: IntentProtocolSchema, + order: CowSwapOrderSchema, + // Optional metadata to aid submission/routing + settlementContract: optional(HexAddressSchema), + relayer: optional(HexAddressSchema), + quoteId: optional(nullable(string())), +}); + + export const QuoteSchema = type({ requestId: string(), srcChainId: ChainIdSchema, @@ -204,6 +239,7 @@ export const QuoteSchema = type({ priceImpact: optional(string()), }), ), + intent: optional(IntentSchema), }); export const TxDataSchema = type({ diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 42907a8e5d6..1dc2eb583ce 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -5,6 +5,7 @@ import type { RequiredEventContextFromClient, TxData, QuoteResponse, + Intent, } from '@metamask/bridge-controller'; import { formatChainIdToHex, @@ -75,6 +76,21 @@ import { } from './utils/transaction'; import { generateActionId } from './utils/transaction'; +// CoW intent: API base and network path mapping (adjust as needed) +const COW_API_BASE = 'https://api.cow.fi'; +const COW_NETWORK_PATHS: Record = { + // Ethereum Mainnet + 1: 'mainnet', + // Arbitrum One + 42161: 'arbitrum_one', + // Base + 8453: 'base', + // Avalanche C-Chain + 43114: 'avalanche', + // Polygon PoS + 137: 'polygon', +}; + const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list // basically match the behavior of TransactionController @@ -181,6 +197,10 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { type, status, id } = transactionMeta; + + // Skip intent transactions - they have their own tracking via CoW API + if ((transactionMeta as any).swapMetaData?.isIntentTx) { + return; + } + if ( type && [ @@ -435,6 +461,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { txHistory } = this.state; + // Intent-based items: poll CoW API instead of Bridge API + if (bridgeTxMetaId.startsWith('intent:')) { + await this.#fetchCowOrderStatus({ bridgeTxMetaId }); + return; + } + if ( shouldSkipFetchDueToFetchFailures(txHistory[bridgeTxMetaId]?.attempts) ) { @@ -652,6 +685,186 @@ export class BridgeStatusController extends StaticIntervalPollingController { + const { txHistory } = this.state; + const historyItem = txHistory[bridgeTxMetaId]; + if (!historyItem) { + return; + } + + // Backoff handling + if (shouldSkipFetchDueToFetchFailures(historyItem.attempts)) { + return; + } + + const orderUid = bridgeTxMetaId.replace(/^intent:/, ''); + const srcChainId = historyItem.quote.srcChainId; + const networkPath = COW_NETWORK_PATHS[srcChainId]; + if (!networkPath) { + // Unsupported mapping: stop polling with failure + this.update((state) => { + state.txHistory[bridgeTxMetaId].status = { + status: StatusTypes.FAILED, + srcChain: { + chainId: srcChainId, + txHash: '', + }, + } as unknown as typeof state.txHistory[string]['status']; + }); + return; + } + + try { + const url = `${COW_API_BASE}/${networkPath}/api/v1/orders/${orderUid}`; + const res: any = await this.#fetchFn(url, { method: 'GET' }); + + // CoW API status enum mapping + const rawStatus = res?.status ?? ''; + const isComplete = rawStatus === 'fulfilled'; + const isFailed = ['cancelled', 'expired'].includes(rawStatus); + const isPending = ['presignaturePending', 'open'].includes(rawStatus); + + // Try to find a tx hash in common fields + let txHash = ''; + let allHashes: string[] = []; + + if (isComplete) { + // Prefer authoritative trades endpoint for one or multiple fills + const tradesUrl = `${COW_API_BASE}/${networkPath}/api/v1/trades?orderUid=${orderUid}`; + const trades: any[] = (await this.#fetchFn(tradesUrl, { method: 'GET' })) ?? []; + allHashes = trades + .map((t) => t?.txHash || t?.transactionHash) + .filter((h: unknown): h is string => typeof h === 'string' && h.length > 0); + // Fallback to any hash on order if trades missing + if (allHashes.length === 0) { + const possible = [ + res?.txHash, + res?.transactionHash, + res?.executedTransaction, + res?.executedTransactionHash, + ].filter((h: unknown): h is string => typeof h === 'string' && h.length > 0); + allHashes = possible; + } + txHash = allHashes[allHashes.length - 1] || ''; + } + + const newStatus = { + status: isComplete + ? StatusTypes.COMPLETE + : isFailed + ? StatusTypes.FAILED + : isPending + ? StatusTypes.PENDING + : StatusTypes.PENDING, // Default to pending for unknown statuses + srcChain: { + chainId: srcChainId, + txHash: txHash || historyItem.status.srcChain.txHash || '', + }, + } as typeof historyItem.status; + + const newBridgeHistoryItem = { + ...historyItem, + status: newStatus, + completionTime: + newStatus.status === StatusTypes.COMPLETE || + newStatus.status === StatusTypes.FAILED + ? Date.now() + : undefined, + attempts: undefined, + srcTxHashes: + allHashes.length > 0 + ? Array.from(new Set([...(historyItem.srcTxHashes ?? []), ...allHashes])) + : historyItem.srcTxHashes, + }; + + this.update((state) => { + state.txHistory[bridgeTxMetaId] = newBridgeHistoryItem; + }); + + // Update the actual transaction in TransactionController to sync with CoW status + // Use the original transaction ID (not the intent: prefixed bridge history key) + const originalTxId = (historyItem as any).originalTransactionId || historyItem.txMetaId; + if (originalTxId && !originalTxId.startsWith('intent:')) { + try { + const transactionStatus = isComplete + ? TransactionStatus.confirmed + : isFailed + ? TransactionStatus.failed + : isPending + ? TransactionStatus.submitted + : TransactionStatus.submitted; // Default to submitted for unknown statuses + + // Merge with existing TransactionMeta to avoid wiping required fields + const { transactions } = this.messagingSystem.call( + 'TransactionController:getState', + ); + const existingTxMeta = (transactions as TransactionMeta[]).find( + (t) => t.id === originalTxId, + ); + + if (!existingTxMeta) { + console.warn( + '📝 [fetchCowOrderStatus] Skipping update; transaction not found', + { originalTxId, bridgeHistoryKey: bridgeTxMetaId }, + ); + } else { + const updatedTxMeta: TransactionMeta = { + ...existingTxMeta, + status: transactionStatus, + ...(txHash ? { hash: txHash } : {}), + ...(txHash + ? ({ + txReceipt: { + ...(existingTxMeta as any).txReceipt, + transactionHash: txHash, + status: (isComplete ? '0x1' : '0x0') as unknown as string, + }, + } as Partial) + : {}), + } as TransactionMeta; + + this.#updateTransactionFn( + updatedTxMeta, + `BridgeStatusController - CoW order status updated: ${rawStatus}`, + ); + } + } catch (error) { + console.error('📝 [fetchCowOrderStatus] Failed to update transaction status', { + originalTxId, + bridgeHistoryKey: bridgeTxMetaId, + error, + }); + } + } + + const pollingToken = this.#pollingTokensByTxMetaId[bridgeTxMetaId]; + const isFinal = + newStatus.status === StatusTypes.COMPLETE || + newStatus.status === StatusTypes.FAILED; + if (isFinal && pollingToken) { + this.stopPollingByPollingToken(pollingToken); + delete this.#pollingTokensByTxMetaId[bridgeTxMetaId]; + + if (newStatus.status === StatusTypes.COMPLETE) { + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Completed, + bridgeTxMetaId, + ); + } else if (newStatus.status === StatusTypes.FAILED) { + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Failed, + bridgeTxMetaId, + ); + } + } + } catch (e) { + // Network or API error: apply backoff + this.#handleFetchFailure(bridgeTxMetaId); + } + }; + readonly #getSrcTxHash = (bridgeTxMetaId: string): string | undefined => { const { txHistory } = this.state; // Prefer the srcTxHash from bridgeStatusState so we don't have to l ook up in TransactionController @@ -789,6 +1002,51 @@ export class BridgeStatusController extends StaticIntervalPollingController => { + const start = Date.now(); + // Poll the TransactionController state for status changes + // We intentionally keep this simple to avoid extra wiring/subscriptions in this controller + // and because we only need it for the rare intent+approval path. + // eslint-disable-next-line no-constant-condition + while (true) { + const { transactions } = this.messagingSystem.call( + 'TransactionController:getState', + ); + const meta = transactions.find((t: TransactionMeta) => t.id === txId); + + if (meta) { + // Treat both 'confirmed' and 'finalized' as success to match TC lifecycle + if ( + meta.status === TransactionStatus.confirmed || + // Some environments move directly to finalized + (TransactionStatus as any).finalized === meta.status + ) { + return meta; + } + if ( + meta.status === TransactionStatus.failed || + meta.status === TransactionStatus.dropped || + meta.status === TransactionStatus.rejected + ) { + throw new Error('Approval transaction did not confirm'); + } + } + + if (Date.now() - start > timeoutMs) { + throw new Error('Timed out waiting for approval confirmation'); + } + + await new Promise((resolve) => setTimeout(resolve, pollMs)); + } + }; + readonly #handleApprovalTx = async ( isBridgeTx: boolean, quoteResponse: QuoteResponse & QuoteMetadata, @@ -871,13 +1129,19 @@ export class BridgeStatusController extends StaticIntervalPollingController[0] = { - ...trade, + ...tradeWithoutGasLimit, chainId: hexChainId, - gasLimit: trade.gasLimit?.toString(), - gas: trade.gasLimit?.toString(), + // Only add gasLimit and gas if they're valid (not undefined/null/zero) + ...(tradeGasLimit && tradeGasLimit !== 0 && { + gasLimit: tradeGasLimit.toString(), + gas: tradeGasLimit.toString(), + }), }; const transactionParamsWithMaxGas: TransactionParams = { ...transactionParams, @@ -928,7 +1192,10 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata; + signature: string; + accountAddress: string; + }, + ): Promise> => { + const { quoteResponse, signature, accountAddress } = params; + + // Build pre-confirmation properties for error tracking parity with submitTx + const account = this.messagingSystem.call( + 'AccountsController:getAccountByAddress', + accountAddress, + ); + const isHardwareAccount = !!account && isHardwareWallet(account); + const preConfirmationProperties = getPreConfirmationPropertiesFromQuote( + quoteResponse, + false, + isHardwareAccount, + ); + + try { + const intent = (quoteResponse as QuoteResponse & { intent?: Intent }).quote.intent; + if (!intent || intent.protocol !== 'cowswap') { + throw new Error('submitIntent: missing or unsupported intent'); + } + + // If backend provided an approval tx for this intent quote, submit it first (on-chain), + // then proceed with off-chain intent submission. + let approvalTxId: string | undefined; + if (quoteResponse.approval) { + const isBridgeTx = isCrossChain( + quoteResponse.quote.srcChainId, + quoteResponse.quote.destChainId, + ); + + // Handle approval silently for better UX in intent flows + const approvalTxMeta = await this.#handleApprovalTx( + isBridgeTx, + quoteResponse, + /* requireApproval */ false, + ); + approvalTxId = approvalTxMeta?.id; + + // Optionally wait for approval confirmation with timeout and graceful fallback + // CoW order can be created before allowance is mined, but waiting helps avoid MEV issues + if (approvalTxId) { + try { + // Wait with a shorter timeout and continue if it fails + await this.#waitForTxConfirmation(approvalTxId, { + timeoutMs: 30_000, // 30 seconds instead of 5 minutes + pollMs: 3_000 // Poll less frequently to avoid rate limits + }); + } catch (error) { + // Log but don't throw - continue with CoW order submission + console.warn('Approval confirmation failed, continuing with intent submission:', error); + } + } + } + + // Map chainId to CoW API server path (prod) + const chainId = quoteResponse.quote.srcChainId; + const serverPath = COW_NETWORK_PATHS[chainId]; + if (!serverPath) { + throw new Error(`submitIntent: unsupported chainId for CoW intents: ${chainId}`); + } + + // Build OrderCreation payload (simplified) + const orderBody = { + ...intent.order, + feeAmount: '0', + from: accountAddress, + signature, + signingScheme: 'eip712', + } as Record; + + // POST to CoW prod + const url = `https://api.cow.fi/${serverPath}/api/v1/orders`; + const res = await this.#fetchFn(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(orderBody), + }); + + const orderUid: string | undefined = typeof res === 'string' ? res : res?.uid ?? res?.orderUid ?? res?.id; + if (!orderUid) { + throw new Error('submitIntent: failed to submit order'); + } + + // Get initial order status from CoW API + let initialOrderStatus = 'open'; // Default status + try { + const orderStatusUrl = `${COW_API_BASE}/${serverPath}/api/v1/orders/${orderUid}`; + const orderStatusRes = await this.#fetchFn(orderStatusUrl, { method: 'GET' }); + initialOrderStatus = orderStatusRes?.status ?? 'open'; + } catch (error) { + console.warn('📝 [submitIntent] Failed to get initial order status, using default:', error); + } + + // Determine transaction type: swap for same-chain, bridge for cross-chain + const isCrossChainTx = isCrossChain( + quoteResponse.quote.srcChainId, + quoteResponse.quote.destChainId, + ); + const transactionType = isCrossChainTx ? TransactionType.bridge : TransactionType.swap; + + // Create actual transaction in Transaction Controller first + const networkClientId = this.messagingSystem.call( + 'NetworkController:findNetworkClientIdByChainId', + formatChainIdToHex(chainId), + ); + + const intentTransactionParams = { + chainId: formatChainIdToHex(chainId), + from: accountAddress, + to: intent.settlementContract || '0x9008D19f58AAbd9eD0D60971565AA8510560ab41', // CoW 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 { transactionMeta: txMetaPromise } = await this.#addTransactionFn( + intentTransactionParams, + { + origin: 'metamask', + actionId: generateActionId(), + requireApproval: false, + networkClientId, + type: transactionType, + swaps: { + meta: { + swapMetaData: { + isIntentTx: true, + orderUid: orderUid, + intentType: isCrossChainTx ? 'bridge' : 'swap', + }, + }, + }, + }, + ); + + const intentTxMeta = await txMetaPromise; + + // Map initial CoW order status to TransactionController status + const isComplete = initialOrderStatus === 'fulfilled'; + const isFailed = ['cancelled', 'expired'].includes(initialOrderStatus); + const isPending = ['presignaturePending', 'open'].includes(initialOrderStatus); + + const initialTransactionStatus = isComplete + ? TransactionStatus.confirmed + : isFailed + ? TransactionStatus.failed + : isPending + ? TransactionStatus.submitted + : TransactionStatus.submitted; + + // Update transaction with proper initial status based on CoW order + const statusUpdatedTxMeta = { + ...intentTxMeta, + status: initialTransactionStatus, + }; + + this.#updateTransactionFn( + statusUpdatedTxMeta, + `BridgeStatusController - Initial CoW order status: ${initialOrderStatus}` + ); + + // Update with actual transaction metadata + const syntheticMeta = { + ...statusUpdatedTxMeta, + isIntentTx: true, + orderUid, + intentType: isCrossChainTx ? 'bridge' : 'swap', + } as unknown as TransactionMeta; + + // Record in bridge history with actual transaction metadata + try { + // Use intent: prefix for CoW transactions to route to CoW API + const bridgeHistoryKey = `intent:${orderUid}`; + + // Create a bridge transaction metadata that includes the original txId + const bridgeTxMetaForHistory = { + ...syntheticMeta, + id: bridgeHistoryKey, // Use intent: prefix for bridge history key + originalTransactionId: syntheticMeta.id, // Keep original txId for TransactionController updates + } as any; + + this.#addTxToHistory({ + accountAddress, + bridgeTxMeta: bridgeTxMetaForHistory, + statusRequest: { + ...getStatusRequestParams(quoteResponse), + srcTxHash: syntheticMeta.hash || '', + }, + quoteResponse, + slippagePercentage: 0, + isStxEnabled: false, + approvalTxId, + }); + + // Debug: Check if the bridge history was added + const bridgeHistoryState = this.state.txHistory; + + // Start polling using the intent: prefixed key to route to CoW API + this.#startPollingForTxId(bridgeHistoryKey); + } catch (error) { + console.error('📝 [submitIntent] Failed to add to bridge history', error); + // non-fatal but log the error + } + + return syntheticMeta; + } catch (error) { + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Failed, + undefined, + { + error_message: (error as Error)?.message, + ...preConfirmationProperties, + }, + ); + throw error; + } + }; + /** * Tracks post-submission events for a cross-chain swap based on the history item * @@ -1209,9 +1710,9 @@ export class BridgeStatusController extends StaticIntervalPollingController id === txMetaId); + const txMeta = transactions?.find((t: TransactionMeta) => t.id === txMetaId); const approvalTxMeta = transactions?.find( - ({ id }) => id === historyItem.approvalTxId, + (t: TransactionMeta) => t.id === historyItem.approvalTxId, ); const requestParamProperties = getRequestParamFromHistory(historyItem); diff --git a/packages/bridge-status-controller/src/types.ts b/packages/bridge-status-controller/src/types.ts index 696c6ce766f..4e854d9ef6a 100644 --- a/packages/bridge-status-controller/src/types.ts +++ b/packages/bridge-status-controller/src/types.ts @@ -108,6 +108,13 @@ export type BridgeHistoryItem = { batchId?: string; quote: Quote; status: StatusResponse; + /** + * For intent-based orders (e.g., CoW) that can be partially filled across + * multiple on-chain settlements, we keep all discovered source tx hashes here. + * The canonical status.srcChain.txHash continues to hold the latest known hash + * for backward compatibility with consumers expecting a single hash. + */ + srcTxHashes?: string[]; startTime?: number; // timestamp in ms estimatedProcessingTimeInSeconds: number; slippagePercentage: number; @@ -146,6 +153,7 @@ export enum BridgeStatusAction { GET_STATE = 'getState', RESET_STATE = 'resetState', SUBMIT_TX = 'submitTx', + SUBMIT_INTENT = 'submitIntent', RESTART_POLLING_FOR_FAILED_ATTEMPTS = 'restartPollingForFailedAttempts', } @@ -243,6 +251,9 @@ export type BridgeStatusControllerResetStateAction = export type BridgeStatusControllerSubmitTxAction = BridgeStatusControllerAction; +export type BridgeStatusControllerSubmitIntentAction = + BridgeStatusControllerAction; + export type BridgeStatusControllerRestartPollingForFailedAttemptsAction = BridgeStatusControllerAction; @@ -252,6 +263,7 @@ export type BridgeStatusControllerActions = | BridgeStatusControllerResetStateAction | BridgeStatusControllerGetStateAction | BridgeStatusControllerSubmitTxAction + | BridgeStatusControllerSubmitIntentAction | BridgeStatusControllerRestartPollingForFailedAttemptsAction; // Events diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index e8f0f025820..ac150361684 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1296,13 +1296,33 @@ export class TransactionController extends BaseController< updateTransaction(addedTransactionMeta); } - await this.#trace( - { name: 'Estimate Gas Properties', parentContext: traceContext }, - (context) => - this.#updateGasProperties(addedTransactionMeta, { - traceContext: context, - }), - ); + // Skip gas estimation for intent transactions as they are not executed on-chain + const isIntentTransaction = swaps?.meta?.swapMetaData?.isIntentTx === true; + + if (!isIntentTransaction) { + await this.#trace( + { name: 'Estimate Gas Properties', parentContext: traceContext }, + (context) => + this.#updateGasProperties(addedTransactionMeta, { + traceContext: context, + }), + ); + } else { + // For intent transactions, ensure proper gas fee structure and envelope type + const isEIP1559Compatible = + await this.#getEIP1559Compatibility(addedTransactionMeta.networkClientId); + + if (isEIP1559Compatible && addedTransactionMeta.txParams.gasPrice && !addedTransactionMeta.txParams.maxFeePerGas) { + // Convert legacy gasPrice to EIP-1559 fees for intent transactions on EIP-1559 networks + addedTransactionMeta.txParams.maxFeePerGas = addedTransactionMeta.txParams.gasPrice; + addedTransactionMeta.txParams.maxPriorityFeePerGas = addedTransactionMeta.txParams.gasPrice; + addedTransactionMeta.txParams.type = TransactionEnvelopeType.feeMarket; + delete addedTransactionMeta.txParams.gasPrice; // Remove legacy gas price + } else if (!isEIP1559Compatible && addedTransactionMeta.txParams.gasPrice) { + // Ensure legacy type for non-EIP-1559 networks + addedTransactionMeta.txParams.type = TransactionEnvelopeType.legacy; + } + } // Checks if a transaction already exists with a given actionId if (!existingTransactionMeta) { @@ -2885,6 +2905,49 @@ export class TransactionController extends BaseController< } } + // For intent-based transactions (e.g., CoW intents) that are not meant to be + // published on-chain by the TransactionController, skip the approve/publish flow. + // These are tracked externally and should not be signed or sent. + const isIntentTransaction = Boolean( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.#getTransaction(transactionId) as any)?.swapMetaData?.isIntentTx === + true, + ); + + if (requireApproval === false && isIntentTransaction) { + const submittedTxMeta = this.#updateTransactionInternal( + { + transactionId, + note: 'TransactionController#processApproval - Intent transaction auto-submitted', + skipValidation: true, + }, + (draftTxMeta) => { + draftTxMeta.status = TransactionStatus.submitted; + draftTxMeta.submittedTime = new Date().getTime(); + }, + ); + + this.messagingSystem.publish( + `${controllerName}:transactionSubmitted`, + { + transactionMeta: submittedTxMeta, + }, + ); + + this.messagingSystem.publish( + `${controllerName}:transactionFinished`, + submittedTxMeta, + ); + this.#internalEvents.emit( + `${transactionId}:finished`, + submittedTxMeta, + ); + + // Short-circuit normal flow; result callbacks will be handled by the + // finished promise below. + return ApprovalState.Approved; + } + const { isCompleted: isTxCompleted } = this.#isTransactionCompleted(transactionId); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index 316866f56c6..27dbe9e0155 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -543,7 +543,8 @@ export class PendingTransactionTracker { (tx) => tx.status === TransactionStatus.submitted && !tx.verifiedOnBlockchain && - !tx.isUserOperation, + !tx.isUserOperation && + !(tx as any).swapMetaData?.isIntentTx, // Exclude intent transactions from pending tracking ); } From 0c8f25e56b6964b7c12546a356e367fe175ff748 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 3 Sep 2025 13:48:01 +0200 Subject: [PATCH 02/85] fix: include missing metadata on intent swap transaction --- .../src/bridge-status-controller.ts | 100 ++++++++++++------ 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 1dc2eb583ce..2d5b5a754a3 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1433,13 +1433,11 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata; - signature: string; - accountAddress: string; - }, - ): Promise> => { + submitIntent = async (params: { + quoteResponse: QuoteResponse & QuoteMetadata; + signature: string; + accountAddress: string; + }): Promise> => { const { quoteResponse, signature, accountAddress } = params; // Build pre-confirmation properties for error tracking parity with submitTx @@ -1447,7 +1445,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Mon, 8 Sep 2025 07:47:53 +0100 Subject: [PATCH 03/85] Fix all lint relative error and give proper type declaration. --- .../src/bridge-status-controller.ts | 145 +++++++++++------- .../src/TransactionController.ts | 41 ++--- .../src/helpers/PendingTransactionTracker.ts | 1 + scripts/generate-method-action-types.ts | 23 +-- scripts/run-eslint.ts | 5 +- 5 files changed, 129 insertions(+), 86 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index b0de08d1c8f..3ba464f82f7 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -219,6 +219,7 @@ export class BridgeStatusController extends StaticIntervalPollingController t?.txHash || t?.transactionHash) - .filter((h: unknown): h is string => typeof h === 'string' && h.length > 0); + .map( + (t: { txHash: unknown; transactionHash: unknown }) => + t?.txHash || t?.transactionHash, + ) + .filter( + (h: unknown): h is string => typeof h === 'string' && h.length > 0, + ); // Fallback to any hash on order if trades missing if (allHashes.length === 0) { const possible = [ @@ -747,20 +757,27 @@ export class BridgeStatusController extends StaticIntervalPollingController typeof h === 'string' && h.length > 0); + ].filter( + (h: unknown): h is string => typeof h === 'string' && h.length > 0, + ); allHashes = possible; } txHash = allHashes[allHashes.length - 1] || ''; } + let statusType: StatusTypes; + if (isComplete) { + statusType = StatusTypes.COMPLETE; + } else if (isFailed) { + statusType = StatusTypes.FAILED; + } else if (isPending) { + statusType = StatusTypes.PENDING; + } else { + statusType = StatusTypes.PENDING; // Default to pending for unknown statuses + } + const newStatus = { - status: isComplete - ? StatusTypes.COMPLETE - : isFailed - ? StatusTypes.FAILED - : isPending - ? StatusTypes.PENDING - : StatusTypes.PENDING, // Default to pending for unknown statuses + status: statusType, srcChain: { chainId: srcChainId, txHash: txHash || historyItem.status.srcChain.txHash || '', @@ -778,7 +795,9 @@ export class BridgeStatusController extends StaticIntervalPollingController 0 - ? Array.from(new Set([...(historyItem.srcTxHashes ?? []), ...allHashes])) + ? Array.from( + new Set([...(historyItem.srcTxHashes ?? []), ...allHashes]), + ) : historyItem.srcTxHashes, }; @@ -788,16 +807,21 @@ export class BridgeStatusController extends StaticIntervalPollingController[0] = { ...tradeWithoutGasLimit, chainId: hexChainId, // Only add gasLimit and gas if they're valid (not undefined/null/zero) - ...(tradeGasLimit && tradeGasLimit !== 0 && { - gasLimit: tradeGasLimit.toString(), - gas: tradeGasLimit.toString(), - }), + ...(tradeGasLimit && + tradeGasLimit !== 0 && { + gasLimit: tradeGasLimit.toString(), + gas: tradeGasLimit.toString(), + }), }; const transactionParamsWithMaxGas: TransactionParams = { ...transactionParams, @@ -1195,10 +1227,12 @@ export class BridgeStatusController extends StaticIntervalPollingController t.id === txMetaId); + const txMeta = transactions?.find( + (t: TransactionMeta) => t.id === txMetaId, + ); const approvalTxMeta = transactions?.find( (t: TransactionMeta) => t.id === historyItem.approvalTxId, ); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 4ac8b2c7fe3..616a3ea7723 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1311,25 +1311,28 @@ export class TransactionController extends BaseController< traceContext: context, }), ); - } else { - // For intent transactions, ensure proper gas fee structure and envelope type - const isEIP1559Compatible = - await this.#getEIP1559Compatibility(addedTransactionMeta.networkClientId); - - if (isEIP1559Compatible && addedTransactionMeta.txParams.gasPrice && !addedTransactionMeta.txParams.maxFeePerGas) { - // Convert legacy gasPrice to EIP-1559 fees for intent transactions on EIP-1559 networks - addedTransactionMeta.txParams.maxFeePerGas = addedTransactionMeta.txParams.gasPrice; - addedTransactionMeta.txParams.maxPriorityFeePerGas = addedTransactionMeta.txParams.gasPrice; - addedTransactionMeta.txParams.type = TransactionEnvelopeType.feeMarket; - delete addedTransactionMeta.txParams.gasPrice; // Remove legacy gas price - } else if (!isEIP1559Compatible && addedTransactionMeta.txParams.gasPrice) { - // Ensure legacy type for non-EIP-1559 networks - addedTransactionMeta.txParams.type = TransactionEnvelopeType.legacy; - } + } else if ( + isEIP1559Compatible && + addedTransactionMeta.txParams.gasPrice && + !addedTransactionMeta.txParams.maxFeePerGas + ) { + // Convert legacy gasPrice to EIP-1559 fees for intent transactions on EIP-1559 networks + addedTransactionMeta.txParams.maxFeePerGas = + addedTransactionMeta.txParams.gasPrice; + addedTransactionMeta.txParams.maxPriorityFeePerGas = + addedTransactionMeta.txParams.gasPrice; + addedTransactionMeta.txParams.type = TransactionEnvelopeType.feeMarket; + delete addedTransactionMeta.txParams.gasPrice; // Remove legacy gas price + } else if ( + !isEIP1559Compatible && + addedTransactionMeta.txParams.gasPrice + ) { + // Ensure legacy type for non-EIP-1559 networks + addedTransactionMeta.txParams.type = TransactionEnvelopeType.legacy; } - // Checks if a transaction already exists with a given actionId - if (!existingTransactionMeta) { + // Checks if a transaction already exists with a given actionId + if (!existingTransactionMeta) { // Set security provider response if (method && this.#securityProviderRequest) { const securityProviderResponse = await this.#securityProviderRequest( @@ -2914,8 +2917,8 @@ export class TransactionController extends BaseController< // These are tracked externally and should not be signed or sent. const isIntentTransaction = Boolean( // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.#getTransaction(transactionId) as any)?.swapMetaData?.isIntentTx === - true, + (this.#getTransaction(transactionId) as any)?.swapMetaData + ?.isIntentTx === true, ); if (requireApproval === false && isIntentTransaction) { diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index 27dbe9e0155..1d8ea07b356 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -544,6 +544,7 @@ export class PendingTransactionTracker { tx.status === TransactionStatus.submitted && !tx.verifiedOnBlockchain && !tx.isUserOperation && + // eslint-disable-next-line @typescript-eslint/no-explicit-any !(tx as any).swapMetaData?.isIntentTx, // Exclude intent transactions from pending tracking ); } diff --git a/scripts/generate-method-action-types.ts b/scripts/generate-method-action-types.ts index 2835e484347..dc73eb282ac 100755 --- a/scripts/generate-method-action-types.ts +++ b/scripts/generate-method-action-types.ts @@ -39,7 +39,7 @@ type CommandLineArguments = { * @returns The command line arguments. */ async function parseCommandLineArguments(): Promise { - const { check, fix } = await yargs(process.argv.slice(2)) + const { check, fix } = yargs(process.argv.slice(2)) .option('check', { type: 'boolean', description: 'Check if generated action type files are up to date', @@ -282,6 +282,7 @@ function createASTVisitor(context: VisitorContext) { if (ts.isVariableStatement(node)) { const declaration = node.declarationList.declarations[0]; if ( + declaration && ts.isIdentifier(declaration.name) && declaration.name.text === 'MESSENGER_EXPOSED_METHODS' ) { @@ -408,7 +409,7 @@ function extractJSDoc( } const jsDoc = jsDocTags[0]; - if (ts.isJSDoc(jsDoc)) { + if (jsDoc && ts.isJSDoc(jsDoc)) { const fullText = sourceFile.getFullText(); const start = jsDoc.getFullStart(); const end = jsDoc.getEnd(); @@ -437,17 +438,17 @@ function formatJSDoc(rawJsDoc: string): string { } else if (i === lines.length - 1) { // Last line should be */ formattedLines.push(' */'); - } else { + } else if (typeof line === 'undefined') { // Middle lines should start with ' * ' + formattedLines.push(' *'); + } else if (line.trim().startsWith('*')) { + // Remove existing * and normalize + const content = line.trim().substring(1).trim(); + formattedLines.push(content ? ` * ${content}` : ' *'); + } else { + // Handle lines that don't start with * const trimmed = line.trim(); - if (trimmed.startsWith('*')) { - // Remove existing * and normalize - const content = trimmed.substring(1).trim(); - formattedLines.push(content ? ` * ${content}` : ' *'); - } else { - // Handle lines that don't start with * - formattedLines.push(trimmed ? ` * ${trimmed}` : ' *'); - } + formattedLines.push(trimmed ? ` * ${trimmed}` : ' *'); } } diff --git a/scripts/run-eslint.ts b/scripts/run-eslint.ts index b0f0fe6e2ca..07801b578a7 100644 --- a/scripts/run-eslint.ts +++ b/scripts/run-eslint.ts @@ -160,7 +160,7 @@ async function main() { * @returns The parsed arguments. */ async function parseCommandLineArguments(): Promise { - const { cache, fix, quiet, ...rest } = await yargs(process.argv.slice(2)) + const { cache, fix, quiet, ...rest } = yargs(process.argv.slice(2)) .option('cache', { type: 'boolean', description: 'Cache results to speed up future runs', @@ -168,8 +168,7 @@ async function parseCommandLineArguments(): Promise { }) .option('fix', { type: 'boolean', - description: - 'Automatically fix all problems; pair with --quiet to only fix errors', + description: 'Automatically fix all problems; pair with --quiet to only fix errors', default: false, }) .option('quiet', { From c2bf9a26c36103fb533ceb03cdf5f33e8be1315e Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Mon, 8 Sep 2025 09:03:58 +0100 Subject: [PATCH 04/85] Fix pipeline lint issue, --- .../bridge-controller/src/utils/validators.ts | 2 - .../src/bridge-status-controller.ts | 64 ++++++++++++------- .../bridge-status-controller/src/types.ts | 3 +- .../src/TransactionController.ts | 9 +-- scripts/run-eslint.ts | 3 +- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 10650a5da17..c0aeb494e13 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -156,7 +156,6 @@ const RefuelDataSchema = StepSchema; // Allow digit strings for amounts/validTo for flexibility across providers const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); - // Intent support (e.g., CoW Swap EIP-712 order signing) const IntentProtocolSchema = enums(['cowswap']); @@ -187,7 +186,6 @@ export const IntentSchema = type({ quoteId: optional(nullable(string())), }); - export const QuoteSchema = type({ requestId: string(), srcChainId: ChainIdSchema, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 3ba464f82f7..2675c26de24 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -219,8 +219,11 @@ export class BridgeStatusController extends StaticIntervalPollingController - t?.txHash || t?.transactionHash, - ) - .filter( - (h: unknown): h is string => typeof h === 'string' && h.length > 0, - ); + allHashes = Array.isArray(trades) + ? trades + .map( + (t: { txHash?: unknown; transactionHash?: unknown }) => + t?.txHash || t?.transactionHash, + ) + .filter( + (h: unknown): h is string => + typeof h === 'string' && h.length > 0, + ) + : []; // Fallback to any hash on order if trades missing if (allHashes.length === 0) { const possible = [ - res?.txHash, - res?.transactionHash, - res?.executedTransaction, - res?.executedTransactionHash, + (res as { txHash?: string }).txHash, + (res as { transactionHash?: string }).transactionHash, + (res as { executedTransaction?: string }).executedTransaction, + (res as { executedTransactionHash?: string }) + .executedTransactionHash, ].filter( (h: unknown): h is string => typeof h === 'string' && h.length > 0, ); @@ -846,7 +856,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; } ).txReceipt, transactionHash: txHash, @@ -893,7 +903,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Promise; +) => Promise; /** * These fields are specific to Solana transactions and can likely be infered from TransactionMeta diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 616a3ea7723..8e94a99a0ef 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1323,16 +1323,13 @@ export class TransactionController extends BaseController< addedTransactionMeta.txParams.gasPrice; addedTransactionMeta.txParams.type = TransactionEnvelopeType.feeMarket; delete addedTransactionMeta.txParams.gasPrice; // Remove legacy gas price - } else if ( - !isEIP1559Compatible && - addedTransactionMeta.txParams.gasPrice - ) { + } else if (!isEIP1559Compatible && addedTransactionMeta.txParams.gasPrice) { // Ensure legacy type for non-EIP-1559 networks addedTransactionMeta.txParams.type = TransactionEnvelopeType.legacy; } - // Checks if a transaction already exists with a given actionId - if (!existingTransactionMeta) { + // Checks if a transaction already exists with a given actionId + if (!existingTransactionMeta) { // Set security provider response if (method && this.#securityProviderRequest) { const securityProviderResponse = await this.#securityProviderRequest( diff --git a/scripts/run-eslint.ts b/scripts/run-eslint.ts index 07801b578a7..c8af03a416f 100644 --- a/scripts/run-eslint.ts +++ b/scripts/run-eslint.ts @@ -168,7 +168,8 @@ async function parseCommandLineArguments(): Promise { }) .option('fix', { type: 'boolean', - description: 'Automatically fix all problems; pair with --quiet to only fix errors', + description: + 'Automatically fix all problems; pair with --quiet to only fix errors', default: false, }) .option('quiet', { From c76a00c1c48df1014c1b4c3b674956a25b70f83a Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Mon, 8 Sep 2025 09:44:30 +0100 Subject: [PATCH 05/85] fix node 22 lint and build issue. --- scripts/generate-method-action-types.ts | 11 ++++++----- scripts/run-eslint.ts | 15 ++++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/generate-method-action-types.ts b/scripts/generate-method-action-types.ts index dc73eb282ac..393ace0edb1 100755 --- a/scripts/generate-method-action-types.ts +++ b/scripts/generate-method-action-types.ts @@ -39,7 +39,7 @@ type CommandLineArguments = { * @returns The command line arguments. */ async function parseCommandLineArguments(): Promise { - const { check, fix } = yargs(process.argv.slice(2)) + const argv = await yargs(process.argv.slice(2)) .option('check', { type: 'boolean', description: 'Check if generated action type files are up to date', @@ -51,14 +51,15 @@ async function parseCommandLineArguments(): Promise { default: false, }) .help() - .check((argv) => { - if (!argv.check && !argv.fix) { + .check((args) => { + if (!args.check && !args.fix) { throw new Error('Either --check or --fix must be provided.\n'); } return true; - }).argv; + }) + .parseAsync(); - return { check, fix }; + return { check: argv.check, fix: argv.fix }; } /** diff --git a/scripts/run-eslint.ts b/scripts/run-eslint.ts index c8af03a416f..e8bb2a6bb50 100644 --- a/scripts/run-eslint.ts +++ b/scripts/run-eslint.ts @@ -160,7 +160,7 @@ async function main() { * @returns The parsed arguments. */ async function parseCommandLineArguments(): Promise { - const { cache, fix, quiet, ...rest } = yargs(process.argv.slice(2)) + const argv = await yargs(process.argv.slice(2)) .option('cache', { type: 'boolean', description: 'Cache results to speed up future runs', @@ -178,12 +178,17 @@ async function parseCommandLineArguments(): Promise { default: false, }) .help() - .string('_').argv; + .string('_') + .parseAsync(); - // Type assertion: The types for `yargs`'s `string` method are wrong. - const files = rest._ as string[]; + const files = argv._ as string[]; - return { cache, fix, quiet, files }; + return { + cache: argv.cache, + fix: argv.fix, + quiet: argv.quiet, + files, + }; } /** From 63dc8ac93ff2d01ea6e6bbf8d9561cab40e46ce7 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Mon, 8 Sep 2025 10:08:29 +0100 Subject: [PATCH 06/85] Update snapshot for unit tests. --- .../bridge-status-controller.test.ts.snap | 61 ++++--------------- 1 file changed, 13 insertions(+), 48 deletions(-) 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 33600cc7b71..b8e482ec505 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 @@ -212,6 +212,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "bridgeTxMetaId1", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": undefined, @@ -325,54 +326,6 @@ Object { } `; -exports[`BridgeStatusController startPollingForBridgeTxStatus stops polling when the status response is complete 1`] = ` -Array [ - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "TransactionController:getState", - ], - Array [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Completed", - Object { - "action_type": "swapbridge-v1", - "actual_time_minutes": 105213.34261666666, - "allowance_reset_transaction": undefined, - "approval_transaction": undefined, - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "custom_slippage": true, - "destination_transaction": "COMPLETE", - "gas_included": false, - "gas_included_7702": false, - "is_hardware_wallet": false, - "price_impact": 0, - "provider": "lifi_across", - "quote_vs_execution_ratio": 0, - "quoted_time_minutes": 0.25, - "quoted_vs_used_gas_ratio": 0, - "security_warnings": Array [], - "slippage_limit": 0, - "source_transaction": "COMPLETE", - "stx_enabled": true, - "swap_type": "crosschain", - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_actual_gas": 0, - "usd_actual_return": 0, - "usd_amount_source": 0, - "usd_quoted_gas": 2.5778, - "usd_quoted_return": 0, - }, - ], -] -`; - exports[`BridgeStatusController submitTx: EVM bridge should call handleMobileHardwareWalletDelay for hardware wallet on mobile 1`] = ` Object { "chainId": "0xa4b1", @@ -405,6 +358,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -636,6 +590,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -868,6 +823,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": true, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1111,6 +1067,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1342,6 +1299,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1573,6 +1531,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1913,6 +1872,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -2164,6 +2124,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -2541,6 +2502,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": true, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -2827,6 +2789,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -3164,6 +3127,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "100", @@ -3492,6 +3456,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "100", From 331bec5d919514384a4f360fbbe649a6a600b57e Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Mon, 8 Sep 2025 10:27:48 +0100 Subject: [PATCH 07/85] Fix all unit tests failure for bridge-status-controller.test.ts --- .../bridge-status-controller.test.ts.snap | 49 +++++++++++++++++++ .../src/bridge-status-controller.test.ts | 6 +++ .../bridge-status-controller/src/types.ts | 1 + 3 files changed, 56 insertions(+) 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 b8e482ec505..fb382cb2991 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 @@ -12,6 +12,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "bridgeTxMetaId1", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": undefined, @@ -326,6 +327,54 @@ Object { } `; +exports[`BridgeStatusController startPollingForBridgeTxStatus stops polling when the status response is complete 1`] = ` +Array [ + Array [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Completed", + Object { + "action_type": "swapbridge-v1", + "actual_time_minutes": 105213.34261666666, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "custom_slippage": true, + "destination_transaction": "COMPLETE", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "lifi_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0.25, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": Array [], + "slippage_limit": 0, + "source_transaction": "COMPLETE", + "stx_enabled": true, + "swap_type": "crosschain", + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0, + }, + ], +] +`; + exports[`BridgeStatusController submitTx: EVM bridge should call handleMobileHardwareWalletDelay for hardware wallet on mobile 1`] = ` Object { "chainId": "0xa4b1", 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 bf8daa9d631..caf7ee59c8d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -329,6 +329,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, + originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -352,6 +353,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, + originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -377,6 +379,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, + originalTransactionId: txMetaId, batchId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, @@ -412,6 +415,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, + originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -446,6 +450,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, + originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -479,6 +484,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, + originalTransactionId: txMetaId, batchId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, diff --git a/packages/bridge-status-controller/src/types.ts b/packages/bridge-status-controller/src/types.ts index 306ca824d4d..270965fa1e1 100644 --- a/packages/bridge-status-controller/src/types.ts +++ b/packages/bridge-status-controller/src/types.ts @@ -104,6 +104,7 @@ export type RefuelStatusResponse = object & StatusResponse; export type BridgeHistoryItem = { txMetaId: string; // Need this to handle STX that might not have a txHash immediately + originalTransactionId?: string; // Keep original transaction ID for intent transactions batchId?: string; quote: Quote; status: StatusResponse; From 37f72b59aa85250767437fc590d6a21451bb294f Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 10 Sep 2025 04:10:12 +0100 Subject: [PATCH 08/85] Add stub module called `Intent-manager` --- packages/Intent-manager/LICENSE | 21 + packages/Intent-manager/README.md | 109 +++ packages/Intent-manager/jest.config.js | 26 + packages/Intent-manager/package.json | 77 ++ packages/Intent-manager/spec.md | 693 ++++++++++++++++++ packages/Intent-manager/src/constants.ts | 32 + packages/Intent-manager/src/index.ts | 15 + packages/Intent-manager/src/intent-manager.ts | 7 + packages/Intent-manager/src/types.ts | 12 + packages/Intent-manager/tsconfig.build.json | 10 + packages/Intent-manager/tsconfig.json | 9 + packages/Intent-manager/typedoc.json | 7 + yarn.lock | 18 + 13 files changed, 1036 insertions(+) create mode 100644 packages/Intent-manager/LICENSE create mode 100644 packages/Intent-manager/README.md create mode 100644 packages/Intent-manager/jest.config.js create mode 100644 packages/Intent-manager/package.json create mode 100644 packages/Intent-manager/spec.md create mode 100644 packages/Intent-manager/src/constants.ts create mode 100644 packages/Intent-manager/src/index.ts create mode 100644 packages/Intent-manager/src/intent-manager.ts create mode 100644 packages/Intent-manager/src/types.ts create mode 100644 packages/Intent-manager/tsconfig.build.json create mode 100644 packages/Intent-manager/tsconfig.json create mode 100644 packages/Intent-manager/typedoc.json diff --git a/packages/Intent-manager/LICENSE b/packages/Intent-manager/LICENSE new file mode 100644 index 00000000000..c259cd7ebcf --- /dev/null +++ b/packages/Intent-manager/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/Intent-manager/README.md b/packages/Intent-manager/README.md new file mode 100644 index 00000000000..e1e01bbbce3 --- /dev/null +++ b/packages/Intent-manager/README.md @@ -0,0 +1,109 @@ +# `@metamask/intent-manager` + +Manages intent-related functionality for MetaMask. This package provides a controller for handling user intents, intent validation, execution, and state management within the MetaMask ecosystem. + +## Overview + +The Intent Manager is responsible for: + +- **Intent Creation**: Creating and validating user intents +- **Intent Execution**: Managing the execution lifecycle of intents +- **State Management**: Tracking intent states and history +- **Intent Validation**: Ensuring intents meet required criteria +- **Event Handling**: Emitting events for intent lifecycle changes + +## Installation + +```bash +yarn add @metamask/intent-manager +``` + +or + +```bash +npm install @metamask/intent-manager +``` + +## Usage + +```typescript +import { IntentManagerController } from '@metamask/intent-manager'; + +// Initialize the controller +const intentManager = new IntentManagerController({ + messenger, // Controller messenger + state: { + // Initial state + }, +}); + +// Create a new intent +const intent = await intentManager.createIntent({ + type: 'swap', + data: { + // Intent-specific data + }, +}); + +// Execute an intent +await intentManager.executeIntent(intent.id); + +// Get intent status +const status = intentManager.getIntentStatus(intent.id); +``` + +## API Reference + +### IntentManagerController + +The main controller class that manages intent operations. + +#### Methods + +- `createIntent(intentData)` - Creates a new intent +- `executeIntent(intentId)` - Executes an existing intent +- `cancelIntent(intentId)` - Cancels a pending intent +- `getIntentStatus(intentId)` - Gets the current status of an intent +- `getIntentHistory()` - Retrieves intent history + +#### Events + +- `IntentCreated` - Emitted when a new intent is created +- `IntentExecuted` - Emitted when an intent is executed +- `IntentCancelled` - Emitted when an intent is cancelled +- `IntentFailed` - Emitted when an intent execution fails + +## Types + +### Intent + +```typescript +interface Intent { + id: string; + type: IntentType; + status: IntentStatus; + data: IntentData; + createdAt: number; + updatedAt: number; +} +``` + +### IntentStatus + +```typescript +enum IntentStatus { + PENDING = 'pending', + EXECUTING = 'executing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', +} +``` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). + +## License + +MIT diff --git a/packages/Intent-manager/jest.config.js b/packages/Intent-manager/jest.config.js new file mode 100644 index 00000000000..6ef43e12723 --- /dev/null +++ b/packages/Intent-manager/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 0, + functions: 0, + lines: 0, + statements: 0, + }, + }, +}); diff --git a/packages/Intent-manager/package.json b/packages/Intent-manager/package.json new file mode 100644 index 00000000000..525289aca6e --- /dev/null +++ b/packages/Intent-manager/package.json @@ -0,0 +1,77 @@ +{ + "name": "@metamask/intent-manager", + "version": "1.0.0", + "description": "Manages intent-related functionality for MetaMask", + "keywords": [ + "MetaMask", + "Ethereum", + "Intent" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/Intent-manager#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/intent-manager", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/intent-manager", + "publish:preview": "yarn npm publish --tag preview", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/utils": "^11.4.2" + }, + "devDependencies": { + "@lavamoat/allow-scripts": "^3.0.4", + "@metamask/auto-changelog": "^3.4.4", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "lavamoat": { + "allowScripts": { + "@lavamoat/allow-scripts>@lavamoat/preinstall-always-fail": false + } + } +} diff --git a/packages/Intent-manager/spec.md b/packages/Intent-manager/spec.md new file mode 100644 index 00000000000..7b28f67a886 --- /dev/null +++ b/packages/Intent-manager/spec.md @@ -0,0 +1,693 @@ +## 1. Current State Analysis + +### 1.1 Module Overview + +Based on the codebase analysis, the current architecture consists of three main modules: + +**Bridge Controller** (`@metamask/bridge-controller`): + +- Manages quote fetching and transaction submission +- Handles both traditional bridge transactions and intent-based swaps +- Contains hardcoded CowSwap integration logic + +**Bridge Status Controller** (`@metamask/bridge-status-controller`): + +- Polls transaction status for both bridge and intent transactions +- Contains CowSwap-specific API polling logic +- Manages transaction history and status updates + +**Transaction Controller** (`@metamask/transaction-controller`): + +- Manages transaction lifecycle and metadata +- Stores transaction data and provides updates to UI + +### 1.2 Current Intent Integration + +The current CowSwap integration is tightly coupled and hardcoded: + +```tsx +// CoW intent: API base and network path mapping (adjust as needed) +const COW_API_BASE = ''; +const COW_NETWORK_PATHS: Record = { + // Ethereum Mainnet + 1: 'mainnet', + // Arbitrum One + 42161: 'arbitrum_one', + // Base + 8453: 'base', + // Avalanche C-Chain + 43114: 'avalanche', + // Polygon PoS + 137: 'polygon', +}; + +``` + +### 1.3 Current Data Flow + +``` +User Request → Bridge Controller → Quote Generation → Intent Submission → Bridge Status Controller → Status Polling → Transaction Controller Updates + +``` + +## 2. Proposed Modular Architecture + +### 2.1 Architecture Diagram + +```mermaid +graph TB + subgraph "Core Controllers" + BC[Bridge Controller] + BSC[Bridge Status Controller] + TC[Transaction Controller] + end + + subgraph "Intent Provider System" + IPM[Intent Provider Manager] + IPI[Intent Provider Interface] + + subgraph "Provider Implementations" + COW[CowSwap Provider] + ONEINCH[1inch Provider] + ZX[0x Protocol Provider] + CUSTOM[Custom Provider] + end + end + + subgraph "Shared Infrastructure" + VL[Validation Layer] + EH[Error Handler] + ML[Metrics Logger] + CH[Config Handler] + end + + BC --> IPM + BSC --> IPM + IPM --> IPI + IPI --> COW + IPI --> ONEINCH + IPI --> ZX + IPI --> CUSTOM + + IPM --> VL + IPM --> EH + IPM --> ML + IPM --> CH + + BSC --> TC + +``` + +### 2.2 Data Flow Diagram + +```mermaid +sequenceDiagram + participant UI + participant BC as Bridge Controller + participant IPM as Intent Provider Manager + participant IP as Intent Provider + participant BSC as Bridge Status Controller + participant TC as Transaction Controller + + UI->>BC: Submit Intent Request + BC->>IPM: Get Available Providers + IPM->>BC: Return Provider List + BC->>IPM: Generate Quote + IPM->>IP: Generate Quote + IP->>IPM: Return Quote + IPM->>BC: Return Standardized Quote + BC->>UI: Display Quote + + UI->>BC: Confirm Intent + BC->>IPM: Submit Intent + IPM->>IP: Submit Order + IP->>IPM: Return Order ID + IPM->>BC: Return Standardized Response + BC->>BSC: Start Status Polling + BC->>TC: Create Transaction Record + + loop Status Polling + BSC->>IPM: Check Order Status + IPM->>IP: Get Status + IP->>IPM: Return Status + IPM->>BSC: Return Standardized Status + BSC->>TC: Update Transaction + end + +``` + +## 3. Base Intent Interface Design + +```tsx +import type { Hex } from '@metamask/utils'; + +export interface IntentQuoteRequest { + srcChainId: number; + destChainId: number; + srcTokenAddress: string; + destTokenAddress: string; + amount: string; + userAddress: string; + slippage?: number; +} + +export interface IntentQuote { + id: string; + provider: string; + srcAmount: string; + destAmount: string; + estimatedGas: string; + estimatedTime: number; // seconds + priceImpact: number; + fees: IntentFee[]; + validUntil: number; // timestamp + metadata: Record; +} + +export interface IntentFee { + type: 'network' | 'protocol' | 'bridge'; + amount: string; + token: string; +} + +export interface IntentOrder { + id: string; + status: IntentOrderStatus; + txHash?: string; + createdAt: number; + updatedAt: number; + metadata: Record; +} + +export enum IntentOrderStatus { + PENDING = 'pending', + SUBMITTED = 'submitted', + CONFIRMED = 'confirmed', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} + +export interface IntentSubmissionParams { + quote: IntentQuote; + signature: string; + userAddress: string; +} + +export interface IntentProviderConfig { + name: string; + version: string; + supportedChains: number[]; + apiBaseUrl: string; + features: string[]; + rateLimit?: { + requestsPerMinute: number; + burstLimit: number; + }; +} + +export abstract class BaseIntentProvider { + protected config: IntentProviderConfig; + + constructor(config: IntentProviderConfig) { + this.config = config; + } + + abstract getName(): string; + abstract getVersion(): string; + abstract getSupportedChains(): number[]; + + abstract generateQuote(request: IntentQuoteRequest): Promise; + abstract submitOrder(params: IntentSubmissionParams): Promise; + abstract getOrderStatus(orderId: string, chainId: number): Promise; + abstract cancelOrder(orderId: string, chainId: number): Promise; + + abstract validateQuoteRequest(request: IntentQuoteRequest): Promise; + abstract estimateGas(quote: IntentQuote): Promise; + + // Lifecycle hooks for provider-specific logic + protected onQuoteGenerated?(quote: IntentQuote): Promise; + protected onOrderSubmitted?(order: IntentOrder): Promise; + protected onOrderStatusChanged?(order: IntentOrder, previousStatus: IntentOrderStatus): Promise; + + // Error handling + protected handleError(error: Error, context: string): Error { + return new Error(`${this.getName()}: ${context} - ${error.message}`); + } +} + +``` + +## 4. Intent Provider Manager + +```tsx +import type { BaseIntentProvider, IntentQuote, IntentQuoteRequest, IntentOrder, IntentSubmissionParams } from './base-intent-provider'; + +export interface ProviderRegistry { + [providerName: string]: BaseIntentProvider; +} + +export interface ProviderSelectionCriteria { + chainId: number; + tokenPair: [string, string]; + amount: string; + preferredProviders?: string[]; + excludedProviders?: string[]; +} + +export class IntentProviderManager { + private providers: ProviderRegistry = {}; + private defaultProvider?: string; + + registerProvider(provider: BaseIntentProvider): void { + const name = provider.getName(); + this.providers[name] = provider; + + // Set first registered provider as default + if (!this.defaultProvider) { + this.defaultProvider = name; + } + } + + unregisterProvider(providerName: string): boolean { + if (this.providers[providerName]) { + delete this.providers[providerName]; + + // Update default if needed + if (this.defaultProvider === providerName) { + this.defaultProvider = Object.keys(this.providers)[0]; + } + return true; + } + return false; + } + + getAvailableProviders(criteria?: ProviderSelectionCriteria): BaseIntentProvider[] { + let availableProviders = Object.values(this.providers); + + if (criteria) { + availableProviders = availableProviders.filter(provider => { + // Filter by supported chains + if (criteria.chainId && !provider.getSupportedChains().includes(criteria.chainId)) { + return false; + } + + // Filter by excluded providers + if (criteria.excludedProviders?.includes(provider.getName())) { + return false; + } + + return true; + }); + + // Prioritize preferred providers + if (criteria.preferredProviders) { + availableProviders.sort((a, b) => { + const aIndex = criteria.preferredProviders!.indexOf(a.getName()); + const bIndex = criteria.preferredProviders!.indexOf(b.getName()); + + if (aIndex === -1 && bIndex === -1) return 0; + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + + return aIndex - bIndex; + }); + } + } + + return availableProviders; + } + + async generateQuotes(request: IntentQuoteRequest, criteria?: ProviderSelectionCriteria): Promise { + const providers = this.getAvailableProviders(criteria); + const quotes: IntentQuote[] = []; + + await Promise.allSettled( + providers.map(async (provider) => { + try { + const isValid = await provider.validateQuoteRequest(request); + if (isValid) { + const quote = await provider.generateQuote(request); + quotes.push(quote); + } + } catch (error) { + console.warn(`Failed to get quote from ${provider.getName()}:`, error); + } + }) + ); + + return quotes.sort((a, b) => parseFloat(b.destAmount) - parseFloat(a.destAmount)); + } + + async submitOrder(params: IntentSubmissionParams): Promise { + const provider = this.providers[params.quote.provider]; + if (!provider) { + throw new Error(`Provider ${params.quote.provider} not found`); + } + + return provider.submitOrder(params); + } + + async getOrderStatus(orderId: string, providerName: string, chainId: number): Promise { + const provider = this.providers[providerName]; + if (!provider) { + throw new Error(`Provider ${providerName} not found`); + } + + return provider.getOrderStatus(orderId, chainId); + } + + getProvider(name: string): BaseIntentProvider | undefined { + return this.providers[name]; + } + + getDefaultProvider(): BaseIntentProvider | undefined { + return this.defaultProvider ? this.providers[this.defaultProvider] : undefined; + } +} + +``` + +## 5. CowSwap Provider Implementation + +```tsx +import { BaseIntentProvider, type IntentQuote, type IntentQuoteRequest, type IntentOrder, type IntentSubmissionParams, IntentOrderStatus } from './base-intent-provider'; + +const COW_NETWORK_PATHS: Record = { + 1: 'mainnet', + 42161: 'arbitrum_one', + 8453: 'base', + 43114: 'avalanche', + 137: 'polygon', +}; + +export class CowSwapProvider extends BaseIntentProvider { + private readonly apiBaseUrl = ''; + + getName(): string { + return 'cowswap'; + } + + getVersion(): string { + return '1.0.0'; + } + + getSupportedChains(): number[] { + return Object.keys(COW_NETWORK_PATHS).map(Number); + } + + async generateQuote(request: IntentQuoteRequest): Promise { + const networkPath = COW_NETWORK_PATHS[request.srcChainId]; + if (!networkPath) { + throw new Error(`Unsupported chain: ${request.srcChainId}`); + } + + // Implementation for CowSwap quote generation + // This would call the actual CowSwap API + const response = await this.fetchQuote(request, networkPath); + + return { + id: response.id, + provider: this.getName(), + srcAmount: request.amount, + destAmount: response.buyAmount, + estimatedGas: response.estimatedGas || '21000', + estimatedTime: 300, // 5 minutes typical for CowSwap + priceImpact: response.priceImpact || 0, + fees: [{ + type: 'protocol', + amount: response.feeAmount, + token: request.srcTokenAddress, + }], + validUntil: response.validTo, + metadata: { + order: response.order, + settlementContract: response.settlementContract, + }, + }; + } + + async submitOrder(params: IntentSubmissionParams): Promise { + const chainId = params.quote.metadata.chainId as number; + const networkPath = COW_NETWORK_PATHS[chainId]; + + const orderBody = { + ...params.quote.metadata.order, + from: params.userAddress, + signature: params.signature, + signingScheme: 'eip712', + }; + + const url = `${this.apiBaseUrl}/${networkPath}/api/v1/orders`; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(orderBody), + }); + + if (!response.ok) { + throw new Error(`Failed to submit order: ${response.statusText}`); + } + + const orderUid = await response.text(); + + return { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + createdAt: Date.now(), + updatedAt: Date.now(), + metadata: { chainId, networkPath }, + }; + } + + async getOrderStatus(orderId: string, chainId: number): Promise { + const networkPath = COW_NETWORK_PATHS[chainId]; + const url = `${this.apiBaseUrl}/${networkPath}/api/v1/orders/${orderId}`; + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to get order status: ${response.statusText}`); + } + + const data = await response.json(); + + return { + id: orderId, + status: this.mapCowSwapStatus(data.status), + txHash: data.executedSellAmount ? data.txHash : undefined, + createdAt: new Date(data.creationDate).getTime(), + updatedAt: Date.now(), + metadata: data, + }; + } + + async cancelOrder(orderId: string, chainId: number): Promise { + // CowSwap doesn't support order cancellation via API + // Orders expire naturally + return false; + } + + async validateQuoteRequest(request: IntentQuoteRequest): Promise { + return this.getSupportedChains().includes(request.srcChainId); + } + + async estimateGas(quote: IntentQuote): Promise { + // CowSwap uses meta-transactions, so gas estimation is minimal + return '21000'; + } + + private mapCowSwapStatus(cowStatus: string): IntentOrderStatus { + switch (cowStatus) { + case 'presignaturePending': + case 'open': + return IntentOrderStatus.PENDING; + case 'fulfilled': + return IntentOrderStatus.COMPLETED; + case 'cancelled': + return IntentOrderStatus.CANCELLED; + case 'expired': + return IntentOrderStatus.EXPIRED; + default: + return IntentOrderStatus.FAILED; + } + } + + private async fetchQuote(request: IntentQuoteRequest, networkPath: string): Promise { + // Implement actual CowSwap quote API call + // This is a placeholder for the actual implementation + throw new Error('Not implemented'); + } +} + +``` + +## 6. Bridge Controller Integration + +```tsx +import { IntentProviderManager } from './intent-providers/intent-provider-manager'; +import { CowSwapProvider } from './intent-providers/cowswap-provider'; + +export class BridgeController extends StaticIntervalPollingController< + typeof BRIDGE_CONTROLLER_NAME, + BridgeControllerState, + BridgeControllerMessenger +> { + private intentProviderManager: IntentProviderManager; + + constructor({ + messenger, + state, + fetchFunction = globalThis.fetch, + }: { + messenger: BridgeControllerMessenger; + state?: Partial; + fetchFunction?: typeof fetch; + }) { + super({ + name: BRIDGE_CONTROLLER_NAME, + metadata: { + // ... existing metadata + }, + messenger, + state: { ...DEFAULT_BRIDGE_CONTROLLER_STATE, ...state }, + }); + + this.intentProviderManager = new IntentProviderManager(); + this.#initializeIntentProviders(); + } + + #initializeIntentProviders(): void { + // Register CowSwap provider + const cowSwapProvider = new CowSwapProvider({ + name: 'cowswap', + version: '1.0.0', + supportedChains: [1, 42161, 8453, 43114, 137], + apiBaseUrl: '', + features: ['eip712-signing', 'meta-transactions'], + }); + + this.intentProviderManager.registerProvider(cowSwapProvider); + } + + async submitIntent(params: { + quoteResponse: QuoteResponse & QuoteMetadata; + signature: string; + accountAddress: string; + }): Promise> { + const { quoteResponse, signature, accountAddress } = params; + + try { + const intentQuote = this.#convertToIntentQuote(quoteResponse); + + const order = await this.intentProviderManager.submitOrder({ + quote: intentQuote, + signature, + userAddress: accountAddress, + }); + + // Create transaction record and bridge history entry + return this.#createIntentTransaction(order, quoteResponse, accountAddress); + } catch (error) { + // Handle error and emit metrics + throw error; + } + } + + getIntentProviderManager(): IntentProviderManager { + return this.intentProviderManager; + } + + #convertToIntentQuote(quoteResponse: QuoteResponse & QuoteMetadata): any { + // Convert bridge quote format to intent quote format + // Implementation details... + } + + #createIntentTransaction(order: any, quoteResponse: any, accountAddress: string): any { + // Create transaction record for intent + // Implementation details... + } +} + +``` + +## 7. Bridge Status Controller Integration + +```tsx +import type { IntentProviderManager } from '@metamask/bridge-controller'; + +export class BridgeStatusController extends StaticIntervalPollingController< + typeof BRIDGE_STATUS_CONTROLLER_NAME, + BridgeStatusControllerState, + BridgeStatusControllerMessenger +> { + #intentProviderManager?: IntentProviderManager; + + setIntentProviderManager(manager: IntentProviderManager): void { + this.#intentProviderManager = manager; + } + + readonly #fetchBridgeTxStatus = async ({ + bridgeTxMetaId, + }: FetchBridgeTxStatusArgs) => { + const { txHistory } = this.state; + + // Intent-based items: use provider manager instead of hardcoded logic + if (bridgeTxMetaId.startsWith('intent:')) { + await this.#fetchIntentOrderStatus({ bridgeTxMetaId }); + return; + } + + // ... existing bridge status logic + }; + + readonly #fetchIntentOrderStatus = async ({ + bridgeTxMetaId, + }: FetchBridgeTxStatusArgs) => { + if (!this.#intentProviderManager) { + console.warn('Intent provider manager not available'); + return; + } + + const { txHistory } = this.state; + const historyItem = txHistory[bridgeTxMetaId]; + if (!historyItem) { + return; + } + + if (shouldSkipFetchDueToFetchFailures(historyItem.attempts)) { + return; + } + + try { + const orderId = bridgeTxMetaId.replace(/^intent:/u, ''); + const [providerName, actualOrderId] = orderId.split(':'); + const { srcChainId } = historyItem.quote; + + const order = await this.#intentProviderManager.getOrderStatus( + actualOrderId, + providerName, + srcChainId + ); + + // Update transaction status based on intent order status + this.#updateTransactionFromIntentOrder(bridgeTxMetaId, order); + } catch (error) { + console.error('Failed to fetch intent order status:', error); + this.#handleIntentStatusError(bridgeTxMetaId, error); + } + }; + + #updateTransactionFromIntentOrder(bridgeTxMetaId: string, order: any): void { + // Convert intent order status to transaction status + // Update transaction controller + // Implementation details... + } + + #handleIntentStatusError(bridgeTxMetaId: string, error: Error): void { + // Handle status polling errors + // Implementation details... + } +} + +``` diff --git a/packages/Intent-manager/src/constants.ts b/packages/Intent-manager/src/constants.ts new file mode 100644 index 00000000000..ea512104009 --- /dev/null +++ b/packages/Intent-manager/src/constants.ts @@ -0,0 +1,32 @@ +/** + * Controller name for the Intent Manager + */ +export const INTENT_MANAGER_NAME = 'IntentManager'; + +/** + * Default state for the Intent Manager Controller + */ +export const DEFAULT_INTENT_MANAGER_CONTROLLER_STATE = { + intents: {}, + intentHistory: [], +}; + +/** + * Default polling interval for intent status updates (5 seconds) + */ +export const DEFAULT_POLLING_INTERVAL_MS = 5000; + +/** + * Maximum number of retry attempts for intent execution + */ +export const MAX_RETRY_ATTEMPTS = 3; + +/** + * Timeout for intent execution (5 minutes) + */ +export const INTENT_EXECUTION_TIMEOUT_MS = 5 * 60 * 1000; + +/** + * Maximum number of intents to keep in history + */ +export const MAX_INTENT_HISTORY_SIZE = 100; diff --git a/packages/Intent-manager/src/index.ts b/packages/Intent-manager/src/index.ts new file mode 100644 index 00000000000..bfe32f53459 --- /dev/null +++ b/packages/Intent-manager/src/index.ts @@ -0,0 +1,15 @@ +// Export constants +export { + DEFAULT_INTENT_MANAGER_CONTROLLER_STATE, + DEFAULT_POLLING_INTERVAL_MS, + INTENT_EXECUTION_TIMEOUT_MS, + INTENT_MANAGER_NAME, + MAX_INTENT_HISTORY_SIZE, + MAX_RETRY_ATTEMPTS, +} from './constants'; + +// Export enums +export { IntentStatus } from './types'; + +// Export main controller +export { IntentManager } from './intent-manager'; diff --git a/packages/Intent-manager/src/intent-manager.ts b/packages/Intent-manager/src/intent-manager.ts new file mode 100644 index 00000000000..b7d599bcea4 --- /dev/null +++ b/packages/Intent-manager/src/intent-manager.ts @@ -0,0 +1,7 @@ +/** + * Intent Manager Controller + * + * Manages the lifecycle of user intents including creation, execution, + * cancellation, and state tracking. + */ +export class IntentManager {} diff --git a/packages/Intent-manager/src/types.ts b/packages/Intent-manager/src/types.ts new file mode 100644 index 00000000000..93066875877 --- /dev/null +++ b/packages/Intent-manager/src/types.ts @@ -0,0 +1,12 @@ +/** + * Intent status enumeration + */ +export enum IntentStatus { + PENDING = 'pending', + EXECUTING = 'executing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', +} + +// Import the constant here to avoid circular dependency diff --git a/packages/Intent-manager/tsconfig.build.json b/packages/Intent-manager/tsconfig.build.json new file mode 100644 index 00000000000..02a0eea03fe --- /dev/null +++ b/packages/Intent-manager/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/Intent-manager/tsconfig.json b/packages/Intent-manager/tsconfig.json new file mode 100644 index 00000000000..b05e3335c94 --- /dev/null +++ b/packages/Intent-manager/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "resolveJsonModule": true + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/Intent-manager/typedoc.json b/packages/Intent-manager/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/Intent-manager/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/yarn.lock b/yarn.lock index 6749a82662f..dc308afa8f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3591,6 +3591,24 @@ __metadata: languageName: unknown linkType: soft +"@metamask/intent-manager@workspace:packages/Intent-manager": + version: 0.0.0-use.local + resolution: "@metamask/intent-manager@workspace:packages/Intent-manager" + dependencies: + "@lavamoat/allow-scripts": "npm:^3.0.4" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/utils": "npm:^11.4.2" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.1" + jest-environment-jsdom: "npm:^27.5.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + languageName: unknown + linkType: soft + "@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" From 2493ab2715126244f492ae92c175f08f00d1a990 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 10 Sep 2025 04:13:10 +0100 Subject: [PATCH 09/85] add ts-bridge as dev dependancies for build --- packages/Intent-manager/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/Intent-manager/package.json b/packages/Intent-manager/package.json index 525289aca6e..8e14e172958 100644 --- a/packages/Intent-manager/package.json +++ b/packages/Intent-manager/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@metamask/auto-changelog": "^3.4.4", + "@ts-bridge/cli": "^0.6.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/yarn.lock b/yarn.lock index dc308afa8f4..c9390a0c856 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3598,6 +3598,7 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/utils": "npm:^11.4.2" + "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" From 1673f1f7a494808b9c8eeee90af07f3eceac5223 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 10 Sep 2025 05:16:36 +0100 Subject: [PATCH 10/85] wip: buildable version of intent manager and bridge-status-controller. --- packages/Intent-manager/README.md | 390 ++++++++-- packages/Intent-manager/package.json | 3 + packages/Intent-manager/spec.md | 693 ------------------ packages/Intent-manager/src/index.ts | 19 +- .../Intent-manager/src/intent-manager.test.ts | 174 +++++ packages/Intent-manager/src/intent-manager.ts | 230 +++++- .../src/providers/base-intent-provider.ts | 154 ++++ .../src/providers/cowswap/constants.ts | 40 + .../src/providers/cowswap/cowswap-provider.ts | 264 +++++++ .../src/providers/cowswap/index.ts | 7 + .../Intent-manager/src/providers/index.ts | 2 + packages/Intent-manager/src/types.ts | 120 ++- .../bridge-status-controller/package.json | 1 + .../src/bridge-status-controller.ts | 530 +++++++------- .../bridge-status-controller/tsconfig.json | 1 + 15 files changed, 1603 insertions(+), 1025 deletions(-) delete mode 100644 packages/Intent-manager/spec.md create mode 100644 packages/Intent-manager/src/intent-manager.test.ts create mode 100644 packages/Intent-manager/src/providers/base-intent-provider.ts create mode 100644 packages/Intent-manager/src/providers/cowswap/constants.ts create mode 100644 packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts create mode 100644 packages/Intent-manager/src/providers/cowswap/index.ts create mode 100644 packages/Intent-manager/src/providers/index.ts diff --git a/packages/Intent-manager/README.md b/packages/Intent-manager/README.md index e1e01bbbce3..50b0440e95a 100644 --- a/packages/Intent-manager/README.md +++ b/packages/Intent-manager/README.md @@ -1,16 +1,27 @@ + + # `@metamask/intent-manager` -Manages intent-related functionality for MetaMask. This package provides a controller for handling user intents, intent validation, execution, and state management within the MetaMask ecosystem. +A comprehensive intent management system for MetaMask that orchestrates cross-chain token swaps and bridging operations through multiple decentralized exchange (DEX) providers. This package provides quote aggregation, order execution, and lifecycle management for user intents. ## Overview -The Intent Manager is responsible for: +The Intent Manager provides a unified interface for: + +- **Multi-Provider Quote Aggregation**: Get quotes from multiple DEX providers (CowSwap, 1inch, etc.) +- **Cross-Chain Operations**: Support for token swaps and bridging across different blockchain networks +- **Order Lifecycle Management**: Handle quote generation, order submission, execution tracking, and status monitoring +- **Provider Management**: Pluggable architecture for adding new DEX providers +- **State Management**: Track intent orders and maintain execution history -- **Intent Creation**: Creating and validating user intents -- **Intent Execution**: Managing the execution lifecycle of intents -- **State Management**: Tracking intent states and history -- **Intent Validation**: Ensuring intents meet required criteria -- **Event Handling**: Emitting events for intent lifecycle changes +## Key Features + +- 🔄 **Cross-chain token swaps** with automatic best-rate selection +- 🏪 **Multi-provider support** with extensible provider architecture +- 📊 **Real-time order tracking** and status updates +- ⚡ **Gas estimation** and fee calculation +- 🛡️ **Slippage protection** and validation +- 📈 **Price impact analysis** for informed decision making ## Installation @@ -24,79 +35,341 @@ or npm install @metamask/intent-manager ``` -## Usage +## Quick Start ```typescript -import { IntentManagerController } from '@metamask/intent-manager'; +import { IntentManager } from '@metamask/intent-manager'; +import type { IntentQuoteRequest } from '@metamask/intent-manager'; + +// Initialize the intent manager +const intentManager = new IntentManager(); -// Initialize the controller -const intentManager = new IntentManagerController({ - messenger, // Controller messenger - state: { - // Initial state - }, +// Request quotes for a cross-chain swap +const quoteRequest: IntentQuoteRequest = { + srcChainId: 1, // Ethereum Mainnet + destChainId: 42161, // Arbitrum One + srcTokenAddress: '0xA0b86a33E6441e6e80D0c4C6C7527d72', // USDC + destTokenAddress: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH + amount: '1000000000000000000', // 1 token (18 decimals) + userAddress: '0x742d35Cc6634C0532925a3b8D4C9db96', + slippage: 0.005, // 0.5% slippage tolerance +}; + +// Get quotes from all available providers +const quotes = await intentManager.generateQuotes(quoteRequest); + +// Submit the best quote +const bestQuote = quotes[0]; // Quotes are sorted by best rate +const order = await intentManager.submitIntent({ + quote: bestQuote, + signature: '0x...', // User signature + userAddress: quoteRequest.userAddress, }); -// Create a new intent -const intent = await intentManager.createIntent({ - type: 'swap', - data: { - // Intent-specific data - }, +// Monitor order status +const status = await intentManager.getOrderStatus( + order.id, + bestQuote.provider, + quoteRequest.srcChainId +); +``` + +## API Reference + +### IntentManager + +The main class that orchestrates intent operations across multiple providers. + +#### Constructor + +```typescript +new IntentManager(initialState?: Partial) +``` + +#### Core Methods + +##### `generateQuotes(request, criteria?): Promise` + +Generates quotes from available providers for a given request. + +```typescript +const quotes = await intentManager.generateQuotes({ + srcChainId: 1, + destChainId: 42161, + srcTokenAddress: '0x...', + destTokenAddress: '0x...', + amount: '1000000000000000000', + userAddress: '0x...', }); +``` + +##### `submitIntent(params): Promise` -// Execute an intent -await intentManager.executeIntent(intent.id); +Submits an intent order based on a selected quote. -// Get intent status -const status = intentManager.getIntentStatus(intent.id); +```typescript +const order = await intentManager.submitIntent({ + quote: selectedQuote, + signature: '0x...', + userAddress: '0x...', +}); ``` -## API Reference +##### `getOrderStatus(orderId, providerName, chainId): Promise` + +Retrieves the current status of an order. + +```typescript +const status = await intentManager.getOrderStatus( + 'order-123', + 'cowswap', + 1 +); +``` -### IntentManagerController +##### `cancelOrder(orderId, providerName, chainId): Promise` -The main controller class that manages intent operations. +Cancels a pending order. -#### Methods +```typescript +const cancelled = await intentManager.cancelOrder( + 'order-123', + 'cowswap', + 1 +); +``` -- `createIntent(intentData)` - Creates a new intent -- `executeIntent(intentId)` - Executes an existing intent -- `cancelIntent(intentId)` - Cancels a pending intent -- `getIntentStatus(intentId)` - Gets the current status of an intent -- `getIntentHistory()` - Retrieves intent history +#### Provider Management -#### Events +##### `registerProvider(provider): void` -- `IntentCreated` - Emitted when a new intent is created -- `IntentExecuted` - Emitted when an intent is executed -- `IntentCancelled` - Emitted when an intent is cancelled -- `IntentFailed` - Emitted when an intent execution fails +Registers a new intent provider. + +```typescript +const customProvider = new CustomProvider(config); +intentManager.registerProvider(customProvider); +``` -## Types +##### `unregisterProvider(providerName): boolean` -### Intent +Unregisters an intent provider. ```typescript -interface Intent { +const removed = intentManager.unregisterProvider('cowswap'); +``` + +##### `getAvailableProviders(criteria?): BaseIntentProvider[]` + +Gets available providers, optionally filtered by criteria. + +```typescript +const providers = intentManager.getAvailableProviders({ + chainId: 1, + tokenPair: ['0x...', '0x...'], + amount: '1000000000000000000', +}); +``` + +## Core Types + +### IntentQuoteRequest + +Parameters for requesting quotes from providers. + +```typescript +type IntentQuoteRequest = { + srcChainId: number; + destChainId: number; + srcTokenAddress: string; + destTokenAddress: string; + amount: string; + userAddress: string; + slippage?: number; +}; +``` + +### IntentQuote + +Quote response from a provider. + +```typescript +type IntentQuote = { id: string; - type: IntentType; - status: IntentStatus; - data: IntentData; + provider: string; + srcAmount: string; + destAmount: string; + estimatedGas: string; + estimatedTime: number; // seconds + priceImpact: number; + fees: IntentFee[]; + validUntil: number; // timestamp + metadata: Record; +}; +``` + +### IntentOrder + +Order information and status. + +```typescript +type IntentOrder = { + id: string; + status: IntentOrderStatus; + txHash?: string; createdAt: number; updatedAt: number; -} + metadata: Record; +}; ``` -### IntentStatus +### IntentOrderStatus ```typescript -enum IntentStatus { +enum IntentOrderStatus { PENDING = 'pending', - EXECUTING = 'executing', + SUBMITTED = 'submitted', + CONFIRMED = 'confirmed', COMPLETED = 'completed', FAILED = 'failed', CANCELLED = 'cancelled', + EXPIRED = 'expired', +} +``` + +### ProviderSelectionCriteria + +Criteria for filtering and sorting providers. + +```typescript +type ProviderSelectionCriteria = { + chainId: number; + tokenPair: [string, string]; + amount: string; + preferredProviders?: string[]; + excludedProviders?: string[]; +}; +``` + +## Supported Providers + +### CowSwap + +The package includes built-in support for CowSwap, a DEX aggregator that provides: + +- **MEV Protection**: Orders are protected from front-running and sandwich attacks +- **Gas-free Trading**: No gas fees for failed transactions +- **Multi-chain Support**: Ethereum, Arbitrum, Base, Avalanche, and Polygon +- **Batch Auctions**: Efficient price discovery through batch settlement + +### Adding Custom Providers + +You can extend the system by implementing the `BaseIntentProvider` interface: + +```typescript +import { BaseIntentProvider } from '@metamask/intent-manager'; + +class CustomProvider extends BaseIntentProvider { + constructor(config: IntentProviderConfig) { + super(config); + } + + getName(): string { + return 'custom-dex'; + } + + getVersion(): string { + return '1.0.0'; + } + + getSupportedChains(): number[] { + return [1, 42161]; // Ethereum and Arbitrum + } + + async generateQuote(request: IntentQuoteRequest): Promise { + // Implement quote generation logic + } + + async submitOrder(params: IntentSubmissionParams): Promise { + // Implement order submission logic + } + + async getOrderStatus(orderId: string, chainId: number): Promise { + // Implement status checking logic + } + + // ... implement other required methods +} + +// Register the custom provider +const customProvider = new CustomProvider(config); +intentManager.registerProvider(customProvider); +``` + +## Error Handling + +The Intent Manager provides comprehensive error handling: + +```typescript +try { + const quotes = await intentManager.generateQuotes(request); +} catch (error) { + if (error.message.includes('Unsupported chain')) { + // Handle unsupported chain error + } else if (error.message.includes('Insufficient liquidity')) { + // Handle liquidity error + } else { + // Handle other errors + } +} +``` + +## Best Practices + +### Quote Selection + +- **Compare Multiple Quotes**: Always request quotes from multiple providers +- **Consider Total Cost**: Factor in gas fees, protocol fees, and price impact +- **Check Validity**: Ensure quotes haven't expired before submission + +```typescript +const quotes = await intentManager.generateQuotes(request); + +// Filter valid quotes +const validQuotes = quotes.filter(quote => quote.validUntil > Date.now()); + +// Sort by best net amount (considering fees) +const sortedQuotes = validQuotes.sort((a, b) => { + const aNet = BigInt(a.destAmount) - BigInt(a.estimatedGas); + const bNet = BigInt(b.destAmount) - BigInt(b.estimatedGas); + return bNet > aNet ? 1 : -1; +}); +``` + +### Order Monitoring + +- **Poll Status Regularly**: Check order status periodically for updates +- **Handle Timeouts**: Implement timeout logic for long-running orders +- **Retry Failed Orders**: Consider retrying with different providers + +```typescript +async function monitorOrder(orderId: string, provider: string, chainId: number) { + const maxAttempts = 60; // 5 minutes with 5-second intervals + let attempts = 0; + + while (attempts < maxAttempts) { + const order = await intentManager.getOrderStatus(orderId, provider, chainId); + + if (order.status === IntentOrderStatus.COMPLETED) { + return order; + } else if (order.status === IntentOrderStatus.FAILED) { + throw new Error(`Order failed: ${order.id}`); + } + + await new Promise(resolve => setTimeout(resolve, 5000)); + attempts++; + } + + throw new Error('Order monitoring timeout'); } ``` @@ -104,6 +377,25 @@ enum IntentStatus { This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). +### Development + +```bash +# Install dependencies +yarn install + +# Run tests +yarn test + +# Run tests in watch mode +yarn test:watch + +# Build the package +yarn build + +# Generate documentation +yarn build:docs +``` + ## License MIT diff --git a/packages/Intent-manager/package.json b/packages/Intent-manager/package.json index 8e14e172958..d9d1a155ccc 100644 --- a/packages/Intent-manager/package.json +++ b/packages/Intent-manager/package.json @@ -48,6 +48,9 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { + "@metamask/base-controller": "^8.3.0", + "@metamask/controller-utils": "^11.12.0", + "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.4.2" }, "devDependencies": { diff --git a/packages/Intent-manager/spec.md b/packages/Intent-manager/spec.md deleted file mode 100644 index 7b28f67a886..00000000000 --- a/packages/Intent-manager/spec.md +++ /dev/null @@ -1,693 +0,0 @@ -## 1. Current State Analysis - -### 1.1 Module Overview - -Based on the codebase analysis, the current architecture consists of three main modules: - -**Bridge Controller** (`@metamask/bridge-controller`): - -- Manages quote fetching and transaction submission -- Handles both traditional bridge transactions and intent-based swaps -- Contains hardcoded CowSwap integration logic - -**Bridge Status Controller** (`@metamask/bridge-status-controller`): - -- Polls transaction status for both bridge and intent transactions -- Contains CowSwap-specific API polling logic -- Manages transaction history and status updates - -**Transaction Controller** (`@metamask/transaction-controller`): - -- Manages transaction lifecycle and metadata -- Stores transaction data and provides updates to UI - -### 1.2 Current Intent Integration - -The current CowSwap integration is tightly coupled and hardcoded: - -```tsx -// CoW intent: API base and network path mapping (adjust as needed) -const COW_API_BASE = ''; -const COW_NETWORK_PATHS: Record = { - // Ethereum Mainnet - 1: 'mainnet', - // Arbitrum One - 42161: 'arbitrum_one', - // Base - 8453: 'base', - // Avalanche C-Chain - 43114: 'avalanche', - // Polygon PoS - 137: 'polygon', -}; - -``` - -### 1.3 Current Data Flow - -``` -User Request → Bridge Controller → Quote Generation → Intent Submission → Bridge Status Controller → Status Polling → Transaction Controller Updates - -``` - -## 2. Proposed Modular Architecture - -### 2.1 Architecture Diagram - -```mermaid -graph TB - subgraph "Core Controllers" - BC[Bridge Controller] - BSC[Bridge Status Controller] - TC[Transaction Controller] - end - - subgraph "Intent Provider System" - IPM[Intent Provider Manager] - IPI[Intent Provider Interface] - - subgraph "Provider Implementations" - COW[CowSwap Provider] - ONEINCH[1inch Provider] - ZX[0x Protocol Provider] - CUSTOM[Custom Provider] - end - end - - subgraph "Shared Infrastructure" - VL[Validation Layer] - EH[Error Handler] - ML[Metrics Logger] - CH[Config Handler] - end - - BC --> IPM - BSC --> IPM - IPM --> IPI - IPI --> COW - IPI --> ONEINCH - IPI --> ZX - IPI --> CUSTOM - - IPM --> VL - IPM --> EH - IPM --> ML - IPM --> CH - - BSC --> TC - -``` - -### 2.2 Data Flow Diagram - -```mermaid -sequenceDiagram - participant UI - participant BC as Bridge Controller - participant IPM as Intent Provider Manager - participant IP as Intent Provider - participant BSC as Bridge Status Controller - participant TC as Transaction Controller - - UI->>BC: Submit Intent Request - BC->>IPM: Get Available Providers - IPM->>BC: Return Provider List - BC->>IPM: Generate Quote - IPM->>IP: Generate Quote - IP->>IPM: Return Quote - IPM->>BC: Return Standardized Quote - BC->>UI: Display Quote - - UI->>BC: Confirm Intent - BC->>IPM: Submit Intent - IPM->>IP: Submit Order - IP->>IPM: Return Order ID - IPM->>BC: Return Standardized Response - BC->>BSC: Start Status Polling - BC->>TC: Create Transaction Record - - loop Status Polling - BSC->>IPM: Check Order Status - IPM->>IP: Get Status - IP->>IPM: Return Status - IPM->>BSC: Return Standardized Status - BSC->>TC: Update Transaction - end - -``` - -## 3. Base Intent Interface Design - -```tsx -import type { Hex } from '@metamask/utils'; - -export interface IntentQuoteRequest { - srcChainId: number; - destChainId: number; - srcTokenAddress: string; - destTokenAddress: string; - amount: string; - userAddress: string; - slippage?: number; -} - -export interface IntentQuote { - id: string; - provider: string; - srcAmount: string; - destAmount: string; - estimatedGas: string; - estimatedTime: number; // seconds - priceImpact: number; - fees: IntentFee[]; - validUntil: number; // timestamp - metadata: Record; -} - -export interface IntentFee { - type: 'network' | 'protocol' | 'bridge'; - amount: string; - token: string; -} - -export interface IntentOrder { - id: string; - status: IntentOrderStatus; - txHash?: string; - createdAt: number; - updatedAt: number; - metadata: Record; -} - -export enum IntentOrderStatus { - PENDING = 'pending', - SUBMITTED = 'submitted', - CONFIRMED = 'confirmed', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', - EXPIRED = 'expired', -} - -export interface IntentSubmissionParams { - quote: IntentQuote; - signature: string; - userAddress: string; -} - -export interface IntentProviderConfig { - name: string; - version: string; - supportedChains: number[]; - apiBaseUrl: string; - features: string[]; - rateLimit?: { - requestsPerMinute: number; - burstLimit: number; - }; -} - -export abstract class BaseIntentProvider { - protected config: IntentProviderConfig; - - constructor(config: IntentProviderConfig) { - this.config = config; - } - - abstract getName(): string; - abstract getVersion(): string; - abstract getSupportedChains(): number[]; - - abstract generateQuote(request: IntentQuoteRequest): Promise; - abstract submitOrder(params: IntentSubmissionParams): Promise; - abstract getOrderStatus(orderId: string, chainId: number): Promise; - abstract cancelOrder(orderId: string, chainId: number): Promise; - - abstract validateQuoteRequest(request: IntentQuoteRequest): Promise; - abstract estimateGas(quote: IntentQuote): Promise; - - // Lifecycle hooks for provider-specific logic - protected onQuoteGenerated?(quote: IntentQuote): Promise; - protected onOrderSubmitted?(order: IntentOrder): Promise; - protected onOrderStatusChanged?(order: IntentOrder, previousStatus: IntentOrderStatus): Promise; - - // Error handling - protected handleError(error: Error, context: string): Error { - return new Error(`${this.getName()}: ${context} - ${error.message}`); - } -} - -``` - -## 4. Intent Provider Manager - -```tsx -import type { BaseIntentProvider, IntentQuote, IntentQuoteRequest, IntentOrder, IntentSubmissionParams } from './base-intent-provider'; - -export interface ProviderRegistry { - [providerName: string]: BaseIntentProvider; -} - -export interface ProviderSelectionCriteria { - chainId: number; - tokenPair: [string, string]; - amount: string; - preferredProviders?: string[]; - excludedProviders?: string[]; -} - -export class IntentProviderManager { - private providers: ProviderRegistry = {}; - private defaultProvider?: string; - - registerProvider(provider: BaseIntentProvider): void { - const name = provider.getName(); - this.providers[name] = provider; - - // Set first registered provider as default - if (!this.defaultProvider) { - this.defaultProvider = name; - } - } - - unregisterProvider(providerName: string): boolean { - if (this.providers[providerName]) { - delete this.providers[providerName]; - - // Update default if needed - if (this.defaultProvider === providerName) { - this.defaultProvider = Object.keys(this.providers)[0]; - } - return true; - } - return false; - } - - getAvailableProviders(criteria?: ProviderSelectionCriteria): BaseIntentProvider[] { - let availableProviders = Object.values(this.providers); - - if (criteria) { - availableProviders = availableProviders.filter(provider => { - // Filter by supported chains - if (criteria.chainId && !provider.getSupportedChains().includes(criteria.chainId)) { - return false; - } - - // Filter by excluded providers - if (criteria.excludedProviders?.includes(provider.getName())) { - return false; - } - - return true; - }); - - // Prioritize preferred providers - if (criteria.preferredProviders) { - availableProviders.sort((a, b) => { - const aIndex = criteria.preferredProviders!.indexOf(a.getName()); - const bIndex = criteria.preferredProviders!.indexOf(b.getName()); - - if (aIndex === -1 && bIndex === -1) return 0; - if (aIndex === -1) return 1; - if (bIndex === -1) return -1; - - return aIndex - bIndex; - }); - } - } - - return availableProviders; - } - - async generateQuotes(request: IntentQuoteRequest, criteria?: ProviderSelectionCriteria): Promise { - const providers = this.getAvailableProviders(criteria); - const quotes: IntentQuote[] = []; - - await Promise.allSettled( - providers.map(async (provider) => { - try { - const isValid = await provider.validateQuoteRequest(request); - if (isValid) { - const quote = await provider.generateQuote(request); - quotes.push(quote); - } - } catch (error) { - console.warn(`Failed to get quote from ${provider.getName()}:`, error); - } - }) - ); - - return quotes.sort((a, b) => parseFloat(b.destAmount) - parseFloat(a.destAmount)); - } - - async submitOrder(params: IntentSubmissionParams): Promise { - const provider = this.providers[params.quote.provider]; - if (!provider) { - throw new Error(`Provider ${params.quote.provider} not found`); - } - - return provider.submitOrder(params); - } - - async getOrderStatus(orderId: string, providerName: string, chainId: number): Promise { - const provider = this.providers[providerName]; - if (!provider) { - throw new Error(`Provider ${providerName} not found`); - } - - return provider.getOrderStatus(orderId, chainId); - } - - getProvider(name: string): BaseIntentProvider | undefined { - return this.providers[name]; - } - - getDefaultProvider(): BaseIntentProvider | undefined { - return this.defaultProvider ? this.providers[this.defaultProvider] : undefined; - } -} - -``` - -## 5. CowSwap Provider Implementation - -```tsx -import { BaseIntentProvider, type IntentQuote, type IntentQuoteRequest, type IntentOrder, type IntentSubmissionParams, IntentOrderStatus } from './base-intent-provider'; - -const COW_NETWORK_PATHS: Record = { - 1: 'mainnet', - 42161: 'arbitrum_one', - 8453: 'base', - 43114: 'avalanche', - 137: 'polygon', -}; - -export class CowSwapProvider extends BaseIntentProvider { - private readonly apiBaseUrl = ''; - - getName(): string { - return 'cowswap'; - } - - getVersion(): string { - return '1.0.0'; - } - - getSupportedChains(): number[] { - return Object.keys(COW_NETWORK_PATHS).map(Number); - } - - async generateQuote(request: IntentQuoteRequest): Promise { - const networkPath = COW_NETWORK_PATHS[request.srcChainId]; - if (!networkPath) { - throw new Error(`Unsupported chain: ${request.srcChainId}`); - } - - // Implementation for CowSwap quote generation - // This would call the actual CowSwap API - const response = await this.fetchQuote(request, networkPath); - - return { - id: response.id, - provider: this.getName(), - srcAmount: request.amount, - destAmount: response.buyAmount, - estimatedGas: response.estimatedGas || '21000', - estimatedTime: 300, // 5 minutes typical for CowSwap - priceImpact: response.priceImpact || 0, - fees: [{ - type: 'protocol', - amount: response.feeAmount, - token: request.srcTokenAddress, - }], - validUntil: response.validTo, - metadata: { - order: response.order, - settlementContract: response.settlementContract, - }, - }; - } - - async submitOrder(params: IntentSubmissionParams): Promise { - const chainId = params.quote.metadata.chainId as number; - const networkPath = COW_NETWORK_PATHS[chainId]; - - const orderBody = { - ...params.quote.metadata.order, - from: params.userAddress, - signature: params.signature, - signingScheme: 'eip712', - }; - - const url = `${this.apiBaseUrl}/${networkPath}/api/v1/orders`; - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(orderBody), - }); - - if (!response.ok) { - throw new Error(`Failed to submit order: ${response.statusText}`); - } - - const orderUid = await response.text(); - - return { - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - createdAt: Date.now(), - updatedAt: Date.now(), - metadata: { chainId, networkPath }, - }; - } - - async getOrderStatus(orderId: string, chainId: number): Promise { - const networkPath = COW_NETWORK_PATHS[chainId]; - const url = `${this.apiBaseUrl}/${networkPath}/api/v1/orders/${orderId}`; - - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to get order status: ${response.statusText}`); - } - - const data = await response.json(); - - return { - id: orderId, - status: this.mapCowSwapStatus(data.status), - txHash: data.executedSellAmount ? data.txHash : undefined, - createdAt: new Date(data.creationDate).getTime(), - updatedAt: Date.now(), - metadata: data, - }; - } - - async cancelOrder(orderId: string, chainId: number): Promise { - // CowSwap doesn't support order cancellation via API - // Orders expire naturally - return false; - } - - async validateQuoteRequest(request: IntentQuoteRequest): Promise { - return this.getSupportedChains().includes(request.srcChainId); - } - - async estimateGas(quote: IntentQuote): Promise { - // CowSwap uses meta-transactions, so gas estimation is minimal - return '21000'; - } - - private mapCowSwapStatus(cowStatus: string): IntentOrderStatus { - switch (cowStatus) { - case 'presignaturePending': - case 'open': - return IntentOrderStatus.PENDING; - case 'fulfilled': - return IntentOrderStatus.COMPLETED; - case 'cancelled': - return IntentOrderStatus.CANCELLED; - case 'expired': - return IntentOrderStatus.EXPIRED; - default: - return IntentOrderStatus.FAILED; - } - } - - private async fetchQuote(request: IntentQuoteRequest, networkPath: string): Promise { - // Implement actual CowSwap quote API call - // This is a placeholder for the actual implementation - throw new Error('Not implemented'); - } -} - -``` - -## 6. Bridge Controller Integration - -```tsx -import { IntentProviderManager } from './intent-providers/intent-provider-manager'; -import { CowSwapProvider } from './intent-providers/cowswap-provider'; - -export class BridgeController extends StaticIntervalPollingController< - typeof BRIDGE_CONTROLLER_NAME, - BridgeControllerState, - BridgeControllerMessenger -> { - private intentProviderManager: IntentProviderManager; - - constructor({ - messenger, - state, - fetchFunction = globalThis.fetch, - }: { - messenger: BridgeControllerMessenger; - state?: Partial; - fetchFunction?: typeof fetch; - }) { - super({ - name: BRIDGE_CONTROLLER_NAME, - metadata: { - // ... existing metadata - }, - messenger, - state: { ...DEFAULT_BRIDGE_CONTROLLER_STATE, ...state }, - }); - - this.intentProviderManager = new IntentProviderManager(); - this.#initializeIntentProviders(); - } - - #initializeIntentProviders(): void { - // Register CowSwap provider - const cowSwapProvider = new CowSwapProvider({ - name: 'cowswap', - version: '1.0.0', - supportedChains: [1, 42161, 8453, 43114, 137], - apiBaseUrl: '', - features: ['eip712-signing', 'meta-transactions'], - }); - - this.intentProviderManager.registerProvider(cowSwapProvider); - } - - async submitIntent(params: { - quoteResponse: QuoteResponse & QuoteMetadata; - signature: string; - accountAddress: string; - }): Promise> { - const { quoteResponse, signature, accountAddress } = params; - - try { - const intentQuote = this.#convertToIntentQuote(quoteResponse); - - const order = await this.intentProviderManager.submitOrder({ - quote: intentQuote, - signature, - userAddress: accountAddress, - }); - - // Create transaction record and bridge history entry - return this.#createIntentTransaction(order, quoteResponse, accountAddress); - } catch (error) { - // Handle error and emit metrics - throw error; - } - } - - getIntentProviderManager(): IntentProviderManager { - return this.intentProviderManager; - } - - #convertToIntentQuote(quoteResponse: QuoteResponse & QuoteMetadata): any { - // Convert bridge quote format to intent quote format - // Implementation details... - } - - #createIntentTransaction(order: any, quoteResponse: any, accountAddress: string): any { - // Create transaction record for intent - // Implementation details... - } -} - -``` - -## 7. Bridge Status Controller Integration - -```tsx -import type { IntentProviderManager } from '@metamask/bridge-controller'; - -export class BridgeStatusController extends StaticIntervalPollingController< - typeof BRIDGE_STATUS_CONTROLLER_NAME, - BridgeStatusControllerState, - BridgeStatusControllerMessenger -> { - #intentProviderManager?: IntentProviderManager; - - setIntentProviderManager(manager: IntentProviderManager): void { - this.#intentProviderManager = manager; - } - - readonly #fetchBridgeTxStatus = async ({ - bridgeTxMetaId, - }: FetchBridgeTxStatusArgs) => { - const { txHistory } = this.state; - - // Intent-based items: use provider manager instead of hardcoded logic - if (bridgeTxMetaId.startsWith('intent:')) { - await this.#fetchIntentOrderStatus({ bridgeTxMetaId }); - return; - } - - // ... existing bridge status logic - }; - - readonly #fetchIntentOrderStatus = async ({ - bridgeTxMetaId, - }: FetchBridgeTxStatusArgs) => { - if (!this.#intentProviderManager) { - console.warn('Intent provider manager not available'); - return; - } - - const { txHistory } = this.state; - const historyItem = txHistory[bridgeTxMetaId]; - if (!historyItem) { - return; - } - - if (shouldSkipFetchDueToFetchFailures(historyItem.attempts)) { - return; - } - - try { - const orderId = bridgeTxMetaId.replace(/^intent:/u, ''); - const [providerName, actualOrderId] = orderId.split(':'); - const { srcChainId } = historyItem.quote; - - const order = await this.#intentProviderManager.getOrderStatus( - actualOrderId, - providerName, - srcChainId - ); - - // Update transaction status based on intent order status - this.#updateTransactionFromIntentOrder(bridgeTxMetaId, order); - } catch (error) { - console.error('Failed to fetch intent order status:', error); - this.#handleIntentStatusError(bridgeTxMetaId, error); - } - }; - - #updateTransactionFromIntentOrder(bridgeTxMetaId: string, order: any): void { - // Convert intent order status to transaction status - // Update transaction controller - // Implementation details... - } - - #handleIntentStatusError(bridgeTxMetaId: string, error: Error): void { - // Handle status polling errors - // Implementation details... - } -} - -``` diff --git a/packages/Intent-manager/src/index.ts b/packages/Intent-manager/src/index.ts index bfe32f53459..ba3914eb22c 100644 --- a/packages/Intent-manager/src/index.ts +++ b/packages/Intent-manager/src/index.ts @@ -8,8 +8,23 @@ export { MAX_RETRY_ATTEMPTS, } from './constants'; -// Export enums -export { IntentStatus } from './types'; +// Export types and enums +export { IntentStatus, IntentOrderStatus } from './types'; + +export type { + IntentQuoteRequest, + IntentQuote, + IntentOrder, + IntentFee, + IntentSubmissionParams, + IntentProviderConfig, + ProviderRegistry, + ProviderSelectionCriteria, +} from './types'; // Export main controller export { IntentManager } from './intent-manager'; +export type { IntentManagerState } from './intent-manager'; + +// Export providers +export * from './providers'; diff --git a/packages/Intent-manager/src/intent-manager.test.ts b/packages/Intent-manager/src/intent-manager.test.ts new file mode 100644 index 00000000000..1808e9fdbd6 --- /dev/null +++ b/packages/Intent-manager/src/intent-manager.test.ts @@ -0,0 +1,174 @@ +// cSpell:words cowswap xsignature xtxhash + +import { IntentManager } from './intent-manager'; +import type { + IntentQuote, + IntentSubmissionParams, + BaseIntentProvider, + IntentQuoteRequest, + IntentOrder, +} from './types'; +import { IntentOrderStatus } from './types'; + +describe('IntentManager', () => { + let intentManager: IntentManager; + + beforeEach(() => { + intentManager = new IntentManager(); + // Clear all providers to start with clean state + intentManager.unregisterProvider('cowswap'); + }); + + describe('constructor', () => { + it('should initialize with default state', () => { + expect(intentManager).toBeDefined(); + }); + }); + + describe('generateQuotes', () => { + it('should generate quotes using the provider manager', async () => { + const quoteRequest: IntentQuoteRequest = { + srcChainId: 1, + destChainId: 42161, + srcTokenAddress: '0xA0b86a33E6441e6e80D0c4C6C7527d72', + destTokenAddress: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + amount: '1000000000000000000', + userAddress: '0x123', + }; + + const mockQuotes: IntentQuote[] = [ + { + id: 'test-quote-1', + provider: 'cowswap', + srcAmount: '1000000000000000000', + destAmount: '900000000000000000', + estimatedGas: '21000', + estimatedTime: 300, + priceImpact: 0.01, + fees: [], + validUntil: Date.now() + 300000, + metadata: {}, + }, + ]; + + // Create a complete mock provider that implements BaseIntentProvider + const mockProvider: BaseIntentProvider = { + getName: () => 'cowswap', + getVersion: () => '1.0.0', + getSupportedChains: () => [1, 42161], + validateQuoteRequest: jest.fn().mockResolvedValue(true), + generateQuote: jest.fn().mockResolvedValue(mockQuotes[0]), + submitOrder: jest.fn(), + getOrderStatus: jest.fn(), + cancelOrder: jest.fn(), + estimateGas: jest.fn(), + }; + + // Register the mock provider + intentManager.registerProvider(mockProvider); + + const result = await intentManager.generateQuotes(quoteRequest); + + expect(result).toStrictEqual(mockQuotes); + expect(mockProvider.generateQuote).toHaveBeenCalledWith(quoteRequest); + }); + }); + + describe('submitIntent', () => { + it('should submit an intent and return an order', async () => { + const mockQuote: IntentQuote = { + id: 'test-quote-1', + provider: 'cowswap', + srcAmount: '1000000000000000000', + destAmount: '900000000000000000', + estimatedGas: '21000', + estimatedTime: 300, + priceImpact: 0.01, + fees: [], + validUntil: Date.now() + 300000, + metadata: {}, + }; + + const submissionParams: IntentSubmissionParams = { + quote: mockQuote, + signature: '0xsignature', + userAddress: '0x123', + }; + + const mockOrder: IntentOrder = { + id: 'test-order-1', + status: IntentOrderStatus.PENDING, + txHash: undefined, + createdAt: Date.now(), + updatedAt: Date.now(), + metadata: {}, + }; + + // Create a complete mock provider that implements BaseIntentProvider + const mockProvider: BaseIntentProvider = { + getName: () => 'cowswap', + getVersion: () => '1.0.0', + getSupportedChains: () => [1, 42161], + validateQuoteRequest: jest.fn(), + generateQuote: jest.fn(), + submitOrder: jest.fn().mockResolvedValue(mockOrder), + getOrderStatus: jest.fn(), + cancelOrder: jest.fn(), + estimateGas: jest.fn(), + }; + + // Register the mock provider + intentManager.registerProvider(mockProvider); + + const result = await intentManager.submitIntent(submissionParams); + + expect(result).toStrictEqual(mockOrder); + expect(mockProvider.submitOrder).toHaveBeenCalledWith(submissionParams); + }); + }); + + describe('getOrderStatus', () => { + it('should get order status from the provider', async () => { + const orderId = 'test-order-1'; + const providerName = 'cowswap'; + const chainId = 1; + + const mockOrder: IntentOrder = { + id: orderId, + status: IntentOrderStatus.CONFIRMED, + txHash: '0xtxhash', + createdAt: Date.now(), + updatedAt: Date.now(), + metadata: {}, + }; + + // Create a complete mock provider that implements BaseIntentProvider + const mockProvider: BaseIntentProvider = { + getName: () => 'cowswap', + getVersion: () => '1.0.0', + getSupportedChains: () => [1, 42161], + validateQuoteRequest: jest.fn(), + generateQuote: jest.fn(), + submitOrder: jest.fn(), + getOrderStatus: jest.fn().mockResolvedValue(mockOrder), + cancelOrder: jest.fn(), + estimateGas: jest.fn(), + }; + + // Register the mock provider + intentManager.registerProvider(mockProvider); + + const result = await intentManager.getOrderStatus( + orderId, + providerName, + chainId, + ); + + expect(result).toStrictEqual(mockOrder); + expect(mockProvider.getOrderStatus).toHaveBeenCalledWith( + orderId, + chainId, + ); + }); + }); +}); diff --git a/packages/Intent-manager/src/intent-manager.ts b/packages/Intent-manager/src/intent-manager.ts index b7d599bcea4..18d11b226ba 100644 --- a/packages/Intent-manager/src/intent-manager.ts +++ b/packages/Intent-manager/src/intent-manager.ts @@ -1,7 +1,233 @@ +import { DEFAULT_INTENT_MANAGER_CONTROLLER_STATE } from './constants'; +import { CowSwapProvider } from './providers/cowswap'; +import type { + BaseIntentProvider, + IntentOrder, + IntentQuote, + IntentQuoteRequest, + IntentSubmissionParams, + ProviderSelectionCriteria, + ProviderRegistry, +} from './types'; + +/** + * Intent Manager Controller State + */ +export type IntentManagerState = { + intents: Record; + intentHistory: IntentOrder[]; +}; + /** * Intent Manager Controller * * Manages the lifecycle of user intents including creation, execution, - * cancellation, and state tracking. + * cancellation, and state tracking. Orchestrates multiple intent providers + * and provides a unified interface for intent operations. */ -export class IntentManager {} +export class IntentManager { + private providers: ProviderRegistry = {}; + + private defaultProvider?: string; + + private readonly state: IntentManagerState; + + constructor(initialState?: Partial) { + this.state = { + ...DEFAULT_INTENT_MANAGER_CONTROLLER_STATE, + ...initialState, + }; + + this.#initializeProviders(); + } + + /** + * Initialize default providers + */ + #initializeProviders(): void { + // Register CowSwap provider by default + const cowSwapProvider = new CowSwapProvider(); + this.registerProvider(cowSwapProvider); + } + + /** + * Register a new intent provider + * + * @param provider - The provider to register + */ + registerProvider(provider: BaseIntentProvider): void { + const name = provider.getName(); + this.providers[name] = provider; + + // Set first registered provider as default + if (!this.defaultProvider) { + this.defaultProvider = name; + } + } + + /** + * Unregister an intent provider + * + * @param providerName - The name of the provider to unregister + * @returns True if the provider was successfully unregistered + */ + unregisterProvider(providerName: string): boolean { + if (this.providers[providerName]) { + delete this.providers[providerName]; + + // Update default if needed + if (this.defaultProvider === providerName) { + this.defaultProvider = Object.keys(this.providers)[0]; + } + return true; + } + return false; + } + + getState(): IntentManagerState { + return { ...this.state }; + } + + /** + * Get available providers based on criteria + * + * @param criteria - Optional criteria for provider selection + * @returns Array of available providers + */ + getAvailableProviders( + criteria?: ProviderSelectionCriteria, + ): BaseIntentProvider[] { + let availableProviders = Object.values(this.providers); + + if (criteria) { + // Filter by supported chains + availableProviders = availableProviders.filter((provider) => + provider.getSupportedChains().includes(criteria.chainId), + ); + + // Filter by excluded providers + if (criteria.excludedProviders && criteria.excludedProviders.length > 0) { + availableProviders = availableProviders.filter( + (provider) => + !criteria.excludedProviders?.includes(provider.getName()), + ); + } + + // Sort by preferred providers + if ( + criteria.preferredProviders && + criteria.preferredProviders.length > 0 + ) { + availableProviders.sort((a, b) => { + const aIndex = + criteria.preferredProviders?.indexOf(a.getName()) ?? -1; + const bIndex = + criteria.preferredProviders?.indexOf(b.getName()) ?? -1; + + if (aIndex === -1 && bIndex === -1) { + return 0; + } + if (aIndex === -1) { + return 1; + } + if (bIndex === -1) { + return -1; + } + + return aIndex - bIndex; + }); + } + } + + return availableProviders; + } + + /** + * Generate quotes from multiple providers + * + * @param request - The quote request parameters + * @param criteria - Optional criteria for provider selection + * @returns Array of quotes sorted by best rate (highest destAmount) + */ + async generateQuotes( + request: IntentQuoteRequest, + criteria?: ProviderSelectionCriteria, + ): Promise { + const providers = this.getAvailableProviders(criteria); + const quotes: IntentQuote[] = []; + + await Promise.allSettled( + providers.map(async (provider) => { + try { + const isValid = await provider.validateQuoteRequest(request); + if (isValid) { + const quote = await provider.generateQuote(request); + quotes.push(quote); + } + } catch (error) { + console.warn( + `Failed to get quote from ${provider.getName()}:`, + error, + ); + } + }), + ); + + // Sort by best rate (highest destination amount) + return quotes.sort( + (a, b) => parseFloat(b.destAmount) - parseFloat(a.destAmount), + ); + } + + async submitIntent(params: IntentSubmissionParams): Promise { + const provider = this.providers[params.quote.provider]; + if (!provider) { + throw new Error(`Provider ${params.quote.provider} not found`); + } + + const order = await provider.submitOrder(params); + + // Update state + this.state.intents[order.id] = order; + this.state.intentHistory.push(order); + + return order; + } + + async getOrderStatus( + orderId: string, + providerName: string, + chainId: number, + ): Promise { + const provider = this.providers[providerName]; + if (!provider) { + throw new Error(`Provider ${providerName} not found`); + } + + const order = await provider.getOrderStatus(orderId, chainId); + + // Update state if order exists + if (this.state.intents[orderId]) { + this.state.intents[orderId] = order; + } + + return order; + } + + async cancelOrder( + orderId: string, + providerName: string, + chainId: number, + ): Promise { + const provider = this.providers[providerName]; + if (!provider) { + throw new Error(`Provider ${providerName} not found`); + } + + return provider.cancelOrder(orderId, chainId); + } + + getProviders(criteria?: ProviderSelectionCriteria): BaseIntentProvider[] { + return this.getAvailableProviders(criteria); + } +} diff --git a/packages/Intent-manager/src/providers/base-intent-provider.ts b/packages/Intent-manager/src/providers/base-intent-provider.ts new file mode 100644 index 00000000000..ebe410f156b --- /dev/null +++ b/packages/Intent-manager/src/providers/base-intent-provider.ts @@ -0,0 +1,154 @@ +import type { + IntentQuoteRequest, + IntentQuote, + IntentOrder, + IntentSubmissionParams, + IntentProviderConfig, + IntentOrderStatus, +} from '../types'; + +/** + * Abstract base class for intent providers + * + * This class provides the foundation for implementing specific intent providers + * like CowSwap, 1inch, 0x Protocol, etc. Each provider must implement the + * abstract methods to handle their specific API and business logic. + */ +export abstract class BaseIntentProvider { + protected config: IntentProviderConfig; + + constructor(config: IntentProviderConfig) { + this.config = config; + } + + /** + * Get the name of this provider + */ + abstract getName(): string; + + /** + * Get the version of this provider + */ + abstract getVersion(): string; + + /** + * Get the list of supported chain IDs + */ + abstract getSupportedChains(): number[]; + + /** + * Generate a quote for the given request + * + * @param request - The quote request parameters + * @returns Promise resolving to an intent quote + */ + abstract generateQuote(request: IntentQuoteRequest): Promise; + + /** + * Submit an order based on the quote and signature + * + * @param params - The submission parameters including quote and signature + * @returns Promise resolving to the created order + */ + abstract submitOrder(params: IntentSubmissionParams): Promise; + + /** + * Get the current status of an order + * + * @param orderId - The order ID to check + * @param chainId - The chain ID where the order was placed + * @returns Promise resolving to the order status + */ + abstract getOrderStatus(orderId: string, chainId: number): Promise; + + /** + * Cancel an existing order + * + * @param orderId - The order ID to cancel + * @param chainId - The chain ID where the order was placed + * @returns Promise resolving to true if cancellation was successful + */ + abstract cancelOrder(orderId: string, chainId: number): Promise; + + /** + * Validate that a quote request is valid for this provider + * + * @param request - The quote request to validate + * @returns Promise resolving to true if the request is valid + */ + abstract validateQuoteRequest(request: IntentQuoteRequest): Promise; + + /** + * Estimate gas cost for executing the given quote + * + * @param quote - The quote to estimate gas for + * @returns Promise resolving to the estimated gas amount as a string + */ + abstract estimateGas(quote: IntentQuote): Promise; + + /** + * Lifecycle hook called after a quote is generated + * Override this method to add provider-specific post-quote logic + * + * @param quote - The generated quote + */ + protected async onQuoteGenerated?(quote: IntentQuote): Promise; + + /** + * Lifecycle hook called after an order is submitted + * Override this method to add provider-specific post-submission logic + * + * @param order - The submitted order + */ + protected async onOrderSubmitted?(order: IntentOrder): Promise; + + /** + * Lifecycle hook called when an order status changes + * Override this method to add provider-specific status change logic + * + * @param order - The order with updated status + * @param previousStatus - The previous status of the order + */ + protected async onOrderStatusChanged?( + order: IntentOrder, + previousStatus: IntentOrderStatus, + ): Promise; + + /** + * Handle errors in a consistent way across providers + * + * @param error - The original error + * @param context - Context about where the error occurred + * @returns A new error with provider-specific context + */ + protected handleError(error: Error, context: string): Error { + return new Error(`${this.getName()}: ${context} - ${error.message}`); + } + + /** + * Get the configuration for this provider + */ + getConfig(): IntentProviderConfig { + return { ...this.config }; + } + + /** + * Check if this provider supports the given chain + * + * @param chainId - The chain ID to check + * @returns True if the chain is supported + */ + supportsChain(chainId: number): boolean { + return this.getSupportedChains().includes(chainId); + } + + /** + * Check if this provider has a specific feature + * + * @param feature - The feature name to check + * @returns True if the feature is supported + */ + hasFeature(feature: string): boolean { + return this.config.features.includes(feature); + } +} diff --git a/packages/Intent-manager/src/providers/cowswap/constants.ts b/packages/Intent-manager/src/providers/cowswap/constants.ts new file mode 100644 index 00000000000..3dd9fcd0d2f --- /dev/null +++ b/packages/Intent-manager/src/providers/cowswap/constants.ts @@ -0,0 +1,40 @@ +/** + * CowSwap API base URL + */ +export const COW_API_BASE = 'https://api.cow.fi'; + +/** + * Mapping of chain IDs to CowSwap network paths + */ +export const COW_NETWORK_PATHS: Record = { + // Ethereum Mainnet + 1: 'mainnet', + // Arbitrum One + 42161: 'arbitrum_one', + // Base + 8453: 'base', + // Avalanche C-Chain + 43114: 'avalanche', + // Polygon PoS + 137: 'polygon', +}; + +/** + * Default CowSwap settlement contract address + */ +export const COW_SETTLEMENT_CONTRACT = '0x9008D19f58AAbd9eD0D60971565AA8510560ab41'; + +/** + * CowSwap provider configuration + */ +export const COWSWAP_PROVIDER_CONFIG = { + name: 'cowswap', + version: '1.0.0', + supportedChains: Object.keys(COW_NETWORK_PATHS).map(Number), + apiBaseUrl: COW_API_BASE, + features: ['eip712-signing', 'meta-transactions', 'gasless-trading'], + rateLimit: { + requestsPerMinute: 60, + burstLimit: 10, + }, +}; diff --git a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts new file mode 100644 index 00000000000..b33bdc942ec --- /dev/null +++ b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts @@ -0,0 +1,264 @@ +import { BaseIntentProvider } from '../base-intent-provider'; +import type { + IntentQuote, + IntentQuoteRequest, + IntentOrder, + IntentSubmissionParams, + IntentOrderStatus, +} from '../../types'; +import { IntentOrderStatus as OrderStatus } from '../../types'; +import { + COW_API_BASE, + COW_NETWORK_PATHS, + COW_SETTLEMENT_CONTRACT, + COWSWAP_PROVIDER_CONFIG, +} from './constants'; + +/** + * CowSwap intent provider implementation + * + * Handles quote generation, order submission, and status polling for CowSwap intents. + * Based on the existing CowSwap integration logic from bridge-status-controller. + */ +export class CowSwapProvider extends BaseIntentProvider { + constructor() { + super(COWSWAP_PROVIDER_CONFIG); + } + + getName(): string { + return 'cowswap'; + } + + getVersion(): string { + return '1.0.0'; + } + + getSupportedChains(): number[] { + return Object.keys(COW_NETWORK_PATHS).map(Number); + } + + async generateQuote(request: IntentQuoteRequest): Promise { + const networkPath = COW_NETWORK_PATHS[request.srcChainId]; + if (!networkPath) { + throw this.handleError( + new Error(`Unsupported chain: ${request.srcChainId}`), + 'generateQuote', + ); + } + + try { + // Implementation for CowSwap quote generation + // This would call the actual CowSwap API + const response = await this.fetchQuote(request, networkPath); + + const quote: IntentQuote = { + id: response.id || `cow-${Date.now()}`, + provider: this.getName(), + srcAmount: request.amount, + destAmount: response.buyAmount || '0', + estimatedGas: response.estimatedGas || '21000', + estimatedTime: 300, // 5 minutes typical for CowSwap + priceImpact: response.priceImpact || 0, + fees: [ + { + type: 'protocol', + amount: response.feeAmount || '0', + token: request.srcTokenAddress, + }, + ], + validUntil: response.validTo || Date.now() + 300000, // 5 minutes from now + metadata: { + order: response.order, + settlementContract: + response.settlementContract || COW_SETTLEMENT_CONTRACT, + chainId: request.srcChainId, + networkPath, + }, + }; + + await this.onQuoteGenerated?.(quote); + return quote; + } catch (error) { + throw this.handleError(error as Error, 'generateQuote'); + } + } + + async submitOrder(params: IntentSubmissionParams): Promise { + const chainId = params.quote.metadata.chainId as number; + const networkPath = COW_NETWORK_PATHS[chainId]; + + if (!networkPath) { + throw this.handleError( + new Error(`Unsupported chain: ${chainId}`), + 'submitOrder', + ); + } + + try { + const orderBody = { + ...(params.quote.metadata?.order || {}), + feeAmount: '0', + from: params.userAddress, + signature: params.signature, + signingScheme: 'eip712', + }; + + const url = `${COW_API_BASE}/${networkPath}/api/v1/orders`; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(orderBody), + }); + + if (!response.ok) { + throw new Error(`Failed to submit order: ${response.statusText}`); + } + + const orderUid = await response.text(); + + const order: IntentOrder = { + id: orderUid, + status: OrderStatus.SUBMITTED, + createdAt: Date.now(), + updatedAt: Date.now(), + metadata: { + chainId, + networkPath, + orderBody, + }, + }; + + await this.onOrderSubmitted?.(order); + return order; + } catch (error) { + throw this.handleError(error as Error, 'submitOrder'); + } + } + + async getOrderStatus(orderId: string, chainId: number): Promise { + const networkPath = COW_NETWORK_PATHS[chainId]; + if (!networkPath) { + throw this.handleError( + new Error(`Unsupported chain: ${chainId}`), + 'getOrderStatus', + ); + } + + try { + const url = `${COW_API_BASE}/${networkPath}/api/v1/orders/${orderId}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to get order status: ${response.statusText}`); + } + + const data = await response.json(); + const status = this.mapCowSwapStatus(data.status); + + // Try to get transaction hashes from trades endpoint for completed orders + let txHash: string | undefined; + let allHashes: string[] = []; + + if (status === OrderStatus.COMPLETED) { + try { + const tradesUrl = `${COW_API_BASE}/${networkPath}/api/v1/trades?orderUid=${orderId}`; + const tradesResponse = await fetch(tradesUrl); + + if (tradesResponse.ok) { + const trades = await tradesResponse.json(); + allHashes = Array.isArray(trades) + ? trades + .map( + (t: { txHash?: string; transactionHash?: string }) => + t?.txHash || t?.transactionHash, + ) + .filter( + (h: unknown): h is string => + typeof h === 'string' && h.length > 0, + ) + : []; + txHash = allHashes[allHashes.length - 1]; + } + } catch (error) { + console.warn('Failed to fetch trade hashes:', error); + } + } + + const order: IntentOrder = { + id: orderId, + status, + txHash, + createdAt: new Date(data.creationDate).getTime(), + updatedAt: Date.now(), + metadata: { + ...data, + allHashes, + chainId, + networkPath, + }, + }; + + return order; + } catch (error) { + throw this.handleError(error as Error, 'getOrderStatus'); + } + } + + async cancelOrder(orderId: string, chainId: number): Promise { + // CowSwap doesn't support order cancellation via API + // Orders expire naturally based on their validTo timestamp + console.warn( + `CowSwap orders cannot be cancelled via API. Order ${orderId} will expire naturally.`, + ); + return false; + } + + async validateQuoteRequest(request: IntentQuoteRequest): Promise { + // Basic validation - check if chain is supported + if (!this.getSupportedChains().includes(request.srcChainId)) { + return false; + } + + // Additional validation could be added here + // e.g., token address validation, amount validation, etc. + return true; + } + + async estimateGas(quote: IntentQuote): Promise { + // CowSwap uses meta-transactions, so gas estimation is minimal + // The actual settlement is handled by solvers + return '21000'; + } + + /** + * Map CowSwap API status to our internal status enum + */ + private mapCowSwapStatus(cowStatus: string): IntentOrderStatus { + switch (cowStatus) { + case 'presignaturePending': + case 'open': + return OrderStatus.PENDING; + case 'fulfilled': + return OrderStatus.COMPLETED; + case 'cancelled': + return OrderStatus.CANCELLED; + case 'expired': + return OrderStatus.EXPIRED; + default: + return OrderStatus.FAILED; + } + } + + /** + * Fetch quote from CowSwap API + * This is a placeholder for the actual implementation + */ + private async fetchQuote( + request: IntentQuoteRequest, + networkPath: string, + ): Promise { + // TODO: Implement actual CowSwap quote API call + // For now, return a mock response structure + throw new Error('CowSwap quote fetching not yet implemented'); + } +} diff --git a/packages/Intent-manager/src/providers/cowswap/index.ts b/packages/Intent-manager/src/providers/cowswap/index.ts new file mode 100644 index 00000000000..b9e400f6882 --- /dev/null +++ b/packages/Intent-manager/src/providers/cowswap/index.ts @@ -0,0 +1,7 @@ +export { CowSwapProvider } from './cowswap-provider'; +export { + COW_API_BASE, + COW_NETWORK_PATHS, + COW_SETTLEMENT_CONTRACT, + COWSWAP_PROVIDER_CONFIG +} from './constants'; diff --git a/packages/Intent-manager/src/providers/index.ts b/packages/Intent-manager/src/providers/index.ts new file mode 100644 index 00000000000..739b2148aab --- /dev/null +++ b/packages/Intent-manager/src/providers/index.ts @@ -0,0 +1,2 @@ +export { BaseIntentProvider } from './base-intent-provider'; +export * from './cowswap'; diff --git a/packages/Intent-manager/src/types.ts b/packages/Intent-manager/src/types.ts index 93066875877..3e75db5fe8c 100644 --- a/packages/Intent-manager/src/types.ts +++ b/packages/Intent-manager/src/types.ts @@ -9,4 +9,122 @@ export enum IntentStatus { CANCELLED = 'cancelled', } -// Import the constant here to avoid circular dependency +/** + * Intent order status enumeration - more granular than IntentStatus + */ +export enum IntentOrderStatus { + PENDING = 'pending', + SUBMITTED = 'submitted', + CONFIRMED = 'confirmed', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} + +/** + * Request parameters for generating an intent quote + */ +export type IntentQuoteRequest = { + srcChainId: number; + destChainId: number; + srcTokenAddress: string; + destTokenAddress: string; + amount: string; + userAddress: string; + slippage?: number; +}; + +/** + * Fee information for an intent + */ +export type IntentFee = { + type: 'network' | 'protocol' | 'bridge'; + amount: string; + token: string; +}; + +/** + * Quote response from an intent provider + */ +export type IntentQuote = { + id: string; + provider: string; + srcAmount: string; + destAmount: string; + estimatedGas: string; + estimatedTime: number; // seconds + priceImpact: number; + fees: IntentFee[]; + validUntil: number; // timestamp + metadata: Record; +}; + +/** + * Intent order information + */ +export type IntentOrder = { + id: string; + status: IntentOrderStatus; + txHash?: string; + createdAt: number; + updatedAt: number; + metadata: Record; +}; + +/** + * Parameters for submitting an intent order + */ +export type IntentSubmissionParams = { + quote: IntentQuote; + signature: string; + userAddress: string; +}; + +/** + * Configuration for an intent provider + */ +export type IntentProviderConfig = { + name: string; + version: string; + supportedChains: number[]; + apiBaseUrl: string; + features: string[]; + rateLimit?: { + requestsPerMinute: number; + burstLimit: number; + }; +}; + +/** + * Registry of intent providers + */ +export type ProviderRegistry = { + [providerName: string]: BaseIntentProvider; +}; + +/** + * Criteria for selecting intent providers + */ +export type ProviderSelectionCriteria = { + chainId: number; + tokenPair: [string, string]; + amount: string; + preferredProviders?: string[]; + excludedProviders?: string[]; +}; + +/** + * Base interface for intent providers + */ +export type BaseIntentProvider = { + getName(): string; + getVersion(): string; + getSupportedChains(): number[]; + generateQuote(request: IntentQuoteRequest): Promise; + submitOrder(params: IntentSubmissionParams): Promise; + getOrderStatus(orderId: string, chainId: number): Promise; + cancelOrder(orderId: string, chainId: number): Promise; + validateQuoteRequest(request: IntentQuoteRequest): Promise; + estimateGas(quote: IntentQuote): Promise; +}; diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index 4943b26235a..99c7ef099d3 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -49,6 +49,7 @@ "dependencies": { "@metamask/base-controller": "^8.3.0", "@metamask/controller-utils": "^11.12.0", + "@metamask/intent-manager": "^1.0.0", "@metamask/keyring-api": "^20.1.0", "@metamask/polling-controller": "^14.0.0", "@metamask/superstruct": "^3.1.0", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 2675c26de24..269013f0e6c 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -19,6 +19,8 @@ import { } from '@metamask/bridge-controller'; import type { TraceCallback } from '@metamask/controller-utils'; import { toHex } from '@metamask/controller-utils'; +import type { IntentManager, IntentOrder } from '@metamask/intent-manager'; +import { IntentOrderStatus } from '@metamask/intent-manager'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { TransactionController, @@ -76,21 +78,6 @@ import { } from './utils/transaction'; import { generateActionId } from './utils/transaction'; -// CoW intent: API base and network path mapping (adjust as needed) -const COW_API_BASE = 'https://api.cow.fi'; -const COW_NETWORK_PATHS: Record = { - // Ethereum Mainnet - 1: 'mainnet', - // Arbitrum One - 42161: 'arbitrum_one', - // Base - 8453: 'base', - // Avalanche C-Chain - 43114: 'avalanche', - // Polygon PoS - 137: 'polygon', -}; - const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list // basically match the behavior of TransactionController @@ -128,6 +115,8 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { txHistory } = this.state; - // Intent-based items: poll CoW API instead of Bridge API + // Intent-based items: poll intent provider instead of Bridge API if (bridgeTxMetaId.startsWith('intent:')) { - await this.#fetchCowOrderStatus({ bridgeTxMetaId }); + await this.#fetchIntentOrderStatus({ bridgeTxMetaId }); return; } @@ -695,9 +693,14 @@ export class BridgeStatusController extends StaticIntervalPollingController { + if (!this.#intentManager) { + console.warn('Intent manager not available for status polling'); + return; + } + const { txHistory } = this.state; const historyItem = txHistory[bridgeTxMetaId]; if (!historyItem) { @@ -709,205 +712,183 @@ export class BridgeStatusController extends StaticIntervalPollingController { - state.txHistory[bridgeTxMetaId].status = { - status: StatusTypes.FAILED, - srcChain: { - chainId: srcChainId, - txHash: '', - }, - } as unknown as (typeof state.txHistory)[string]['status']; - }); - return; + try { + const orderUid = bridgeTxMetaId.replace(/^intent:/u, ''); + const { srcChainId } = historyItem.quote; + + // Extract provider name from order metadata or default to empty and let intent manager throw error + const providerName = historyItem.quote.intent?.protocol || ''; + + const intentOrder = await this.#intentManager.getOrderStatus( + orderUid, + providerName, + srcChainId, + ); + + // Update bridge history with intent order status + this.#updateBridgeHistoryFromIntentOrder( + bridgeTxMetaId, + intentOrder, + historyItem, + ); + } catch (error) { + console.error('Failed to fetch intent order status:', error); + this.#handleFetchFailure(bridgeTxMetaId); } + }; - try { - const url = `${COW_API_BASE}/${networkPath}/api/v1/orders/${orderUid}`; - const res = await this.#fetchFn(url, { method: 'GET' }); - - // CoW API status enum mapping - const rawStatus = - typeof res === 'object' && res !== null && 'status' in res - ? ((res as { status?: string }).status ?? '') - : ''; - const isComplete = rawStatus === 'fulfilled'; - const isFailed = ['cancelled', 'expired'].includes(rawStatus); - const isPending = ['presignaturePending', 'open'].includes(rawStatus); - - // Try to find a tx hash in common fields - let txHash = ''; - let allHashes: string[] = []; - - if (isComplete) { - // Prefer authoritative trades endpoint for one or multiple fills - const tradesUrl = `${COW_API_BASE}/${networkPath}/api/v1/trades?orderUid=${orderUid}`; - const trades = - (await this.#fetchFn(tradesUrl, { method: 'GET' })) ?? []; - allHashes = Array.isArray(trades) - ? trades - .map( - (t: { txHash?: unknown; transactionHash?: unknown }) => - t?.txHash || t?.transactionHash, - ) - .filter( - (h: unknown): h is string => - typeof h === 'string' && h.length > 0, - ) - : []; - // Fallback to any hash on order if trades missing - if (allHashes.length === 0) { - const possible = [ - (res as { txHash?: string }).txHash, - (res as { transactionHash?: string }).transactionHash, - (res as { executedTransaction?: string }).executedTransaction, - (res as { executedTransactionHash?: string }) - .executedTransactionHash, - ].filter( - (h: unknown): h is string => typeof h === 'string' && h.length > 0, - ); - allHashes = possible; - } - txHash = allHashes[allHashes.length - 1] || ''; - } + #updateBridgeHistoryFromIntentOrder( + bridgeTxMetaId: string, + intentOrder: IntentOrder, + historyItem: BridgeHistoryItem, + ) { + const { srcChainId } = historyItem.quote; - let statusType: StatusTypes; - if (isComplete) { - statusType = StatusTypes.COMPLETE; - } else if (isFailed) { - statusType = StatusTypes.FAILED; - } else if (isPending) { - statusType = StatusTypes.PENDING; - } else { - statusType = StatusTypes.PENDING; // Default to pending for unknown statuses - } + // Map intent order status to bridge status using enum values + let statusType: StatusTypes; + const isComplete = [ + IntentOrderStatus.CONFIRMED, + IntentOrderStatus.COMPLETED, + ].includes(intentOrder.status); + const isFailed = [ + IntentOrderStatus.FAILED, + IntentOrderStatus.CANCELLED, + IntentOrderStatus.EXPIRED, + ].includes(intentOrder.status); + const isPending = [ + IntentOrderStatus.PENDING, + IntentOrderStatus.SUBMITTED, + ].includes(intentOrder.status); + + if (isComplete) { + statusType = StatusTypes.COMPLETE; + } else if (isFailed) { + statusType = StatusTypes.FAILED; + } else if (isPending) { + statusType = StatusTypes.PENDING; + } else { + statusType = StatusTypes.PENDING; // Default to pending for unknown statuses + } - const newStatus = { - status: statusType, - srcChain: { - chainId: srcChainId, - txHash: txHash || historyItem.status.srcChain.txHash || '', - }, - } as typeof historyItem.status; + // Extract transaction hashes from intent order + const txHash = intentOrder.txHash || ''; + // Check metadata for additional transaction hashes + const metadataTxHashes = Array.isArray(intentOrder.metadata.txHashes) + ? (intentOrder.metadata.txHashes as string[]) + : []; + const allHashes = + metadataTxHashes.length > 0 ? metadataTxHashes : txHash ? [txHash] : []; + + const newStatus = { + status: statusType, + srcChain: { + chainId: srcChainId, + txHash: txHash || historyItem.status.srcChain.txHash || '', + }, + } as typeof historyItem.status; - const newBridgeHistoryItem = { - ...historyItem, - status: newStatus, - completionTime: - newStatus.status === StatusTypes.COMPLETE || - newStatus.status === StatusTypes.FAILED - ? Date.now() - : undefined, - attempts: undefined, - srcTxHashes: - allHashes.length > 0 - ? Array.from( - new Set([...(historyItem.srcTxHashes ?? []), ...allHashes]), - ) - : historyItem.srcTxHashes, - }; + const newBridgeHistoryItem = { + ...historyItem, + status: newStatus, + completionTime: + newStatus.status === StatusTypes.COMPLETE || + newStatus.status === StatusTypes.FAILED + ? Date.now() + : undefined, + attempts: undefined, + srcTxHashes: + allHashes.length > 0 + ? Array.from( + new Set([...(historyItem.srcTxHashes ?? []), ...allHashes]), + ) + : historyItem.srcTxHashes, + }; - this.update((state) => { - state.txHistory[bridgeTxMetaId] = newBridgeHistoryItem; - }); + this.update((state) => { + state.txHistory[bridgeTxMetaId] = newBridgeHistoryItem; + }); - // Update the actual transaction in TransactionController to sync with CoW status - // Use the original transaction ID (not the intent: prefixed bridge history key) - const originalTxId = - (historyItem as unknown as { originalTransactionId: string }) - .originalTransactionId || historyItem.txMetaId; - if (originalTxId && !originalTxId.startsWith('intent:')) { - try { - let transactionStatus: TransactionStatus; - if (isComplete) { - transactionStatus = TransactionStatus.confirmed; - } else if (isFailed) { - transactionStatus = TransactionStatus.failed; - } else if (isPending) { - transactionStatus = TransactionStatus.submitted; - } else { - transactionStatus = TransactionStatus.submitted; // Default to submitted for unknown statuses - } + // Update the actual transaction in TransactionController to sync with intent status + // Use the original transaction ID (not the intent: prefixed bridge history key) + const originalTxId = + historyItem.originalTransactionId || historyItem.txMetaId; + if (originalTxId && !originalTxId.startsWith('intent:')) { + try { + const transactionStatus = this.#mapIntentOrderStatusToTransactionStatus( + intentOrder.status, + ); - // Merge with existing TransactionMeta to avoid wiping required fields - const { transactions } = this.messagingSystem.call( - 'TransactionController:getState', - ); - const existingTxMeta = (transactions as TransactionMeta[]).find( - (t) => t.id === originalTxId, - ); + // Merge with existing TransactionMeta to avoid wiping required fields + const { transactions } = this.messagingSystem.call( + 'TransactionController:getState', + ); + const existingTxMeta = (transactions as TransactionMeta[]).find( + (t) => t.id === originalTxId, + ); - if (!existingTxMeta) { - console.warn( - '📝 [fetchCowOrderStatus] Skipping update; transaction not found', - { originalTxId, bridgeHistoryKey: bridgeTxMetaId }, - ); - } else { - const updatedTxMeta: TransactionMeta = { - ...existingTxMeta, - status: transactionStatus, - ...(txHash ? { hash: txHash } : {}), - ...(txHash - ? ({ - txReceipt: { - ...( - existingTxMeta as unknown as { - txReceipt: Record; - } - ).txReceipt, - transactionHash: txHash, - status: (isComplete ? '0x1' : '0x0') as unknown as string, - }, - } as Partial) - : {}), - } as TransactionMeta; - - this.#updateTransactionFn( - updatedTxMeta, - `BridgeStatusController - CoW order status updated: ${rawStatus}`, - ); - } - } catch (error) { - console.error( - '📝 [fetchCowOrderStatus] Failed to update transaction status', - { - originalTxId, - bridgeHistoryKey: bridgeTxMetaId, - error, - }, + if (!existingTxMeta) { + console.warn( + '📝 [fetchIntentOrderStatus] Skipping update; transaction not found', + { originalTxId, bridgeHistoryKey: bridgeTxMetaId }, + ); + } else { + const updatedTxMeta: TransactionMeta = { + ...existingTxMeta, + status: transactionStatus, + ...(txHash ? { hash: txHash } : {}), + ...(txHash + ? ({ + txReceipt: { + ...( + existingTxMeta as unknown as { + txReceipt: Record; + } + ).txReceipt, + transactionHash: txHash, + status: (isComplete ? '0x1' : '0x0') as unknown as string, + }, + } as Partial) + : {}), + } as TransactionMeta; + + this.#updateTransactionFn( + updatedTxMeta, + `BridgeStatusController - Intent order status updated: ${intentOrder.status}`, ); } + } catch (error) { + console.error( + '📝 [fetchIntentOrderStatus] Failed to update transaction status', + { + originalTxId, + bridgeHistoryKey: bridgeTxMetaId, + error, + }, + ); } + } - const pollingToken = this.#pollingTokensByTxMetaId[bridgeTxMetaId]; - const isFinal = - newStatus.status === StatusTypes.COMPLETE || - newStatus.status === StatusTypes.FAILED; - if (isFinal && pollingToken) { - this.stopPollingByPollingToken(pollingToken); - delete this.#pollingTokensByTxMetaId[bridgeTxMetaId]; + const pollingToken = this.#pollingTokensByTxMetaId[bridgeTxMetaId]; + const isFinal = + newStatus.status === StatusTypes.COMPLETE || + newStatus.status === StatusTypes.FAILED; + if (isFinal && pollingToken) { + this.stopPollingByPollingToken(pollingToken); + delete this.#pollingTokensByTxMetaId[bridgeTxMetaId]; - if (newStatus.status === StatusTypes.COMPLETE) { - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.Completed, - bridgeTxMetaId, - ); - } else if (newStatus.status === StatusTypes.FAILED) { - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.Failed, - bridgeTxMetaId, - ); - } + if (newStatus.status === StatusTypes.COMPLETE) { + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Completed, + bridgeTxMetaId, + ); + } else if (newStatus.status === StatusTypes.FAILED) { + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Failed, + bridgeTxMetaId, + ); } - } catch { - // Network or API error: apply backoff - this.#handleFetchFailure(bridgeTxMetaId); } - }; + } readonly #getSrcTxHash = (bridgeTxMetaId: string): string | undefined => { const { txHistory } = this.state; @@ -1476,7 +1457,7 @@ export class BridgeStatusController extends StaticIntervalPollingController> => { const { quoteResponse, signature, accountAddress } = params; + if (!this.#intentManager) { + throw new Error('Intent manager not initialized'); + } + // Build pre-confirmation properties for error tracking parity with submitTx const account = this.messagingSystem.call( 'AccountsController:getAccountByAddress', @@ -1506,8 +1491,8 @@ export class BridgeStatusController extends StaticIntervalPollingController; - - // POST to CoW prod - const url = `https://api.cow.fi/${serverPath}/api/v1/orders`; - const res = await this.#fetchFn(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(orderBody), + userAddress: accountAddress, }); - const orderUid: string | undefined = - typeof res === 'string' - ? res - : ((res as { uid?: string; orderUid?: string; id?: string })?.uid ?? - (res as { uid?: string; orderUid?: string; id?: string }) - ?.orderUid ?? - (res as { uid?: string; orderUid?: string; id?: string })?.id); - if (!orderUid) { - throw new Error('submitIntent: failed to submit order'); - } - - // Get initial order status from CoW API - let initialOrderStatus = 'open'; // Default status - try { - const orderStatusUrl = `${COW_API_BASE}/${serverPath}/api/v1/orders/${orderUid}`; - const orderStatusRes = await this.#fetchFn(orderStatusUrl, { - method: 'GET', - }); - initialOrderStatus = - (orderStatusRes as { status?: string })?.status ?? 'open'; - } catch (error) { - console.warn( - '📝 [submitIntent] Failed to get initial order status, using default:', - error, - ); - } + const chainId = quoteResponse.quote.srcChainId; + const orderUid = intentOrder.id; // Determine transaction type: swap for same-chain, bridge for cross-chain const isCrossChainTx = isCrossChain( @@ -1619,7 +1567,7 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, + intent: Intent, + ) { + return { + id: `bridge-${Date.now()}`, + provider: intent.protocol, + srcAmount: quoteResponse.quote.srcTokenAmount, + destAmount: quoteResponse.quote.destTokenAmount, + estimatedGas: '21000', + estimatedTime: 300, // 5 minutes + priceImpact: 0, + fees: [], + validUntil: Date.now() + 300000, // 5 minutes from now + metadata: { + order: intent.order, + settlementContract: intent.settlementContract, + chainId: quoteResponse.quote.srcChainId, + bridgeQuote: quoteResponse, + }, + }; + } + + #mapIntentOrderStatusToTransactionStatus( + intentStatus: IntentOrderStatus, + ): TransactionStatus { + switch (intentStatus) { + case IntentOrderStatus.PENDING: + case IntentOrderStatus.SUBMITTED: + return TransactionStatus.submitted; + case IntentOrderStatus.CONFIRMED: + case IntentOrderStatus.COMPLETED: + return TransactionStatus.confirmed; + case IntentOrderStatus.FAILED: + case IntentOrderStatus.CANCELLED: + case IntentOrderStatus.EXPIRED: + return TransactionStatus.failed; + default: + return TransactionStatus.submitted; + } + } + /** * Tracks post-submission events for a cross-chain swap based on the history item * diff --git a/packages/bridge-status-controller/tsconfig.json b/packages/bridge-status-controller/tsconfig.json index e41150bdaf3..567f874afd2 100644 --- a/packages/bridge-status-controller/tsconfig.json +++ b/packages/bridge-status-controller/tsconfig.json @@ -9,6 +9,7 @@ { "path": "../base-controller" }, { "path": "../bridge-controller" }, { "path": "../controller-utils" }, + { "path": "../intent-manager" }, { "path": "../network-controller" }, { "path": "../polling-controller" }, { "path": "../transaction-controller" }, From 2ea099e6d7ea62ba9164383829be9a5f145d6f8f Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 10 Sep 2025 06:43:44 +0100 Subject: [PATCH 11/85] wip: fix the build issue. --- packages/Intent-manager/package.json | 7 ---- .../src/providers/base-intent-provider.ts | 32 +++++++++------- .../src/providers/cowswap/constants.ts | 3 +- .../src/providers/cowswap/cowswap-provider.ts | 38 +++++++++++-------- .../src/providers/cowswap/index.ts | 10 ++--- .../bridge-status-controller/tsconfig.json | 1 - tsconfig.json | 1 + tsconfig.packages.json | 1 + yarn.lock | 8 ++-- 9 files changed, 55 insertions(+), 46 deletions(-) diff --git a/packages/Intent-manager/package.json b/packages/Intent-manager/package.json index d9d1a155ccc..0b3580d1508 100644 --- a/packages/Intent-manager/package.json +++ b/packages/Intent-manager/package.json @@ -54,9 +54,7 @@ "@metamask/utils": "^11.4.2" }, "devDependencies": { - "@lavamoat/allow-scripts": "^3.0.4", "@metamask/auto-changelog": "^3.4.4", - "@ts-bridge/cli": "^0.6.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -72,10 +70,5 @@ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" - }, - "lavamoat": { - "allowScripts": { - "@lavamoat/allow-scripts>@lavamoat/preinstall-always-fail": false - } } } diff --git a/packages/Intent-manager/src/providers/base-intent-provider.ts b/packages/Intent-manager/src/providers/base-intent-provider.ts index ebe410f156b..8d61bde50ca 100644 --- a/packages/Intent-manager/src/providers/base-intent-provider.ts +++ b/packages/Intent-manager/src/providers/base-intent-provider.ts @@ -9,7 +9,7 @@ import type { /** * Abstract base class for intent providers - * + * * This class provides the foundation for implementing specific intent providers * like CowSwap, 1inch, 0x Protocol, etc. Each provider must implement the * abstract methods to handle their specific API and business logic. @@ -38,7 +38,7 @@ export abstract class BaseIntentProvider { /** * Generate a quote for the given request - * + * * @param request - The quote request parameters * @returns Promise resolving to an intent quote */ @@ -46,7 +46,7 @@ export abstract class BaseIntentProvider { /** * Submit an order based on the quote and signature - * + * * @param params - The submission parameters including quote and signature * @returns Promise resolving to the created order */ @@ -54,16 +54,19 @@ export abstract class BaseIntentProvider { /** * Get the current status of an order - * + * * @param orderId - The order ID to check * @param chainId - The chain ID where the order was placed * @returns Promise resolving to the order status */ - abstract getOrderStatus(orderId: string, chainId: number): Promise; + abstract getOrderStatus( + orderId: string, + chainId: number, + ): Promise; /** * Cancel an existing order - * + * * @param orderId - The order ID to cancel * @param chainId - The chain ID where the order was placed * @returns Promise resolving to true if cancellation was successful @@ -72,7 +75,7 @@ export abstract class BaseIntentProvider { /** * Validate that a quote request is valid for this provider - * + * * @param request - The quote request to validate * @returns Promise resolving to true if the request is valid */ @@ -80,7 +83,7 @@ export abstract class BaseIntentProvider { /** * Estimate gas cost for executing the given quote - * + * * @param quote - The quote to estimate gas for * @returns Promise resolving to the estimated gas amount as a string */ @@ -89,7 +92,7 @@ export abstract class BaseIntentProvider { /** * Lifecycle hook called after a quote is generated * Override this method to add provider-specific post-quote logic - * + * * @param quote - The generated quote */ protected async onQuoteGenerated?(quote: IntentQuote): Promise; @@ -97,7 +100,7 @@ export abstract class BaseIntentProvider { /** * Lifecycle hook called after an order is submitted * Override this method to add provider-specific post-submission logic - * + * * @param order - The submitted order */ protected async onOrderSubmitted?(order: IntentOrder): Promise; @@ -105,7 +108,7 @@ export abstract class BaseIntentProvider { /** * Lifecycle hook called when an order status changes * Override this method to add provider-specific status change logic - * + * * @param order - The order with updated status * @param previousStatus - The previous status of the order */ @@ -116,7 +119,7 @@ export abstract class BaseIntentProvider { /** * Handle errors in a consistent way across providers - * + * * @param error - The original error * @param context - Context about where the error occurred * @returns A new error with provider-specific context @@ -127,6 +130,7 @@ export abstract class BaseIntentProvider { /** * Get the configuration for this provider + * @returns The provider configuration */ getConfig(): IntentProviderConfig { return { ...this.config }; @@ -134,7 +138,7 @@ export abstract class BaseIntentProvider { /** * Check if this provider supports the given chain - * + * * @param chainId - The chain ID to check * @returns True if the chain is supported */ @@ -144,7 +148,7 @@ export abstract class BaseIntentProvider { /** * Check if this provider has a specific feature - * + * * @param feature - The feature name to check * @returns True if the feature is supported */ diff --git a/packages/Intent-manager/src/providers/cowswap/constants.ts b/packages/Intent-manager/src/providers/cowswap/constants.ts index 3dd9fcd0d2f..a0b8785ce31 100644 --- a/packages/Intent-manager/src/providers/cowswap/constants.ts +++ b/packages/Intent-manager/src/providers/cowswap/constants.ts @@ -22,7 +22,8 @@ export const COW_NETWORK_PATHS: Record = { /** * Default CowSwap settlement contract address */ -export const COW_SETTLEMENT_CONTRACT = '0x9008D19f58AAbd9eD0D60971565AA8510560ab41'; +export const COW_SETTLEMENT_CONTRACT = + '0x9008D19f58AAbd9eD0D60971565AA8510560ab41'; /** * CowSwap provider configuration diff --git a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts index b33bdc942ec..a91a5bdccec 100644 --- a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts +++ b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts @@ -1,4 +1,9 @@ -import { BaseIntentProvider } from '../base-intent-provider'; +import { + COW_API_BASE, + COW_NETWORK_PATHS, + COW_SETTLEMENT_CONTRACT, + COWSWAP_PROVIDER_CONFIG, +} from './constants'; import type { IntentQuote, IntentQuoteRequest, @@ -7,12 +12,21 @@ import type { IntentOrderStatus, } from '../../types'; import { IntentOrderStatus as OrderStatus } from '../../types'; -import { - COW_API_BASE, - COW_NETWORK_PATHS, - COW_SETTLEMENT_CONTRACT, - COWSWAP_PROVIDER_CONFIG, -} from './constants'; +import { BaseIntentProvider } from '../base-intent-provider'; + +/** + * CowSwap quote response interface + */ +type CowSwapQuoteResponse = { + id?: string; + buyAmount?: string; + estimatedGas?: string; + priceImpact?: number; + feeAmount?: string; + validTo?: number; + order?: Record; + settlementContract?: string; +}; /** * CowSwap intent provider implementation @@ -230,9 +244,6 @@ export class CowSwapProvider extends BaseIntentProvider { return '21000'; } - /** - * Map CowSwap API status to our internal status enum - */ private mapCowSwapStatus(cowStatus: string): IntentOrderStatus { switch (cowStatus) { case 'presignaturePending': @@ -249,16 +260,13 @@ export class CowSwapProvider extends BaseIntentProvider { } } - /** - * Fetch quote from CowSwap API - * This is a placeholder for the actual implementation - */ private async fetchQuote( request: IntentQuoteRequest, networkPath: string, - ): Promise { + ): Promise { // TODO: Implement actual CowSwap quote API call // For now, return a mock response structure + // This logic currently was handled by the Birdge controller call our bridge API backend throw new Error('CowSwap quote fetching not yet implemented'); } } diff --git a/packages/Intent-manager/src/providers/cowswap/index.ts b/packages/Intent-manager/src/providers/cowswap/index.ts index b9e400f6882..3b30b1fa2ac 100644 --- a/packages/Intent-manager/src/providers/cowswap/index.ts +++ b/packages/Intent-manager/src/providers/cowswap/index.ts @@ -1,7 +1,7 @@ export { CowSwapProvider } from './cowswap-provider'; -export { - COW_API_BASE, - COW_NETWORK_PATHS, - COW_SETTLEMENT_CONTRACT, - COWSWAP_PROVIDER_CONFIG +export { + COW_API_BASE, + COW_NETWORK_PATHS, + COW_SETTLEMENT_CONTRACT, + COWSWAP_PROVIDER_CONFIG, } from './constants'; diff --git a/packages/bridge-status-controller/tsconfig.json b/packages/bridge-status-controller/tsconfig.json index 567f874afd2..e41150bdaf3 100644 --- a/packages/bridge-status-controller/tsconfig.json +++ b/packages/bridge-status-controller/tsconfig.json @@ -9,7 +9,6 @@ { "path": "../base-controller" }, { "path": "../bridge-controller" }, { "path": "../controller-utils" }, - { "path": "../intent-manager" }, { "path": "../network-controller" }, { "path": "../polling-controller" }, { "path": "../transaction-controller" }, diff --git a/tsconfig.json b/tsconfig.json index 3aca2850cd1..0b4d9232a85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "noEmit": true }, "references": [ + { "path": "./packages/Intent-manager" }, { "path": "./packages/account-tree-controller" }, { "path": "./packages/accounts-controller" }, { "path": "./packages/address-book-controller" }, diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 327abba7f81..f431bdddf63 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -14,6 +14,7 @@ */ "paths": { "@metamask/base-controller/next": ["../base-controller/src/next"], + "@metamask/intent-manager": ["../Intent-manager/dist/index"], "@metamask/*": ["../*/src"] }, "strict": true, diff --git a/yarn.lock b/yarn.lock index c9390a0c856..3ddf99e459e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2786,6 +2786,7 @@ __metadata: "@metamask/bridge-controller": "npm:^42.0.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/gas-fee-controller": "npm:^24.0.0" + "@metamask/intent-manager": "npm:^1.0.0" "@metamask/keyring-api": "npm:^20.1.0" "@metamask/network-controller": "npm:^24.1.0" "@metamask/polling-controller": "npm:^14.0.0" @@ -3591,14 +3592,15 @@ __metadata: languageName: unknown linkType: soft -"@metamask/intent-manager@workspace:packages/Intent-manager": +"@metamask/intent-manager@npm:^1.0.0, @metamask/intent-manager@workspace:packages/Intent-manager": version: 0.0.0-use.local resolution: "@metamask/intent-manager@workspace:packages/Intent-manager" dependencies: - "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.3.0" + "@metamask/controller-utils": "npm:^11.12.0" + "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" From 74e68936edd994789f0c474fa6c2b0a3d1f2df7b Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 10 Sep 2025 10:19:10 +0100 Subject: [PATCH 12/85] WIP: Remove the generateQuote and relative private method calls. --- packages/Intent-manager/README.md | 135 ++++-------------- packages/Intent-manager/src/index.ts | 9 +- .../Intent-manager/src/intent-manager.test.ts | 78 +--------- packages/Intent-manager/src/intent-manager.ts | 39 ----- .../src/providers/base-intent-provider.ts | 34 ----- .../src/providers/cowswap/cowswap-provider.ts | 92 +----------- packages/Intent-manager/src/types.ts | 16 --- packages/Intent-manager/src/validators.ts | 61 ++++++++ packages/Intent-manager/tsconfig.build.json | 2 +- packages/bridge-controller/package.json | 1 + packages/bridge-controller/src/index.ts | 2 +- packages/bridge-controller/src/types.ts | 2 +- .../bridge-controller/src/utils/validators.ts | 35 +---- packages/bridge-controller/tsconfig.json | 1 + .../bridge-status-controller/package.json | 2 +- .../src/bridge-status-controller.ts | 12 +- .../bridge-status-controller/tsconfig.json | 1 + tsconfig.build.json | 1 + yarn.lock | 29 +++- 19 files changed, 154 insertions(+), 398 deletions(-) create mode 100644 packages/Intent-manager/src/validators.ts diff --git a/packages/Intent-manager/README.md b/packages/Intent-manager/README.md index 50b0440e95a..5e21d34a0fe 100644 --- a/packages/Intent-manager/README.md +++ b/packages/Intent-manager/README.md @@ -2,26 +2,23 @@ # `@metamask/intent-manager` -A comprehensive intent management system for MetaMask that orchestrates cross-chain token swaps and bridging operations through multiple decentralized exchange (DEX) providers. This package provides quote aggregation, order execution, and lifecycle management for user intents. +A comprehensive intent management system for MetaMask that orchestrates cross-chain token swaps and bridging operations through multiple decentralized exchange (DEX) providers. This package provides order execution and lifecycle management for user intents. ## Overview The Intent Manager provides a unified interface for: -- **Multi-Provider Quote Aggregation**: Get quotes from multiple DEX providers (CowSwap, 1inch, etc.) - **Cross-Chain Operations**: Support for token swaps and bridging across different blockchain networks -- **Order Lifecycle Management**: Handle quote generation, order submission, execution tracking, and status monitoring +- **Order Lifecycle Management**: Handle order submission, execution tracking, and status monitoring - **Provider Management**: Pluggable architecture for adding new DEX providers - **State Management**: Track intent orders and maintain execution history ## Key Features -- 🔄 **Cross-chain token swaps** with automatic best-rate selection +- 🔄 **Cross-chain token swaps** through multiple providers - 🏪 **Multi-provider support** with extensible provider architecture - 📊 **Real-time order tracking** and status updates -- ⚡ **Gas estimation** and fee calculation -- 🛡️ **Slippage protection** and validation -- 📈 **Price impact analysis** for informed decision making +- 🛡️ **Order lifecycle management** with comprehensive error handling ## Installation @@ -39,38 +36,32 @@ npm install @metamask/intent-manager ```typescript import { IntentManager } from '@metamask/intent-manager'; -import type { IntentQuoteRequest } from '@metamask/intent-manager'; +import type { IntentSubmissionParams } from '@metamask/intent-manager'; // Initialize the intent manager const intentManager = new IntentManager(); -// Request quotes for a cross-chain swap -const quoteRequest: IntentQuoteRequest = { - srcChainId: 1, // Ethereum Mainnet - destChainId: 42161, // Arbitrum One - srcTokenAddress: '0xA0b86a33E6441e6e80D0c4C6C7527d72', // USDC - destTokenAddress: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH - amount: '1000000000000000000', // 1 token (18 decimals) +// Submit an intent order (order data would come from backend) +const submissionParams: IntentSubmissionParams = { + providerName: 'cowswap', + chainId: 1, // Ethereum Mainnet + orderData: { + sellToken: '0xA0b86a33E6441e6e80D0c4C6C7527d72', // USDC + buyToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH + sellAmount: '1000000000000000000', // 1 token (18 decimals) + // ... other order data from backend + }, + signature: '0x...', // User signature userAddress: '0x742d35Cc6634C0532925a3b8D4C9db96', - slippage: 0.005, // 0.5% slippage tolerance }; -// Get quotes from all available providers -const quotes = await intentManager.generateQuotes(quoteRequest); - -// Submit the best quote -const bestQuote = quotes[0]; // Quotes are sorted by best rate -const order = await intentManager.submitIntent({ - quote: bestQuote, - signature: '0x...', // User signature - userAddress: quoteRequest.userAddress, -}); +const order = await intentManager.submitIntent(submissionParams); // Monitor order status const status = await intentManager.getOrderStatus( order.id, - bestQuote.provider, - quoteRequest.srcChainId + 'cowswap', + 1 ); ``` @@ -88,28 +79,17 @@ new IntentManager(initialState?: Partial) #### Core Methods -##### `generateQuotes(request, criteria?): Promise` - -Generates quotes from available providers for a given request. - -```typescript -const quotes = await intentManager.generateQuotes({ - srcChainId: 1, - destChainId: 42161, - srcTokenAddress: '0x...', - destTokenAddress: '0x...', - amount: '1000000000000000000', - userAddress: '0x...', -}); -``` - ##### `submitIntent(params): Promise` -Submits an intent order based on a selected quote. +Submits an intent order with the provided order data. ```typescript const order = await intentManager.submitIntent({ - quote: selectedQuote, + providerName: 'cowswap', + chainId: 1, + orderData: { + // Order data from backend + }, signature: '0x...', userAddress: '0x...', }); @@ -172,40 +152,7 @@ const providers = intentManager.getAvailableProviders({ ## Core Types -### IntentQuoteRequest - -Parameters for requesting quotes from providers. - -```typescript -type IntentQuoteRequest = { - srcChainId: number; - destChainId: number; - srcTokenAddress: string; - destTokenAddress: string; - amount: string; - userAddress: string; - slippage?: number; -}; -``` - -### IntentQuote - -Quote response from a provider. -```typescript -type IntentQuote = { - id: string; - provider: string; - srcAmount: string; - destAmount: string; - estimatedGas: string; - estimatedTime: number; // seconds - priceImpact: number; - fees: IntentFee[]; - validUntil: number; // timestamp - metadata: Record; -}; -``` ### IntentOrder @@ -285,10 +232,6 @@ class CustomProvider extends BaseIntentProvider { return [1, 42161]; // Ethereum and Arbitrum } - async generateQuote(request: IntentQuoteRequest): Promise { - // Implement quote generation logic - } - async submitOrder(params: IntentSubmissionParams): Promise { // Implement order submission logic } @@ -297,7 +240,9 @@ class CustomProvider extends BaseIntentProvider { // Implement status checking logic } - // ... implement other required methods + async cancelOrder(orderId: string, chainId: number): Promise { + // Implement order cancellation logic + } } // Register the custom provider @@ -311,12 +256,12 @@ The Intent Manager provides comprehensive error handling: ```typescript try { - const quotes = await intentManager.generateQuotes(request); + const order = await intentManager.submitIntent(submissionParams); } catch (error) { if (error.message.includes('Unsupported chain')) { // Handle unsupported chain error - } else if (error.message.includes('Insufficient liquidity')) { - // Handle liquidity error + } else if (error.message.includes('Provider not found')) { + // Handle provider error } else { // Handle other errors } @@ -325,25 +270,7 @@ try { ## Best Practices -### Quote Selection -- **Compare Multiple Quotes**: Always request quotes from multiple providers -- **Consider Total Cost**: Factor in gas fees, protocol fees, and price impact -- **Check Validity**: Ensure quotes haven't expired before submission - -```typescript -const quotes = await intentManager.generateQuotes(request); - -// Filter valid quotes -const validQuotes = quotes.filter(quote => quote.validUntil > Date.now()); - -// Sort by best net amount (considering fees) -const sortedQuotes = validQuotes.sort((a, b) => { - const aNet = BigInt(a.destAmount) - BigInt(a.estimatedGas); - const bNet = BigInt(b.destAmount) - BigInt(b.estimatedGas); - return bNet > aNet ? 1 : -1; -}); -``` ### Order Monitoring diff --git a/packages/Intent-manager/src/index.ts b/packages/Intent-manager/src/index.ts index ba3914eb22c..0aac6f193da 100644 --- a/packages/Intent-manager/src/index.ts +++ b/packages/Intent-manager/src/index.ts @@ -12,8 +12,6 @@ export { export { IntentStatus, IntentOrderStatus } from './types'; export type { - IntentQuoteRequest, - IntentQuote, IntentOrder, IntentFee, IntentSubmissionParams, @@ -28,3 +26,10 @@ export type { IntentManagerState } from './intent-manager'; // Export providers export * from './providers'; + +// Export validators and schemas +export { IntentOrderSchema, IntentSchema } from './validators'; +export type { + IntentOrder as IntentOrderType, + Intent as IntentType, +} from './validators'; diff --git a/packages/Intent-manager/src/intent-manager.test.ts b/packages/Intent-manager/src/intent-manager.test.ts index 1808e9fdbd6..ffe7e1fa792 100644 --- a/packages/Intent-manager/src/intent-manager.test.ts +++ b/packages/Intent-manager/src/intent-manager.test.ts @@ -2,10 +2,8 @@ import { IntentManager } from './intent-manager'; import type { - IntentQuote, IntentSubmissionParams, BaseIntentProvider, - IntentQuoteRequest, IntentOrder, } from './types'; import { IntentOrderStatus } from './types'; @@ -25,72 +23,16 @@ describe('IntentManager', () => { }); }); - describe('generateQuotes', () => { - it('should generate quotes using the provider manager', async () => { - const quoteRequest: IntentQuoteRequest = { - srcChainId: 1, - destChainId: 42161, - srcTokenAddress: '0xA0b86a33E6441e6e80D0c4C6C7527d72', - destTokenAddress: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', - amount: '1000000000000000000', - userAddress: '0x123', - }; - - const mockQuotes: IntentQuote[] = [ - { - id: 'test-quote-1', - provider: 'cowswap', - srcAmount: '1000000000000000000', - destAmount: '900000000000000000', - estimatedGas: '21000', - estimatedTime: 300, - priceImpact: 0.01, - fees: [], - validUntil: Date.now() + 300000, - metadata: {}, - }, - ]; - - // Create a complete mock provider that implements BaseIntentProvider - const mockProvider: BaseIntentProvider = { - getName: () => 'cowswap', - getVersion: () => '1.0.0', - getSupportedChains: () => [1, 42161], - validateQuoteRequest: jest.fn().mockResolvedValue(true), - generateQuote: jest.fn().mockResolvedValue(mockQuotes[0]), - submitOrder: jest.fn(), - getOrderStatus: jest.fn(), - cancelOrder: jest.fn(), - estimateGas: jest.fn(), - }; - - // Register the mock provider - intentManager.registerProvider(mockProvider); - - const result = await intentManager.generateQuotes(quoteRequest); - - expect(result).toStrictEqual(mockQuotes); - expect(mockProvider.generateQuote).toHaveBeenCalledWith(quoteRequest); - }); - }); - describe('submitIntent', () => { it('should submit an intent and return an order', async () => { - const mockQuote: IntentQuote = { - id: 'test-quote-1', - provider: 'cowswap', - srcAmount: '1000000000000000000', - destAmount: '900000000000000000', - estimatedGas: '21000', - estimatedTime: 300, - priceImpact: 0.01, - fees: [], - validUntil: Date.now() + 300000, - metadata: {}, - }; - const submissionParams: IntentSubmissionParams = { - quote: mockQuote, + providerName: 'cowswap', + chainId: 1, + orderData: { + sellToken: '0xA0b86a33E6441e6e80D0c4C6C7527d72', + buyToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + sellAmount: '1000000000000000000', + }, signature: '0xsignature', userAddress: '0x123', }; @@ -109,12 +51,9 @@ describe('IntentManager', () => { getName: () => 'cowswap', getVersion: () => '1.0.0', getSupportedChains: () => [1, 42161], - validateQuoteRequest: jest.fn(), - generateQuote: jest.fn(), submitOrder: jest.fn().mockResolvedValue(mockOrder), getOrderStatus: jest.fn(), cancelOrder: jest.fn(), - estimateGas: jest.fn(), }; // Register the mock provider @@ -147,12 +86,9 @@ describe('IntentManager', () => { getName: () => 'cowswap', getVersion: () => '1.0.0', getSupportedChains: () => [1, 42161], - validateQuoteRequest: jest.fn(), - generateQuote: jest.fn(), submitOrder: jest.fn(), getOrderStatus: jest.fn().mockResolvedValue(mockOrder), cancelOrder: jest.fn(), - estimateGas: jest.fn(), }; // Register the mock provider diff --git a/packages/Intent-manager/src/intent-manager.ts b/packages/Intent-manager/src/intent-manager.ts index 18d11b226ba..437d2055249 100644 --- a/packages/Intent-manager/src/intent-manager.ts +++ b/packages/Intent-manager/src/intent-manager.ts @@ -3,8 +3,6 @@ import { CowSwapProvider } from './providers/cowswap'; import type { BaseIntentProvider, IntentOrder, - IntentQuote, - IntentQuoteRequest, IntentSubmissionParams, ProviderSelectionCriteria, ProviderRegistry, @@ -142,43 +140,6 @@ export class IntentManager { return availableProviders; } - /** - * Generate quotes from multiple providers - * - * @param request - The quote request parameters - * @param criteria - Optional criteria for provider selection - * @returns Array of quotes sorted by best rate (highest destAmount) - */ - async generateQuotes( - request: IntentQuoteRequest, - criteria?: ProviderSelectionCriteria, - ): Promise { - const providers = this.getAvailableProviders(criteria); - const quotes: IntentQuote[] = []; - - await Promise.allSettled( - providers.map(async (provider) => { - try { - const isValid = await provider.validateQuoteRequest(request); - if (isValid) { - const quote = await provider.generateQuote(request); - quotes.push(quote); - } - } catch (error) { - console.warn( - `Failed to get quote from ${provider.getName()}:`, - error, - ); - } - }), - ); - - // Sort by best rate (highest destination amount) - return quotes.sort( - (a, b) => parseFloat(b.destAmount) - parseFloat(a.destAmount), - ); - } - async submitIntent(params: IntentSubmissionParams): Promise { const provider = this.providers[params.quote.provider]; if (!provider) { diff --git a/packages/Intent-manager/src/providers/base-intent-provider.ts b/packages/Intent-manager/src/providers/base-intent-provider.ts index 8d61bde50ca..4a179463933 100644 --- a/packages/Intent-manager/src/providers/base-intent-provider.ts +++ b/packages/Intent-manager/src/providers/base-intent-provider.ts @@ -1,6 +1,4 @@ import type { - IntentQuoteRequest, - IntentQuote, IntentOrder, IntentSubmissionParams, IntentProviderConfig, @@ -36,14 +34,6 @@ export abstract class BaseIntentProvider { */ abstract getSupportedChains(): number[]; - /** - * Generate a quote for the given request - * - * @param request - The quote request parameters - * @returns Promise resolving to an intent quote - */ - abstract generateQuote(request: IntentQuoteRequest): Promise; - /** * Submit an order based on the quote and signature * @@ -73,30 +63,6 @@ export abstract class BaseIntentProvider { */ abstract cancelOrder(orderId: string, chainId: number): Promise; - /** - * Validate that a quote request is valid for this provider - * - * @param request - The quote request to validate - * @returns Promise resolving to true if the request is valid - */ - abstract validateQuoteRequest(request: IntentQuoteRequest): Promise; - - /** - * Estimate gas cost for executing the given quote - * - * @param quote - The quote to estimate gas for - * @returns Promise resolving to the estimated gas amount as a string - */ - abstract estimateGas(quote: IntentQuote): Promise; - - /** - * Lifecycle hook called after a quote is generated - * Override this method to add provider-specific post-quote logic - * - * @param quote - The generated quote - */ - protected async onQuoteGenerated?(quote: IntentQuote): Promise; - /** * Lifecycle hook called after an order is submitted * Override this method to add provider-specific post-submission logic diff --git a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts index a91a5bdccec..c75842ff571 100644 --- a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts +++ b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts @@ -1,12 +1,9 @@ import { COW_API_BASE, COW_NETWORK_PATHS, - COW_SETTLEMENT_CONTRACT, COWSWAP_PROVIDER_CONFIG, } from './constants'; import type { - IntentQuote, - IntentQuoteRequest, IntentOrder, IntentSubmissionParams, IntentOrderStatus, @@ -14,24 +11,10 @@ import type { import { IntentOrderStatus as OrderStatus } from '../../types'; import { BaseIntentProvider } from '../base-intent-provider'; -/** - * CowSwap quote response interface - */ -type CowSwapQuoteResponse = { - id?: string; - buyAmount?: string; - estimatedGas?: string; - priceImpact?: number; - feeAmount?: string; - validTo?: number; - order?: Record; - settlementContract?: string; -}; - /** * CowSwap intent provider implementation * - * Handles quote generation, order submission, and status polling for CowSwap intents. + * Handles order submission and status polling for CowSwap intents. * Based on the existing CowSwap integration logic from bridge-status-controller. */ export class CowSwapProvider extends BaseIntentProvider { @@ -51,52 +34,6 @@ export class CowSwapProvider extends BaseIntentProvider { return Object.keys(COW_NETWORK_PATHS).map(Number); } - async generateQuote(request: IntentQuoteRequest): Promise { - const networkPath = COW_NETWORK_PATHS[request.srcChainId]; - if (!networkPath) { - throw this.handleError( - new Error(`Unsupported chain: ${request.srcChainId}`), - 'generateQuote', - ); - } - - try { - // Implementation for CowSwap quote generation - // This would call the actual CowSwap API - const response = await this.fetchQuote(request, networkPath); - - const quote: IntentQuote = { - id: response.id || `cow-${Date.now()}`, - provider: this.getName(), - srcAmount: request.amount, - destAmount: response.buyAmount || '0', - estimatedGas: response.estimatedGas || '21000', - estimatedTime: 300, // 5 minutes typical for CowSwap - priceImpact: response.priceImpact || 0, - fees: [ - { - type: 'protocol', - amount: response.feeAmount || '0', - token: request.srcTokenAddress, - }, - ], - validUntil: response.validTo || Date.now() + 300000, // 5 minutes from now - metadata: { - order: response.order, - settlementContract: - response.settlementContract || COW_SETTLEMENT_CONTRACT, - chainId: request.srcChainId, - networkPath, - }, - }; - - await this.onQuoteGenerated?.(quote); - return quote; - } catch (error) { - throw this.handleError(error as Error, 'generateQuote'); - } - } - async submitOrder(params: IntentSubmissionParams): Promise { const chainId = params.quote.metadata.chainId as number; const networkPath = COW_NETWORK_PATHS[chainId]; @@ -227,23 +164,6 @@ export class CowSwapProvider extends BaseIntentProvider { return false; } - async validateQuoteRequest(request: IntentQuoteRequest): Promise { - // Basic validation - check if chain is supported - if (!this.getSupportedChains().includes(request.srcChainId)) { - return false; - } - - // Additional validation could be added here - // e.g., token address validation, amount validation, etc. - return true; - } - - async estimateGas(quote: IntentQuote): Promise { - // CowSwap uses meta-transactions, so gas estimation is minimal - // The actual settlement is handled by solvers - return '21000'; - } - private mapCowSwapStatus(cowStatus: string): IntentOrderStatus { switch (cowStatus) { case 'presignaturePending': @@ -259,14 +179,4 @@ export class CowSwapProvider extends BaseIntentProvider { return OrderStatus.FAILED; } } - - private async fetchQuote( - request: IntentQuoteRequest, - networkPath: string, - ): Promise { - // TODO: Implement actual CowSwap quote API call - // For now, return a mock response structure - // This logic currently was handled by the Birdge controller call our bridge API backend - throw new Error('CowSwap quote fetching not yet implemented'); - } } diff --git a/packages/Intent-manager/src/types.ts b/packages/Intent-manager/src/types.ts index 3e75db5fe8c..1e79ee341ba 100644 --- a/packages/Intent-manager/src/types.ts +++ b/packages/Intent-manager/src/types.ts @@ -22,19 +22,6 @@ export enum IntentOrderStatus { EXPIRED = 'expired', } -/** - * Request parameters for generating an intent quote - */ -export type IntentQuoteRequest = { - srcChainId: number; - destChainId: number; - srcTokenAddress: string; - destTokenAddress: string; - amount: string; - userAddress: string; - slippage?: number; -}; - /** * Fee information for an intent */ @@ -121,10 +108,7 @@ export type BaseIntentProvider = { getName(): string; getVersion(): string; getSupportedChains(): number[]; - generateQuote(request: IntentQuoteRequest): Promise; submitOrder(params: IntentSubmissionParams): Promise; getOrderStatus(orderId: string, chainId: number): Promise; cancelOrder(orderId: string, chainId: number): Promise; - validateQuoteRequest(request: IntentQuoteRequest): Promise; - estimateGas(quote: IntentQuote): Promise; }; diff --git a/packages/Intent-manager/src/validators.ts b/packages/Intent-manager/src/validators.ts new file mode 100644 index 00000000000..fcb479444c1 --- /dev/null +++ b/packages/Intent-manager/src/validators.ts @@ -0,0 +1,61 @@ +import { isValidHexAddress } from '@metamask/controller-utils'; +import type { Infer } from '@metamask/superstruct'; +import { + string, + boolean, + number, + type, + optional, + enums, + define, + union, + pattern, +} from '@metamask/superstruct'; +import { isStrictHexString } from '@metamask/utils'; + +// Helper schemas for Intent validation +const HexAddressSchema = define('HexAddress', (v: unknown) => + isValidHexAddress(v as string, { allowNonPrefixed: false }), +); + +const HexStringSchema = define('HexString', (v: unknown) => + isStrictHexString(v as string), +); + +const TruthyDigitStringSchema = pattern(string(), /^\d+$/u); + +// Allow digit strings for amounts/validTo for flexibility across providers +const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); + +// Intent support (e.g., CoW Swap EIP-712 order signing) +const IntentProtocolSchema = enums(['cowswap']); + +export const IntentOrderSchema = type({ + // EIP-712 Order fields (subset required for signing/submission) + sellToken: HexAddressSchema, + buyToken: HexAddressSchema, + receiver: optional(HexAddressSchema), + validTo: DigitStringOrNumberSchema, + appData: string(), + appDataHash: HexStringSchema, + feeAmount: TruthyDigitStringSchema, + kind: enums(['sell', 'buy']), + partiallyFillable: boolean(), + // One of these is required by CoW depending on kind; we keep both optional here and rely on backend validation + sellAmount: optional(TruthyDigitStringSchema), + buyAmount: optional(TruthyDigitStringSchema), + // Optional owner/from for convenience when building domain/message + from: optional(HexAddressSchema), +}); + +export const IntentSchema = type({ + protocol: IntentProtocolSchema, + order: IntentOrderSchema, + // Optional metadata to aid submission/routing + settlementContract: optional(HexAddressSchema), + relayer: optional(HexAddressSchema), +}); + +// Export types for use in other modules +export type IntentOrder = Infer; +export type Intent = Infer; diff --git a/packages/Intent-manager/tsconfig.build.json b/packages/Intent-manager/tsconfig.build.json index 02a0eea03fe..40ae7f7c4db 100644 --- a/packages/Intent-manager/tsconfig.build.json +++ b/packages/Intent-manager/tsconfig.build.json @@ -5,6 +5,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "references": [], + "references": [{ "path": "../controller-utils/tsconfig.build.json" }], "include": ["../../types", "./src"] } diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 59a1272f347..e21d8826acd 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -55,6 +55,7 @@ "@metamask/base-controller": "^8.3.0", "@metamask/controller-utils": "^11.12.0", "@metamask/gas-fee-controller": "^24.0.0", + "@metamask/intent-manager": "file:../Intent-manager", "@metamask/keyring-api": "^20.1.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/multichain-network-controller": "^0.12.0", diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index b4fcff1004e..01ec5418fce 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -41,7 +41,7 @@ export type { FeeData, TxData, Intent, - CowSwapOrderLike, + IntentOrderLike, BridgeControllerState, BridgeControllerAction, BridgeControllerActions, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 2355be15a18..11ed797ac27 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -257,7 +257,7 @@ export type Quote = Infer; export type TxData = Infer; export type Intent = Infer; -export type CowSwapOrderLike = Intent['order']; +export type IntentOrderLike = Intent['order']; /** * This is the type for the quote response from the bridge-api * TxDataType can be overriden to be a string when the quote is non-evm diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index c0aeb494e13..ff0f7395762 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -1,4 +1,5 @@ import { isValidHexAddress } from '@metamask/controller-utils'; +import { IntentSchema } from '@metamask/intent-manager'; import type { Infer } from '@metamask/superstruct'; import { string, @@ -153,38 +154,8 @@ export const StepSchema = type({ const RefuelDataSchema = StepSchema; -// Allow digit strings for amounts/validTo for flexibility across providers -const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); - -// Intent support (e.g., CoW Swap EIP-712 order signing) -const IntentProtocolSchema = enums(['cowswap']); - -export const CowSwapOrderSchema = type({ - // EIP-712 Order fields (subset required for signing/submission) - sellToken: HexAddressSchema, - buyToken: HexAddressSchema, - receiver: optional(HexAddressSchema), - validTo: DigitStringOrNumberSchema, - appData: string(), - appDataHash: HexStringSchema, - feeAmount: TruthyDigitStringSchema, - kind: enums(['sell', 'buy']), - partiallyFillable: boolean(), - // One of these is required by CoW depending on kind; we keep both optional here and rely on backend validation - sellAmount: optional(TruthyDigitStringSchema), - buyAmount: optional(TruthyDigitStringSchema), - // Optional owner/from for convenience when building domain/message - from: optional(HexAddressSchema), -}); - -export const IntentSchema = type({ - protocol: IntentProtocolSchema, - order: CowSwapOrderSchema, - // Optional metadata to aid submission/routing - settlementContract: optional(HexAddressSchema), - relayer: optional(HexAddressSchema), - quoteId: optional(nullable(string())), -}); +// Re-export IntentSchema from intent-manager for backward compatibility +export { IntentSchema }; export const QuoteSchema = type({ requestId: string(), diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json index 861f0ab721c..505e07ff250 100644 --- a/packages/bridge-controller/tsconfig.json +++ b/packages/bridge-controller/tsconfig.json @@ -6,6 +6,7 @@ }, "references": [ { "path": "../accounts-controller" }, + { "path": "../intent-manager" }, { "path": "../base-controller" }, { "path": "../controller-utils" }, { "path": "../network-controller" }, diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index 99c7ef099d3..bef5d279e65 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -49,7 +49,7 @@ "dependencies": { "@metamask/base-controller": "^8.3.0", "@metamask/controller-utils": "^11.12.0", - "@metamask/intent-manager": "^1.0.0", + "@metamask/intent-manager": "file:../Intent-manager", "@metamask/keyring-api": "^20.1.0", "@metamask/polling-controller": "^14.0.0", "@metamask/superstruct": "^3.1.0", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 269013f0e6c..fc7de39ccb0 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -19,7 +19,11 @@ import { } from '@metamask/bridge-controller'; import type { TraceCallback } from '@metamask/controller-utils'; import { toHex } from '@metamask/controller-utils'; -import type { IntentManager, IntentOrder } from '@metamask/intent-manager'; +import type { + IntentManager, + IntentOrder, + IntentSubmissionParams, +} from '@metamask/intent-manager'; import { IntentOrderStatus } from '@metamask/intent-manager'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { @@ -1538,11 +1542,13 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Wed, 24 Sep 2025 11:02:10 +0300 Subject: [PATCH 13/85] feat: add a temp onlyIntent parameter. --- packages/bridge-controller/src/utils/fetch.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 625c2998842..41461f9a2b8 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -110,6 +110,10 @@ export async function fetchBridgeQuotes( Object.entries(normalizedRequest).forEach(([key, value]) => { queryParams.append(key, value.toString()); }); + + // TODO remove this flag after testing to return both quotes and intents quotes + queryParams.append('onlyIntent', 'true'); + const url = `${bridgeApiBaseUrl}/getQuote?${queryParams}`; const quotes: unknown[] = await fetchFn(url, { headers: getClientIdHeader(clientId), From 018cd69c3435ba87ec4b5c109116ff40b1c17722 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 14:59:57 +0300 Subject: [PATCH 14/85] rebase from main --- packages/bridge-controller/src/utils/fetch.ts | 1 + yarn.lock | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 41461f9a2b8..118a2747bf9 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -113,6 +113,7 @@ export async function fetchBridgeQuotes( // TODO remove this flag after testing to return both quotes and intents quotes queryParams.append('onlyIntent', 'true'); + queryParams.append('intent', 'true'); const url = `${bridgeApiBaseUrl}/getQuote?${queryParams}`; const quotes: unknown[] = await fetchFn(url, { diff --git a/yarn.lock b/yarn.lock index 4c2a93a1ace..2f1bb28f546 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2888,7 +2888,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.14.0, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.12.0, @metamask/controller-utils@npm:^11.14.0, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3625,6 +3625,7 @@ __metadata: "@metamask/snaps-controllers": ^14.0.1 languageName: unknown linkType: soft + "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=0af8ad&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" @@ -3668,7 +3669,7 @@ __metadata: typescript: "npm:~5.2.2" languageName: unknown linkType: soft - + "@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" @@ -3725,6 +3726,18 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^20.1.0": + version: 20.1.1 + resolution: "@metamask/keyring-api@npm:20.1.1" + dependencies: + "@metamask/keyring-utils": "npm:^3.1.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + bitcoin-address-validation: "npm:^2.2.3" + checksum: 10/f261205687c9aa1c39a2221e30a6edd4f530d2b0afd86a8ab13ea23d2af2a0bf1c89a8c4ad5646368e3b06909613d64195c335063319edd018636c9fa6ee63ee + languageName: node + linkType: hard + "@metamask/keyring-api@npm:^21.0.0": version: 21.0.0 resolution: "@metamask/keyring-api@npm:21.0.0" @@ -4902,7 +4915,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.8.0": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.0": version: 11.8.0 resolution: "@metamask/utils@npm:11.8.0" dependencies: From fdf4f04dde9243feb3534e350724dd6f98917423 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 15:00:40 +0300 Subject: [PATCH 15/85] Rebase from main --- packages/bridge-status-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index 06594fab45b..3df9da26b4c 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -47,9 +47,9 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/intent-manager": "file:../Intent-manager", "@metamask/base-controller": "^8.4.0", "@metamask/controller-utils": "^11.14.0", + "@metamask/intent-manager": "file:../Intent-manager", "@metamask/keyring-api": "^21.0.0", "@metamask/polling-controller": "^14.0.0", "@metamask/superstruct": "^3.1.0", From 4e200c774c33a201278dec55bb38da275a0a0758 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 15:31:56 +0300 Subject: [PATCH 16/85] revert back to main version --- yarn.lock | 64 +++---------------------------------------------------- 1 file changed, 3 insertions(+), 61 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2f1bb28f546..07e9b555049 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2739,8 +2739,7 @@ __metadata: "@metamask/controller-utils": "npm:^11.14.0" "@metamask/eth-json-rpc-provider": "npm:^5.0.0" "@metamask/gas-fee-controller": "npm:^24.0.0" - "@metamask/intent-manager": "file:../Intent-manager" - "@metamask/keyring-api": "npm:^20.1.0" + "@metamask/keyring-api": "npm:^21.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/multichain-network-controller": "npm:^1.0.0" "@metamask/network-controller": "npm:^24.2.0" @@ -2783,7 +2782,6 @@ __metadata: "@metamask/bridge-controller": "npm:^44.0.1" "@metamask/controller-utils": "npm:^11.14.0" "@metamask/gas-fee-controller": "npm:^24.0.0" - "@metamask/intent-manager": "file:../Intent-manager" "@metamask/keyring-api": "npm:^21.0.0" "@metamask/network-controller": "npm:^24.2.0" "@metamask/polling-controller": "npm:^14.0.0" @@ -2888,7 +2886,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.12.0, @metamask/controller-utils@npm:^11.14.0, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.14.0, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3626,50 +3624,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": - version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=0af8ad&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" - dependencies: - "@metamask/base-controller": "npm:^8.3.0" - "@metamask/controller-utils": "npm:^11.12.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.4.2" - checksum: 10/effe87983e84fcbbd662f5794c0835565ba880a21ba7cbf795ba43669729e6713f7e978b0627da86ab68ea61391940cb3fc1a4b64624ec031c12a7e8b1905420 - languageName: node - linkType: hard - -"@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": - version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=0af8ad&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" - dependencies: - "@metamask/base-controller": "npm:^8.3.0" - "@metamask/controller-utils": "npm:^11.12.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.4.2" - checksum: 10/effe87983e84fcbbd662f5794c0835565ba880a21ba7cbf795ba43669729e6713f7e978b0627da86ab68ea61391940cb3fc1a4b64624ec031c12a7e8b1905420 - languageName: node - linkType: hard - -"@metamask/intent-manager@workspace:packages/Intent-manager": - version: 0.0.0-use.local - resolution: "@metamask/intent-manager@workspace:packages/Intent-manager" - dependencies: - "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^8.3.0" - "@metamask/controller-utils": "npm:^11.12.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.4.2" - "@types/jest": "npm:^27.4.1" - deepmerge: "npm:^4.2.2" - jest: "npm:^27.5.1" - jest-environment-jsdom: "npm:^27.5.1" - ts-jest: "npm:^27.1.4" - typedoc: "npm:^0.24.8" - typedoc-plugin-missing-exports: "npm:^2.0.0" - typescript: "npm:~5.2.2" - languageName: unknown - linkType: soft - "@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" @@ -3726,18 +3680,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^20.1.0": - version: 20.1.1 - resolution: "@metamask/keyring-api@npm:20.1.1" - dependencies: - "@metamask/keyring-utils": "npm:^3.1.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.1.0" - bitcoin-address-validation: "npm:^2.2.3" - checksum: 10/f261205687c9aa1c39a2221e30a6edd4f530d2b0afd86a8ab13ea23d2af2a0bf1c89a8c4ad5646368e3b06909613d64195c335063319edd018636c9fa6ee63ee - languageName: node - linkType: hard - "@metamask/keyring-api@npm:^21.0.0": version: 21.0.0 resolution: "@metamask/keyring-api@npm:21.0.0" @@ -4915,7 +4857,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.0": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.8.0": version: 11.8.0 resolution: "@metamask/utils@npm:11.8.0" dependencies: From 83a36c1917f48e35b682438f683d88ff22b167e3 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 15:34:04 +0300 Subject: [PATCH 17/85] update lock file --- yarn.lock | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 07e9b555049..43e34f5c488 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2739,7 +2739,8 @@ __metadata: "@metamask/controller-utils": "npm:^11.14.0" "@metamask/eth-json-rpc-provider": "npm:^5.0.0" "@metamask/gas-fee-controller": "npm:^24.0.0" - "@metamask/keyring-api": "npm:^21.0.0" + "@metamask/intent-manager": "file:../Intent-manager" + "@metamask/keyring-api": "npm:^20.1.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/multichain-network-controller": "npm:^1.0.0" "@metamask/network-controller": "npm:^24.2.0" @@ -2782,6 +2783,7 @@ __metadata: "@metamask/bridge-controller": "npm:^44.0.1" "@metamask/controller-utils": "npm:^11.14.0" "@metamask/gas-fee-controller": "npm:^24.0.0" + "@metamask/intent-manager": "file:../Intent-manager" "@metamask/keyring-api": "npm:^21.0.0" "@metamask/network-controller": "npm:^24.2.0" "@metamask/polling-controller": "npm:^14.0.0" @@ -2886,7 +2888,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.14.0, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.12.0, @metamask/controller-utils@npm:^11.14.0, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3624,6 +3626,50 @@ __metadata: languageName: unknown linkType: soft +"@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": + version: 1.0.0 + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=f1cd90&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + dependencies: + "@metamask/base-controller": "npm:^8.3.0" + "@metamask/controller-utils": "npm:^11.12.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.4.2" + checksum: 10/cdc87ae1c9b46f7ab3914f1ca4a3b1681a83a799dbaeaf55ccec83621a51bb21834a25b97e50c1dcdfe5749035e480f9adfca4cef1eed3f0ac8db9c5140c757d + languageName: node + linkType: hard + +"@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": + version: 1.0.0 + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=f1cd90&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + dependencies: + "@metamask/base-controller": "npm:^8.3.0" + "@metamask/controller-utils": "npm:^11.12.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.4.2" + checksum: 10/cdc87ae1c9b46f7ab3914f1ca4a3b1681a83a799dbaeaf55ccec83621a51bb21834a25b97e50c1dcdfe5749035e480f9adfca4cef1eed3f0ac8db9c5140c757d + languageName: node + linkType: hard + +"@metamask/intent-manager@workspace:packages/Intent-manager": + version: 0.0.0-use.local + resolution: "@metamask/intent-manager@workspace:packages/Intent-manager" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.3.0" + "@metamask/controller-utils": "npm:^11.12.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.4.2" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.1" + jest-environment-jsdom: "npm:^27.5.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + languageName: unknown + linkType: soft + "@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" @@ -3680,6 +3726,18 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^20.1.0": + version: 20.1.1 + resolution: "@metamask/keyring-api@npm:20.1.1" + dependencies: + "@metamask/keyring-utils": "npm:^3.1.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + bitcoin-address-validation: "npm:^2.2.3" + checksum: 10/f261205687c9aa1c39a2221e30a6edd4f530d2b0afd86a8ab13ea23d2af2a0bf1c89a8c4ad5646368e3b06909613d64195c335063319edd018636c9fa6ee63ee + languageName: node + linkType: hard + "@metamask/keyring-api@npm:^21.0.0": version: 21.0.0 resolution: "@metamask/keyring-api@npm:21.0.0" @@ -4876,6 +4934,25 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^11.4.2": + version: 11.8.1 + resolution: "@metamask/utils@npm:11.8.1" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + "@types/lodash": "npm:^4.17.20" + debug: "npm:^4.3.4" + lodash: "npm:^4.17.21" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/efd3aab7f86b4a74d396cf1d5fc76e748ff78906802fdc15ec9ce2d1a9bd6b035e8e036ea93eb6b9ea33782c70adb9000772eb7a5e0164e8e9e2ebb077dca3ab + languageName: node + linkType: hard + "@metamask/utils@npm:^8.2.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" From 6a6ee79a455a403cdfc3e60760dd6e739ba2deb3 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 15:55:12 +0300 Subject: [PATCH 18/85] update lock file to fix the pipeline failure --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 43e34f5c488..2957f9a0675 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,25 +3628,25 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=f1cd90&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=888849&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/cdc87ae1c9b46f7ab3914f1ca4a3b1681a83a799dbaeaf55ccec83621a51bb21834a25b97e50c1dcdfe5749035e480f9adfca4cef1eed3f0ac8db9c5140c757d + checksum: 10/00e33da873f02bc35d3d98d59a0c949d0e204921f590576926837de5a5ca3bfd7fcd1647fd10d6970a78ba8abbbca5ddb3fa16c585962cf14b637edf477a35f5 languageName: node linkType: hard "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=f1cd90&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=888849&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/cdc87ae1c9b46f7ab3914f1ca4a3b1681a83a799dbaeaf55ccec83621a51bb21834a25b97e50c1dcdfe5749035e480f9adfca4cef1eed3f0ac8db9c5140c757d + checksum: 10/00e33da873f02bc35d3d98d59a0c949d0e204921f590576926837de5a5ca3bfd7fcd1647fd10d6970a78ba8abbbca5ddb3fa16c585962cf14b637edf477a35f5 languageName: node linkType: hard From 7516d3bff9a3822e2651a3fd60aab35447505085 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 16:31:01 +0300 Subject: [PATCH 19/85] fix unit tests and pipeline issue. --- .../Intent-manager/src/intent-manager.test.ts | 20 +++++++++---- yarn.lock | 29 ++++--------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/packages/Intent-manager/src/intent-manager.test.ts b/packages/Intent-manager/src/intent-manager.test.ts index ffe7e1fa792..df7ffe26e33 100644 --- a/packages/Intent-manager/src/intent-manager.test.ts +++ b/packages/Intent-manager/src/intent-manager.test.ts @@ -26,12 +26,20 @@ describe('IntentManager', () => { describe('submitIntent', () => { it('should submit an intent and return an order', async () => { const submissionParams: IntentSubmissionParams = { - providerName: 'cowswap', - chainId: 1, - orderData: { - sellToken: '0xA0b86a33E6441e6e80D0c4C6C7527d72', - buyToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', - sellAmount: '1000000000000000000', + quote: { + id: 'test-quote-1', + provider: 'cowswap', + srcAmount: '1000000000000000000', + destAmount: '2000000000000000000', + estimatedGas: '100000', + estimatedTime: 300, + priceImpact: 0.01, + fees: [], + validUntil: Date.now() + 300000, + metadata: { + sellToken: '0xA0b86a33E6441e6e80D0c4C6C7527d72', + buyToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + }, }, signature: '0xsignature', userAddress: '0x123', diff --git a/yarn.lock b/yarn.lock index 2957f9a0675..f3a1e607a7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,25 +3628,25 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=888849&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=e9b1c0&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/00e33da873f02bc35d3d98d59a0c949d0e204921f590576926837de5a5ca3bfd7fcd1647fd10d6970a78ba8abbbca5ddb3fa16c585962cf14b637edf477a35f5 + checksum: 10/80f3151dacdf16644d7b3ca5127c6c70ad70e0bc432478b217b00847c4d72b319a9e20a59eb63bf3a9f4f94386b62c0804a11c23980132bfb189746fa9c4999d languageName: node linkType: hard "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=888849&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=e9b1c0&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/00e33da873f02bc35d3d98d59a0c949d0e204921f590576926837de5a5ca3bfd7fcd1647fd10d6970a78ba8abbbca5ddb3fa16c585962cf14b637edf477a35f5 + checksum: 10/80f3151dacdf16644d7b3ca5127c6c70ad70e0bc432478b217b00847c4d72b319a9e20a59eb63bf3a9f4f94386b62c0804a11c23980132bfb189746fa9c4999d languageName: node linkType: hard @@ -4915,26 +4915,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.8.0": - version: 11.8.0 - resolution: "@metamask/utils@npm:11.8.0" - dependencies: - "@ethereumjs/tx": "npm:^4.2.0" - "@metamask/superstruct": "npm:^3.1.0" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.3" - "@types/debug": "npm:^4.1.7" - "@types/lodash": "npm:^4.17.20" - debug: "npm:^4.3.4" - lodash: "npm:^4.17.21" - pony-cause: "npm:^2.1.10" - semver: "npm:^7.5.4" - uuid: "npm:^9.0.1" - checksum: 10/d5a9d8c04223fc62b0d4a078b505e062f5d1d47e752df36802189bec19a9e68aee7a9b0df9b15e7e6fa15fd9d65f61c7e4909604209dddc21f0943cd9a2fd5d1 - languageName: node - linkType: hard - -"@metamask/utils@npm:^11.4.2": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.0": version: 11.8.1 resolution: "@metamask/utils@npm:11.8.1" dependencies: From 3e9f3794c732061fd3343b0a1e24d4ae39b21359 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 17:01:30 +0300 Subject: [PATCH 20/85] Try to fix the pipeline failure of lock file change. --- yarn.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index f3a1e607a7d..19c5c9a5fb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3634,7 +3634,6 @@ __metadata: "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/80f3151dacdf16644d7b3ca5127c6c70ad70e0bc432478b217b00847c4d72b319a9e20a59eb63bf3a9f4f94386b62c0804a11c23980132bfb189746fa9c4999d languageName: node linkType: hard @@ -3646,7 +3645,6 @@ __metadata: "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/80f3151dacdf16644d7b3ca5127c6c70ad70e0bc432478b217b00847c4d72b319a9e20a59eb63bf3a9f4f94386b62c0804a11c23980132bfb189746fa9c4999d languageName: node linkType: hard From 944562091e11899f548d1334310d5327f9e96d23 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 17:13:16 +0300 Subject: [PATCH 21/85] try to avoid the pipeline failure of lock file change. --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 19c5c9a5fb6..dde21934a9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,7 +3628,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=e9b1c0&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=b4e94f&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" @@ -3639,7 +3639,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=e9b1c0&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=b4e94f&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" From 7f670b58ef969ead7c162cea28d6d1e39b53e1df Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 17:22:32 +0300 Subject: [PATCH 22/85] refactor: simplify logic for determining transaction hashes in BridgeStatusController --- .../src/bridge-status-controller.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 61552fffae9..335471215ca 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -782,8 +782,15 @@ export class BridgeStatusController extends StaticIntervalPollingController 0 ? metadataTxHashes : txHash ? [txHash] : []; + + let allHashes: string[]; + if (metadataTxHashes.length > 0) { + allHashes = metadataTxHashes; + } else if (txHash) { + allHashes = [txHash]; + } else { + allHashes = []; + } const newStatus = { status: statusType, From e8eb450eafed1a63edd93c925078b7f7eb91f9ff Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 18:15:35 +0300 Subject: [PATCH 23/85] Fix the lint error in node 22 version --- packages/Intent-manager/src/providers/base-intent-provider.ts | 1 + .../Intent-manager/src/providers/cowswap/cowswap-provider.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/Intent-manager/src/providers/base-intent-provider.ts b/packages/Intent-manager/src/providers/base-intent-provider.ts index 4a179463933..7cf72525aa3 100644 --- a/packages/Intent-manager/src/providers/base-intent-provider.ts +++ b/packages/Intent-manager/src/providers/base-intent-provider.ts @@ -96,6 +96,7 @@ export abstract class BaseIntentProvider { /** * Get the configuration for this provider + * * @returns The provider configuration */ getConfig(): IntentProviderConfig { diff --git a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts index c75842ff571..7ce282c325b 100644 --- a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts +++ b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts @@ -155,7 +155,7 @@ export class CowSwapProvider extends BaseIntentProvider { } } - async cancelOrder(orderId: string, chainId: number): Promise { + async cancelOrder(orderId: string, _chainId: number): Promise { // CowSwap doesn't support order cancellation via API // Orders expire naturally based on their validTo timestamp console.warn( From ce7a2d12d4d86406be700c01388d179fecdcf17b Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 18:27:21 +0300 Subject: [PATCH 24/85] Fix weird lock file change failure issue in pipeline. --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index dde21934a9f..9742d162871 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,7 +3628,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=b4e94f&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=bf2939&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" @@ -3639,7 +3639,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=b4e94f&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=bf2939&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" From 75475539ce2e54c4fc0c20f89578539810a806bd Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 18:34:54 +0300 Subject: [PATCH 25/85] Run prettier format the README.md file. --- packages/Intent-manager/README.md | 36 ++++++++++++------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/Intent-manager/README.md b/packages/Intent-manager/README.md index 5e21d34a0fe..d40e92ed7a5 100644 --- a/packages/Intent-manager/README.md +++ b/packages/Intent-manager/README.md @@ -58,11 +58,7 @@ const submissionParams: IntentSubmissionParams = { const order = await intentManager.submitIntent(submissionParams); // Monitor order status -const status = await intentManager.getOrderStatus( - order.id, - 'cowswap', - 1 -); +const status = await intentManager.getOrderStatus(order.id, 'cowswap', 1); ``` ## API Reference @@ -100,11 +96,7 @@ const order = await intentManager.submitIntent({ Retrieves the current status of an order. ```typescript -const status = await intentManager.getOrderStatus( - 'order-123', - 'cowswap', - 1 -); +const status = await intentManager.getOrderStatus('order-123', 'cowswap', 1); ``` ##### `cancelOrder(orderId, providerName, chainId): Promise` @@ -112,11 +104,7 @@ const status = await intentManager.getOrderStatus( Cancels a pending order. ```typescript -const cancelled = await intentManager.cancelOrder( - 'order-123', - 'cowswap', - 1 -); +const cancelled = await intentManager.cancelOrder('order-123', 'cowswap', 1); ``` #### Provider Management @@ -152,8 +140,6 @@ const providers = intentManager.getAvailableProviders({ ## Core Types - - ### IntentOrder Order information and status. @@ -270,8 +256,6 @@ try { ## Best Practices - - ### Order Monitoring - **Poll Status Regularly**: Check order status periodically for updates @@ -279,12 +263,20 @@ try { - **Retry Failed Orders**: Consider retrying with different providers ```typescript -async function monitorOrder(orderId: string, provider: string, chainId: number) { +async function monitorOrder( + orderId: string, + provider: string, + chainId: number, +) { const maxAttempts = 60; // 5 minutes with 5-second intervals let attempts = 0; while (attempts < maxAttempts) { - const order = await intentManager.getOrderStatus(orderId, provider, chainId); + const order = await intentManager.getOrderStatus( + orderId, + provider, + chainId, + ); if (order.status === IntentOrderStatus.COMPLETED) { return order; @@ -292,7 +284,7 @@ async function monitorOrder(orderId: string, provider: string, chainId: number) throw new Error(`Order failed: ${order.id}`); } - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); attempts++; } From 5d56313719b548029956025e9c2cf1fbde7e9f46 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Wed, 24 Sep 2025 18:42:36 +0300 Subject: [PATCH 26/85] change lock file again --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9742d162871..0a2625a171b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,7 +3628,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=bf2939&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" @@ -3639,7 +3639,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=bf2939&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" From 60a4a139664385ea485e59a9a6a77f3adad8c61c Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Thu, 25 Sep 2025 03:26:41 +0300 Subject: [PATCH 27/85] update lock file after rebase from main to resolve conflict. --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5d6436d29b3..06d3167642e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,7 +3628,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" @@ -3639,7 +3639,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" From ae7c3cc5f91489fb0ada22017d2cfbff1b5dc878 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Thu, 25 Sep 2025 03:32:02 +0300 Subject: [PATCH 28/85] Reapply lock version with correct hash to pass the pipeline. --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 06d3167642e..5d6436d29b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,7 +3628,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" @@ -3639,7 +3639,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" From 48b82307b4fd6ed29acc695fa9f073cd99e4c571 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Thu, 25 Sep 2025 03:38:39 +0300 Subject: [PATCH 29/85] Try to fix th lock file change pipeline broken --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5d6436d29b3..06d3167642e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3628,7 +3628,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" @@ -3639,7 +3639,7 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" From 58632946decf4d30503c906d229fd11200c3410b Mon Sep 17 00:00:00 2001 From: "dawnseeker8@gmail.com" Date: Wed, 15 Oct 2025 13:26:33 +0800 Subject: [PATCH 30/85] Resolve the conflict of merge from main --- package.json | 5 ----- yarn.lock | 50 +++++++++++++++++++++++++------------------------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 85f61cc85ab..9cd00e2793f 100644 --- a/package.json +++ b/package.json @@ -44,11 +44,6 @@ "simple-git-hooks": { "pre-push": "yarn lint" }, - "resolutions": { - "elliptic@6.5.4": "^6.5.7", - "fast-xml-parser@^4.3.4": "^4.4.1", - "ws@7.4.6": "^7.5.10" - }, "devDependencies": { "@babel/core": "^7.23.5", "@babel/plugin-transform-modules-commonjs": "^7.23.3", diff --git a/yarn.lock b/yarn.lock index 081bd92e698..8fb9a9194b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2904,11 +2904,6 @@ __metadata: "@metamask/controller-utils": "npm:^11.14.1" "@metamask/eth-json-rpc-provider": "npm:^5.0.1" "@metamask/gas-fee-controller": "npm:^24.1.0" - "@metamask/keyring-api": "npm:^21.0.0" - "@metamask/base-controller": "npm:^8.4.0" - "@metamask/controller-utils": "npm:^11.14.0" - "@metamask/eth-json-rpc-provider": "npm:^5.0.0" - "@metamask/gas-fee-controller": "npm:^24.0.0" "@metamask/intent-manager": "file:../Intent-manager" "@metamask/keyring-api": "npm:^20.1.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" @@ -2954,16 +2949,10 @@ __metadata: "@metamask/bridge-controller": "npm:^52.0.0" "@metamask/controller-utils": "npm:^11.14.1" "@metamask/gas-fee-controller": "npm:^24.1.0" - "@metamask/network-controller": "npm:^24.2.1" - "@metamask/polling-controller": "npm:^14.0.1" - "@metamask/base-controller": "npm:^8.4.0" - "@metamask/bridge-controller": "npm:^46.0.0" - "@metamask/controller-utils": "npm:^11.14.0" - "@metamask/gas-fee-controller": "npm:^24.0.0" "@metamask/intent-manager": "file:../Intent-manager" "@metamask/keyring-api": "npm:^21.0.0" - "@metamask/network-controller": "npm:^24.2.0" - "@metamask/polling-controller": "npm:^14.0.0" + "@metamask/network-controller": "npm:^24.2.1" + "@metamask/polling-controller": "npm:^14.0.1" "@metamask/snaps-controllers": "npm:^14.0.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/transaction-controller": "npm:^60.6.1" @@ -3065,8 +3054,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.14.1, @metamask/controller-utils@workspace:packages/controller-utils": -"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.12.0, @metamask/controller-utils@npm:^11.14.0, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.10.0, @metamask/controller-utils@npm:^11.12.0, @metamask/controller-utils@npm:^11.14.1, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3830,26 +3818,27 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" + checksum: 10/22d4ff420291faca223d5907a4dc6b96f57f5324b3e14efae9240dface8dd244c3567de8a3b98fe404e00b8c7387e45e4e4bbffa1dbcb127d81368736d95a573 languageName: node linkType: hard "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=70ffcd&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" + checksum: 10/22d4ff420291faca223d5907a4dc6b96f57f5324b3e14efae9240dface8dd244c3567de8a3b98fe404e00b8c7387e45e4e4bbffa1dbcb127d81368736d95a573 languageName: node linkType: hard @@ -3873,7 +3862,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" dependencies: @@ -5124,10 +5113,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.7.0, @metamask/utils@npm:^11.8.1": - version: 11.8.1 - resolution: "@metamask/utils@npm:11.8.1" -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.0": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.7.0, @metamask/utils@npm:^11.8.1": version: 11.8.1 resolution: "@metamask/utils@npm:11.8.1" dependencies: @@ -5143,7 +5129,6 @@ __metadata: semver: "npm:^7.5.4" uuid: "npm:^9.0.1" checksum: 10/efd3aab7f86b4a74d396cf1d5fc76e748ff78906802fdc15ec9ce2d1a9bd6b035e8e036ea93eb6b9ea33782c70adb9000772eb7a5e0164e8e9e2ebb077dca3ab - checksum: 10/efd3aab7f86b4a74d396cf1d5fc76e748ff78906802fdc15ec9ce2d1a9bd6b035e8e036ea93eb6b9ea33782c70adb9000772eb7a5e0164e8e9e2ebb077dca3ab languageName: node linkType: hard @@ -15179,6 +15164,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:7.4.6": + version: 7.4.6 + resolution: "ws@npm:7.4.6" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/150e3f917b7cde568d833a5ea6ccc4132e59c38d04218afcf2b6c7b845752bd011a9e0dc1303c8694d3c402a0bdec5893661a390b71ff88f0fc81a4e4e66b09c + languageName: node + linkType: hard + "ws@npm:8.17.1": version: 8.17.1 resolution: "ws@npm:8.17.1" @@ -15194,7 +15194,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.10, ws@npm:^7.5.5": +"ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.5": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: From 3930e271d319ae9d95a3469d7eb702b3aafe2dc1 Mon Sep 17 00:00:00 2001 From: "dawnseeker8@gmail.com" Date: Wed, 15 Oct 2025 16:37:14 +0800 Subject: [PATCH 31/85] Update Jest configuration to include the new `@metamask/intent-manager` package, adjust TypeScript references in `bridge-controller`, and remove unused code from `bridge-status-controller`. Additionally, add type exports in `build-utils` for better modularity. --- jest.config.packages.js | 3 +++ packages/bridge-controller/tsconfig.json | 2 +- .../src/bridge-status-controller.ts | 14 -------------- packages/build-utils/src/index.d.ts | 3 +++ yarn.lock | 8 ++++---- 5 files changed, 11 insertions(+), 19 deletions(-) create mode 100644 packages/build-utils/src/index.d.ts diff --git a/jest.config.packages.js b/jest.config.packages.js index 09cdfa9efe5..1b886604ba8 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -83,6 +83,9 @@ module.exports = { '^@metamask/base-controller/next': [ '/../base-controller/src/next', ], + '^@metamask/intent-manager$': [ + '/../Intent-manager/src', + ], '^@metamask/(.+)$': [ '/../$1/src', // Some @metamask/* packages we are referencing aren't in this monorepo, diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json index 008d2c22a5b..3bd844d4f61 100644 --- a/packages/bridge-controller/tsconfig.json +++ b/packages/bridge-controller/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { "path": "../accounts-controller" }, - { "path": "../intent-manager" }, + { "path": "../Intent-manager" }, { "path": "../base-controller" }, { "path": "../controller-utils" }, { "path": "../network-controller" }, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index cdd22e8c946..bf7f0c91b6d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1846,20 +1846,6 @@ export class BridgeStatusController extends StaticIntervalPollingController id === txMetaId); - const approvalTxMeta = transactions?.find( - ({ id }) => id === historyItem.approvalTxId, - ); - const requiredEventProperties = { ...baseProperties, ...requestParamProperties, diff --git a/packages/build-utils/src/index.d.ts b/packages/build-utils/src/index.d.ts new file mode 100644 index 00000000000..0e5dc2315ed --- /dev/null +++ b/packages/build-utils/src/index.d.ts @@ -0,0 +1,3 @@ +export type { FeatureLabels } from './transforms/remove-fenced-code'; +export { removeFencedCode } from './transforms/remove-fenced-code'; +export { lintTransformedFile } from './transforms/utils'; diff --git a/yarn.lock b/yarn.lock index 8fb9a9194b4..1ab65550997 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3820,25 +3820,25 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=262b8f&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/22d4ff420291faca223d5907a4dc6b96f57f5324b3e14efae9240dface8dd244c3567de8a3b98fe404e00b8c7387e45e4e4bbffa1dbcb127d81368736d95a573 + checksum: 10/bd20b20e284e4f0a1221da61a2aed3f927151a6b0f30d64c16570fc6d3b7dbec910e579bb611fb9d17ec4640ce0702117175878d832bed42d9ac46ff9dc2797c languageName: node linkType: hard "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=4a1f49&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=262b8f&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/22d4ff420291faca223d5907a4dc6b96f57f5324b3e14efae9240dface8dd244c3567de8a3b98fe404e00b8c7387e45e4e4bbffa1dbcb127d81368736d95a573 + checksum: 10/bd20b20e284e4f0a1221da61a2aed3f927151a6b0f30d64c16570fc6d3b7dbec910e579bb611fb9d17ec4640ce0702117175878d832bed42d9ac46ff9dc2797c languageName: node linkType: hard From 778df56dbb1c9029b03b79389877a4e7751da7ff Mon Sep 17 00:00:00 2001 From: "dawnseeker8@gmail.com" Date: Fri, 17 Oct 2025 15:10:00 +0800 Subject: [PATCH 32/85] Remove temporary query parameters from `formatQueryParams` in `fetch.ts` and instantiate `IntentManager` in `bridge-status-controller.ts` for improved intent handling. --- packages/bridge-controller/src/utils/fetch.ts | 4 ---- .../bridge-status-controller/src/bridge-status-controller.ts | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index f49d214ed9c..f5889c08595 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -103,10 +103,6 @@ const formatQueryParams = (request: GenericQuoteRequest): URLSearchParams => { queryParams.append(key, value.toString()); }); - // TODO remove this flag after testing to return both quotes and intents quotes - queryParams.append('onlyIntent', 'true'); - queryParams.append('intent', 'true'); - return queryParams; }; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index bf7f0c91b6d..a47b2fc0a19 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -20,10 +20,10 @@ import { import type { TraceCallback } from '@metamask/controller-utils'; import { toHex } from '@metamask/controller-utils'; import type { - IntentManager, IntentOrder, IntentSubmissionParams, } from '@metamask/intent-manager'; +import { IntentManager } from '@metamask/intent-manager'; import { IntentOrderStatus } from '@metamask/intent-manager'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { @@ -205,6 +205,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Fri, 17 Oct 2025 16:04:40 +0800 Subject: [PATCH 33/85] Fix the get order status sometimes the orderId contain url not friendly string. --- .../src/providers/cowswap/cowswap-provider.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts index 7ce282c325b..f9f2eebfd89 100644 --- a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts +++ b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts @@ -96,7 +96,17 @@ export class CowSwapProvider extends BaseIntentProvider { } try { - const url = `${COW_API_BASE}/${networkPath}/api/v1/orders/${orderId}`; + // orderId need to url path friendly + const urlPathFriendlyOrderId = orderId + .toString() + .normalize('NFD') + .replace(/[\u0300-\u036f]/gu, '') // Remove diacritics + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/gu, '-') // Replace non-alphanumeric with hyphens + .replace(/^-+|-+$/gu, '') // Remove leading/trailing hyphens + .replace(/-+/gu, '-'); // Replace multiple hyphens with single + const url = `${COW_API_BASE}/${networkPath}/api/v1/orders/${urlPathFriendlyOrderId}`; const response = await fetch(url); if (!response.ok) { @@ -112,7 +122,7 @@ export class CowSwapProvider extends BaseIntentProvider { if (status === OrderStatus.COMPLETED) { try { - const tradesUrl = `${COW_API_BASE}/${networkPath}/api/v1/trades?orderUid=${orderId}`; + const tradesUrl = `${COW_API_BASE}/${networkPath}/api/v1/trades?orderUid=${urlPathFriendlyOrderId}`; const tradesResponse = await fetch(tradesUrl); if (tradesResponse.ok) { From cfbf731309de737e5c972178768b56255462762e Mon Sep 17 00:00:00 2001 From: "dawnseeker8@gmail.com" Date: Fri, 17 Oct 2025 17:37:27 +0800 Subject: [PATCH 34/85] Fix order ID formatting in CowSwapProvider by removing quotes from the orderUid before assignment to the IntentOrder object. --- .../Intent-manager/src/providers/cowswap/cowswap-provider.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts index f9f2eebfd89..43d76f18287 100644 --- a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts +++ b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts @@ -66,9 +66,10 @@ export class CowSwapProvider extends BaseIntentProvider { } const orderUid = await response.text(); - + // remove " in the orderUid + const escapeOrderUid = orderUid.replace(/"/gu, ''); const order: IntentOrder = { - id: orderUid, + id: escapeOrderUid, status: OrderStatus.SUBMITTED, createdAt: Date.now(), updatedAt: Date.now(), From d53f3e612604b46ff390acfaff1fa5f56aac4ffb Mon Sep 17 00:00:00 2001 From: "dawnseeker8@gmail.com" Date: Sun, 19 Oct 2025 21:05:00 +0800 Subject: [PATCH 35/85] Update yarn.lock with new hash --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1ab65550997..ab15b29624e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3820,25 +3820,25 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=262b8f&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=d3eeb0&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/bd20b20e284e4f0a1221da61a2aed3f927151a6b0f30d64c16570fc6d3b7dbec910e579bb611fb9d17ec4640ce0702117175878d832bed42d9ac46ff9dc2797c + checksum: 10/e2b4bc4c21e3ec29c5db997e6255eab08beb5c4f2180423842acc9268b45ac3906e3fee2b418ec737033ccbe2de6ba52551352111ac55edf526614e8a734cce9 languageName: node linkType: hard "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=262b8f&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=d3eeb0&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/bd20b20e284e4f0a1221da61a2aed3f927151a6b0f30d64c16570fc6d3b7dbec910e579bb611fb9d17ec4640ce0702117175878d832bed42d9ac46ff9dc2797c + checksum: 10/e2b4bc4c21e3ec29c5db997e6255eab08beb5c4f2180423842acc9268b45ac3906e3fee2b418ec737033ccbe2de6ba52551352111ac55edf526614e8a734cce9 languageName: node linkType: hard From 0ab4bb07a19387b54fe881a7d6444eb39d0a6f84 Mon Sep 17 00:00:00 2001 From: "dawnseeker8@gmail.com" Date: Mon, 20 Oct 2025 21:22:28 +0800 Subject: [PATCH 36/85] Merge from `main` to resolve conflicts. --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index aebcc50b7d8..4d63bcf6854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5116,7 +5116,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.8.1": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1": version: 11.8.1 resolution: "@metamask/utils@npm:11.8.1" dependencies: From a4d00f94e86eb6b6bc5ffddfa9cd7e7eece5f25c Mon Sep 17 00:00:00 2001 From: "dawnseeker8@gmail.com" Date: Tue, 21 Oct 2025 14:16:23 +0800 Subject: [PATCH 37/85] feat: add SUBMITTED status type to enhance order status handling This commit introduces a new `SUBMITTED` status type in the `StatusTypes` enum and updates the `BridgeStatusController` to handle this status appropriately. The logic now distinguishes between `PENDING` and `SUBMITTED` states, improving clarity in order processing. --- packages/bridge-controller/src/types.ts | 1 + .../src/bridge-status-controller.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 745d023a127..83f336ed196 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -219,6 +219,7 @@ export type QuoteRequest< }; export enum StatusTypes { + SUBMITTED = 'SUBMITTED', UNKNOWN = 'UNKNOWN', FAILED = 'FAILED', PENDING = 'PENDING', diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index a47b2fc0a19..4edd091f50f 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -769,10 +769,10 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Tue, 21 Oct 2025 19:49:55 +0800 Subject: [PATCH 38/85] Temprately disabled SSE after backend change is ready. --- packages/bridge-controller/src/bridge-controller.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index a9cee690955..8db7f0c881f 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -551,9 +551,11 @@ export class BridgeController extends StaticIntervalPollingController { state.quoteRequest = updatedQuoteRequest; From 2e2ad4c4e301a3789fcf665d7eebadcfad8393d2 Mon Sep 17 00:00:00 2001 From: Oscar Roche Date: Tue, 4 Nov 2025 12:12:10 +0100 Subject: [PATCH 39/85] feat!: (wip) use crosschain api instead of intent manager for intent order submission (#6963) --- .../bridge-status-controller/CHANGELOG.md | 4 ++ .../src/bridge-status-controller.ts | 63 +++++++------------ .../src/intent-api.ts | 51 +++++++++++++++ .../src/intent-order-status.ts | 9 +++ .../src/intent-order.ts | 10 +++ yarn.lock | 8 +-- 6 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 packages/bridge-status-controller/src/intent-api.ts create mode 100644 packages/bridge-status-controller/src/intent-order-status.ts create mode 100644 packages/bridge-status-controller/src/intent-order.ts diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 93a28dde9b2..d2b157b710d 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING** Use CrossChain API instead of the intent manager package for intent order submission ([#6963](https://github.com/MetaMask/core/pull/6963)) + ## [52.0.0] ### Changed diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 4edd091f50f..244d3a748d7 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -19,12 +19,6 @@ import { } from '@metamask/bridge-controller'; import type { TraceCallback } from '@metamask/controller-utils'; import { toHex } from '@metamask/controller-utils'; -import type { - IntentOrder, - IntentSubmissionParams, -} from '@metamask/intent-manager'; -import { IntentManager } from '@metamask/intent-manager'; -import { IntentOrderStatus } from '@metamask/intent-manager'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { TransactionController, @@ -54,6 +48,9 @@ import type { } from './types'; import { type BridgeStatusControllerMessenger } from './types'; import { BridgeClientId } from './types'; +import { IntentApiImpl } from './intent-api'; +import { IntentOrderStatus } from './intent-order-status'; +import type { IntentOrder } from './intent-order'; import { fetchBridgeTxStatus, getStatusRequestWithSrcTxHash, @@ -121,8 +118,6 @@ export class BridgeStatusController extends StaticIntervalPollingController { - if (!this.#intentManager) { - console.warn('Intent manager not available for status polling'); - return; - } - const { txHistory } = this.state; const historyItem = txHistory[bridgeTxMetaId]; if (!historyItem) { @@ -727,16 +707,20 @@ export class BridgeStatusController extends StaticIntervalPollingController> => { const { quoteResponse, signature, accountAddress } = params; - if (!this.#intentManager) { - throw new Error('Intent manager not initialized'); - } - // Build pre-confirmation properties for error tracking parity with submitTx const account = this.messagingSystem.call( 'AccountsController:getAccountByAddress', @@ -1576,16 +1556,21 @@ export class BridgeStatusController extends StaticIntervalPollingController; +} + +export class IntentApiImpl implements IntentApi { + private baseUrl: string; + private fetchFn: FetchFunction; + + constructor(baseUrl: string, fetchFn: FetchFunction) { + this.baseUrl = baseUrl; + this.fetchFn = fetchFn; + } + + async submitIntent(params: IntentSubmissionParams): Promise { + const endpoint = `${this.baseUrl}/submitOrder`; + const response = (await this.fetchFn(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + })) as Response; + if (!response.ok) { + throw new Error(`Failed to submit intent: ${response.statusText}`); + } + return response.json(); + } + + async getOrderStatus( + orderId: string, + aggregatorId: string, + chainId: string, + ): Promise { + const endpoint = `${this.baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&chainId=${chainId}`; + const response = (await this.fetchFn(endpoint, { + method: 'GET', + })) as Response; + if (!response.ok) { + throw new Error(`Failed to get order status: ${response.statusText}`); + } + return response.json(); + } +} diff --git a/packages/bridge-status-controller/src/intent-order-status.ts b/packages/bridge-status-controller/src/intent-order-status.ts new file mode 100644 index 00000000000..81dfa895324 --- /dev/null +++ b/packages/bridge-status-controller/src/intent-order-status.ts @@ -0,0 +1,9 @@ +export enum IntentOrderStatus { + PENDING = 'pending', + SUBMITTED = 'submitted', + CONFIRMED = 'confirmed', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} diff --git a/packages/bridge-status-controller/src/intent-order.ts b/packages/bridge-status-controller/src/intent-order.ts new file mode 100644 index 00000000000..cb72a209860 --- /dev/null +++ b/packages/bridge-status-controller/src/intent-order.ts @@ -0,0 +1,10 @@ +import { IntentOrderStatus } from './intent-order-status'; + +export interface IntentOrder { + id: string; + status: IntentOrderStatus; + txHash?: string; + metadata: { + txHashes?: string[] | string; + }; +} diff --git a/yarn.lock b/yarn.lock index 4d63bcf6854..c0f54b1a51c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3822,25 +3822,25 @@ __metadata: "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=d3eeb0&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=bf88dd&locator=%40metamask%2Fbridge-controller%40workspace%3Apackages%2Fbridge-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/e2b4bc4c21e3ec29c5db997e6255eab08beb5c4f2180423842acc9268b45ac3906e3fee2b418ec737033ccbe2de6ba52551352111ac55edf526614e8a734cce9 + checksum: 10/45f9c1dcf7da910d866f675473313fdce5d9e1e67008861c43912345d2e0903a44a8167763b74c7a86c89fe9d818d06c3d99788d102f81893104b23773a94acd languageName: node linkType: hard "@metamask/intent-manager@file:../Intent-manager::locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller": version: 1.0.0 - resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=d3eeb0&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" + resolution: "@metamask/intent-manager@file:../Intent-manager#../Intent-manager::hash=bf88dd&locator=%40metamask%2Fbridge-status-controller%40workspace%3Apackages%2Fbridge-status-controller" dependencies: "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.4.2" - checksum: 10/e2b4bc4c21e3ec29c5db997e6255eab08beb5c4f2180423842acc9268b45ac3906e3fee2b418ec737033ccbe2de6ba52551352111ac55edf526614e8a734cce9 + checksum: 10/45f9c1dcf7da910d866f675473313fdce5d9e1e67008861c43912345d2e0903a44a8167763b74c7a86c89fe9d818d06c3d99788d102f81893104b23773a94acd languageName: node linkType: hard From 3ad0d660450ff5bad9ad1167c5d672a68a1819f0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Nov 2025 21:24:00 +0100 Subject: [PATCH 40/85] feat: update api call with new param and error handling --- .../src/bridge-status-controller.ts | 1 + .../src/intent-api.ts | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 244d3a748d7..b84576d0500 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1564,6 +1564,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { const endpoint = `${this.baseUrl}/submitOrder`; - const response = (await this.fetchFn(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(params), - })) as Response; - if (!response.ok) { - throw new Error(`Failed to submit intent: ${response.statusText}`); + try { + const response = await this.fetchFn(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }); + return response; + } catch (e) { + throw new Error(`Failed to submit intent: ${e}`); } - return response.json(); } async getOrderStatus( @@ -40,12 +42,13 @@ export class IntentApiImpl implements IntentApi { chainId: string, ): Promise { const endpoint = `${this.baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&chainId=${chainId}`; - const response = (await this.fetchFn(endpoint, { - method: 'GET', - })) as Response; - if (!response.ok) { - throw new Error(`Failed to get order status: ${response.statusText}`); + try { + const response = await this.fetchFn(endpoint, { + method: 'GET', + }); + return response; + } catch (e) { + throw new Error(`Failed to get order status: ${e}`); } - return response.json(); } } From 0ea9cd2c3d659e3e377aaec799b1eeb55abbb087 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Nov 2025 21:24:31 +0100 Subject: [PATCH 41/85] refactor: remove obsolete cancelled intent order status --- .../bridge-status-controller/src/bridge-status-controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index b84576d0500..63de451cef4 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -750,7 +750,6 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Mon, 24 Nov 2025 19:01:17 +0100 Subject: [PATCH 42/85] fix: replace chain id with src chain id --- .../src/bridge-status-controller.ts | 2 +- packages/bridge-status-controller/src/intent-api.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 4cb0e3977af..d243e8d70f1 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1610,7 +1610,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { - const endpoint = `${this.baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&chainId=${chainId}`; + const endpoint = `${this.baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; try { const response = await this.fetchFn(endpoint, { method: 'GET', From ae0e4ecf91de9f7f447823eb9559d83cd5dd4951 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Dec 2025 15:08:49 +0100 Subject: [PATCH 43/85] refactor: delete intent manager --- packages/Intent-manager/LICENSE | 21 -- packages/Intent-manager/README.md | 320 ------------------ packages/Intent-manager/jest.config.js | 26 -- packages/Intent-manager/package.json | 74 ---- packages/Intent-manager/src/constants.ts | 32 -- packages/Intent-manager/src/index.ts | 35 -- .../Intent-manager/src/intent-manager.test.ts | 118 ------- packages/Intent-manager/src/intent-manager.ts | 194 ----------- .../src/providers/base-intent-provider.ts | 125 ------- .../src/providers/cowswap/constants.ts | 41 --- .../src/providers/cowswap/cowswap-provider.ts | 193 ----------- .../src/providers/cowswap/index.ts | 7 - .../Intent-manager/src/providers/index.ts | 2 - packages/Intent-manager/src/types.ts | 114 ------- packages/Intent-manager/src/validators.ts | 61 ---- packages/Intent-manager/tsconfig.build.json | 10 - packages/Intent-manager/tsconfig.json | 9 - packages/Intent-manager/typedoc.json | 7 - 18 files changed, 1389 deletions(-) delete mode 100644 packages/Intent-manager/LICENSE delete mode 100644 packages/Intent-manager/README.md delete mode 100644 packages/Intent-manager/jest.config.js delete mode 100644 packages/Intent-manager/package.json delete mode 100644 packages/Intent-manager/src/constants.ts delete mode 100644 packages/Intent-manager/src/index.ts delete mode 100644 packages/Intent-manager/src/intent-manager.test.ts delete mode 100644 packages/Intent-manager/src/intent-manager.ts delete mode 100644 packages/Intent-manager/src/providers/base-intent-provider.ts delete mode 100644 packages/Intent-manager/src/providers/cowswap/constants.ts delete mode 100644 packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts delete mode 100644 packages/Intent-manager/src/providers/cowswap/index.ts delete mode 100644 packages/Intent-manager/src/providers/index.ts delete mode 100644 packages/Intent-manager/src/types.ts delete mode 100644 packages/Intent-manager/src/validators.ts delete mode 100644 packages/Intent-manager/tsconfig.build.json delete mode 100644 packages/Intent-manager/tsconfig.json delete mode 100644 packages/Intent-manager/typedoc.json diff --git a/packages/Intent-manager/LICENSE b/packages/Intent-manager/LICENSE deleted file mode 100644 index c259cd7ebcf..00000000000 --- a/packages/Intent-manager/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 MetaMask - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/Intent-manager/README.md b/packages/Intent-manager/README.md deleted file mode 100644 index d40e92ed7a5..00000000000 --- a/packages/Intent-manager/README.md +++ /dev/null @@ -1,320 +0,0 @@ - - -# `@metamask/intent-manager` - -A comprehensive intent management system for MetaMask that orchestrates cross-chain token swaps and bridging operations through multiple decentralized exchange (DEX) providers. This package provides order execution and lifecycle management for user intents. - -## Overview - -The Intent Manager provides a unified interface for: - -- **Cross-Chain Operations**: Support for token swaps and bridging across different blockchain networks -- **Order Lifecycle Management**: Handle order submission, execution tracking, and status monitoring -- **Provider Management**: Pluggable architecture for adding new DEX providers -- **State Management**: Track intent orders and maintain execution history - -## Key Features - -- 🔄 **Cross-chain token swaps** through multiple providers -- 🏪 **Multi-provider support** with extensible provider architecture -- 📊 **Real-time order tracking** and status updates -- 🛡️ **Order lifecycle management** with comprehensive error handling - -## Installation - -```bash -yarn add @metamask/intent-manager -``` - -or - -```bash -npm install @metamask/intent-manager -``` - -## Quick Start - -```typescript -import { IntentManager } from '@metamask/intent-manager'; -import type { IntentSubmissionParams } from '@metamask/intent-manager'; - -// Initialize the intent manager -const intentManager = new IntentManager(); - -// Submit an intent order (order data would come from backend) -const submissionParams: IntentSubmissionParams = { - providerName: 'cowswap', - chainId: 1, // Ethereum Mainnet - orderData: { - sellToken: '0xA0b86a33E6441e6e80D0c4C6C7527d72', // USDC - buyToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH - sellAmount: '1000000000000000000', // 1 token (18 decimals) - // ... other order data from backend - }, - signature: '0x...', // User signature - userAddress: '0x742d35Cc6634C0532925a3b8D4C9db96', -}; - -const order = await intentManager.submitIntent(submissionParams); - -// Monitor order status -const status = await intentManager.getOrderStatus(order.id, 'cowswap', 1); -``` - -## API Reference - -### IntentManager - -The main class that orchestrates intent operations across multiple providers. - -#### Constructor - -```typescript -new IntentManager(initialState?: Partial) -``` - -#### Core Methods - -##### `submitIntent(params): Promise` - -Submits an intent order with the provided order data. - -```typescript -const order = await intentManager.submitIntent({ - providerName: 'cowswap', - chainId: 1, - orderData: { - // Order data from backend - }, - signature: '0x...', - userAddress: '0x...', -}); -``` - -##### `getOrderStatus(orderId, providerName, chainId): Promise` - -Retrieves the current status of an order. - -```typescript -const status = await intentManager.getOrderStatus('order-123', 'cowswap', 1); -``` - -##### `cancelOrder(orderId, providerName, chainId): Promise` - -Cancels a pending order. - -```typescript -const cancelled = await intentManager.cancelOrder('order-123', 'cowswap', 1); -``` - -#### Provider Management - -##### `registerProvider(provider): void` - -Registers a new intent provider. - -```typescript -const customProvider = new CustomProvider(config); -intentManager.registerProvider(customProvider); -``` - -##### `unregisterProvider(providerName): boolean` - -Unregisters an intent provider. - -```typescript -const removed = intentManager.unregisterProvider('cowswap'); -``` - -##### `getAvailableProviders(criteria?): BaseIntentProvider[]` - -Gets available providers, optionally filtered by criteria. - -```typescript -const providers = intentManager.getAvailableProviders({ - chainId: 1, - tokenPair: ['0x...', '0x...'], - amount: '1000000000000000000', -}); -``` - -## Core Types - -### IntentOrder - -Order information and status. - -```typescript -type IntentOrder = { - id: string; - status: IntentOrderStatus; - txHash?: string; - createdAt: number; - updatedAt: number; - metadata: Record; -}; -``` - -### IntentOrderStatus - -```typescript -enum IntentOrderStatus { - PENDING = 'pending', - SUBMITTED = 'submitted', - CONFIRMED = 'confirmed', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', - EXPIRED = 'expired', -} -``` - -### ProviderSelectionCriteria - -Criteria for filtering and sorting providers. - -```typescript -type ProviderSelectionCriteria = { - chainId: number; - tokenPair: [string, string]; - amount: string; - preferredProviders?: string[]; - excludedProviders?: string[]; -}; -``` - -## Supported Providers - -### CowSwap - -The package includes built-in support for CowSwap, a DEX aggregator that provides: - -- **MEV Protection**: Orders are protected from front-running and sandwich attacks -- **Gas-free Trading**: No gas fees for failed transactions -- **Multi-chain Support**: Ethereum, Arbitrum, Base, Avalanche, and Polygon -- **Batch Auctions**: Efficient price discovery through batch settlement - -### Adding Custom Providers - -You can extend the system by implementing the `BaseIntentProvider` interface: - -```typescript -import { BaseIntentProvider } from '@metamask/intent-manager'; - -class CustomProvider extends BaseIntentProvider { - constructor(config: IntentProviderConfig) { - super(config); - } - - getName(): string { - return 'custom-dex'; - } - - getVersion(): string { - return '1.0.0'; - } - - getSupportedChains(): number[] { - return [1, 42161]; // Ethereum and Arbitrum - } - - async submitOrder(params: IntentSubmissionParams): Promise { - // Implement order submission logic - } - - async getOrderStatus(orderId: string, chainId: number): Promise { - // Implement status checking logic - } - - async cancelOrder(orderId: string, chainId: number): Promise { - // Implement order cancellation logic - } -} - -// Register the custom provider -const customProvider = new CustomProvider(config); -intentManager.registerProvider(customProvider); -``` - -## Error Handling - -The Intent Manager provides comprehensive error handling: - -```typescript -try { - const order = await intentManager.submitIntent(submissionParams); -} catch (error) { - if (error.message.includes('Unsupported chain')) { - // Handle unsupported chain error - } else if (error.message.includes('Provider not found')) { - // Handle provider error - } else { - // Handle other errors - } -} -``` - -## Best Practices - -### Order Monitoring - -- **Poll Status Regularly**: Check order status periodically for updates -- **Handle Timeouts**: Implement timeout logic for long-running orders -- **Retry Failed Orders**: Consider retrying with different providers - -```typescript -async function monitorOrder( - orderId: string, - provider: string, - chainId: number, -) { - const maxAttempts = 60; // 5 minutes with 5-second intervals - let attempts = 0; - - while (attempts < maxAttempts) { - const order = await intentManager.getOrderStatus( - orderId, - provider, - chainId, - ); - - if (order.status === IntentOrderStatus.COMPLETED) { - return order; - } else if (order.status === IntentOrderStatus.FAILED) { - throw new Error(`Order failed: ${order.id}`); - } - - await new Promise((resolve) => setTimeout(resolve, 5000)); - attempts++; - } - - throw new Error('Order monitoring timeout'); -} -``` - -## Contributing - -This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). - -### Development - -```bash -# Install dependencies -yarn install - -# Run tests -yarn test - -# Run tests in watch mode -yarn test:watch - -# Build the package -yarn build - -# Generate documentation -yarn build:docs -``` - -## License - -MIT diff --git a/packages/Intent-manager/jest.config.js b/packages/Intent-manager/jest.config.js deleted file mode 100644 index 6ef43e12723..00000000000 --- a/packages/Intent-manager/jest.config.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property and type check, visit: - * https://jestjs.io/docs/configuration - */ - -const merge = require('deepmerge'); -const path = require('path'); - -const baseConfig = require('../../jest.config.packages'); - -const displayName = path.basename(__dirname); - -module.exports = merge(baseConfig, { - // The display name when running multiple projects - displayName, - - // An object that configures minimum threshold enforcement for coverage results - coverageThreshold: { - global: { - branches: 0, - functions: 0, - lines: 0, - statements: 0, - }, - }, -}); diff --git a/packages/Intent-manager/package.json b/packages/Intent-manager/package.json deleted file mode 100644 index 0b3580d1508..00000000000 --- a/packages/Intent-manager/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "@metamask/intent-manager", - "version": "1.0.0", - "description": "Manages intent-related functionality for MetaMask", - "keywords": [ - "MetaMask", - "Ethereum", - "Intent" - ], - "homepage": "https://github.com/MetaMask/core/tree/main/packages/Intent-manager#readme", - "bugs": { - "url": "https://github.com/MetaMask/core/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/MetaMask/core.git" - }, - "license": "MIT", - "sideEffects": false, - "exports": { - ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } - }, - "./package.json": "./package.json" - }, - "main": "./dist/index.cjs", - "types": "./dist/index.d.cts", - "files": [ - "dist/" - ], - "scripts": { - "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", - "build:docs": "typedoc", - "changelog:update": "../../scripts/update-changelog.sh @metamask/intent-manager", - "changelog:validate": "../../scripts/validate-changelog.sh @metamask/intent-manager", - "publish:preview": "yarn npm publish --tag preview", - "since-latest-release": "../../scripts/since-latest-release.sh", - "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", - "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", - "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", - "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" - }, - "dependencies": { - "@metamask/base-controller": "^8.3.0", - "@metamask/controller-utils": "^11.12.0", - "@metamask/superstruct": "^3.1.0", - "@metamask/utils": "^11.4.2" - }, - "devDependencies": { - "@metamask/auto-changelog": "^3.4.4", - "@types/jest": "^27.4.1", - "deepmerge": "^4.2.2", - "jest": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "ts-jest": "^27.1.4", - "typedoc": "^0.24.8", - "typedoc-plugin-missing-exports": "^2.0.0", - "typescript": "~5.2.2" - }, - "engines": { - "node": "^18.18 || >=20" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - } -} diff --git a/packages/Intent-manager/src/constants.ts b/packages/Intent-manager/src/constants.ts deleted file mode 100644 index ea512104009..00000000000 --- a/packages/Intent-manager/src/constants.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Controller name for the Intent Manager - */ -export const INTENT_MANAGER_NAME = 'IntentManager'; - -/** - * Default state for the Intent Manager Controller - */ -export const DEFAULT_INTENT_MANAGER_CONTROLLER_STATE = { - intents: {}, - intentHistory: [], -}; - -/** - * Default polling interval for intent status updates (5 seconds) - */ -export const DEFAULT_POLLING_INTERVAL_MS = 5000; - -/** - * Maximum number of retry attempts for intent execution - */ -export const MAX_RETRY_ATTEMPTS = 3; - -/** - * Timeout for intent execution (5 minutes) - */ -export const INTENT_EXECUTION_TIMEOUT_MS = 5 * 60 * 1000; - -/** - * Maximum number of intents to keep in history - */ -export const MAX_INTENT_HISTORY_SIZE = 100; diff --git a/packages/Intent-manager/src/index.ts b/packages/Intent-manager/src/index.ts deleted file mode 100644 index 0aac6f193da..00000000000 --- a/packages/Intent-manager/src/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Export constants -export { - DEFAULT_INTENT_MANAGER_CONTROLLER_STATE, - DEFAULT_POLLING_INTERVAL_MS, - INTENT_EXECUTION_TIMEOUT_MS, - INTENT_MANAGER_NAME, - MAX_INTENT_HISTORY_SIZE, - MAX_RETRY_ATTEMPTS, -} from './constants'; - -// Export types and enums -export { IntentStatus, IntentOrderStatus } from './types'; - -export type { - IntentOrder, - IntentFee, - IntentSubmissionParams, - IntentProviderConfig, - ProviderRegistry, - ProviderSelectionCriteria, -} from './types'; - -// Export main controller -export { IntentManager } from './intent-manager'; -export type { IntentManagerState } from './intent-manager'; - -// Export providers -export * from './providers'; - -// Export validators and schemas -export { IntentOrderSchema, IntentSchema } from './validators'; -export type { - IntentOrder as IntentOrderType, - Intent as IntentType, -} from './validators'; diff --git a/packages/Intent-manager/src/intent-manager.test.ts b/packages/Intent-manager/src/intent-manager.test.ts deleted file mode 100644 index df7ffe26e33..00000000000 --- a/packages/Intent-manager/src/intent-manager.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -// cSpell:words cowswap xsignature xtxhash - -import { IntentManager } from './intent-manager'; -import type { - IntentSubmissionParams, - BaseIntentProvider, - IntentOrder, -} from './types'; -import { IntentOrderStatus } from './types'; - -describe('IntentManager', () => { - let intentManager: IntentManager; - - beforeEach(() => { - intentManager = new IntentManager(); - // Clear all providers to start with clean state - intentManager.unregisterProvider('cowswap'); - }); - - describe('constructor', () => { - it('should initialize with default state', () => { - expect(intentManager).toBeDefined(); - }); - }); - - describe('submitIntent', () => { - it('should submit an intent and return an order', async () => { - const submissionParams: IntentSubmissionParams = { - quote: { - id: 'test-quote-1', - provider: 'cowswap', - srcAmount: '1000000000000000000', - destAmount: '2000000000000000000', - estimatedGas: '100000', - estimatedTime: 300, - priceImpact: 0.01, - fees: [], - validUntil: Date.now() + 300000, - metadata: { - sellToken: '0xA0b86a33E6441e6e80D0c4C6C7527d72', - buyToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', - }, - }, - signature: '0xsignature', - userAddress: '0x123', - }; - - const mockOrder: IntentOrder = { - id: 'test-order-1', - status: IntentOrderStatus.PENDING, - txHash: undefined, - createdAt: Date.now(), - updatedAt: Date.now(), - metadata: {}, - }; - - // Create a complete mock provider that implements BaseIntentProvider - const mockProvider: BaseIntentProvider = { - getName: () => 'cowswap', - getVersion: () => '1.0.0', - getSupportedChains: () => [1, 42161], - submitOrder: jest.fn().mockResolvedValue(mockOrder), - getOrderStatus: jest.fn(), - cancelOrder: jest.fn(), - }; - - // Register the mock provider - intentManager.registerProvider(mockProvider); - - const result = await intentManager.submitIntent(submissionParams); - - expect(result).toStrictEqual(mockOrder); - expect(mockProvider.submitOrder).toHaveBeenCalledWith(submissionParams); - }); - }); - - describe('getOrderStatus', () => { - it('should get order status from the provider', async () => { - const orderId = 'test-order-1'; - const providerName = 'cowswap'; - const chainId = 1; - - const mockOrder: IntentOrder = { - id: orderId, - status: IntentOrderStatus.CONFIRMED, - txHash: '0xtxhash', - createdAt: Date.now(), - updatedAt: Date.now(), - metadata: {}, - }; - - // Create a complete mock provider that implements BaseIntentProvider - const mockProvider: BaseIntentProvider = { - getName: () => 'cowswap', - getVersion: () => '1.0.0', - getSupportedChains: () => [1, 42161], - submitOrder: jest.fn(), - getOrderStatus: jest.fn().mockResolvedValue(mockOrder), - cancelOrder: jest.fn(), - }; - - // Register the mock provider - intentManager.registerProvider(mockProvider); - - const result = await intentManager.getOrderStatus( - orderId, - providerName, - chainId, - ); - - expect(result).toStrictEqual(mockOrder); - expect(mockProvider.getOrderStatus).toHaveBeenCalledWith( - orderId, - chainId, - ); - }); - }); -}); diff --git a/packages/Intent-manager/src/intent-manager.ts b/packages/Intent-manager/src/intent-manager.ts deleted file mode 100644 index 437d2055249..00000000000 --- a/packages/Intent-manager/src/intent-manager.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { DEFAULT_INTENT_MANAGER_CONTROLLER_STATE } from './constants'; -import { CowSwapProvider } from './providers/cowswap'; -import type { - BaseIntentProvider, - IntentOrder, - IntentSubmissionParams, - ProviderSelectionCriteria, - ProviderRegistry, -} from './types'; - -/** - * Intent Manager Controller State - */ -export type IntentManagerState = { - intents: Record; - intentHistory: IntentOrder[]; -}; - -/** - * Intent Manager Controller - * - * Manages the lifecycle of user intents including creation, execution, - * cancellation, and state tracking. Orchestrates multiple intent providers - * and provides a unified interface for intent operations. - */ -export class IntentManager { - private providers: ProviderRegistry = {}; - - private defaultProvider?: string; - - private readonly state: IntentManagerState; - - constructor(initialState?: Partial) { - this.state = { - ...DEFAULT_INTENT_MANAGER_CONTROLLER_STATE, - ...initialState, - }; - - this.#initializeProviders(); - } - - /** - * Initialize default providers - */ - #initializeProviders(): void { - // Register CowSwap provider by default - const cowSwapProvider = new CowSwapProvider(); - this.registerProvider(cowSwapProvider); - } - - /** - * Register a new intent provider - * - * @param provider - The provider to register - */ - registerProvider(provider: BaseIntentProvider): void { - const name = provider.getName(); - this.providers[name] = provider; - - // Set first registered provider as default - if (!this.defaultProvider) { - this.defaultProvider = name; - } - } - - /** - * Unregister an intent provider - * - * @param providerName - The name of the provider to unregister - * @returns True if the provider was successfully unregistered - */ - unregisterProvider(providerName: string): boolean { - if (this.providers[providerName]) { - delete this.providers[providerName]; - - // Update default if needed - if (this.defaultProvider === providerName) { - this.defaultProvider = Object.keys(this.providers)[0]; - } - return true; - } - return false; - } - - getState(): IntentManagerState { - return { ...this.state }; - } - - /** - * Get available providers based on criteria - * - * @param criteria - Optional criteria for provider selection - * @returns Array of available providers - */ - getAvailableProviders( - criteria?: ProviderSelectionCriteria, - ): BaseIntentProvider[] { - let availableProviders = Object.values(this.providers); - - if (criteria) { - // Filter by supported chains - availableProviders = availableProviders.filter((provider) => - provider.getSupportedChains().includes(criteria.chainId), - ); - - // Filter by excluded providers - if (criteria.excludedProviders && criteria.excludedProviders.length > 0) { - availableProviders = availableProviders.filter( - (provider) => - !criteria.excludedProviders?.includes(provider.getName()), - ); - } - - // Sort by preferred providers - if ( - criteria.preferredProviders && - criteria.preferredProviders.length > 0 - ) { - availableProviders.sort((a, b) => { - const aIndex = - criteria.preferredProviders?.indexOf(a.getName()) ?? -1; - const bIndex = - criteria.preferredProviders?.indexOf(b.getName()) ?? -1; - - if (aIndex === -1 && bIndex === -1) { - return 0; - } - if (aIndex === -1) { - return 1; - } - if (bIndex === -1) { - return -1; - } - - return aIndex - bIndex; - }); - } - } - - return availableProviders; - } - - async submitIntent(params: IntentSubmissionParams): Promise { - const provider = this.providers[params.quote.provider]; - if (!provider) { - throw new Error(`Provider ${params.quote.provider} not found`); - } - - const order = await provider.submitOrder(params); - - // Update state - this.state.intents[order.id] = order; - this.state.intentHistory.push(order); - - return order; - } - - async getOrderStatus( - orderId: string, - providerName: string, - chainId: number, - ): Promise { - const provider = this.providers[providerName]; - if (!provider) { - throw new Error(`Provider ${providerName} not found`); - } - - const order = await provider.getOrderStatus(orderId, chainId); - - // Update state if order exists - if (this.state.intents[orderId]) { - this.state.intents[orderId] = order; - } - - return order; - } - - async cancelOrder( - orderId: string, - providerName: string, - chainId: number, - ): Promise { - const provider = this.providers[providerName]; - if (!provider) { - throw new Error(`Provider ${providerName} not found`); - } - - return provider.cancelOrder(orderId, chainId); - } - - getProviders(criteria?: ProviderSelectionCriteria): BaseIntentProvider[] { - return this.getAvailableProviders(criteria); - } -} diff --git a/packages/Intent-manager/src/providers/base-intent-provider.ts b/packages/Intent-manager/src/providers/base-intent-provider.ts deleted file mode 100644 index 7cf72525aa3..00000000000 --- a/packages/Intent-manager/src/providers/base-intent-provider.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { - IntentOrder, - IntentSubmissionParams, - IntentProviderConfig, - IntentOrderStatus, -} from '../types'; - -/** - * Abstract base class for intent providers - * - * This class provides the foundation for implementing specific intent providers - * like CowSwap, 1inch, 0x Protocol, etc. Each provider must implement the - * abstract methods to handle their specific API and business logic. - */ -export abstract class BaseIntentProvider { - protected config: IntentProviderConfig; - - constructor(config: IntentProviderConfig) { - this.config = config; - } - - /** - * Get the name of this provider - */ - abstract getName(): string; - - /** - * Get the version of this provider - */ - abstract getVersion(): string; - - /** - * Get the list of supported chain IDs - */ - abstract getSupportedChains(): number[]; - - /** - * Submit an order based on the quote and signature - * - * @param params - The submission parameters including quote and signature - * @returns Promise resolving to the created order - */ - abstract submitOrder(params: IntentSubmissionParams): Promise; - - /** - * Get the current status of an order - * - * @param orderId - The order ID to check - * @param chainId - The chain ID where the order was placed - * @returns Promise resolving to the order status - */ - abstract getOrderStatus( - orderId: string, - chainId: number, - ): Promise; - - /** - * Cancel an existing order - * - * @param orderId - The order ID to cancel - * @param chainId - The chain ID where the order was placed - * @returns Promise resolving to true if cancellation was successful - */ - abstract cancelOrder(orderId: string, chainId: number): Promise; - - /** - * Lifecycle hook called after an order is submitted - * Override this method to add provider-specific post-submission logic - * - * @param order - The submitted order - */ - protected async onOrderSubmitted?(order: IntentOrder): Promise; - - /** - * Lifecycle hook called when an order status changes - * Override this method to add provider-specific status change logic - * - * @param order - The order with updated status - * @param previousStatus - The previous status of the order - */ - protected async onOrderStatusChanged?( - order: IntentOrder, - previousStatus: IntentOrderStatus, - ): Promise; - - /** - * Handle errors in a consistent way across providers - * - * @param error - The original error - * @param context - Context about where the error occurred - * @returns A new error with provider-specific context - */ - protected handleError(error: Error, context: string): Error { - return new Error(`${this.getName()}: ${context} - ${error.message}`); - } - - /** - * Get the configuration for this provider - * - * @returns The provider configuration - */ - getConfig(): IntentProviderConfig { - return { ...this.config }; - } - - /** - * Check if this provider supports the given chain - * - * @param chainId - The chain ID to check - * @returns True if the chain is supported - */ - supportsChain(chainId: number): boolean { - return this.getSupportedChains().includes(chainId); - } - - /** - * Check if this provider has a specific feature - * - * @param feature - The feature name to check - * @returns True if the feature is supported - */ - hasFeature(feature: string): boolean { - return this.config.features.includes(feature); - } -} diff --git a/packages/Intent-manager/src/providers/cowswap/constants.ts b/packages/Intent-manager/src/providers/cowswap/constants.ts deleted file mode 100644 index a0b8785ce31..00000000000 --- a/packages/Intent-manager/src/providers/cowswap/constants.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * CowSwap API base URL - */ -export const COW_API_BASE = 'https://api.cow.fi'; - -/** - * Mapping of chain IDs to CowSwap network paths - */ -export const COW_NETWORK_PATHS: Record = { - // Ethereum Mainnet - 1: 'mainnet', - // Arbitrum One - 42161: 'arbitrum_one', - // Base - 8453: 'base', - // Avalanche C-Chain - 43114: 'avalanche', - // Polygon PoS - 137: 'polygon', -}; - -/** - * Default CowSwap settlement contract address - */ -export const COW_SETTLEMENT_CONTRACT = - '0x9008D19f58AAbd9eD0D60971565AA8510560ab41'; - -/** - * CowSwap provider configuration - */ -export const COWSWAP_PROVIDER_CONFIG = { - name: 'cowswap', - version: '1.0.0', - supportedChains: Object.keys(COW_NETWORK_PATHS).map(Number), - apiBaseUrl: COW_API_BASE, - features: ['eip712-signing', 'meta-transactions', 'gasless-trading'], - rateLimit: { - requestsPerMinute: 60, - burstLimit: 10, - }, -}; diff --git a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts b/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts deleted file mode 100644 index 43d76f18287..00000000000 --- a/packages/Intent-manager/src/providers/cowswap/cowswap-provider.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { - COW_API_BASE, - COW_NETWORK_PATHS, - COWSWAP_PROVIDER_CONFIG, -} from './constants'; -import type { - IntentOrder, - IntentSubmissionParams, - IntentOrderStatus, -} from '../../types'; -import { IntentOrderStatus as OrderStatus } from '../../types'; -import { BaseIntentProvider } from '../base-intent-provider'; - -/** - * CowSwap intent provider implementation - * - * Handles order submission and status polling for CowSwap intents. - * Based on the existing CowSwap integration logic from bridge-status-controller. - */ -export class CowSwapProvider extends BaseIntentProvider { - constructor() { - super(COWSWAP_PROVIDER_CONFIG); - } - - getName(): string { - return 'cowswap'; - } - - getVersion(): string { - return '1.0.0'; - } - - getSupportedChains(): number[] { - return Object.keys(COW_NETWORK_PATHS).map(Number); - } - - async submitOrder(params: IntentSubmissionParams): Promise { - const chainId = params.quote.metadata.chainId as number; - const networkPath = COW_NETWORK_PATHS[chainId]; - - if (!networkPath) { - throw this.handleError( - new Error(`Unsupported chain: ${chainId}`), - 'submitOrder', - ); - } - - try { - const orderBody = { - ...(params.quote.metadata?.order || {}), - feeAmount: '0', - from: params.userAddress, - signature: params.signature, - signingScheme: 'eip712', - }; - - const url = `${COW_API_BASE}/${networkPath}/api/v1/orders`; - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(orderBody), - }); - - if (!response.ok) { - throw new Error(`Failed to submit order: ${response.statusText}`); - } - - const orderUid = await response.text(); - // remove " in the orderUid - const escapeOrderUid = orderUid.replace(/"/gu, ''); - const order: IntentOrder = { - id: escapeOrderUid, - status: OrderStatus.SUBMITTED, - createdAt: Date.now(), - updatedAt: Date.now(), - metadata: { - chainId, - networkPath, - orderBody, - }, - }; - - await this.onOrderSubmitted?.(order); - return order; - } catch (error) { - throw this.handleError(error as Error, 'submitOrder'); - } - } - - async getOrderStatus(orderId: string, chainId: number): Promise { - const networkPath = COW_NETWORK_PATHS[chainId]; - if (!networkPath) { - throw this.handleError( - new Error(`Unsupported chain: ${chainId}`), - 'getOrderStatus', - ); - } - - try { - // orderId need to url path friendly - const urlPathFriendlyOrderId = orderId - .toString() - .normalize('NFD') - .replace(/[\u0300-\u036f]/gu, '') // Remove diacritics - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/gu, '-') // Replace non-alphanumeric with hyphens - .replace(/^-+|-+$/gu, '') // Remove leading/trailing hyphens - .replace(/-+/gu, '-'); // Replace multiple hyphens with single - const url = `${COW_API_BASE}/${networkPath}/api/v1/orders/${urlPathFriendlyOrderId}`; - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`Failed to get order status: ${response.statusText}`); - } - - const data = await response.json(); - const status = this.mapCowSwapStatus(data.status); - - // Try to get transaction hashes from trades endpoint for completed orders - let txHash: string | undefined; - let allHashes: string[] = []; - - if (status === OrderStatus.COMPLETED) { - try { - const tradesUrl = `${COW_API_BASE}/${networkPath}/api/v1/trades?orderUid=${urlPathFriendlyOrderId}`; - const tradesResponse = await fetch(tradesUrl); - - if (tradesResponse.ok) { - const trades = await tradesResponse.json(); - allHashes = Array.isArray(trades) - ? trades - .map( - (t: { txHash?: string; transactionHash?: string }) => - t?.txHash || t?.transactionHash, - ) - .filter( - (h: unknown): h is string => - typeof h === 'string' && h.length > 0, - ) - : []; - txHash = allHashes[allHashes.length - 1]; - } - } catch (error) { - console.warn('Failed to fetch trade hashes:', error); - } - } - - const order: IntentOrder = { - id: orderId, - status, - txHash, - createdAt: new Date(data.creationDate).getTime(), - updatedAt: Date.now(), - metadata: { - ...data, - allHashes, - chainId, - networkPath, - }, - }; - - return order; - } catch (error) { - throw this.handleError(error as Error, 'getOrderStatus'); - } - } - - async cancelOrder(orderId: string, _chainId: number): Promise { - // CowSwap doesn't support order cancellation via API - // Orders expire naturally based on their validTo timestamp - console.warn( - `CowSwap orders cannot be cancelled via API. Order ${orderId} will expire naturally.`, - ); - return false; - } - - private mapCowSwapStatus(cowStatus: string): IntentOrderStatus { - switch (cowStatus) { - case 'presignaturePending': - case 'open': - return OrderStatus.PENDING; - case 'fulfilled': - return OrderStatus.COMPLETED; - case 'cancelled': - return OrderStatus.CANCELLED; - case 'expired': - return OrderStatus.EXPIRED; - default: - return OrderStatus.FAILED; - } - } -} diff --git a/packages/Intent-manager/src/providers/cowswap/index.ts b/packages/Intent-manager/src/providers/cowswap/index.ts deleted file mode 100644 index 3b30b1fa2ac..00000000000 --- a/packages/Intent-manager/src/providers/cowswap/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { CowSwapProvider } from './cowswap-provider'; -export { - COW_API_BASE, - COW_NETWORK_PATHS, - COW_SETTLEMENT_CONTRACT, - COWSWAP_PROVIDER_CONFIG, -} from './constants'; diff --git a/packages/Intent-manager/src/providers/index.ts b/packages/Intent-manager/src/providers/index.ts deleted file mode 100644 index 739b2148aab..00000000000 --- a/packages/Intent-manager/src/providers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BaseIntentProvider } from './base-intent-provider'; -export * from './cowswap'; diff --git a/packages/Intent-manager/src/types.ts b/packages/Intent-manager/src/types.ts deleted file mode 100644 index 1e79ee341ba..00000000000 --- a/packages/Intent-manager/src/types.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Intent status enumeration - */ -export enum IntentStatus { - PENDING = 'pending', - EXECUTING = 'executing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} - -/** - * Intent order status enumeration - more granular than IntentStatus - */ -export enum IntentOrderStatus { - PENDING = 'pending', - SUBMITTED = 'submitted', - CONFIRMED = 'confirmed', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', - EXPIRED = 'expired', -} - -/** - * Fee information for an intent - */ -export type IntentFee = { - type: 'network' | 'protocol' | 'bridge'; - amount: string; - token: string; -}; - -/** - * Quote response from an intent provider - */ -export type IntentQuote = { - id: string; - provider: string; - srcAmount: string; - destAmount: string; - estimatedGas: string; - estimatedTime: number; // seconds - priceImpact: number; - fees: IntentFee[]; - validUntil: number; // timestamp - metadata: Record; -}; - -/** - * Intent order information - */ -export type IntentOrder = { - id: string; - status: IntentOrderStatus; - txHash?: string; - createdAt: number; - updatedAt: number; - metadata: Record; -}; - -/** - * Parameters for submitting an intent order - */ -export type IntentSubmissionParams = { - quote: IntentQuote; - signature: string; - userAddress: string; -}; - -/** - * Configuration for an intent provider - */ -export type IntentProviderConfig = { - name: string; - version: string; - supportedChains: number[]; - apiBaseUrl: string; - features: string[]; - rateLimit?: { - requestsPerMinute: number; - burstLimit: number; - }; -}; - -/** - * Registry of intent providers - */ -export type ProviderRegistry = { - [providerName: string]: BaseIntentProvider; -}; - -/** - * Criteria for selecting intent providers - */ -export type ProviderSelectionCriteria = { - chainId: number; - tokenPair: [string, string]; - amount: string; - preferredProviders?: string[]; - excludedProviders?: string[]; -}; - -/** - * Base interface for intent providers - */ -export type BaseIntentProvider = { - getName(): string; - getVersion(): string; - getSupportedChains(): number[]; - submitOrder(params: IntentSubmissionParams): Promise; - getOrderStatus(orderId: string, chainId: number): Promise; - cancelOrder(orderId: string, chainId: number): Promise; -}; diff --git a/packages/Intent-manager/src/validators.ts b/packages/Intent-manager/src/validators.ts deleted file mode 100644 index fcb479444c1..00000000000 --- a/packages/Intent-manager/src/validators.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { isValidHexAddress } from '@metamask/controller-utils'; -import type { Infer } from '@metamask/superstruct'; -import { - string, - boolean, - number, - type, - optional, - enums, - define, - union, - pattern, -} from '@metamask/superstruct'; -import { isStrictHexString } from '@metamask/utils'; - -// Helper schemas for Intent validation -const HexAddressSchema = define('HexAddress', (v: unknown) => - isValidHexAddress(v as string, { allowNonPrefixed: false }), -); - -const HexStringSchema = define('HexString', (v: unknown) => - isStrictHexString(v as string), -); - -const TruthyDigitStringSchema = pattern(string(), /^\d+$/u); - -// Allow digit strings for amounts/validTo for flexibility across providers -const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); - -// Intent support (e.g., CoW Swap EIP-712 order signing) -const IntentProtocolSchema = enums(['cowswap']); - -export const IntentOrderSchema = type({ - // EIP-712 Order fields (subset required for signing/submission) - sellToken: HexAddressSchema, - buyToken: HexAddressSchema, - receiver: optional(HexAddressSchema), - validTo: DigitStringOrNumberSchema, - appData: string(), - appDataHash: HexStringSchema, - feeAmount: TruthyDigitStringSchema, - kind: enums(['sell', 'buy']), - partiallyFillable: boolean(), - // One of these is required by CoW depending on kind; we keep both optional here and rely on backend validation - sellAmount: optional(TruthyDigitStringSchema), - buyAmount: optional(TruthyDigitStringSchema), - // Optional owner/from for convenience when building domain/message - from: optional(HexAddressSchema), -}); - -export const IntentSchema = type({ - protocol: IntentProtocolSchema, - order: IntentOrderSchema, - // Optional metadata to aid submission/routing - settlementContract: optional(HexAddressSchema), - relayer: optional(HexAddressSchema), -}); - -// Export types for use in other modules -export type IntentOrder = Infer; -export type Intent = Infer; diff --git a/packages/Intent-manager/tsconfig.build.json b/packages/Intent-manager/tsconfig.build.json deleted file mode 100644 index 40ae7f7c4db..00000000000 --- a/packages/Intent-manager/tsconfig.build.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.packages.build.json", - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist", - "rootDir": "./src" - }, - "references": [{ "path": "../controller-utils/tsconfig.build.json" }], - "include": ["../../types", "./src"] -} diff --git a/packages/Intent-manager/tsconfig.json b/packages/Intent-manager/tsconfig.json deleted file mode 100644 index b05e3335c94..00000000000 --- a/packages/Intent-manager/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.packages.json", - "compilerOptions": { - "baseUrl": "./", - "resolveJsonModule": true - }, - "references": [], - "include": ["../../types", "./src"] -} diff --git a/packages/Intent-manager/typedoc.json b/packages/Intent-manager/typedoc.json deleted file mode 100644 index c9da015dbf8..00000000000 --- a/packages/Intent-manager/typedoc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "entryPoints": ["./src/index.ts"], - "excludePrivate": true, - "hideGenerator": true, - "out": "docs", - "tsconfig": "./tsconfig.build.json" -} From cf7c71fbbe3c94073e0cab6337124014b51375c9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Dec 2025 15:49:39 +0100 Subject: [PATCH 44/85] refactor: finish removing intent manager --- jest.config.packages.js | 3 -- .../bridge-controller/src/utils/validators.ts | 38 +++++++++++++++++-- tsconfig.build.json | 1 - tsconfig.packages.json | 1 - 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/jest.config.packages.js b/jest.config.packages.js index 0f390177aac..55c7d9aeeb0 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -83,9 +83,6 @@ module.exports = { '^@metamask/json-rpc-engine/v2$': [ '/../json-rpc-engine/src/v2/index.ts', ], - '^@metamask/intent-manager$': [ - '/../Intent-manager/src', - ], '^@metamask/(.+)$': [ '/../$1/src', // Some @metamask/* packages we are referencing aren't in this monorepo, diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 11976a4d819..20051e70f7b 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -1,5 +1,4 @@ import { isValidHexAddress } from '@metamask/controller-utils'; -import { IntentSchema } from '@metamask/intent-manager'; import type { Infer } from '@metamask/superstruct'; import { string, @@ -193,8 +192,41 @@ export const StepSchema = type({ const RefuelDataSchema = StepSchema; -// Re-export IntentSchema from intent-manager for backward compatibility -export { IntentSchema }; +// Allow digit strings for amounts/validTo for flexibility across providers +const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); + +// Intent support (e.g., CoW Swap EIP-712 order signing) +const IntentProtocolSchema = enums(['cowswap']); + +export const IntentOrderSchema = type({ + // EIP-712 Order fields (subset required for signing/submission) + sellToken: HexAddressSchema, + buyToken: HexAddressSchema, + receiver: optional(HexAddressSchema), + validTo: DigitStringOrNumberSchema, + appData: string(), + appDataHash: HexStringSchema, + feeAmount: TruthyDigitStringSchema, + kind: enums(['sell', 'buy']), + partiallyFillable: boolean(), + // One of these is required by CoW depending on kind; we keep both optional here and rely on backend validation + sellAmount: optional(TruthyDigitStringSchema), + buyAmount: optional(TruthyDigitStringSchema), + // Optional owner/from for convenience when building domain/message + from: optional(HexAddressSchema), +}); + +export const IntentSchema = type({ + protocol: IntentProtocolSchema, + order: IntentOrderSchema, + // Optional metadata to aid submission/routing + settlementContract: optional(HexAddressSchema), + relayer: optional(HexAddressSchema), +}); + +// Export types for use in other modules +export type IntentOrder = Infer; +export type Intent = Infer; export const QuoteSchema = type({ requestId: string(), diff --git a/tsconfig.build.json b/tsconfig.build.json index 8bd43319774..10a7c244911 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -39,7 +39,6 @@ { "path": "./packages/foundryup/tsconfig.build.json" }, { "path": "./packages/gas-fee-controller/tsconfig.build.json" }, { "path": "./packages/gator-permissions-controller/tsconfig.build.json" }, - { "path": "./packages/intent-manager/tsconfig.build.json" }, { "path": "./packages/json-rpc-engine/tsconfig.build.json" }, { "path": "./packages/json-rpc-middleware-stream/tsconfig.build.json" }, { "path": "./packages/keyring-controller/tsconfig.build.json" }, diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 1ccbab23b9e..600a1bf5bcc 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -13,7 +13,6 @@ */ "paths": { "@metamask/base-controller/next": ["../base-controller/src/next"], - "@metamask/intent-manager": ["../Intent-manager/dist/index"], "@metamask/json-rpc-engine/v2": ["../json-rpc-engine/src/v2/index.ts"], "@metamask/*": ["../*/src"] } From 0b7c1fda54abd665c73883d87f296d256f6ac493 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Dec 2025 11:16:47 +0100 Subject: [PATCH 45/85] fix: fix linter errors --- .../src/bridge-status-controller.ts | 77 ++++++++++--------- .../src/intent-api.ts | 37 ++++----- .../src/intent-order.ts | 4 +- .../bridge-status-controller/src/types.ts | 16 ++-- .../src/TransactionController.ts | 1 - 5 files changed, 70 insertions(+), 65 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 30277d5b3ec..23a8fb250ff 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -45,6 +45,9 @@ import { REFRESH_INTERVAL_MS, TraceName, } from './constants'; +import { IntentApiImpl } from './intent-api'; +import { IntentOrderStatus } from './intent-order-status'; +import type { IntentOrder } from './intent-order'; import type { BridgeStatusControllerState, StartPollingForBridgeTxStatusArgsSerialized, @@ -54,9 +57,6 @@ import type { } from './types'; import type { BridgeStatusControllerMessenger } from './types'; import { BridgeClientId } from './types'; -import { IntentApiImpl } from './intent-api'; -import { IntentOrderStatus } from './intent-order-status'; -import type { IntentOrder } from './intent-order'; import { fetchBridgeTxStatus, getStatusRequestWithSrcTxHash, @@ -273,7 +273,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #markTxAsFailed = ({ id }: TransactionMeta): void => { const txHistoryKey = this.state.txHistory[id] ? id : Object.keys(this.state.txHistory).find( @@ -287,7 +287,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + resetState = (): void => { this.update((state) => { state.txHistory = DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE.txHistory; }); @@ -299,7 +299,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + }): void => { // Wipe all networks for this address if (ignoreNetwork) { this.update((state) => { @@ -330,7 +330,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + }): void => { const { txMetaId, txHash } = identifier; if (!txMetaId && !txHash) { @@ -403,7 +403,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #restartPollingForIncompleteHistoryItems = (): void => { // Check for historyItems that do not have a status of complete and restart polling const { txHistory } = this.state; const historyItems = Object.values(txHistory); @@ -445,7 +445,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + ): void => { const { bridgeTxMeta, statusRequest, @@ -503,7 +503,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #startPollingForTxId = (txId: string): void => { // If we are already polling for this tx, stop polling for it before restarting const existingPollingToken = this.#pollingTokensByTxMetaId[txId]; if (existingPollingToken) { @@ -536,7 +536,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + ): void => { const { bridgeTxMeta } = txHistoryMeta; this.#addTxToHistory(txHistoryMeta); @@ -545,11 +545,13 @@ export class BridgeStatusController extends StaticIntervalPollingController { + _executePoll = async ( + pollingInput: BridgeStatusPollingInput, + ): Promise => { await this.#fetchBridgeTxStatus(pollingInput); }; - #getMultichainSelectedAccount(accountAddress: string) { + #getMultichainSelectedAccount(accountAddress: string): unknown { return this.messenger.call( 'AccountsController:getAccountByAddress', accountAddress, @@ -565,7 +567,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #handleFetchFailure = (bridgeTxMetaId: string): void => { const { attempts } = this.state.txHistory[bridgeTxMetaId]; const newAttempts = attempts @@ -593,7 +595,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + }: FetchBridgeTxStatusArgs): Promise => { const { txHistory } = this.state; // Intent-based items: poll intent provider instead of Bridge API @@ -703,7 +705,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + }: FetchBridgeTxStatusArgs): Promise => { const { txHistory } = this.state; const historyItem = txHistory[bridgeTxMetaId]; if (!historyItem) { @@ -720,7 +722,7 @@ export class BridgeStatusController extends StaticIntervalPollingController t.id === originalTxId, + (tx) => tx.id === originalTxId, ); - - if (!existingTxMeta) { - console.warn( - '📝 [fetchIntentOrderStatus] Skipping update; transaction not found', - { originalTxId, bridgeHistoryKey: bridgeTxMetaId }, - ); - } else { + if (existingTxMeta) { const updatedTxMeta: TransactionMeta = { ...existingTxMeta, status: transactionStatus, @@ -870,6 +866,11 @@ export class BridgeStatusController extends StaticIntervalPollingController t.id === txId); + const meta = transactions.find((tx: TransactionMeta) => tx.id === txId); if (meta) { // Treat both 'confirmed' and 'finalized' as success to match TC lifecycle @@ -1616,7 +1617,11 @@ export class BridgeStatusController extends StaticIntervalPollingController t.id === txMetaId, + (tx: TransactionMeta) => tx.id === txMetaId, ); const approvalTxMeta = transactions?.find( - (t: TransactionMeta) => t.id === historyItem.approvalTxId, + (tx: TransactionMeta) => tx.id === historyItem.approvalTxId, ); const requestParamProperties = getRequestParamFromHistory(historyItem); diff --git a/packages/bridge-status-controller/src/intent-api.ts b/packages/bridge-status-controller/src/intent-api.ts index c8506008bd5..fa7000e9c1f 100644 --- a/packages/bridge-status-controller/src/intent-api.ts +++ b/packages/bridge-status-controller/src/intent-api.ts @@ -1,38 +1,39 @@ import type { FetchFunction } from './types'; -export interface IntentSubmissionParams { +export type IntentSubmissionParams = { srcChainId: string; quoteId: string; signature: string; order: any; userAddress: string; aggregatorId: string; -} +}; -export interface IntentApi { +export type IntentApi = { submitIntent(params: IntentSubmissionParams): Promise; -} +}; export class IntentApiImpl implements IntentApi { - private baseUrl: string; - private fetchFn: FetchFunction; + readonly #baseUrl: string; + + readonly #fetchFn: FetchFunction; constructor(baseUrl: string, fetchFn: FetchFunction) { - this.baseUrl = baseUrl; - this.fetchFn = fetchFn; + this.#baseUrl = baseUrl; + this.#fetchFn = fetchFn; } - async submitIntent(params: IntentSubmissionParams): Promise { - const endpoint = `${this.baseUrl}/submitOrder`; + async submitIntent(params: IntentSubmissionParams): Promise { + const endpoint = `${this.#baseUrl}/submitOrder`; try { - const response = await this.fetchFn(endpoint, { + const response = await this.#fetchFn(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }); return response; - } catch (e) { - throw new Error(`Failed to submit intent: ${e}`); + } catch (err) { + throw new Error(`Failed to submit intent: ${err}`); } } @@ -40,15 +41,15 @@ export class IntentApiImpl implements IntentApi { orderId: string, aggregatorId: string, srcChainId: string, - ): Promise { - const endpoint = `${this.baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; + ): Promise { + const endpoint = `${this.#baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; try { - const response = await this.fetchFn(endpoint, { + const response = await this.#fetchFn(endpoint, { method: 'GET', }); return response; - } catch (e) { - throw new Error(`Failed to get order status: ${e}`); + } catch (err) { + throw new Error(`Failed to get order status: ${err}`); } } } diff --git a/packages/bridge-status-controller/src/intent-order.ts b/packages/bridge-status-controller/src/intent-order.ts index cb72a209860..946d26e2dda 100644 --- a/packages/bridge-status-controller/src/intent-order.ts +++ b/packages/bridge-status-controller/src/intent-order.ts @@ -1,10 +1,10 @@ import { IntentOrderStatus } from './intent-order-status'; -export interface IntentOrder { +export type IntentOrder = { id: string; status: IntentOrderStatus; txHash?: string; metadata: { txHashes?: string[] | string; }; -} +}; diff --git a/packages/bridge-status-controller/src/types.ts b/packages/bridge-status-controller/src/types.ts index 5583bcf1e6a..2fc1d069d30 100644 --- a/packages/bridge-status-controller/src/types.ts +++ b/packages/bridge-status-controller/src/types.ts @@ -147,14 +147,14 @@ export type BridgeHistoryItem = { }; export enum BridgeStatusAction { - START_POLLING_FOR_BRIDGE_TX_STATUS = 'startPollingForBridgeTxStatus', - WIPE_BRIDGE_STATUS = 'wipeBridgeStatus', - GET_STATE = 'getState', - RESET_STATE = 'resetState', - SUBMIT_TX = 'submitTx', - SUBMIT_INTENT = 'submitIntent', - RESTART_POLLING_FOR_FAILED_ATTEMPTS = 'restartPollingForFailedAttempts', - GET_BRIDGE_HISTORY_ITEM_BY_TX_META_ID = 'getBridgeHistoryItemByTxMetaId', + START_POLLING_FOR_BRIDGE_TX_STATUS = 'StartPollingForBridgeTxStatus', + WIPE_BRIDGE_STATUS = 'WipeBridgeStatus', + GET_STATE = 'GetState', + RESET_STATE = 'ResetState', + SUBMIT_TX = 'SubmitTx', + SUBMIT_INTENT = 'SubmitIntent', + RESTART_POLLING_FOR_FAILED_ATTEMPTS = 'RestartPollingForFailedAttempts', + GET_BRIDGE_HISTORY_ITEM_BY_TX_META_ID = 'GetBridgeHistoryItemByTxMetaId', } export type TokenAmountValuesSerialized = { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 58430305514..08431d47ff0 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1239,7 +1239,6 @@ export class TransactionController extends BaseController< requireApproval, securityAlertResponse, sendFlowHistory, - skipInitialGasEstimate, swaps = {}, traceContext, type, From 5c507fe065762a4120c76b81390a1a878ed8049b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Tue, 9 Dec 2025 16:14:18 +0100 Subject: [PATCH 46/85] feat: remove unnecessary intent check when adding a transaction --- .../transaction-controller/src/TransactionController.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 52d37831c3b..68b3e967ad7 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1272,6 +1272,7 @@ export class TransactionController extends BaseController< requireApproval, securityAlertResponse, sendFlowHistory, + skipInitialGasEstimate, swaps = {}, traceContext, type, @@ -1389,11 +1390,6 @@ export class TransactionController extends BaseController< updateTransaction(addedTransactionMeta); } - // Skip gas estimation for intent transactions as they are not executed on-chain - // const isIntentTransaction = swaps?.meta?.swapMetaData?.isIntentTx === true; - - // if (!isIntentTransaction) { - // eslint-disable-next-line no-negated-condition if (!skipInitialGasEstimate) { await this.#trace( { name: 'Estimate Gas Properties', parentContext: traceContext }, From 3bd6a6f80cacc24b9ffe3938cf884939946180b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 13:50:45 +0100 Subject: [PATCH 47/85] feat: remove useless linebreak --- packages/bridge-controller/src/utils/fetch.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 091af69e616..5a391c777f3 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -98,7 +98,6 @@ const formatQueryParams = (request: GenericQuoteRequest): URLSearchParams => { Object.entries(normalizedRequest).forEach(([key, value]) => { queryParams.append(key, value.toString()); }); - return queryParams; }; From 433ac447a129eb4dfadb39d64b1a6506d42f6552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 13:53:33 +0100 Subject: [PATCH 48/85] feat: remove unused intent manager ref in tsconfig files --- packages/bridge-controller/tsconfig.json | 1 - packages/bridge-status-controller/tsconfig.json | 1 - tsconfig.json | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json index 3bd844d4f61..5a49183c433 100644 --- a/packages/bridge-controller/tsconfig.json +++ b/packages/bridge-controller/tsconfig.json @@ -6,7 +6,6 @@ }, "references": [ { "path": "../accounts-controller" }, - { "path": "../Intent-manager" }, { "path": "../base-controller" }, { "path": "../controller-utils" }, { "path": "../network-controller" }, diff --git a/packages/bridge-status-controller/tsconfig.json b/packages/bridge-status-controller/tsconfig.json index a5f987b52eb..e41150bdaf3 100644 --- a/packages/bridge-status-controller/tsconfig.json +++ b/packages/bridge-status-controller/tsconfig.json @@ -9,7 +9,6 @@ { "path": "../base-controller" }, { "path": "../bridge-controller" }, { "path": "../controller-utils" }, - { "path": "../Intent-manager" }, { "path": "../network-controller" }, { "path": "../polling-controller" }, { "path": "../transaction-controller" }, diff --git a/tsconfig.json b/tsconfig.json index cb59fbd7d46..34a9641f246 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "noEmit": true }, "references": [ - { "path": "./packages/Intent-manager" }, { "path": "./packages/account-tree-controller" }, { "path": "./packages/accounts-controller" }, { "path": "./packages/address-book-controller" }, From 1dde2ef99ce8d7c54a148120249021a57eef0867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 14:27:58 +0100 Subject: [PATCH 49/85] feat: move changelog entry to unreleased section --- packages/bridge-status-controller/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 63ea45529f0..16df3e40470 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,11 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING** Use CrossChain API instead of the intent manager package for intent order submission ([#6963](https://github.com/MetaMask/core/pull/6963)) + ## [64.1.0] ### Changed -- **BREAKING** Use CrossChain API instead of the intent manager package for intent order submission ([#6963](https://github.com/MetaMask/core/pull/6963)) - Bump `@metamask/bridge-controller` from `^64.0.0` to `^64.1.0` ([#7422](https://github.com/MetaMask/core/pull/7422)) - Bump `@metamask/transaction-controller` from `^62.4.0` to `^62.5.0` ([#7325](https://github.com/MetaMask/core/pull/7325)) From 84920fda142476a8906c5d45ad3be2a4741bdf26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 14:47:42 +0100 Subject: [PATCH 50/85] feat: update tx controller changelog --- packages/transaction-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 75f7eb34515..17f3944dd70 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add optional `isTimeoutEnabled` callback to disable for specific transactions. - Ignores transactions with future nonce. - Threshold determined by feature flag. +- Add intent based transaction support ([#6963](https://github.com/MetaMask/core/pull/6963)) ### Changed From 512aae9924bd644cbe0ab94957b8e831634855b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 15:57:01 +0100 Subject: [PATCH 51/85] fix: typo in bridge controller changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index b6e1126fa9f..6e8661b75cb 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -11,8 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/assets-controllers` from `^93.0.0` to `^93.1.0` ([#7309](https://github.com/MetaMask/core/pull/7309) -- Bump `@metamask/remote-feature-flag-controller` from `^2.0.1` to `^3.0.0` ([#7309](https://github.com/MetaMask/core/pull/7309) +- Bump `@metamask/assets-controllers` from `^93.0.0` to `^93.1.0` ([#7309](https://github.com/MetaMask/core/pull/7309)) +- Bump `@metamask/remote-feature-flag-controller` from `^2.0.1` to `^3.0.0` ([#7309](https://github.com/MetaMask/core/pull/7309)) - Bump `@metamask/transaction-controller` from `^62.4.0` to `^62.5.0` ([#7325](https://github.com/MetaMask/core/pull/7325)) ### Fixed From 38e5ace68c15715138454306fea403b4a6e5e83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 16:21:06 +0100 Subject: [PATCH 52/85] fix: move tx controller changelog to proper section --- packages/transaction-controller/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index c65d750530a..272488ce9aa 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add intent based transaction support ([#6963](https://github.com/MetaMask/core/pull/6963)) + ## [62.6.0] ### Added @@ -17,7 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add optional `isTimeoutEnabled` callback to disable for specific transactions. - Ignores transactions with future nonce. - Threshold determined by feature flag. -- Add intent based transaction support ([#6963](https://github.com/MetaMask/core/pull/6963)) ### Changed From ff233d77f3a6a3551e9049ac988608108f216df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 16:25:46 +0100 Subject: [PATCH 53/85] feat; update bridge ctrler changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index a80e101b608..a3509013c9f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add intent based transaction support ([#6963](https://github.com/MetaMask/core/pull/6963)) + ### Changed - Bump `@metamask/transaction-controller` from `^62.5.0` to `^62.6.0` ([#7430](https://github.com/MetaMask/core/pull/7430)) From 4a32dec2dee5c0613816869a63f29662eb88b6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 10 Dec 2025 16:29:47 +0100 Subject: [PATCH 54/85] fix: wrong link for changelog PRs --- packages/bridge-controller/CHANGELOG.md | 2 +- packages/bridge-status-controller/CHANGELOG.md | 2 +- packages/transaction-controller/CHANGELOG.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index a3509013c9f..b618c85acdf 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add intent based transaction support ([#6963](https://github.com/MetaMask/core/pull/6963)) +- Add intent based transaction support ([#6547](https://github.com/MetaMask/core/pull/6547)) ### Changed diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 44cfda64524..99350a29c66 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING** Use CrossChain API instead of the intent manager package for intent order submission ([#6963](https://github.com/MetaMask/core/pull/6963)) +- **BREAKING** Use CrossChain API instead of the intent manager package for intent order submission ([#6547](https://github.com/MetaMask/core/pull/6547)) - Bump `@metamask/transaction-controller` from `^62.5.0` to `^62.6.0` ([#7430](https://github.com/MetaMask/core/pull/7430)) ## [64.1.0] diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 272488ce9aa..de46f1ccf68 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add intent based transaction support ([#6963](https://github.com/MetaMask/core/pull/6963)) +- Add intent based transaction support ([#6547](https://github.com/MetaMask/core/pull/6547)) ## [62.6.0] From 0accdc830d63110d00b10029335ff659f8cca036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Thu, 11 Dec 2025 14:43:37 +0100 Subject: [PATCH 55/85] feat: remove unused declaration file --- packages/build-utils/src/index.d.ts | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 packages/build-utils/src/index.d.ts diff --git a/packages/build-utils/src/index.d.ts b/packages/build-utils/src/index.d.ts deleted file mode 100644 index 0e5dc2315ed..00000000000 --- a/packages/build-utils/src/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { FeatureLabels } from './transforms/remove-fenced-code'; -export { removeFencedCode } from './transforms/remove-fenced-code'; -export { lintTransformedFile } from './transforms/utils'; From 27638559b6edc04c465cc3d20bb0fdd89767bcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Thu, 11 Dec 2025 15:40:20 +0100 Subject: [PATCH 56/85] feat: remove unwanted changes --- scripts/generate-method-action-types.ts | 32 ++++++++++++------------- tsconfig.packages.json | 1 - 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/scripts/generate-method-action-types.ts b/scripts/generate-method-action-types.ts index ca173132fe0..67172a430c6 100755 --- a/scripts/generate-method-action-types.ts +++ b/scripts/generate-method-action-types.ts @@ -39,7 +39,7 @@ type CommandLineArguments = { * @returns The command line arguments. */ async function parseCommandLineArguments(): Promise { - const argv = await yargs(process.argv.slice(2)) + const { check, fix } = await yargs(process.argv.slice(2)) .option('check', { type: 'boolean', description: 'Check if generated action type files are up to date', @@ -51,15 +51,14 @@ async function parseCommandLineArguments(): Promise { default: false, }) .help() - .check((args) => { - if (!args.check && !args.fix) { + .check((argv) => { + if (!argv.check && !argv.fix) { throw new Error('Either --check or --fix must be provided.\n'); } return true; - }) - .parseAsync(); + }).argv; - return { check: argv.check, fix: argv.fix }; + return { check, fix }; } /** @@ -283,7 +282,6 @@ function createASTVisitor(context: VisitorContext) { if (ts.isVariableStatement(node)) { const declaration = node.declarationList.declarations[0]; if ( - declaration && ts.isIdentifier(declaration.name) && declaration.name.text === 'MESSENGER_EXPOSED_METHODS' ) { @@ -410,7 +408,7 @@ function extractJSDoc( } const jsDoc = jsDocTags[0]; - if (jsDoc && ts.isJSDoc(jsDoc)) { + if (ts.isJSDoc(jsDoc)) { const fullText = sourceFile.getFullText(); const start = jsDoc.getFullStart(); const end = jsDoc.getEnd(); @@ -439,17 +437,17 @@ function formatJSDoc(rawJsDoc: string): string { } else if (i === lines.length - 1) { // Last line should be */ formattedLines.push(' */'); - } else if (typeof line === 'undefined') { - // Middle lines should start with ' * ' - formattedLines.push(' *'); - } else if (line.trim().startsWith('*')) { - // Remove existing * and normalize - const content = line.trim().substring(1).trim(); - formattedLines.push(content ? ` * ${content}` : ' *'); } else { - // Handle lines that don't start with * + // Middle lines should start with ' * ' const trimmed = line.trim(); - formattedLines.push(trimmed ? ` * ${trimmed}` : ' *'); + if (trimmed.startsWith('*')) { + // Remove existing * and normalize + const content = trimmed.substring(1).trim(); + formattedLines.push(content ? ` * ${content}` : ' *'); + } else { + // Handle lines that don't start with * + formattedLines.push(trimmed ? ` * ${trimmed}` : ' *'); + } } } diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 600a1bf5bcc..fbde19a5981 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -12,7 +12,6 @@ * `jest.config.packages.js`. */ "paths": { - "@metamask/base-controller/next": ["../base-controller/src/next"], "@metamask/json-rpc-engine/v2": ["../json-rpc-engine/src/v2/index.ts"], "@metamask/*": ["../*/src"] } From b1888d8d8e5650e1da5cf9fc6ab8e68224f0635d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Thu, 11 Dec 2025 15:42:07 +0100 Subject: [PATCH 57/85] feat: add back package resolutions --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 43a846bfa88..38ed10d8779 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,11 @@ "simple-git-hooks": { "pre-push": "yarn lint" }, + "resolutions": { + "elliptic@6.5.4": "^6.5.7", + "fast-xml-parser@^4.3.4": "^4.4.1", + "ws@7.4.6": "^7.5.10" + }, "devDependencies": { "@babel/core": "^7.23.5", "@babel/plugin-transform-modules-commonjs": "^7.23.3", From e6740ecf32c1bcfaae58d4c50c5fd5785b5eb2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Thu, 11 Dec 2025 15:44:04 +0100 Subject: [PATCH 58/85] feat: remove wrongly added eslint script --- scripts/run-eslint.ts | 524 ------------------------------------------ 1 file changed, 524 deletions(-) delete mode 100644 scripts/run-eslint.ts diff --git a/scripts/run-eslint.ts b/scripts/run-eslint.ts deleted file mode 100644 index e8bb2a6bb50..00000000000 --- a/scripts/run-eslint.ts +++ /dev/null @@ -1,524 +0,0 @@ -import chalk from 'chalk'; -import { ESLint } from 'eslint'; -import fs from 'fs'; -import path from 'path'; -import yargs from 'yargs'; - -const PROJECT_DIRECTORY = path.resolve(__dirname, '..'); - -const WARNING_THRESHOLDS_FILE = path.join( - PROJECT_DIRECTORY, - 'eslint-warning-thresholds.json', -); - -/** - * The parsed command-line arguments. - */ -type CommandLineArguments = { - /** - * Whether to cache results to speed up future runs (true) or not (false). - */ - cache: boolean; - /** - * A list of specific files to lint. - */ - files: string[]; - /** - * Whether to automatically fix lint errors (true) or not (false). - */ - fix: boolean; - /** - * Whether to only report errors, disabling the warnings quality gate in the - * process (true) or not (false). - */ - quiet: boolean; -}; - -/** - * A two-level object mapping path to files in which warnings appear to the IDs - * of rules for those warnings, then from rule IDs to the number of warnings for - * the rule. - * - * @example - * ``` ts - * { - * "foo.ts": { - * "rule1": 3, - * "rule2": 4 - * }, - * "bar.ts": { - * "rule3": 17, - * "rule4": 5 - * } - * } - * ``` - */ -type WarningCounts = Record>; - -/** - * An object indicating the difference in warnings for a specific rule. - */ -type WarningComparison = { - /** The file path of the ESLint rule. */ - filePath: string; - /** The ID of the ESLint rule. */ - ruleId: string; - /** The previous count of warnings for the rule. */ - threshold: number; - /** The current count of warnings for the rule. */ - count: number; - /** The difference between the count and the threshold for the rule. */ - difference: number; -}; - -/** - * The severity level for an ESLint message. - */ -const ESLintMessageSeverity = { - Warning: 1, - Error: 2, -} as const; - -/** - * The result of applying the quality gate. - */ -const QualityGateStatus = { - /** - * The number of lint warnings increased. - */ - Increase: 'increase', - /** - * The number of lint warnings decreased. - */ - Decrease: 'decrease', - /** - * There was no change to the number of lint warnings. - */ - NoChange: 'no-change', - /** - * The warning thresholds file did not previously exist. - */ - Initialized: 'initialized', -} as const; - -/** - * The result of applying the quality gate. - */ -type QualityGateStatus = - (typeof QualityGateStatus)[keyof typeof QualityGateStatus]; - -// Run the script. -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); - -/** - * The entrypoint to this script. - */ -async function main() { - const { - cache, - fix, - files: givenFiles, - quiet, - } = await parseCommandLineArguments(); - - const eslint = new ESLint({ - cache, - errorOnUnmatchedPattern: false, - fix, - ruleFilter: ({ severity }) => - !quiet || severity === ESLintMessageSeverity.Error, - }); - - const fileFilteredResults = await eslint.lintFiles( - givenFiles.length > 0 ? givenFiles : ['.'], - ); - - const filteredResults = quiet - ? ESLint.getErrorResults(fileFilteredResults) - : fileFilteredResults; - - await printResults(eslint, filteredResults); - - if (fix) { - await ESLint.outputFixes(filteredResults); - } - const hasErrors = filteredResults.some((result) => result.errorCount > 0); - - const qualityGateStatus = applyWarningThresholdsQualityGate(filteredResults); - - if (hasErrors || qualityGateStatus === QualityGateStatus.Increase) { - process.exitCode = 1; - } -} - -/** - * Uses `yargs` to parse the arguments given to the script. - * - * @returns The parsed arguments. - */ -async function parseCommandLineArguments(): Promise { - const argv = await yargs(process.argv.slice(2)) - .option('cache', { - type: 'boolean', - description: 'Cache results to speed up future runs', - default: false, - }) - .option('fix', { - type: 'boolean', - description: - 'Automatically fix all problems; pair with --quiet to only fix errors', - default: false, - }) - .option('quiet', { - type: 'boolean', - description: 'Only report or fix errors', - default: false, - }) - .help() - .string('_') - .parseAsync(); - - const files = argv._ as string[]; - - return { - cache: argv.cache, - fix: argv.fix, - quiet: argv.quiet, - files, - }; -} - -/** - * Uses the given results to print the output that `eslint` usually generates. - * - * @param eslint - The ESLint instance. - * @param results - The results from running `eslint`. - */ -async function printResults( - eslint: ESLint, - results: ESLint.LintResult[], -): Promise { - const formatter = await eslint.loadFormatter('stylish'); - const resultText = await formatter.format(results); - if (resultText.length > 0) { - console.log(resultText); - } -} - -/** - * This function represents the ESLint warnings quality gate, which will cause - * linting to pass or fail depending on how many new warnings have been - * produced. - * - * - If we have no record of warnings from a previous run, then we simply - * capture the new warnings in a file and continue. - * - If we have a record of warnings from a previous run and there are any - * changes to the number of warnings overall, then we list which ESLint rules - * had increases and decreases. If are were more warnings overall then we fail, - * otherwise we pass. - * - * @param results - The results from running `eslint`. - * @returns True if the number of warnings has increased compared to the - * existing number of warnings, false if they have decreased or stayed the same. - */ -function applyWarningThresholdsQualityGate( - results: ESLint.LintResult[], -): QualityGateStatus { - const warningThresholds = loadWarningThresholds(); - const warningCounts = getWarningCounts(results); - - const completeWarningCounts = removeFilesWithoutWarnings({ - ...warningThresholds, - ...warningCounts, - }); - - let status; - - if (Object.keys(warningThresholds).length === 0) { - console.log( - chalk.blue( - 'The following lint violations were produced and will be captured as thresholds for future runs:\n', - ), - ); - - for (const [filePath, ruleCounts] of Object.entries( - completeWarningCounts, - )) { - console.log(chalk.underline(filePath)); - for (const [ruleId, count] of Object.entries(ruleCounts)) { - console.log(` ${chalk.cyan(ruleId)}: ${count}`); - } - } - - saveWarningThresholds(completeWarningCounts); - - status = QualityGateStatus.Initialized; - } else { - const comparisonsByFile = compareWarnings( - warningThresholds, - completeWarningCounts, - ); - - const changes = Object.values(comparisonsByFile) - .flat() - .filter((comparison) => comparison.difference !== 0); - const regressions = Object.values(comparisonsByFile) - .flat() - .filter((comparison) => comparison.difference > 0); - - if (changes.length > 0) { - if (regressions.length > 0) { - console.log( - chalk.red( - '🛑 New lint violations have been introduced and need to be resolved for linting to pass:\n', - ), - ); - - for (const [filePath, fileChanges] of Object.entries( - comparisonsByFile, - )) { - if (fileChanges.some((fileChange) => fileChange.difference > 0)) { - console.log(chalk.underline(filePath)); - for (const { - ruleId, - threshold, - count, - difference, - } of fileChanges) { - if (difference > 0) { - console.log( - ` ${chalk.cyan(ruleId)}: ${threshold} -> ${count} (${difference > 0 ? chalk.red(`+${difference}`) : chalk.green(difference)})`, - ); - } - } - } - } - - status = QualityGateStatus.Increase; - } else { - console.log( - chalk.green( - 'The overall number of lint warnings has decreased, good work! ❤️ \n', - ), - ); - - for (const [filePath, fileChanges] of Object.entries( - comparisonsByFile, - )) { - if (fileChanges.some((fileChange) => fileChange.difference !== 0)) { - console.log(chalk.underline(filePath)); - for (const { - ruleId, - threshold, - count, - difference, - } of fileChanges) { - if (difference !== 0) { - console.log( - ` ${chalk.cyan(ruleId)}: ${threshold} -> ${count} (${difference > 0 ? chalk.red(`+${difference}`) : chalk.green(difference)})`, - ); - } - } - } - } - - console.log( - `\n${chalk.yellow.bold(path.basename(WARNING_THRESHOLDS_FILE))}${chalk.yellow(' has been updated with the new counts. Please make sure to commit the changes.')}`, - ); - - saveWarningThresholds(completeWarningCounts); - - status = QualityGateStatus.Decrease; - } - } else { - status = QualityGateStatus.NoChange; - } - } - - return status; -} - -/** - * Removes properties from the given warning counts object that have no warnings. - * - * @param warningCounts - The warning counts. - * @returns The transformed warning counts. - */ -function removeFilesWithoutWarnings(warningCounts: WarningCounts) { - return Object.entries(warningCounts).reduce( - (newWarningCounts: WarningCounts, [filePath, warnings]) => { - if (Object.keys(warnings).length === 0) { - return newWarningCounts; - } - return { ...newWarningCounts, [filePath]: warnings }; - }, - {}, - ); -} - -/** - * Loads previous warning thresholds from a file. - * - * @returns The warning thresholds loaded from file. - */ -function loadWarningThresholds(): WarningCounts { - if (fs.existsSync(WARNING_THRESHOLDS_FILE)) { - const data = fs.readFileSync(WARNING_THRESHOLDS_FILE, 'utf-8'); - return JSON.parse(data); - } - return {}; -} - -/** - * Saves current warning counts to a file so they can be referenced in a future - * run. - * - * @param newWarningCounts - The new warning thresholds to save. - */ -function saveWarningThresholds(newWarningCounts: WarningCounts): void { - fs.writeFileSync( - WARNING_THRESHOLDS_FILE, - `${JSON.stringify(newWarningCounts, null, 2)}\n`, - 'utf-8', - ); -} - -/** - * Given a list of results from an the ESLint run, counts the number of warnings - * produced per file and rule. - * - * @param results - The results from running `eslint`. - * @returns A two-level object mapping path to files in which warnings appear to - * the IDs of rules for those warnings, then from rule IDs to the number of - * warnings for the rule. - */ -function getWarningCounts(results: ESLint.LintResult[]): WarningCounts { - const unsortedWarningCounts = results.reduce( - (workingWarningCounts, result) => { - const { filePath } = result; - const relativeFilePath = path.relative(PROJECT_DIRECTORY, filePath); - if (!workingWarningCounts[relativeFilePath]) { - workingWarningCounts[relativeFilePath] = {}; - } - for (const message of result.messages) { - if ( - message.severity === ESLintMessageSeverity.Warning && - message.ruleId - ) { - workingWarningCounts[relativeFilePath][message.ruleId] = - (workingWarningCounts[relativeFilePath][message.ruleId] ?? 0) + 1; - } - } - return workingWarningCounts; - }, - {} as WarningCounts, - ); - - const sortedWarningCounts: WarningCounts = {}; - for (const filePath of Object.keys(unsortedWarningCounts).sort()) { - // We can safely assume this property is present. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const unsortedWarningCountsForFile = unsortedWarningCounts[filePath]!; - sortedWarningCounts[filePath] = Object.keys(unsortedWarningCountsForFile) - .sort(sortRules) - .reduce( - (acc, ruleId) => { - // We can safely assume this property is present. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - acc[ruleId] = unsortedWarningCountsForFile[ruleId]!; - return acc; - }, - {} as Record, - ); - } - return sortedWarningCounts; -} - -/** - * Compares previous and current warning counts. - * - * @param warningThresholds - The previously recorded warning thresholds - * (organized by file and then rule). - * @param warningCounts - The current warning counts (organized by file and then - * rule). - * @returns An object mapping file paths to arrays of objects indicating - * comparisons in warnings. - */ -function compareWarnings( - warningThresholds: WarningCounts, - warningCounts: WarningCounts, -): Record { - const comparisons: Record = {}; - const filePaths = Array.from( - new Set([...Object.keys(warningThresholds), ...Object.keys(warningCounts)]), - ); - - for (const filePath of filePaths) { - const ruleIds = Array.from( - new Set([ - ...Object.keys(warningThresholds[filePath] || {}), - ...Object.keys(warningCounts[filePath] || {}), - ]), - ); - - comparisons[filePath] = ruleIds - .map((ruleId) => { - const threshold = warningThresholds[filePath]?.[ruleId] ?? 0; - const count = warningCounts[filePath]?.[ruleId] ?? 0; - const difference = count - threshold; - return { filePath, ruleId, threshold, count, difference }; - }) - .sort((a, b) => sortRules(a.ruleId, b.ruleId)); - } - - return Object.keys(comparisons) - .sort() - .reduce( - (sortedComparisons, filePath) => { - // We can safely assume this property is present. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - sortedComparisons[filePath] = comparisons[filePath]!; - return sortedComparisons; - }, - {} as Record, - ); -} - -/** - * Sorts rule IDs, ensuring that rules with namespaces appear before rules - * without. - * - * @param ruleIdA - The first rule ID. - * @param ruleIdB - The second rule ID. - * @returns A negative number if the first rule ID should come before the - * second, a positive number if the first should come _after_ the second, or 0 - * if they should stay where they are. - * @example - * ``` typescript - * sortRules( - * '@typescript-eslint/naming-convention', - * '@typescript-eslint/explicit-function-return-type' - * ) //=> 1 (sort A after B) - * sortRules( - * 'explicit-function-return-type', - * '@typescript-eslint/naming-convention' - * ) //=> 1 (sort A after B) - */ -function sortRules(ruleIdA: string, ruleIdB: string): number { - const [namespaceA, ruleA] = ruleIdA.includes('/') - ? ruleIdA.split('/') - : ['', ruleIdA]; - const [namespaceB, ruleB] = ruleIdB.includes('/') - ? ruleIdB.split('/') - : ['', ruleIdB]; - if (namespaceA && !namespaceB) { - return -1; - } - if (!namespaceA && namespaceB) { - return 1; - } - return namespaceA.localeCompare(namespaceB) || ruleA.localeCompare(ruleB); -} From 737ca5840882a0e13b906800536a35d6aead71dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Fri, 12 Dec 2025 14:54:15 +0100 Subject: [PATCH 59/85] feat: update lock file --- yarn.lock | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index ec439228ae9..a0b216ad6ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14310,21 +14310,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:7.4.6": - version: 7.4.6 - resolution: "ws@npm:7.4.6" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/150e3f917b7cde568d833a5ea6ccc4132e59c38d04218afcf2b6c7b845752bd011a9e0dc1303c8694d3c402a0bdec5893661a390b71ff88f0fc81a4e4e66b09c - languageName: node - linkType: hard - "ws@npm:8.17.1": version: 8.17.1 resolution: "ws@npm:8.17.1" @@ -14340,7 +14325,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.4.6": +"ws@npm:^7.4.6, ws@npm:^7.5.10": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: From c8bfce88215f67422835cfdc4bd4fc57afae551b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Mon, 15 Dec 2025 14:51:51 +0100 Subject: [PATCH 60/85] fix: type issue with bridge status controller --- .../src/bridge-status-controller.ts | 87 ++++++++++++------- .../src/intent-api.ts | 16 ++-- .../bridge-status-controller/src/types.ts | 16 ++-- 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 23a8fb250ff..ea87ffddb24 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1,6 +1,7 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import type { StateMetadata } from '@metamask/base-controller'; import type { + ChainId, QuoteMetadata, RequiredEventContextFromClient, TxData, @@ -46,8 +47,8 @@ import { TraceName, } from './constants'; import { IntentApiImpl } from './intent-api'; -import { IntentOrderStatus } from './intent-order-status'; import type { IntentOrder } from './intent-order'; +import { IntentOrderStatus } from './intent-order-status'; import type { BridgeStatusControllerState, StartPollingForBridgeTxStatusArgsSerialized, @@ -728,11 +729,11 @@ export class BridgeStatusController extends StaticIntervalPollingController tx.id === originalTxId, + const existingTxMeta = transactions.find( + (tx: TransactionMeta) => tx.id === originalTxId, ); if (existingTxMeta) { const updatedTxMeta: TransactionMeta = { @@ -926,7 +927,10 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #updateSrcTxHash = ( + bridgeTxMetaId: string, + srcTxHash: string, + ): void => { const { txHistory } = this.state; if (txHistory[bridgeTxMetaId].status.srcChain.txHash) { return; @@ -942,7 +946,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + ): void => { const sourceTxMetaIdsToDelete = Object.keys(this.state.txHistory).filter( (txMetaId) => { const bridgeHistoryItem = this.state.txHistory[txMetaId]; @@ -1000,7 +1004,7 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, selectedAccount: AccountsControllerState['internalAccounts']['accounts'][string], - ) => { + ): Promise => { if (!selectedAccount.metadata?.snap?.id) { throw new Error( 'Failed to submit cross-chain swap transaction: undefined snap id', @@ -1109,7 +1113,7 @@ export class BridgeStatusController extends StaticIntervalPollingController => { if (approval) { - const approveTx = async () => { + const approveTx = async (): Promise => { await this.#handleUSDTAllowanceReset(resetApproval); const approvalTxMeta = await this.#handleEvmTransaction({ @@ -1221,7 +1225,9 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #handleUSDTAllowanceReset = async ( + resetApproval?: TxData, + ): Promise => { if (resetApproval) { await this.#handleEvmTransaction({ transactionType: TransactionType.bridgeApproval, @@ -1235,7 +1241,11 @@ export class BridgeStatusController extends StaticIntervalPollingController { + ): Promise<{ + maxFeePerGas: Hex; + maxPriorityFeePerGas: Hex; + gas: Hex; + }> => { const maxGasLimit = toHex(transactionParams.gas ?? 0); // If txFee is provided (gasIncluded case), use the quote's gas fees @@ -1285,7 +1295,10 @@ export class BridgeStatusController extends StaticIntervalPollingController[0], 'messenger' | 'estimateGasFeeFn' >, - ) => { + ): Promise<{ + approvalMeta?: TransactionMeta; + tradeMeta: TransactionMeta; + }> => { const transactionParams = await getAddTransactionBatchParams({ messenger: this.messenger, estimateGasFeeFn: this.#estimateGasFeeFn, @@ -1665,7 +1678,9 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, intent: Intent, - ) { + ): { + id: string; + provider: string; + srcAmount: string; + destAmount: string; + estimatedGas: string; + estimatedTime: number; + priceImpact: number; + fees: unknown[]; + validUntil: number; + metadata: { + order: unknown; + settlementContract: string; + chainId: ChainId; + bridgeQuote: QuoteResponse & QuoteMetadata; + }; + } { return { id: `bridge-${Date.now()}`, provider: intent.protocol, @@ -1817,7 +1848,7 @@ export class BridgeStatusController extends StaticIntervalPollingController( - eventName: T, + eventName: EventName, txMetaId?: string, - eventProperties?: Pick[T], - ) => { + eventProperties?: Pick< + RequiredEventContextFromClient, + EventName + >[EventName], + ): void => { const baseProperties = { action_type: MetricsActionType.SWAPBRIDGE_V1, ...(eventProperties ?? {}), @@ -1904,21 +1938,16 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Mon, 15 Dec 2025 15:13:13 +0100 Subject: [PATCH 61/85] fix: more types in bridge status controller --- .../src/bridge-status-controller.ts | 6 +++++- .../bridge-status-controller/src/intent-api.ts | 2 +- packages/bridge-status-controller/src/types.ts | 16 ++++++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index ea87ffddb24..218bae97f43 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -552,7 +552,11 @@ export class BridgeStatusController extends StaticIntervalPollingController; + submitIntent(params: IntentSubmissionParams): Promise; }; export class IntentApiImpl implements IntentApi { diff --git a/packages/bridge-status-controller/src/types.ts b/packages/bridge-status-controller/src/types.ts index ee5936278e1..e12bb4eab5e 100644 --- a/packages/bridge-status-controller/src/types.ts +++ b/packages/bridge-status-controller/src/types.ts @@ -31,7 +31,7 @@ import type { import type { CaipAssetType } from '@metamask/utils'; import type { BridgeStatusController } from './bridge-status-controller'; -import type { BRIDGE_STATUS_CONTROLLER_NAME } from './constants'; +import { BRIDGE_STATUS_CONTROLLER_NAME } from './constants'; import type { StatusResponseSchema } from './utils/validators'; // All fields need to be types not interfaces, same with their children fields @@ -240,25 +240,25 @@ export type BridgeStatusControllerGetStateAction = ControllerGetStateAction< // Maps to BridgeController function names export type BridgeStatusControllerStartPollingForBridgeTxStatusAction = - BridgeStatusControllerAction; + BridgeStatusControllerAction<'startPollingForBridgeTxStatus'>; export type BridgeStatusControllerWipeBridgeStatusAction = - BridgeStatusControllerAction; + BridgeStatusControllerAction<'wipeBridgeStatus'>; export type BridgeStatusControllerResetStateAction = - BridgeStatusControllerAction; + BridgeStatusControllerAction<'resetState'>; export type BridgeStatusControllerSubmitTxAction = - BridgeStatusControllerAction; + BridgeStatusControllerAction<'submitTx'>; export type BridgeStatusControllerSubmitIntentAction = - BridgeStatusControllerAction; + BridgeStatusControllerAction<'submitIntent'>; export type BridgeStatusControllerRestartPollingForFailedAttemptsAction = - BridgeStatusControllerAction; + BridgeStatusControllerAction<'restartPollingForFailedAttempts'>; export type BridgeStatusControllerGetBridgeHistoryItemByTxMetaIdAction = - BridgeStatusControllerAction; + BridgeStatusControllerAction<'getBridgeHistoryItemByTxMetaId'>; export type BridgeStatusControllerActions = | BridgeStatusControllerStartPollingForBridgeTxStatusAction From 989c05757fc6754ab48c3526d51bd4b6bdebe853 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 15 Dec 2025 15:39:12 +0100 Subject: [PATCH 62/85] chore: prune suppressions --- eslint-suppressions.json | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 0e2717f487b..7809a0e3f6c 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -820,25 +820,6 @@ "count": 1 } }, - "packages/bridge-status-controller/src/bridge-status-controller.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 20 - }, - "@typescript-eslint/naming-convention": { - "count": 5 - }, - "camelcase": { - "count": 8 - }, - "id-length": { - "count": 1 - } - }, - "packages/bridge-status-controller/src/types.ts": { - "@typescript-eslint/naming-convention": { - "count": 7 - } - }, "packages/bridge-status-controller/src/utils/bridge-status.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 @@ -3187,4 +3168,4 @@ "count": 2 } } -} +} \ No newline at end of file From fb520f4cad0403f9711a45c4bfb91ffacc519d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Mon, 15 Dec 2025 16:22:43 +0100 Subject: [PATCH 63/85] fix: update snapshots for bridge-status-controller --- .../src/__snapshots__/bridge-status-controller.test.ts.snap | 4 ++++ 1 file changed, 4 insertions(+) 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 2ab1cf81607..df0f8dcb6db 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 @@ -653,6 +653,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -3428,6 +3429,7 @@ Object { "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -4437,6 +4439,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "bridge-signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "1", @@ -4655,6 +4658,7 @@ Object { "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, + "originalTransactionId": "swap-signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "1", From 213ad76ed16bf8ead50e90d7883db1a38bcf3ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Tue, 16 Dec 2025 12:28:54 +0100 Subject: [PATCH 64/85] fix: adjust bridge status controller because of failing tests --- .../src/bridge-status-controller.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 218bae97f43..86f666506b7 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1713,6 +1713,7 @@ export class BridgeStatusController extends StaticIntervalPollingController tx.id === txMetaId, - ); - const approvalTxMeta = transactions?.find( - (tx: TransactionMeta) => tx.id === historyItem.approvalTxId, - ); - const requestParamProperties = getRequestParamFromHistory(historyItem); // Always publish StatusValidationFailed event, regardless of featureId if (eventName === UnifiedSwapBridgeEventName.StatusValidationFailed) { @@ -1963,6 +1949,21 @@ export class BridgeStatusController extends StaticIntervalPollingController tx.id === txMetaId, + ); + const approvalTxMeta = transactions?.find( + (tx: TransactionMeta) => tx.id === historyItem.approvalTxId, + ); + const requiredEventProperties = { ...baseProperties, ...requestParamProperties, From 8a0c757ae142b9be23746b91819a8d154f525cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Tue, 16 Dec 2025 12:47:33 +0100 Subject: [PATCH 65/85] feat: update eslint suppressions --- eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 208b6ffcc4d..d118c1c3c76 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2149,4 +2149,4 @@ "count": 1 } } -} \ No newline at end of file +} From ed79addcc32aaada3ff8e344225f26f1c57d661f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 16 Dec 2025 16:47:57 +0100 Subject: [PATCH 66/85] test: add tests for intent api --- .../src/intent-api.test.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 packages/bridge-status-controller/src/intent-api.test.ts diff --git a/packages/bridge-status-controller/src/intent-api.test.ts b/packages/bridge-status-controller/src/intent-api.test.ts new file mode 100644 index 00000000000..f36d6a0461f --- /dev/null +++ b/packages/bridge-status-controller/src/intent-api.test.ts @@ -0,0 +1,91 @@ +// intent-api.test.ts +import { describe, it, expect, jest } from '@jest/globals'; +import { IntentApiImpl, type IntentSubmissionParams } from './intent-api'; // adjust if needed +import type { FetchFunction } from './types'; // adjust if needed + +describe('IntentApiImpl', () => { + const baseUrl = 'https://example.com/api'; + + const makeParams = (): IntentSubmissionParams => ({ + srcChainId: '1', + quoteId: 'quote-123', + signature: '0xsig', + order: { some: 'payload' }, + userAddress: '0xabc', + aggregatorId: 'agg-1', + }); + + // Key part: strongly type the mock as FetchFunction (returns Promise) + const makeFetchMock = () => + jest.fn, Parameters>(); + + it('submitIntent calls POST /submitOrder with JSON body and returns response', async () => { + const fetchFn = makeFetchMock().mockResolvedValue({ ok: true, id: 'resp' }); + const api = new IntentApiImpl(baseUrl, fetchFn); + + const params = makeParams(); + const result = await api.submitIntent(params); + + expect(result).toEqual({ ok: true, id: 'resp' }); + expect(fetchFn).toHaveBeenCalledTimes(1); + expect(fetchFn).toHaveBeenCalledWith(`${baseUrl}/submitOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }); + }); + + it('submitIntent rethrows Errors with a prefixed message', async () => { + const fetchFn = makeFetchMock().mockRejectedValue(new Error('boom')); + const api = new IntentApiImpl(baseUrl, fetchFn); + + await expect(api.submitIntent(makeParams())).rejects.toThrow( + 'Failed to submit intent: boom', + ); + }); + + it('submitIntent returns null when rejection is not an Error', async () => { + const fetchFn = makeFetchMock().mockRejectedValue('boom'); + const api = new IntentApiImpl(baseUrl, fetchFn); + + await expect(api.submitIntent(makeParams())).resolves.toBeNull(); + }); + + it('getOrderStatus calls GET /getOrderStatus with encoded query params and returns response', async () => { + const fetchFn = makeFetchMock().mockResolvedValue({ status: 'filled' }); + const api = new IntentApiImpl(baseUrl, fetchFn); + + const orderId = 'order-1'; + const aggregatorId = 'My Agg/With Spaces'; + const srcChainId = '10'; + + const result = await api.getOrderStatus(orderId, aggregatorId, srcChainId); + + expect(result).toEqual({ status: 'filled' }); + expect(fetchFn).toHaveBeenCalledTimes(1); + + const expectedEndpoint = + `${baseUrl}/getOrderStatus` + + `?orderId=${orderId}` + + `&aggregatorId=${encodeURIComponent(aggregatorId)}` + + `&srcChainId=${srcChainId}`; + + expect(fetchFn).toHaveBeenCalledWith(expectedEndpoint, { method: 'GET' }); + }); + + it('getOrderStatus rethrows Errors with a prefixed message', async () => { + const fetchFn = makeFetchMock().mockRejectedValue(new Error('nope')); + const api = new IntentApiImpl(baseUrl, fetchFn); + + await expect(api.getOrderStatus('o', 'a', '1')).rejects.toThrow( + 'Failed to get order status: nope', + ); + }); + + it('getOrderStatus returns null when rejection is not an Error', async () => { + const fetchFn = makeFetchMock().mockRejectedValue({ message: 'nope' }); + const api = new IntentApiImpl(baseUrl, fetchFn); + + await expect(api.getOrderStatus('o', 'a', '1')).resolves.toBeNull(); + }); +}); From 0fdb78d552696b8b9c55dd0742bca93431fc91d2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 16 Dec 2025 23:29:49 +0100 Subject: [PATCH 67/85] test: add tests for intent in bridge controller status --- .../bridge-status-controller.intent.test.ts | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts 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 new file mode 100644 index 00000000000..90e50d203b7 --- /dev/null +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -0,0 +1,400 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { TransactionMeta } from '@metamask/transaction-controller'; +import { + TransactionStatus, + TransactionType, +} from '@metamask/transaction-controller'; +import { StatusTypes } from '@metamask/bridge-controller'; +import { IntentOrderStatus } from './intent-order-status'; + +import { MAX_ATTEMPTS } from './constants'; + +type Tx = Pick & { + type?: TransactionType; + chainId?: string; + hash?: string; + txReceipt?: any; +}; + +function minimalIntentQuoteResponse( + accountAddress: string, + overrides?: Partial, +) { + return { + quote: { + requestId: 'req-1', + srcChainId: 1, + destChainId: 1, + srcTokenAmount: '1000', + destTokenAmount: '990', + minDestTokenAmount: '900', + srcAsset: { + symbol: 'ETH', + chainId: 1, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + name: 'ETH', + decimals: 18, + }, + destAsset: { + symbol: 'ETH', + chainId: 1, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + name: 'ETH', + decimals: 18, + }, + feeData: { txFee: { maxFeePerGas: '1', maxPriorityFeePerGas: '1' } }, + intent: { + protocol: 'cowswap', + order: { some: 'order' }, + settlementContract: '0x9008D19f58AAbd9eD0D60971565AA8510560ab41', + }, + }, + sentAmount: { amount: '1', usd: '1' }, + gasFee: { effective: { amount: '0', usd: '0' } }, + toTokenAmount: { usd: '1' }, + estimatedProcessingTimeInSeconds: 15, + featureId: undefined, + approval: undefined, + resetApproval: undefined, + trade: '0xdeadbeef', + ...overrides, + }; +} + +function createMessengerHarness(accountAddress: string) { + const transactions: Tx[] = []; + + const messenger = { + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), // REQUIRED by BaseController + subscribe: jest.fn(), + publish: jest.fn(), + call: jest.fn((method: string, ...args: any[]) => { + switch (method) { + case 'AccountsController:getAccountByAddress': { + const addr = (args[0] as string) ?? ''; + if (addr.toLowerCase() !== accountAddress.toLowerCase()) { + return undefined; + } + + // REQUIRED so isHardwareWallet() doesn't throw + return { + address: accountAddress, + metadata: { keyring: { type: 'HD Key Tree' } }, + }; + } + case 'TransactionController:getState': + return { transactions }; + case 'NetworkController:findNetworkClientIdByChainId': + return 'network-client-id-1'; + case 'BridgeController:trackUnifiedSwapBridgeEvent': + return undefined; + case 'GasFeeController:getState': + return { gasFeeEstimates: {} }; + default: + return undefined; + } + }), + }; + + return { messenger, transactions }; +} + +function loadControllerWithMocks() { + const submitIntentMock = jest.fn(); + const getOrderStatusMock = jest.fn(); + + let BridgeStatusController: any; + + jest.resetModules(); + + jest.isolateModules(() => { + jest.doMock('./intent-api', () => ({ + IntentApiImpl: jest.fn().mockImplementation(() => ({ + submitIntent: submitIntentMock, + getOrderStatus: getOrderStatusMock, + })), + })); + + jest.doMock('./utils/bridge-status', () => { + const actual = jest.requireActual('./utils/bridge-status'); + return { + ...actual, + shouldSkipFetchDueToFetchFailures: jest.fn().mockReturnValue(false), + }; + }); + + jest.doMock('./utils/transaction', () => { + const actual = jest.requireActual('./utils/transaction'); + return { + ...actual, + // IMPORTANT: controller calls generateActionId().toString() + generateActionId: jest + .fn() + .mockReturnValue({ toString: () => 'action-id-1' }), + + handleApprovalDelay: jest.fn().mockResolvedValue(undefined), + handleMobileHardwareWalletDelay: jest.fn().mockResolvedValue(undefined), + + // CRITICAL FIX: + // submitIntent uses getStatusRequestParams(quoteResponse) inside the try/catch + // If this throws, the intent:* history item is never added and tests fail. + getStatusRequestParams: jest.fn().mockReturnValue({ + srcChainId: 1, + // submitIntent will override srcTxHash after spreading + srcTxHash: '', + }), + }; + }); + + jest.doMock('./utils/metrics', () => ({ + getFinalizedTxProperties: jest.fn().mockReturnValue({}), + getPriceImpactFromQuote: jest.fn().mockReturnValue({}), + getRequestMetadataFromHistory: jest.fn().mockReturnValue({}), + getRequestParamFromHistory: jest.fn().mockReturnValue({ + chain_id_source: 'eip155:1', + chain_id_destination: 'eip155:10', + token_address_source: '0xsrc', + token_address_destination: '0xdest', + }), + getTradeDataFromHistory: jest.fn().mockReturnValue({}), + getEVMTxPropertiesFromTransactionMeta: jest.fn().mockReturnValue({}), + getTxStatusesFromHistory: jest.fn().mockReturnValue({}), + getPreConfirmationPropertiesFromQuote: jest.fn().mockReturnValue({}), + })); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + BridgeStatusController = + require('./bridge-status-controller').BridgeStatusController; + }); + + return { BridgeStatusController, submitIntentMock, getOrderStatusMock }; +} + +function setup() { + const accountAddress = '0xAccount1'; + const { messenger, transactions } = createMessengerHarness(accountAddress); + + const { BridgeStatusController, submitIntentMock, getOrderStatusMock } = + loadControllerWithMocks(); + + const addTransactionFn = jest.fn(async (txParams: any, reqOpts: any) => { + // Approval TX path (submitIntent -> #handleApprovalTx -> #handleEvmTransaction) + if ( + reqOpts?.type === TransactionType.bridgeApproval || + reqOpts?.type === TransactionType.swapApproval + ) { + const hash = '0xapprovalhash1'; + + const approvalTx: Tx = { + id: 'approvalTxId1', + type: reqOpts.type, + status: TransactionStatus.failed, // makes #waitForTxConfirmation throw quickly + chainId: txParams.chainId, + hash, + }; + transactions.push(approvalTx); + + return { + result: Promise.resolve(hash), + transactionMeta: approvalTx, + }; + } + + // Intent “display tx” path + const intentTx: Tx = { + id: 'intentDisplayTxId1', + type: reqOpts?.type, + status: TransactionStatus.submitted, + chainId: txParams.chainId, + hash: undefined, + }; + transactions.push(intentTx); + + return { + result: Promise.resolve('0xunused'), + transactionMeta: intentTx, + }; + }); + + const controller = new BridgeStatusController({ + messenger, + clientId: 'extension', + fetchFn: jest.fn(), + addTransactionFn, + addTransactionBatchFn: jest.fn(), + updateTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(async () => ({ estimates: {} })), + config: { customBridgeApiBaseUrl: 'http://localhost' }, + traceFn: (_req: any, fn?: any) => fn?.(), + }); + + const startPollingSpy = jest + .spyOn(controller as any, 'startPolling') + .mockReturnValue('poll-token-1'); + + const stopPollingSpy = jest + .spyOn(controller as any, 'stopPollingByPollingToken') + .mockImplementation(() => undefined); + + return { + controller, + messenger, + transactions, + addTransactionFn, + startPollingSpy, + stopPollingSpy, + accountAddress, + submitIntentMock, + getOrderStatusMock, + }; +} + +describe('BridgeStatusController (intent swaps)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('submitIntent: creates TC tx, writes intent:* history item, starts polling, and continues if approval confirmation fails', async () => { + const { controller, accountAddress, submitIntentMock, startPollingSpy } = + setup(); + + const orderUid = 'order-uid-1'; + + submitIntentMock.mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + const quoteResponse = minimalIntentQuoteResponse(accountAddress, { + // Include approval to exercise “continue if approval confirmation fails” + approval: { + chainId: 1, + from: accountAddress, + to: '0x0000000000000000000000000000000000000001', + data: '0x', + value: '0x0', + gasLimit: 21000, + }, + }); + + const res = await controller.submitIntent({ + quoteResponse, + signature: '0xsig', + accountAddress, + }); + + const historyKey = `intent:${orderUid}`; + + expect(controller.state.txHistory[historyKey]).toBeDefined(); + expect(controller.state.txHistory[historyKey].originalTransactionId).toBe( + res.id, + ); + + expect(startPollingSpy).toHaveBeenCalledWith({ + bridgeTxMetaId: historyKey, + }); + }); + + test('intent polling: updates history, merges tx hashes, updates TC tx, and stops polling on COMPLETED', async () => { + const { + controller, + accountAddress, + submitIntentMock, + getOrderStatusMock, + stopPollingSpy, + } = setup(); + + const orderUid = 'order-uid-2'; + + submitIntentMock.mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + const quoteResponse = minimalIntentQuoteResponse(accountAddress); + + await controller.submitIntent({ + quoteResponse, + signature: '0xsig', + accountAddress, + }); + + const historyKey = `intent:${orderUid}`; + + // Seed existing hashes via controller.update (state is frozen) + (controller as any).update((s: any) => { + s.txHistory[historyKey].srcTxHashes = ['0xold1']; + }); + + getOrderStatusMock.mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.COMPLETED, + txHash: '0xnewhash', + metadata: { txHashes: ['0xold1', '0xnewhash'] }, + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: historyKey }); + + const updated = controller.state.txHistory[historyKey]; + expect(updated.status.status).toBe(StatusTypes.COMPLETE); + expect(updated.srcTxHashes).toEqual( + expect.arrayContaining(['0xold1', '0xnewhash']), + ); + + expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + }); + + test('intent polling: stops polling when attempts reach MAX_ATTEMPTS', async () => { + const { + controller, + accountAddress, + submitIntentMock, + getOrderStatusMock, + stopPollingSpy, + } = setup(); + + const orderUid = 'order-uid-3'; + + submitIntentMock.mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + const quoteResponse = minimalIntentQuoteResponse(accountAddress); + + await controller.submitIntent({ + quoteResponse, + signature: '0xsig', + accountAddress, + }); + + const historyKey = `intent:${orderUid}`; + + // Prime attempts so next failure hits MAX_ATTEMPTS + (controller as any).update((s: any) => { + s.txHistory[historyKey].attempts = { + counter: MAX_ATTEMPTS - 1, + lastAttemptTime: 0, + }; + }); + + getOrderStatusMock.mockRejectedValue(new Error('boom')); + + await (controller as any)._executePoll({ bridgeTxMetaId: historyKey }); + + expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + expect(controller.state.txHistory[historyKey].attempts).toEqual( + expect.objectContaining({ counter: MAX_ATTEMPTS }), + ); + }); +}); From 3ccca387769c6d84da0c8b6203b6224e6943bda1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 16:30:06 +0100 Subject: [PATCH 68/85] chore: remove unused export --- packages/bridge-controller/src/utils/validators.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 2b4f26ee7c6..e5a1d8a881a 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -224,10 +224,6 @@ export const IntentSchema = type({ relayer: optional(HexAddressSchema), }); -// Export types for use in other modules -export type IntentOrder = Infer; -export type Intent = Infer; - export const QuoteSchema = type({ requestId: string(), srcChainId: ChainIdSchema, From 4ef2bc6a6a06bf224afab40fa6004edf77746c72 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 16:31:03 +0100 Subject: [PATCH 69/85] refactor: change enum to generic string --- packages/bridge-controller/src/utils/validators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index e5a1d8a881a..c9a817ac504 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -196,7 +196,7 @@ const RefuelDataSchema = StepSchema; const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); // Intent support (e.g., CoW Swap EIP-712 order signing) -const IntentProtocolSchema = enums(['cowswap']); +const IntentProtocolSchema = string; export const IntentOrderSchema = type({ // EIP-712 Order fields (subset required for signing/submission) From 2cc358523a4318df1beec77bf5bf63fae21830c1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 17:20:31 +0100 Subject: [PATCH 70/85] refactor: add validation for intent api + move to utils refactor: intent api in utils with validators --- .../bridge-status-controller.intent.test.ts | 1000 ++++++++++++++++- .../src/bridge-status-controller.ts | 10 +- .../src/intent-order-status.ts | 9 - .../src/{ => utils}/intent-api.test.ts | 0 .../src/{ => utils}/intent-api.ts | 44 +- .../src/utils/validators.ts | 36 + 6 files changed, 1064 insertions(+), 35 deletions(-) rename packages/bridge-status-controller/src/{ => utils}/intent-api.test.ts (100%) rename packages/bridge-status-controller/src/{ => utils}/intent-api.ts (51%) 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 90e50d203b7..9abd409abb7 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 @@ -5,7 +5,10 @@ import { TransactionStatus, TransactionType, } from '@metamask/transaction-controller'; -import { StatusTypes } from '@metamask/bridge-controller'; +import { + StatusTypes, + UnifiedSwapBridgeEventName, +} from '@metamask/bridge-controller'; import { IntentOrderStatus } from './intent-order-status'; import { MAX_ATTEMPTS } from './constants'; @@ -64,7 +67,59 @@ function minimalIntentQuoteResponse( }; } -function createMessengerHarness(accountAddress: string) { +function minimalBridgeQuoteResponse( + accountAddress: string, + overrides?: Partial, +) { + return { + quote: { + requestId: 'req-bridge-1', + srcChainId: 1, + destChainId: 10, + srcTokenAmount: '1000', + destTokenAmount: '990', + minDestTokenAmount: '900', + srcAsset: { + symbol: 'ETH', + chainId: 1, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + name: 'ETH', + decimals: 18, + }, + destAsset: { + symbol: 'ETH', + chainId: 10, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:60', + name: 'ETH', + decimals: 18, + }, + feeData: { txFee: { maxFeePerGas: '1', maxPriorityFeePerGas: '1' } }, + }, + sentAmount: { amount: '1', usd: '1' }, + gasFee: { effective: { amount: '0', usd: '0' } }, + toTokenAmount: { usd: '1' }, + estimatedProcessingTimeInSeconds: 15, + featureId: undefined, + approval: undefined, + resetApproval: undefined, + trade: { + chainId: 1, + from: accountAddress, + to: '0x0000000000000000000000000000000000000001', + data: '0x', + value: '0x0', + gasLimit: 21000, + }, + ...overrides, + }; +} + +function createMessengerHarness( + accountAddress: string, + selectedChainId: string = '0x1', +) { const transactions: Tx[] = []; const messenger = { @@ -90,6 +145,10 @@ function createMessengerHarness(accountAddress: string) { return { transactions }; case 'NetworkController:findNetworkClientIdByChainId': return 'network-client-id-1'; + case 'NetworkController:getState': + return { selectedNetworkClientId: 'selected-network-client-id-1' }; + case 'NetworkController:getNetworkClientById': + return { configuration: { chainId: selectedChainId } }; case 'BridgeController:trackUnifiedSwapBridgeEvent': return undefined; case 'GasFeeController:getState': @@ -107,6 +166,14 @@ function loadControllerWithMocks() { const submitIntentMock = jest.fn(); const getOrderStatusMock = jest.fn(); + const fetchBridgeTxStatusMock = jest.fn(); + const getStatusRequestWithSrcTxHashMock = jest.fn(); + + // ADD THIS + const shouldSkipFetchDueToFetchFailuresMock = jest + .fn() + .mockReturnValue(false); + let BridgeStatusController: any; jest.resetModules(); @@ -123,7 +190,10 @@ function loadControllerWithMocks() { const actual = jest.requireActual('./utils/bridge-status'); return { ...actual, - shouldSkipFetchDueToFetchFailures: jest.fn().mockReturnValue(false), + fetchBridgeTxStatus: fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHash: getStatusRequestWithSrcTxHashMock, + shouldSkipFetchDueToFetchFailures: + shouldSkipFetchDueToFetchFailuresMock, }; }); @@ -131,20 +201,16 @@ function loadControllerWithMocks() { const actual = jest.requireActual('./utils/transaction'); return { ...actual, - // IMPORTANT: controller calls generateActionId().toString() generateActionId: jest .fn() .mockReturnValue({ toString: () => 'action-id-1' }), - handleApprovalDelay: jest.fn().mockResolvedValue(undefined), handleMobileHardwareWalletDelay: jest.fn().mockResolvedValue(undefined), - // CRITICAL FIX: - // submitIntent uses getStatusRequestParams(quoteResponse) inside the try/catch - // If this throws, the intent:* history item is never added and tests fail. + // keep your existing getStatusRequestParams stub here if you have it getStatusRequestParams: jest.fn().mockReturnValue({ srcChainId: 1, - // submitIntent will override srcTxHash after spreading + destChainId: 1, srcTxHash: '', }), }; @@ -171,15 +237,31 @@ function loadControllerWithMocks() { require('./bridge-status-controller').BridgeStatusController; }); - return { BridgeStatusController, submitIntentMock, getOrderStatusMock }; + return { + BridgeStatusController, + submitIntentMock, + getOrderStatusMock, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + shouldSkipFetchDueToFetchFailuresMock, // ADD THIS + }; } -function setup() { +function setup(options?: { selectedChainId?: string }) { const accountAddress = '0xAccount1'; - const { messenger, transactions } = createMessengerHarness(accountAddress); + const { messenger, transactions } = createMessengerHarness( + accountAddress, + options?.selectedChainId ?? '0x1', + ); - const { BridgeStatusController, submitIntentMock, getOrderStatusMock } = - loadControllerWithMocks(); + const { + BridgeStatusController, + submitIntentMock, + getOrderStatusMock, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + shouldSkipFetchDueToFetchFailuresMock, + } = loadControllerWithMocks(); const addTransactionFn = jest.fn(async (txParams: any, reqOpts: any) => { // Approval TX path (submitIntent -> #handleApprovalTx -> #handleEvmTransaction) @@ -250,6 +332,9 @@ function setup() { accountAddress, submitIntentMock, getOrderStatusMock, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + shouldSkipFetchDueToFetchFailuresMock, }; } @@ -352,6 +437,54 @@ describe('BridgeStatusController (intent swaps)', () => { expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); }); + test('intent polling: maps EXPIRED to FAILED, falls back to txHash when metadata hashes empty, and skips TC update if original tx not found', async () => { + const { + controller, + accountAddress, + submitIntentMock, + getOrderStatusMock, + transactions, + stopPollingSpy, + } = setup(); + + const orderUid = 'order-uid-expired-1'; + + submitIntentMock.mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + const quoteResponse = minimalIntentQuoteResponse(accountAddress); + + await controller.submitIntent({ + quoteResponse, + signature: '0xsig', + accountAddress, + }); + + const historyKey = `intent:${orderUid}`; + + // Remove TC tx so update branch logs "transaction not found" + transactions.splice(0, transactions.length); + + getOrderStatusMock.mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.EXPIRED, + txHash: '0xonlyhash', + metadata: { txHashes: [] }, // forces fallback to txHash + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: historyKey }); + + const updated = controller.state.txHistory[historyKey]; + expect(updated.status.status).toBe(StatusTypes.FAILED); + expect(updated.srcTxHashes).toEqual(expect.arrayContaining(['0xonlyhash'])); + + expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + }); + test('intent polling: stops polling when attempts reach MAX_ATTEMPTS', async () => { const { controller, @@ -398,3 +531,842 @@ describe('BridgeStatusController (intent swaps)', () => { ); }); }); + +describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('transactionFailed subscription: marks main tx as FAILED and tracks (non-rejected)', async () => { + const { controller, messenger } = setup(); + + // Seed txHistory with a pending bridge tx + (controller as any).update((s: any) => { + s.txHistory['bridgeTxMetaId1'] = { + txMetaId: 'bridgeTxMetaId1', + originalTransactionId: 'bridgeTxMetaId1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { assetId: 'eip155:1/slip44:60' }, + destAsset: { assetId: 'eip155:10/slip44:60' }, + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, + }; + }); + + const failedCb = (messenger.subscribe as jest.Mock).mock.calls.find( + ([evt]) => evt === 'TransactionController:transactionFailed', + )?.[1]; + expect(typeof failedCb).toBe('function'); + + failedCb({ + transactionMeta: { + id: 'bridgeTxMetaId1', + type: TransactionType.bridge, + status: TransactionStatus.failed, + chainId: '0x1', + }, + }); + + expect(controller.state.txHistory['bridgeTxMetaId1'].status.status).toBe( + StatusTypes.FAILED, + ); + + // ensure tracking was attempted + expect((messenger.call as jest.Mock).mock.calls).toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + 'BridgeController:trackUnifiedSwapBridgeEvent', + ]), + ]), + ); + }); + + test('transactionFailed subscription: maps approval tx id back to main history item', async () => { + const { controller, messenger } = setup(); + + (controller as any).update((s: any) => { + s.txHistory['mainTx'] = { + txMetaId: 'mainTx', + originalTransactionId: 'mainTx', + approvalTxId: 'approvalTx', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { assetId: 'eip155:1/slip44:60' }, + destAsset: { assetId: 'eip155:10/slip44:60' }, + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, + }; + }); + + const failedCb = (messenger.subscribe as jest.Mock).mock.calls.find( + ([evt]) => evt === 'TransactionController:transactionFailed', + )?.[1]; + + failedCb({ + transactionMeta: { + id: 'approvalTx', + type: TransactionType.bridgeApproval, + status: TransactionStatus.failed, + chainId: '0x1', + }, + }); + + expect(controller.state.txHistory['mainTx'].status.status).toBe( + StatusTypes.FAILED, + ); + }); + + test('transactionConfirmed subscription: tracks swap Completed; starts polling on bridge confirmed', async () => { + const { controller, messenger, startPollingSpy } = setup(); + + // Seed history for bridge id so #startPollingForTxId can startPolling() + (controller as any).update((s: any) => { + s.txHistory['bridgeConfirmed1'] = { + txMetaId: 'bridgeConfirmed1', + originalTransactionId: 'bridgeConfirmed1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { assetId: 'eip155:1/slip44:60' }, + destAsset: { assetId: 'eip155:10/slip44:60' }, + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, + }; + }); + + const confirmedCb = (messenger.subscribe as jest.Mock).mock.calls.find( + ([evt]) => evt === 'TransactionController:transactionConfirmed', + )?.[1]; + expect(typeof confirmedCb).toBe('function'); + + // Swap -> Completed tracking + confirmedCb({ + id: 'swap1', + type: TransactionType.swap, + chainId: '0x1', + }); + + // Bridge -> startPolling + confirmedCb({ + id: 'bridgeConfirmed1', + type: TransactionType.bridge, + chainId: '0x1', + }); + + expect(startPollingSpy).toHaveBeenCalledWith({ + bridgeTxMetaId: 'bridgeConfirmed1', + }); + }); + + test('restartPollingForFailedAttempts: throws when identifier missing, and when no match found', async () => { + const { controller } = setup(); + + expect(() => controller.restartPollingForFailedAttempts({})).toThrowError( + /Either txMetaId or txHash must be provided/u, + ); + + expect(() => + controller.restartPollingForFailedAttempts({ + txMetaId: 'does-not-exist', + }), + ).toThrowError(/No bridge transaction history found/u); + }); + + test('restartPollingForFailedAttempts: resets attempts and restarts polling via txHash lookup (bridge tx only)', async () => { + const { controller, startPollingSpy } = setup(); + + (controller as any).update((s: any) => { + s.txHistory['bridgeTx1'] = { + txMetaId: 'bridgeTx1', + originalTransactionId: 'bridgeTx1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { assetId: 'eip155:1/slip44:60' }, + destAsset: { assetId: 'eip155:10/slip44:60' }, + }, + attempts: { counter: 7, lastAttemptTime: 0 }, + account: '0xAccount1', + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xhash-find-me' }, + }, + }; + }); + + controller.restartPollingForFailedAttempts({ txHash: '0xhash-find-me' }); + + expect(controller.state.txHistory['bridgeTx1'].attempts).toBeUndefined(); + expect(startPollingSpy).toHaveBeenCalledWith({ + bridgeTxMetaId: 'bridgeTx1', + }); + }); + + test('restartPollingForFailedAttempts: does not restart polling for same-chain swap tx', async () => { + const { controller, startPollingSpy } = setup(); + + (controller as any).update((s: any) => { + s.txHistory['swapTx1'] = { + txMetaId: 'swapTx1', + originalTransactionId: 'swapTx1', + quote: { + srcChainId: 1, + destChainId: 1, + srcAsset: { assetId: 'eip155:1/slip44:60' }, + destAsset: { assetId: 'eip155:1/slip44:60' }, + }, + attempts: { counter: 7, lastAttemptTime: 0 }, + account: '0xAccount1', + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xhash-samechain' }, + }, + }; + }); + + controller.restartPollingForFailedAttempts({ txMetaId: 'swapTx1' }); + + expect(controller.state.txHistory['swapTx1'].attempts).toBeUndefined(); + expect(startPollingSpy).not.toHaveBeenCalled(); + }); + + test('wipeBridgeStatus(ignoreNetwork=false): stops polling and removes only matching chain+account history', async () => { + const { controller, stopPollingSpy, accountAddress } = setup({ + selectedChainId: '0x1', + }); + + const quoteResponse = minimalBridgeQuoteResponse(accountAddress); + + // Use deprecated method to create history and start polling (so token exists in controller) + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'bridgeToWipe1' }, + statusRequest: { + srcChainId: 1, + srcTxHash: '0xsrc', + destChainId: 10, + }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + }); + + expect(controller.state.txHistory['bridgeToWipe1']).toBeDefined(); + + controller.wipeBridgeStatus({ + address: accountAddress, + ignoreNetwork: false, + }); + + expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + expect(controller.state.txHistory['bridgeToWipe1']).toBeUndefined(); + }); + + test('EVM bridge polling: looks up srcTxHash in TC when missing, updates history, stops polling, and publishes completion', async () => { + const { + controller, + transactions, + accountAddress, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + stopPollingSpy, + messenger, + } = setup(); + + // Create a history item with missing src tx hash + const quoteResponse = minimalBridgeQuoteResponse(accountAddress); + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'bridgePoll1' }, + statusRequest: { + srcChainId: 1, + srcTxHash: '', // force TC lookup + destChainId: 10, + }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + }); + + // Seed TC with tx meta id=bridgePoll1 and a hash for lookup + transactions.push({ + id: 'bridgePoll1', + status: TransactionStatus.confirmed, + type: TransactionType.bridge, + chainId: '0x1', + hash: '0xlooked-up-hash', + }); + + getStatusRequestWithSrcTxHashMock.mockReturnValue({ + srcChainId: 1, + srcTxHash: '0xlooked-up-hash', + destChainId: 10, + }); + + fetchBridgeTxStatusMock.mockResolvedValue({ + status: { + status: StatusTypes.COMPLETE, + srcChain: { chainId: 1, txHash: '0xlooked-up-hash' }, + destChain: { chainId: 10, txHash: '0xdesthash' }, + }, + validationFailures: [], + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'bridgePoll1' }); + + const updated = controller.state.txHistory['bridgePoll1']; + + expect(updated.status.status).toBe(StatusTypes.COMPLETE); + expect(updated.status.srcChain.txHash).toBe('0xlooked-up-hash'); + expect(updated.completionTime).toEqual(expect.any(Number)); + + expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + + expect(messenger.publish).toHaveBeenCalledWith( + 'BridgeStatusController:destinationTransactionCompleted', + quoteResponse.quote.destAsset.assetId, + ); + }); + + test('EVM bridge polling: tracks StatusValidationFailed, increments attempts, and stops polling at MAX_ATTEMPTS', async () => { + const { + controller, + accountAddress, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + stopPollingSpy, + } = setup(); + + const quoteResponse = minimalBridgeQuoteResponse(accountAddress); + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'bridgeValidationFail1' }, + statusRequest: { + srcChainId: 1, + srcTxHash: '0xsrc', + destChainId: 10, + }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + }); + + // Prime attempts to just below MAX so the next failure stops polling + (controller as any).update((s: any) => { + s.txHistory['bridgeValidationFail1'].attempts = { + counter: MAX_ATTEMPTS - 1, + lastAttemptTime: 0, + }; + }); + + getStatusRequestWithSrcTxHashMock.mockReturnValue({ + srcChainId: 1, + srcTxHash: '0xsrc', + destChainId: 10, + }); + + fetchBridgeTxStatusMock.mockResolvedValue({ + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, + validationFailures: ['bad_status_shape'], + }); + + await (controller as any)._executePoll({ + bridgeTxMetaId: 'bridgeValidationFail1', + }); + + expect( + controller.state.txHistory['bridgeValidationFail1'].attempts, + ).toEqual(expect.objectContaining({ counter: MAX_ATTEMPTS })); + expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + }); + + test('bridge polling: returns early (does not fetch) when srcTxHash cannot be determined', async () => { + const { + controller, + accountAddress, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + } = setup(); + + const quoteResponse = minimalBridgeQuoteResponse(accountAddress); + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'bridgeNoHash1' }, + statusRequest: { + srcChainId: 1, + srcTxHash: '', // missing + destChainId: 10, + }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'bridgeNoHash1' }); + + expect(getStatusRequestWithSrcTxHashMock).not.toHaveBeenCalled(); + expect(fetchBridgeTxStatusMock).not.toHaveBeenCalled(); + }); +}); + +describe('BridgeStatusController (target uncovered branches)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('transactionFailed: returns early for intent txs (swapMetaData.isIntentTx)', () => { + const { controller, messenger } = setup(); + + // seed a history item that would otherwise be marked FAILED + (controller as any).update((s: any) => { + s.txHistory['tx1'] = { + txMetaId: 'tx1', + originalTransactionId: 'tx1', + quote: { srcChainId: 1, destChainId: 10 }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0x' }, + }, + }; + }); + + const failedCb = (messenger.subscribe as jest.Mock).mock.calls.find( + ([evt]) => evt === 'TransactionController:transactionFailed', + )?.[1]; + + failedCb({ + transactionMeta: { + id: 'tx1', + type: TransactionType.bridge, + status: TransactionStatus.failed, + swapMetaData: { isIntentTx: true }, // <- triggers early return + }, + }); + + expect(controller.state.txHistory['tx1'].status.status).toBe( + StatusTypes.PENDING, + ); + + // no tracking call should be made from this callback path + expect((messenger.call as jest.Mock).mock.calls).not.toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + 'BridgeController:trackUnifiedSwapBridgeEvent', + ]), + ]), + ); + }); + + test('constructor restartPolling: skips items when shouldSkipFetchDueToFetchFailures returns true', () => { + const accountAddress = '0xAccount1'; + const { messenger } = createMessengerHarness(accountAddress); + + const { BridgeStatusController, shouldSkipFetchDueToFetchFailuresMock } = + loadControllerWithMocks(); + + shouldSkipFetchDueToFetchFailuresMock.mockReturnValue(true); + + const startPollingProtoSpy = jest + .spyOn(BridgeStatusController.prototype as any, 'startPolling') + .mockReturnValue('tok'); + + // seed an incomplete bridge history item (PENDING + cross-chain) + const state = { + txHistory: { + init1: { + txMetaId: 'init1', + originalTransactionId: 'init1', + quote: { srcChainId: 1, destChainId: 10 }, + account: accountAddress, + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, + attempts: { counter: 1, lastAttemptTime: 0 }, + }, + }, + }; + + // constructor calls #restartPollingForIncompleteHistoryItems() + // shouldSkipFetchDueToFetchFailures=true => should NOT call startPolling + // eslint-disable-next-line no-new + new BridgeStatusController({ + messenger, + state, + clientId: 'extension', + fetchFn: jest.fn(), + addTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), + updateTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), + config: { customBridgeApiBaseUrl: 'http://localhost' }, + traceFn: (_r: any, fn?: any) => fn?.(), + }); + + expect(startPollingProtoSpy).not.toHaveBeenCalled(); + startPollingProtoSpy.mockRestore(); + }); + + test('startPollingForTxId: stops existing polling token when restarting same tx', () => { + const { controller, stopPollingSpy, startPollingSpy, accountAddress } = + setup(); + + // make startPolling return different tokens for the same tx + startPollingSpy.mockReturnValueOnce('tok1').mockReturnValueOnce('tok2'); + + const quoteResponse: any = { + quote: { srcChainId: 1, destChainId: 10, destAsset: { assetId: 'x' } }, + estimatedProcessingTimeInSeconds: 1, + sentAmount: { amount: '0' }, + gasFee: { effective: { amount: '0' } }, + toTokenAmount: { usd: '0' }, + }; + + // first time => starts polling tok1 + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'sameTx' }, + statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + } as any); + + // second time => should stop tok1 and start tok2 + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'sameTx' }, + statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + } as any); + + expect(stopPollingSpy).toHaveBeenCalledWith('tok1'); + }); + + test('transactionConfirmed bridge: if history item missing, #startPollingForTxId returns early', () => { + const { messenger } = setup(); + + const confirmedCb = (messenger.subscribe as jest.Mock).mock.calls.find( + ([evt]) => evt === 'TransactionController:transactionConfirmed', + )?.[1]; + + // no history seeded for this id + confirmedCb({ + id: 'missingHistory', + type: TransactionType.bridge, + chainId: '0x1', + }); + }); + + test('bridge polling: returns early when shouldSkipFetchDueToFetchFailures returns true', async () => { + const { + controller, + accountAddress, + shouldSkipFetchDueToFetchFailuresMock, + fetchBridgeTxStatusMock, + } = setup(); + + const quoteResponse: any = { + quote: { srcChainId: 1, destChainId: 10, destAsset: { assetId: 'x' } }, + estimatedProcessingTimeInSeconds: 1, + sentAmount: { amount: '0' }, + gasFee: { effective: { amount: '0' } }, + toTokenAmount: { usd: '0' }, + }; + + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'skipPoll1' }, + statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + } as any); + + shouldSkipFetchDueToFetchFailuresMock.mockReturnValueOnce(true); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'skipPoll1' }); + + expect(fetchBridgeTxStatusMock).not.toHaveBeenCalled(); + }); + + test('bridge polling: final FAILED tracks Failed event', async () => { + const { + controller, + accountAddress, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + messenger, + } = setup(); + + const quoteResponse: any = { + quote: { + srcChainId: 1, + destChainId: 10, + destAsset: { assetId: 'dest' }, + srcAsset: { assetId: 'src' }, + }, + estimatedProcessingTimeInSeconds: 1, + sentAmount: { amount: '0' }, + gasFee: { effective: { amount: '0' } }, + toTokenAmount: { usd: '0' }, + }; + + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'failFinal1' }, + statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + } as any); + + getStatusRequestWithSrcTxHashMock.mockReturnValue({ + srcChainId: 1, + srcTxHash: '0xhash', + destChainId: 10, + }); + + fetchBridgeTxStatusMock.mockResolvedValue({ + status: { + status: StatusTypes.FAILED, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + validationFailures: [], + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'failFinal1' }); + + expect((messenger.call as jest.Mock).mock.calls).toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + 'BridgeController:trackUnifiedSwapBridgeEvent', + UnifiedSwapBridgeEventName.Failed, + expect.any(Object), + ]), + ]), + ); + }); + + test('bridge polling: final COMPLETE with featureId set stops polling but skips tracking', async () => { + const { + controller, + accountAddress, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + stopPollingSpy, + messenger, + } = setup(); + + const quoteResponse: any = { + quote: { + srcChainId: 1, + destChainId: 10, + destAsset: { assetId: 'dest' }, + srcAsset: { assetId: 'src' }, + }, + featureId: 'perps', // <- triggers featureId skip in #fetchBridgeTxStatus + estimatedProcessingTimeInSeconds: 1, + sentAmount: { amount: '0' }, + gasFee: { effective: { amount: '0' } }, + toTokenAmount: { usd: '0' }, + }; + + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'perps1' }, + statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + } as any); + + getStatusRequestWithSrcTxHashMock.mockReturnValue({ + srcChainId: 1, + srcTxHash: '0xhash', + destChainId: 10, + }); + + fetchBridgeTxStatusMock.mockResolvedValue({ + status: { + status: StatusTypes.COMPLETE, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + validationFailures: [], + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'perps1' }); + + expect(stopPollingSpy).toHaveBeenCalled(); + + // should not track Completed because featureId is set + expect((messenger.call as jest.Mock).mock.calls).not.toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + 'BridgeController:trackUnifiedSwapBridgeEvent', + UnifiedSwapBridgeEventName.Completed, + ]), + ]), + ); + }); + + test('StatusValidationFailed event includes refresh_count from attempts', async () => { + const { + controller, + accountAddress, + fetchBridgeTxStatusMock, + getStatusRequestWithSrcTxHashMock, + messenger, + } = setup(); + + const quoteResponse: any = { + quote: { + srcChainId: 1, + destChainId: 10, + destAsset: { assetId: 'dest' }, + srcAsset: { assetId: 'src' }, + }, + estimatedProcessingTimeInSeconds: 1, + sentAmount: { amount: '0' }, + gasFee: { effective: { amount: '0' } }, + toTokenAmount: { usd: '0' }, + }; + + controller.startPollingForBridgeTxStatus({ + accountAddress, + bridgeTxMeta: { id: 'valFail1' }, + statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, + quoteResponse, + slippagePercentage: 0, + startTime: Date.now(), + isStxEnabled: false, + } as any); + + // ensure attempts exists BEFORE validation failure is tracked + (controller as any).update((s: any) => { + s.txHistory['valFail1'].attempts = { counter: 5, lastAttemptTime: 0 }; + }); + + getStatusRequestWithSrcTxHashMock.mockReturnValue({ + srcChainId: 1, + srcTxHash: '0xhash', + destChainId: 10, + }); + + fetchBridgeTxStatusMock.mockResolvedValue({ + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + validationFailures: ['bad_status'], + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'valFail1' }); + + expect((messenger.call as jest.Mock).mock.calls).toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + 'BridgeController:trackUnifiedSwapBridgeEvent', + UnifiedSwapBridgeEventName.StatusValidationFailed, + expect.objectContaining({ refresh_count: 5 }), + ]), + ]), + ); + }); + + test('track event: txMetaId provided but history missing hits history-not-found branch', () => { + const { messenger } = setup(); + + const confirmedCb = (messenger.subscribe as jest.Mock).mock.calls.find( + ([evt]) => evt === 'TransactionController:transactionConfirmed', + )?.[1]; + + // swap completion tracking with an id that is not in txHistory => historyItem undefined branch + confirmedCb({ + id: 'noHistorySwap', + type: TransactionType.swap, + chainId: '0x1', + }); + }); + + test('track event: history has featureId => #trackUnifiedSwapBridgeEvent returns early (skip tracking)', () => { + const { controller, messenger } = setup(); + + (controller as any).update((s: any) => { + s.txHistory['feat1'] = { + txMetaId: 'feat1', + originalTransactionId: 'feat1', + quote: { srcChainId: 1, destChainId: 10 }, + account: '0xAccount1', + featureId: 'perps', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0x' }, + }, + }; + }); + + const failedCb = (messenger.subscribe as jest.Mock).mock.calls.find( + ([evt]) => evt === 'TransactionController:transactionFailed', + )?.[1]; + + failedCb({ + transactionMeta: { + id: 'feat1', + type: TransactionType.bridge, + status: TransactionStatus.failed, + }, + }); + + // should skip due to featureId + expect((messenger.call as jest.Mock).mock.calls).not.toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + 'BridgeController:trackUnifiedSwapBridgeEvent', + ]), + ]), + ); + }); + + test('submitTx: throws when multichain account is undefined', async () => { + const { controller } = setup(); + + await expect( + controller.submitTx( + '0xNotKnownByHarness', + { featureId: undefined } as any, + false, + ), + ).rejects.toThrow(/undefined multichain account/u); + }); +}); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 86f666506b7..627d24e1b27 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -46,9 +46,8 @@ import { REFRESH_INTERVAL_MS, TraceName, } from './constants'; -import { IntentApiImpl } from './intent-api'; +import { IntentApiImpl } from './utils/intent-api.ts~'; import type { IntentOrder } from './intent-order'; -import { IntentOrderStatus } from './intent-order-status'; import type { BridgeStatusControllerState, StartPollingForBridgeTxStatusArgsSerialized, @@ -84,6 +83,7 @@ import { handleNonEvmTxResponse, generateActionId, } from './utils/transaction'; +import { IntentOrderStatus } from '../../bridge-controller/src/utils/validators'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list @@ -733,11 +733,12 @@ export class BridgeStatusController extends StaticIntervalPollingController ({ + 'X-Client-Id': clientId, +}); + export type IntentApi = { - submitIntent(params: IntentSubmissionParams): Promise; + submitIntent( + params: IntentSubmissionParams, + clientId: string, + ): Promise; + getOrderStatus( + orderId: string, + aggregatorId: string, + srcChainId: string, + clientId: string, + ): Promise; }; export class IntentApiImpl implements IntentApi { @@ -23,20 +37,29 @@ export class IntentApiImpl implements IntentApi { this.#fetchFn = fetchFn; } - async submitIntent(params: IntentSubmissionParams): Promise { + async submitIntent( + params: IntentSubmissionParams, + clientId: string, + ): Promise { const endpoint = `${this.#baseUrl}/submitOrder`; try { const response = await this.#fetchFn(endpoint, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...getClientIdHeader(clientId), + }, body: JSON.stringify(params), }); + if (!validateIntentOrderResponse(response)) { + throw new Error('Invalid submitOrder response'); + } return response; } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to submit intent: ${error.message}`); } - return null; + throw new Error('Failed to submit intent'); } } @@ -44,18 +67,23 @@ export class IntentApiImpl implements IntentApi { orderId: string, aggregatorId: string, srcChainId: string, - ): Promise { + clientId: string, + ): Promise { const endpoint = `${this.#baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; try { const response = await this.#fetchFn(endpoint, { method: 'GET', + headers: getClientIdHeader(clientId), }); + if (!validateIntentOrderResponse(response)) { + throw new Error('Invalid submitOrder response'); + } return response; } catch (error: unknown) { if (error instanceof Error) { - throw new Error(`Failed to get order status: ${error.message}`); + throw new Error(`Failed to submit intent: ${error.message}`); } - return null; + throw new Error('Failed to submit intent'); } } } diff --git a/packages/bridge-status-controller/src/utils/validators.ts b/packages/bridge-status-controller/src/utils/validators.ts index 123456bdf18..e0054f3a0fc 100644 --- a/packages/bridge-status-controller/src/utils/validators.ts +++ b/packages/bridge-status-controller/src/utils/validators.ts @@ -9,6 +9,8 @@ import { union, type, assert, + array, + is, } from '@metamask/superstruct'; const ChainIdSchema = number(); @@ -57,3 +59,37 @@ export const validateBridgeStatusResponse = ( assert(data, StatusResponseSchema); return true; }; + +export enum IntentOrderStatus { + PENDING = 'pending', + SUBMITTED = 'submitted', + CONFIRMED = 'confirmed', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} + +export type IntentOrder = { + id: string; + status: IntentOrderStatus; + txHash?: string; + metadata: { + txHashes?: string[] | string; + }; +}; + +export const IntentOrderResponseSchema = type({ + id: string(), + status: enums(Object.values(IntentOrderStatus)), + txHash: optional(string()), + metadata: type({ + txHashes: optional(union([array(string()), string()])), + }), +}); + +export const validateIntentOrderResponse = ( + data: unknown, +): data is Infer => { + return is(data, IntentOrderResponseSchema); +}; From 6bb1938f89be8a70b71c321aaaa4820b61d17dc7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 17:37:23 +0100 Subject: [PATCH 71/85] chore: add emacs files to .gitignore chore: ignore emacs files --- .gitignore | 6 +- .../src/utils/intent-api.ts~ | 71 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/bridge-status-controller/src/utils/intent-api.ts~ diff --git a/.gitignore b/.gitignore index 6c1e52eb80d..943d49c6e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,8 @@ scripts/coverage !.yarn/versions # typescript -packages/*/*.tsbuildinfo \ No newline at end of file +packages/*/*.tsbuildinfo + +# Emacs +\#*\# +.#* diff --git a/packages/bridge-status-controller/src/utils/intent-api.ts~ b/packages/bridge-status-controller/src/utils/intent-api.ts~ new file mode 100644 index 00000000000..dc91f75f561 --- /dev/null +++ b/packages/bridge-status-controller/src/utils/intent-api.ts~ @@ -0,0 +1,71 @@ +import { + IntentOrder, + validateIntentOrderResponse, +} from '../../bridge-controller/src/utils/validators'; +import type { FetchFunction } from './types'; + +export type IntentSubmissionParams = { + srcChainId: string; + quoteId: string; + signature: string; + order: unknown; + userAddress: string; + aggregatorId: string; +}; + +export type IntentApi = { + submitIntent(params: IntentSubmissionParams): Promise; +}; + +export class IntentApiImpl implements IntentApi { + readonly #baseUrl: string; + + readonly #fetchFn: FetchFunction; + + constructor(baseUrl: string, fetchFn: FetchFunction) { + this.#baseUrl = baseUrl; + this.#fetchFn = fetchFn; + } + + async submitIntent(params: IntentSubmissionParams): Promise { + const endpoint = `${this.#baseUrl}/submitOrder`; + try { + const response = await this.#fetchFn(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }); + if (!validateIntentOrderResponse(response)) { + throw new Error('Invalid submitOrder response'); + } + return response; + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Failed to submit intent: ${error.message}`); + } + throw new Error('Failed to submit intent'); + } + } + + async getOrderStatus( + orderId: string, + aggregatorId: string, + srcChainId: string, + ): Promise { + const endpoint = `${this.#baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; + try { + const response = await this.#fetchFn(endpoint, { + method: 'GET', + }); + if (!validateIntentOrderResponse(response)) { + throw new Error('Invalid submitOrder response'); + } + return response; + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Failed to submit intent: ${error.message}`); + } + throw new Error('Failed to submit intent'); + } + } +} From b59021c4b7a273039e88483d275884c952ec8f3a Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 17:38:16 +0100 Subject: [PATCH 72/85] refactor: fix call to get account --- .../bridge-status-controller/src/bridge-status-controller.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 627d24e1b27..dd7d532f462 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1605,10 +1605,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Thu, 18 Dec 2025 17:39:36 +0100 Subject: [PATCH 73/85] refactor: remove tx update --- .../bridge-status-controller/src/bridge-status-controller.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index dd7d532f462..fdb5cfa8016 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1758,11 +1758,6 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Thu, 18 Dec 2025 17:41:06 +0100 Subject: [PATCH 74/85] chore: remove duplicate comment --- .../bridge-status-controller/src/bridge-status-controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index fdb5cfa8016..41ae8d15c1f 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -214,7 +214,6 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { type, status, id } = transactionMeta; - // Skip intent transactions - they have their own tracking via CoW API // Skip intent transactions - they have their own tracking via CoW API if ( (transactionMeta as { swapMetaData?: { isIntentTx?: boolean } }) From 87b90ec8deb41f86bb8f323a72651a2c9aad15c5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 17:45:21 +0100 Subject: [PATCH 75/85] chore: fix import --- .../bridge-status-controller/src/bridge-status-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 41ae8d15c1f..4c93450d60d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -46,7 +46,7 @@ import { REFRESH_INTERVAL_MS, TraceName, } from './constants'; -import { IntentApiImpl } from './utils/intent-api.ts~'; +import { IntentApiImpl } from './utils/intent-api'; import type { IntentOrder } from './intent-order'; import type { BridgeStatusControllerState, From 0b666be99301d9f4760b585d91085ea27e4bf823 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 17:48:55 +0100 Subject: [PATCH 76/85] chore: remove unneeded parameter --- packages/transaction-controller/src/TransactionController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 7fa94a4ba1e..c6950de2761 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -3140,7 +3140,6 @@ export class TransactionController extends BaseController< const submittedTxMeta = this.#updateTransactionInternal( { transactionId, - note: 'TransactionController#processApproval - Intent transaction auto-submitted', skipValidation: true, }, (draftTxMeta) => { From 550997b3d08bb9a919e9577b58e9df7d40c48d52 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 17:51:24 +0100 Subject: [PATCH 77/85] doc: add jsdoc to schema --- .../bridge-controller/src/utils/validators.ts | 99 ++++++++++++++++++- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index c9a817ac504..588ae456726 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -195,32 +195,121 @@ const RefuelDataSchema = StepSchema; // Allow digit strings for amounts/validTo for flexibility across providers const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); -// Intent support (e.g., CoW Swap EIP-712 order signing) +/** + * Identifier of the intent protocol used for order creation and submission. + * + * Examples: + * - CoW Swap + * - Other EIP-712–based intent protocols + */ const IntentProtocolSchema = string; +/** + * Schema for an intent-based order used for EIP-712 signing and submission. + * + * This represents the minimal subset of fields required by intent-based + * protocols (e.g. CoW Swap) to build, sign, and submit an order. + */ export const IntentOrderSchema = type({ - // EIP-712 Order fields (subset required for signing/submission) + /** + * Address of the token being sold. + */ sellToken: HexAddressSchema, + + /** + * Address of the token being bought. + */ buyToken: HexAddressSchema, + + /** + * Optional receiver of the bought tokens. + * If omitted, defaults to the signer / order owner. + */ receiver: optional(HexAddressSchema), + + /** + * Order expiration time. + * + * Can be provided as a UNIX timestamp in seconds, either as a number + * or as a digit string, depending on provider requirements. + */ validTo: DigitStringOrNumberSchema, + + /** + * Arbitrary application-specific data attached to the order. + */ appData: string(), + + /** + * Hash of the `appData` field, used for EIP-712 signing. + */ appDataHash: HexStringSchema, + + /** + * Fee amount paid for order execution, expressed as a digit string. + */ feeAmount: TruthyDigitStringSchema, + + /** + * Order kind. + * + * - `sell`: exact sell amount, variable buy amount + * - `buy`: exact buy amount, variable sell amount + */ kind: enums(['sell', 'buy']), + + /** + * Whether the order can be partially filled. + */ partiallyFillable: boolean(), - // One of these is required by CoW depending on kind; we keep both optional here and rely on backend validation + + /** + * Exact amount of the sell token. + * + * Required for `sell` orders. + */ sellAmount: optional(TruthyDigitStringSchema), + + /** + * Exact amount of the buy token. + * + * Required for `buy` orders. + */ buyAmount: optional(TruthyDigitStringSchema), - // Optional owner/from for convenience when building domain/message + + /** + * Optional order owner / sender address. + * + * Provided for convenience when building the EIP-712 domain and message. + */ from: optional(HexAddressSchema), }); +/** + * Schema representing an intent submission payload. + * + * Wraps the intent order along with protocol and optional routing metadata + * required by the backend or relayer infrastructure. + */ export const IntentSchema = type({ + /** + * Identifier of the intent protocol used to interpret the order. + */ protocol: IntentProtocolSchema, + + /** + * The intent order to be signed and submitted. + */ order: IntentOrderSchema, - // Optional metadata to aid submission/routing + + /** + * Optional settlement contract address used for execution. + */ settlementContract: optional(HexAddressSchema), + + /** + * Optional relayer address responsible for order submission. + */ relayer: optional(HexAddressSchema), }); From f2aa67e7bc359e759e61b8d00fbd96251b1cbd72 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 17:53:04 +0100 Subject: [PATCH 78/85] fix: use superstruct type --- packages/bridge-controller/src/utils/validators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 588ae456726..8f25ca4e973 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -202,7 +202,7 @@ const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); * - CoW Swap * - Other EIP-712–based intent protocols */ -const IntentProtocolSchema = string; +const IntentProtocolSchema = string(); /** * Schema for an intent-based order used for EIP-712 signing and submission. From 4b28212605c44dec2e498cd6f11b3d8210033b9f Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 18:04:14 +0100 Subject: [PATCH 79/85] refactor: fix imports, remove dead code --- .../src/bridge-status-controller.intent.test.ts | 2 +- .../src/bridge-status-controller.ts | 3 +-- .../src/intent-order-status.ts | 0 packages/bridge-status-controller/src/intent-order.ts | 10 ---------- .../src/utils/intent-api.test.ts | 2 +- 5 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 packages/bridge-status-controller/src/intent-order-status.ts delete mode 100644 packages/bridge-status-controller/src/intent-order.ts 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 9abd409abb7..a87fb1e0057 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 @@ -9,7 +9,7 @@ import { StatusTypes, UnifiedSwapBridgeEventName, } from '@metamask/bridge-controller'; -import { IntentOrderStatus } from './intent-order-status'; +import { IntentOrderStatus } from './utils/validators'; import { MAX_ATTEMPTS } from './constants'; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 4c93450d60d..0e47d9ed579 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -47,7 +47,6 @@ import { TraceName, } from './constants'; import { IntentApiImpl } from './utils/intent-api'; -import type { IntentOrder } from './intent-order'; import type { BridgeStatusControllerState, StartPollingForBridgeTxStatusArgsSerialized, @@ -83,7 +82,7 @@ import { handleNonEvmTxResponse, generateActionId, } from './utils/transaction'; -import { IntentOrderStatus } from '../../bridge-controller/src/utils/validators'; +import { IntentOrder, IntentOrderStatus } from './utils/validators'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list diff --git a/packages/bridge-status-controller/src/intent-order-status.ts b/packages/bridge-status-controller/src/intent-order-status.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/bridge-status-controller/src/intent-order.ts b/packages/bridge-status-controller/src/intent-order.ts deleted file mode 100644 index 946d26e2dda..00000000000 --- a/packages/bridge-status-controller/src/intent-order.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IntentOrderStatus } from './intent-order-status'; - -export type IntentOrder = { - id: string; - status: IntentOrderStatus; - txHash?: string; - metadata: { - txHashes?: string[] | string; - }; -}; diff --git a/packages/bridge-status-controller/src/utils/intent-api.test.ts b/packages/bridge-status-controller/src/utils/intent-api.test.ts index f36d6a0461f..3b575c05a49 100644 --- a/packages/bridge-status-controller/src/utils/intent-api.test.ts +++ b/packages/bridge-status-controller/src/utils/intent-api.test.ts @@ -1,7 +1,7 @@ // intent-api.test.ts import { describe, it, expect, jest } from '@jest/globals'; import { IntentApiImpl, type IntentSubmissionParams } from './intent-api'; // adjust if needed -import type { FetchFunction } from './types'; // adjust if needed +import type { FetchFunction } from '../types'; // adjust if needed describe('IntentApiImpl', () => { const baseUrl = 'https://example.com/api'; From c0c42c9f9224f1826b660f227f10c0fed374e593 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 18:23:47 +0100 Subject: [PATCH 80/85] test: fix tests --- .../bridge-status-controller.intent.test.ts | 2 +- .../src/utils/intent-api.test.ts | 58 +++++++++++++------ .../src/utils/intent-api.ts | 4 +- 3 files changed, 44 insertions(+), 20 deletions(-) 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 a87fb1e0057..e628c6169f6 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 @@ -179,7 +179,7 @@ function loadControllerWithMocks() { jest.resetModules(); jest.isolateModules(() => { - jest.doMock('./intent-api', () => ({ + jest.doMock('./utils/intent-api', () => ({ IntentApiImpl: jest.fn().mockImplementation(() => ({ submitIntent: submitIntentMock, getOrderStatus: getOrderStatusMock, diff --git a/packages/bridge-status-controller/src/utils/intent-api.test.ts b/packages/bridge-status-controller/src/utils/intent-api.test.ts index 3b575c05a49..dc9241b9f00 100644 --- a/packages/bridge-status-controller/src/utils/intent-api.test.ts +++ b/packages/bridge-status-controller/src/utils/intent-api.test.ts @@ -1,10 +1,12 @@ // intent-api.test.ts import { describe, it, expect, jest } from '@jest/globals'; -import { IntentApiImpl, type IntentSubmissionParams } from './intent-api'; // adjust if needed -import type { FetchFunction } from '../types'; // adjust if needed +import { IntentApiImpl, type IntentSubmissionParams } from './intent-api'; +import type { FetchFunction } from '../types'; +import { IntentOrderStatus } from './validators'; describe('IntentApiImpl', () => { const baseUrl = 'https://example.com/api'; + const clientId = 'client-id'; const makeParams = (): IntentSubmissionParams => ({ srcChainId: '1', @@ -15,22 +17,30 @@ describe('IntentApiImpl', () => { aggregatorId: 'agg-1', }); - // Key part: strongly type the mock as FetchFunction (returns Promise) const makeFetchMock = () => jest.fn, Parameters>(); + const validIntentOrderResponse = { + id: 'order-1', + status: IntentOrderStatus.SUBMITTED, + metadata: {}, + }; + it('submitIntent calls POST /submitOrder with JSON body and returns response', async () => { - const fetchFn = makeFetchMock().mockResolvedValue({ ok: true, id: 'resp' }); + const fetchFn = makeFetchMock().mockResolvedValue(validIntentOrderResponse); const api = new IntentApiImpl(baseUrl, fetchFn); const params = makeParams(); - const result = await api.submitIntent(params); + const result = await api.submitIntent(params, clientId); - expect(result).toEqual({ ok: true, id: 'resp' }); + expect(result).toEqual(validIntentOrderResponse); expect(fetchFn).toHaveBeenCalledTimes(1); expect(fetchFn).toHaveBeenCalledWith(`${baseUrl}/submitOrder`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'X-Client-Id': clientId, + }, body: JSON.stringify(params), }); }); @@ -39,29 +49,36 @@ describe('IntentApiImpl', () => { const fetchFn = makeFetchMock().mockRejectedValue(new Error('boom')); const api = new IntentApiImpl(baseUrl, fetchFn); - await expect(api.submitIntent(makeParams())).rejects.toThrow( + await expect(api.submitIntent(makeParams(), clientId)).rejects.toThrow( 'Failed to submit intent: boom', ); }); - it('submitIntent returns null when rejection is not an Error', async () => { + it('submitIntent throws generic error when rejection is not an Error', async () => { const fetchFn = makeFetchMock().mockRejectedValue('boom'); const api = new IntentApiImpl(baseUrl, fetchFn); - await expect(api.submitIntent(makeParams())).resolves.toBeNull(); + await expect(api.submitIntent(makeParams(), clientId)).rejects.toThrow( + 'Failed to submit intent', + ); }); it('getOrderStatus calls GET /getOrderStatus with encoded query params and returns response', async () => { - const fetchFn = makeFetchMock().mockResolvedValue({ status: 'filled' }); + const fetchFn = makeFetchMock().mockResolvedValue(validIntentOrderResponse); const api = new IntentApiImpl(baseUrl, fetchFn); const orderId = 'order-1'; const aggregatorId = 'My Agg/With Spaces'; const srcChainId = '10'; - const result = await api.getOrderStatus(orderId, aggregatorId, srcChainId); + const result = await api.getOrderStatus( + orderId, + aggregatorId, + srcChainId, + clientId, + ); - expect(result).toEqual({ status: 'filled' }); + expect(result).toEqual(validIntentOrderResponse); expect(fetchFn).toHaveBeenCalledTimes(1); const expectedEndpoint = @@ -70,22 +87,29 @@ describe('IntentApiImpl', () => { `&aggregatorId=${encodeURIComponent(aggregatorId)}` + `&srcChainId=${srcChainId}`; - expect(fetchFn).toHaveBeenCalledWith(expectedEndpoint, { method: 'GET' }); + expect(fetchFn).toHaveBeenCalledWith(expectedEndpoint, { + method: 'GET', + headers: { + 'X-Client-Id': clientId, + }, + }); }); it('getOrderStatus rethrows Errors with a prefixed message', async () => { const fetchFn = makeFetchMock().mockRejectedValue(new Error('nope')); const api = new IntentApiImpl(baseUrl, fetchFn); - await expect(api.getOrderStatus('o', 'a', '1')).rejects.toThrow( + await expect(api.getOrderStatus('o', 'a', '1', clientId)).rejects.toThrow( 'Failed to get order status: nope', ); }); - it('getOrderStatus returns null when rejection is not an Error', async () => { + it('getOrderStatus throws generic error when rejection is not an Error', async () => { const fetchFn = makeFetchMock().mockRejectedValue({ message: 'nope' }); const api = new IntentApiImpl(baseUrl, fetchFn); - await expect(api.getOrderStatus('o', 'a', '1')).resolves.toBeNull(); + await expect(api.getOrderStatus('o', 'a', '1', clientId)).rejects.toThrow( + 'Failed to get order status', + ); }); }); diff --git a/packages/bridge-status-controller/src/utils/intent-api.ts b/packages/bridge-status-controller/src/utils/intent-api.ts index 2464fc2b290..905f4e1b026 100644 --- a/packages/bridge-status-controller/src/utils/intent-api.ts +++ b/packages/bridge-status-controller/src/utils/intent-api.ts @@ -81,9 +81,9 @@ export class IntentApiImpl implements IntentApi { return response; } catch (error: unknown) { if (error instanceof Error) { - throw new Error(`Failed to submit intent: ${error.message}`); + throw new Error(`Failed to get order status: ${error.message}`); } - throw new Error('Failed to submit intent'); + throw new Error('Failed to get order status'); } } } From 749d063884bb281948eac6de0bfc84d83be696d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Fri, 19 Dec 2025 10:20:10 +0100 Subject: [PATCH 81/85] fix: move skip gas estimate inside tx options --- .../bridge-status-controller/src/bridge-status-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 0e47d9ed579..e34d1119894 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1710,7 +1710,6 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Fri, 19 Dec 2025 12:08:04 +0100 Subject: [PATCH 82/85] refactor: reinstate tracking for failed intent tx --- .../src/bridge-status-controller.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index e34d1119894..bd10b017dcd 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -213,14 +213,6 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { type, status, id } = transactionMeta; - // Skip intent transactions - they have their own tracking via CoW API - if ( - (transactionMeta as { swapMetaData?: { isIntentTx?: boolean } }) - .swapMetaData?.isIntentTx - ) { - return; - } - if ( type && [ From 05b4eb4066ac86784e6e8a0674d754586d28039d Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 19 Dec 2025 12:11:40 +0100 Subject: [PATCH 83/85] chore: add comment for synthetic tx --- .../bridge-status-controller/src/bridge-status-controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index bd10b017dcd..2c3511434d5 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1692,6 +1692,8 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Fri, 19 Dec 2025 18:42:44 +0100 Subject: [PATCH 84/85] fix: fix gas calculation --- .../src/bridge-status-controller.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 2c3511434d5..24fe8030a8c 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1239,17 +1239,15 @@ export class BridgeStatusController extends StaticIntervalPollingController => { - const maxGasLimit = toHex(transactionParams.gas ?? 0); - // If txFee is provided (gasIncluded case), use the quote's gas fees // Convert to hex since txFee values from the quote are decimal strings if (txFee) { return { maxFeePerGas: toHex(txFee.maxFeePerGas ?? 0), maxPriorityFeePerGas: toHex(txFee.maxPriorityFeePerGas ?? 0), - gas: maxGasLimit, + gas: transactionParams.gas ? toHex(transactionParams.gas) : undefined, }; } @@ -1269,7 +1267,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Wed, 24 Dec 2025 12:42:28 +0100 Subject: [PATCH 85/85] test: add / fix tests --- .../bridge-status-controller/jest.config.js | 4 +- .../bridge-status-controller.intent.test.ts | 88 ++++++++++++++++--- .../src/bridge-status-controller.ts | 13 ++- .../src/utils/intent-api.test.ts | 26 ++++++ 4 files changed, 119 insertions(+), 12 deletions(-) diff --git a/packages/bridge-status-controller/jest.config.js b/packages/bridge-status-controller/jest.config.js index 15a04af42e5..ea33c08f392 100644 --- a/packages/bridge-status-controller/jest.config.js +++ b/packages/bridge-status-controller/jest.config.js @@ -14,10 +14,12 @@ module.exports = merge(baseConfig, { // The display name when running multiple projects displayName, + coverageProvider: 'v8', + // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 94, + branches: 91, functions: 100, lines: 100, statements: 100, 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 e628c6169f6..9eab25cf356 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 @@ -12,6 +12,7 @@ import { import { IntentOrderStatus } from './utils/validators'; import { MAX_ATTEMPTS } from './constants'; +import { IntentApiImpl } from './utils/intent-api'; type Tx = Pick & { type?: TransactionType; @@ -20,6 +21,25 @@ type Tx = Pick & { txReceipt?: any; }; +function seedIntentHistory(controller: any) { + controller.update((s: any) => { + s.txHistory['intent:1'] = { + txMetaId: 'intent:1', + originalTransactionId: 'tx1', + quote: { + srcChainId: 1, + destChainId: 1, + intent: { protocol: 'cowswap' }, + }, + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '' }, + }, + attempts: undefined, // IMPORTANT: prevents early return + }; + }); +} + function minimalIntentQuoteResponse( accountAddress: string, overrides?: Partial, @@ -967,16 +987,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); expect(controller.state.txHistory['tx1'].status.status).toBe( - StatusTypes.PENDING, - ); - - // no tracking call should be made from this callback path - expect((messenger.call as jest.Mock).mock.calls).not.toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - 'BridgeController:trackUnifiedSwapBridgeEvent', - ]), - ]), + StatusTypes.FAILED, ); }); @@ -1369,4 +1380,61 @@ describe('BridgeStatusController (target uncovered branches)', () => { ), ).rejects.toThrow(/undefined multichain account/u); }); + + test('intent order PENDING maps to bridge PENDING', async () => { + const { controller, getOrderStatusMock } = setup(); + + seedIntentHistory(controller); + + getOrderStatusMock.mockResolvedValueOnce({ + id: 'order-1', + status: IntentOrderStatus.PENDING, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'intent:1' }); + + expect(controller.state.txHistory['intent:1'].status.status).toBe( + StatusTypes.PENDING, + ); + }); + + test('intent order SUBMITTED maps to bridge SUBMITTED', async () => { + const { controller, getOrderStatusMock } = setup(); + + seedIntentHistory(controller); + + getOrderStatusMock.mockResolvedValueOnce({ + id: 'order-1', + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'intent:1' }); + + expect(controller.state.txHistory['intent:1'].status.status).toBe( + StatusTypes.SUBMITTED, + ); + }); + + test('unknown intent order status maps to bridge UNKNOWN', async () => { + const { controller, getOrderStatusMock } = setup(); + + seedIntentHistory(controller); + + getOrderStatusMock.mockResolvedValueOnce({ + id: 'order-1', + status: 'SOME_NEW_STATUS' as any, // force UNKNOWN branch + txHash: undefined, + metadata: { txHashes: [] }, + }); + + await (controller as any)._executePoll({ bridgeTxMetaId: 'intent:1' }); + + expect(controller.state.txHistory['intent:1'].status.status).toBe( + StatusTypes.UNKNOWN, + ); + }); }); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 24fe8030a8c..ea507e0ab44 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -701,13 +701,16 @@ export class BridgeStatusController extends StaticIntervalPollingController => { + /* c8 ignore start */ const { txHistory } = this.state; const historyItem = txHistory[bridgeTxMetaId]; + if (!historyItem) { return; } // Backoff handling + if (shouldSkipFetchDueToFetchFailures(historyItem.attempts)) { return; } @@ -740,6 +743,7 @@ export class BridgeStatusController extends StaticIntervalPollingController => { + /* c8 ignore start */ const start = Date.now(); // Poll the TransactionController state for status changes // We intentionally keep this simple to avoid extra wiring/subscriptions in this controller @@ -1075,6 +1082,7 @@ export class BridgeStatusController extends StaticIntervalPollingController setTimeout(resolve, pollMs)); } + /* c8 ignore stop */ }; readonly #handleApprovalTx = async ( @@ -1604,6 +1613,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { 'Failed to get order status', ); }); + + it('submitIntent throws when response fails validation', async () => { + const fetchFn = makeFetchMock().mockResolvedValue({ + foo: 'bar', // invalid IntentOrder shape + } as any); + + const api = new IntentApiImpl(baseUrl, fetchFn); + + await expect(api.submitIntent(makeParams(), clientId)).rejects.toThrow( + 'Failed to submit intent: Invalid submitOrder response', + ); + }); + + it('getOrderStatus throws when response fails validation', async () => { + const fetchFn = makeFetchMock().mockResolvedValue({ + foo: 'bar', // invalid IntentOrder shape + } as any); + + const api = new IntentApiImpl(baseUrl, fetchFn); + + await expect( + api.getOrderStatus('order-1', 'agg', '1', clientId), + ).rejects.toThrow( + 'Failed to get order status: Invalid submitOrder response', + ); + }); });