-
-
Notifications
You must be signed in to change notification settings - Fork 262
feat: intent-based swaps support #6547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
831771d to
fdba719
Compare
fdba719 to
e530943
Compare
| // Check metadata for additional transaction hashes | ||
| const metadataTxHashes = Array.isArray(intentOrder.metadata.txHashes) | ||
| ? intentOrder.metadata.txHashes | ||
| : []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String txHashes from API silently discarded as empty array
Medium Severity
The IntentOrder type and schema explicitly allow metadata.txHashes to be either string[] or a single string. However, the code at line 788-790 only handles the array case: when txHashes is a string, Array.isArray() returns false and metadataTxHashes becomes an empty array []. This causes the transaction hash to be silently lost unless it happens to match txHash. The code should handle the string case by wrapping it in an array (e.g., typeof txHashes === 'string' ? [txHashes] : []).
| address: string; | ||
| ignoreNetwork: boolean; | ||
| }) => { | ||
| }): void => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Polling not stopped when wiping status with ignoreNetwork
Medium Severity
When wipeBridgeStatus is called with ignoreNetwork: true, txHistory is cleared but active polling tokens in #pollingTokensByTxMetaId are not stopped. The ignoreNetwork: false path correctly stops polling via #wipeBridgeStatusByChainId, but the ignoreNetwork: true branch just wipes state without cleanup. This causes orphaned polling to continue for deleted history items, potentially leading to errors when the polling callback tries to access non-existent history entries, and constitutes a resource leak.
| // Check metadata for additional transaction hashes | ||
| const metadataTxHashes = Array.isArray(intentOrder.metadata.txHashes) | ||
| ? intentOrder.metadata.txHashes | ||
| : []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String txHashes value lost when not array
Medium Severity
The IntentOrder type allows metadata.txHashes to be either string[] or string, but the code only handles the array case. When txHashes is a string (e.g., "0xabc"), Array.isArray() returns false, causing metadataTxHashes to become an empty array. If txHash is also empty/undefined, the transaction hash is silently lost. The code needs to handle the string case by wrapping it in an array (e.g., [intentOrder.metadata.txHashes]).
| default: | ||
| return TransactionStatus.submitted; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CANCELLED intent orders incorrectly map to submitted status
Low Severity
IntentOrderStatus.CANCELLED is defined in the enum but not handled in either status mapping function. In mapIntentOrderStatusToTransactionStatus, CANCELLED falls to the default case returning TransactionStatus.submitted. In #updateBridgeHistoryFromIntentOrder, CANCELLED falls through to StatusTypes.UNKNOWN. This creates an inconsistency where a cancelled order shows as "submitted" in the TransactionController but "unknown" in bridge status. A cancelled order semantically should map to failed status in both places.
Additional Locations (1)
| default: | ||
| return TransactionStatus.submitted; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CANCELLED status incorrectly maps to submitted
Medium Severity
The mapIntentOrderStatusToTransactionStatus function doesn't handle IntentOrderStatus.CANCELLED in its switch statement. When an order is cancelled, it falls through to the default case and returns TransactionStatus.submitted, which is incorrect. Cancelled orders represent a terminal failed state and should map to TransactionStatus.failed like EXPIRED and FAILED statuses do.
| // Check metadata for additional transaction hashes | ||
| const metadataTxHashes = Array.isArray(intentOrder.metadata.txHashes) | ||
| ? intentOrder.metadata.txHashes | ||
| : []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String txHashes metadata value silently dropped
Medium Severity
The schema in validators.ts allows metadata.txHashes to be either string[] or string, but #updateBridgeHistoryFromIntentOrder only handles the array case. When the API returns txHashes as a single string, the code converts it to an empty array [] instead of wrapping it in an array, causing the transaction hash to be silently lost. This would break tracking for partially-filled intent orders that report a single hash.
| } | ||
| ).txReceipt, | ||
| transactionHash: txHash, | ||
| status: (isComplete ? '0x1' : '0x0') as unknown as string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
txReceipt status incorrectly set for pending transactions
Medium Severity
When updating a transaction that has a txHash but is in PENDING or SUBMITTED status (not complete), the code sets txReceipt.status to '0x0' (failed). This is incorrect because the transaction hasn't failed - it's still in progress. The txReceipt.status assignment only considers isComplete, treating all non-complete states as failures. This could cause the UI to incorrectly display pending intent transactions as failed.
|
|
||
| readonly #addTxToHistory = ( | ||
| startPollingForBridgeTxStatusArgs: StartPollingForBridgeTxStatusArgsSerialized, | ||
| ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same-chain intent swaps not restarted after browser restart
Medium Severity
The #restartPollingForIncompleteHistoryItems method filters history items using only the isBridgeTx check (cross-chain), but #startPollingForTxId was updated to poll for either cross-chain OR intent transactions (isBridgeTx || isIntent). Same-chain intent swaps (e.g., CoW swaps on mainnet) will have polling started initially, but after a browser restart, they're filtered out by the isCrossChain check and polling never resumes. This leaves same-chain intent orders stuck in PENDING/UNKNOWN status indefinitely.
Additional Locations (1)
| // Check metadata for additional transaction hashes | ||
| const metadataTxHashes = Array.isArray(intentOrder.metadata.txHashes) | ||
| ? intentOrder.metadata.txHashes | ||
| : []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String txHashes from API silently ignored in status update
Medium Severity
The IntentOrder.metadata.txHashes field can be either string[] | string per the schema definition, but #updateBridgeHistoryFromIntentOrder only handles the array case. When txHashes is a single string (not an array), Array.isArray() returns false and the code falls back to an empty array, discarding the transaction hash. This could cause transaction hashes returned by the intent provider to be silently lost.
| status: statusType, | ||
| srcChain: { | ||
| chainId: srcChainId, | ||
| txHash: txHash ?? historyItem.status.srcChain.txHash ?? '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Existing txHash overwritten when intent order has none
Medium Severity
At line 786, txHash is assigned intentOrder.txHash ?? '', making it an empty string when no hash is available. Then at line 805, txHash ?? historyItem.status.srcChain.txHash uses nullish coalescing which only falls back for null/undefined, not empty string. This means an empty string overwrites any existing srcChain.txHash value instead of preserving it. The code likely intended to use || or an explicit empty-string check to fall back to the existing hash.
| statusType = StatusTypes.SUBMITTED; | ||
| } else { | ||
| statusType = StatusTypes.UNKNOWN; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CANCELLED intent order status maps to wrong values
Medium Severity
The IntentOrderStatus.CANCELLED status is defined in the enum but not handled in the status mapping logic. In #updateBridgeHistoryFromIntentOrder, a cancelled order falls through to StatusTypes.UNKNOWN instead of StatusTypes.FAILED. In mapIntentOrderStatusToTransactionStatus, it falls through to the default case returning TransactionStatus.submitted instead of failed. A cancelled order should be treated as failed, not as unknown or submitted.
Additional Locations (1)
| }); | ||
| }; | ||
|
|
||
| resetState = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Failed intent transactions not marked failed in bridge history
High Severity
The #markTxAsFailed method cannot find intent transactions when they fail. Intent history entries are keyed by intent:${orderUid} with the actual TransactionController ID stored in originalTransactionId. When transactionFailed fires with the TC transaction ID, the lookup checks only direct txMetaId match and approvalTxId match - it never checks originalTransactionId. This causes failed/rejected intent transactions to remain in PENDING state in bridge history instead of being marked FAILED, leaving users with stuck transactions that never update.
| // Check metadata for additional transaction hashes | ||
| const metadataTxHashes = Array.isArray(intentOrder.metadata.txHashes) | ||
| ? intentOrder.metadata.txHashes | ||
| : []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Single string txHashes value ignored, treated as empty array
Medium Severity
The IntentOrder.metadata.txHashes field can be either string[] | string according to the type definition and schema in validators.ts. However, the code only handles the array case: when txHashes is a single string (not an array), Array.isArray() returns false and the code falls back to an empty array [], causing that transaction hash to be lost. This could result in missing transaction hash data for intent orders that return a single string hash in their metadata.
packages/bridge-status-controller/src/bridge-status-controller.ts
Outdated
Show resolved
Hide resolved
packages/bridge-status-controller/src/bridge-status-controller.ts
Outdated
Show resolved
Hide resolved
4832227 to
126f733
Compare
| traceContext: context, | ||
| }), | ||
| ); | ||
| } else if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converting gasPrice to the EIP-1559 properties should already be done in gas-fees.ts.
We also intentionally encapsulate all that complex gas fee logic in the util to minimise coupling in this file.
| !tx.isUserOperation, | ||
| !tx.isUserOperation && | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| !(tx as any).swapMetaData?.isIntentTx, // Exclude intent transactions from pending tracking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't aware we still used this metadata since unified bridging and creating all swaps and bridges from the BridgeStatusController?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than relying on metadata, would these new transactions not warrant an alternate transactionType that we could exclude here?
Plus if these are EVM transactions submitted by the user's wallet, do we not want to check if they are confirmed here for consistency?
Do we currently rely on an API to confirm their success? Could we do both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only place we use swapMetaData and we plan to remove this tech debt after v1. Unified swaps and bridges don't use the metadata field anymore
Do we currently rely on an API to confirm their success? Could we do both?
The added transaction is basically a dummy/noop that is not meant to be submitted on-chain. The new submitIntent flow adds it as a placeholder for showing the intent order's status in the UI. Eventually when the bridge-api returns the order's final status we update the txMeta with the tx hash
I think the emulated updateTransaction and addTransaction handlers would be a better fit for this use-case (similar to userOperation). However I just found out about those handlers this week so we haven't tried it yet
| networkClientId, | ||
| type: transactionType, | ||
| skipInitialGasEstimate: true, | ||
| swaps: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a legacy pattern we want to avoid, as we can instead reference this controller state from the client, rather than coupling it to the TransactionController.
|
Given the volume of code here, I'd recommend merging the bridge controllers code in isolation, then we can address any required changes to the |
| } | ||
| } | ||
|
|
||
| // For intent-based transactions (e.g., CoW intents) that are not meant to be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have a mechanism to skip publish through the beforePublish hook.
|
|
||
| // 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If they are entirely external, then is the only benefit to display them in the activity view?
If they share none of the transaction lifecycle, perhaps it's cleaner just for the clients to read data from the BridgeStatusController directly?
My understanding from the code is these are instead quotes that are signed via eth_signTypedData_v4 then processed entirely externally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct @matthewwalsh0 . When the intent order is fulfilled it also shows up as an incoming transaction in the activity log
I was considering replacing this logic with emulated add/update transaction calls. See #6547 (comment). Would you recommend that or having clients read data from BridgeStatusController directly?
References
Relates to https://consensyssoftware.atlassian.net/browse/BETR-5
Checklist
Note
Implements intent-based order flow (e.g., CoW Swap) across controllers with submission, polling, and state tracking.
intentsupport and schemas inbridge-controller(Intent,IntentOrder,StatusTypes.SUBMITTED); extendQuoteSchemato includeintentbridge-status-controller: newsubmitIntent, intent polling viautils/intent-api, map intent statuses to bridge statuses, keeporiginalTransactionIdand aggregatesrcTxHashes, update event tracking and restart/wipe logic; optional gas fields handlingutils/intent-api(submit/get status, validation, status mapping); add comprehensive tests for intent and polling flowstransaction-controller: integrate intent TXs—skip on-chain publish whenswapMetaData.isIntentTx, set status from intent, convert legacygasPriceto EIP-1559 whenskipInitialGasEstimate, exclude intent TXs from pending tracker.gitignoreadditionsWritten by Cursor Bugbot for commit d2c4cc7. This will update automatically on new commits. Configure here.