fix: fail cancelled smart transactions through the standard failure path#9400
Conversation
When the relay cancels a smart transaction (e.g. FAILED_GAS_TOO_LOW), the smart-transactions-controller marked the associated regular transaction as failed via `TransactionController:updateTransaction`, which only patches state and does not emit any transaction lifecycle events. Consumers that react to `transactionFailed`/`transactionStatusUpdated` — notably the bridge status controller and transaction metrics — were therefore never notified, leaving cancelled smart transactions (such as bridges) stuck as pending indefinitely. - transaction-controller: add a public `failTransaction(transactionId, error)` method and `TransactionController:failTransaction` messenger action that fails a transaction through the internal fail path, emitting `transactionFailed`, `transactionStatusUpdated`, and `transactionFinished`. - smart-transactions-controller: use `failTransaction` instead of `updateTransaction` when marking regular transactions as failed.
| * @param transactionId - The ID of the transaction to mark as failed. | ||
| * @param error - The error describing why the transaction failed. | ||
| */ | ||
| failTransaction(transactionId: string, error: Error): void { |
There was a problem hiding this comment.
This will ultimately generate the same state as the previous update, except the explicit calls to #onTransactionStatusChange means the status change events are a conscious decision rather than derived from the state which should be the source of truth.
Could we avoid any changes to the SmartTransactionController and capture all status changes entirely automatically, if we instead scrap the calls to #onTransactionStatusChange and instead diff the previous and new status inside #updateTransactionInternal and throw the event(s) there after the update call?
Or Confirmations team could do that in a follow up to minimise effort here if time-sensitive.
There was a problem hiding this comment.
I think this is the cleanest approach but it has more overhead given the additional events too, will come back to this internally later given the risk.
There was a problem hiding this comment.
Yeah, agreed that's the cleaner long-term shape. Deriving the events from the status diff in #updateTransactionInternal would kill off this whole class of "caller forgot to emit" bugs and let us drop the manual #onTransactionStatusChange calls plus this failTransaction wrapper.
I kept it narrow here mainly because it's not just transactionStatusUpdated. Each transition also fires transactionFailed/transactionConfirmed/transactionDropped/transactionFinished, so doing it right means deriving all of those from the diff and pulling out the ~8 manual call sites so we don't double-fire. Felt like too much surface area to fold into a stuck-bridge fix. This version just goes through the existing #failTransaction, so the end state is identical (like you said) and it matches the other failure paths.
I was hoping we could get in this targeted fix now. I opened an issue for the follow-up refactor so Confirmations can pick it up when there's bandwidth: #9412
|
|
||
| ### Fixed | ||
|
|
||
| - Fail the associated regular transaction via the new `TransactionController:failTransaction` action instead of `TransactionController:updateTransaction` when a smart transaction is cancelled ([#9400](https://github.com/MetaMask/core/pull/9400)) |
There was a problem hiding this comment.
Isn't this a breaking change ?
## Explanation Follow-up to MetaMask#9400 (already merged). That PR changed the `smart-transactions-controller` messenger requirement from `TransactionController:updateTransaction` to `TransactionController:failTransaction`. Adding/removing a required allowed action is a **breaking change** for consumers — they must delegate the new action to the controller messenger on the client side (extension/mobile) or they get a type error. The changelog entry was filed under `### Fixed`, which would have the package cut as a patch release and silently break consumers on upgrade. This recategorizes it under `### Changed` with a `**BREAKING:**` marker so the next release is correctly a **major** version bump (`24.2.4` → `25.0.0`), consistent with how allowed-action changes are documented elsewhere in the monorepo (e.g. `AssetsControllerMessenger now requires ...` in assets-controller, `NftController allowed actions` in assets-controllers). No code changes — changelog only. ## References - Follow-up to MetaMask#9400 - Raised in review by @cryptodev-2s ## Changelog ### `@metamask/smart-transactions-controller` - Recategorized the MetaMask#9400 entry from `Fixed` to `Changed` and marked it `BREAKING`; no functional change. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate (N/A — changelog only) - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate (changelog) - [x] I've highlighted breaking changes using the 'BREAKING' category above as appropriate <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only changelog edit with no code, tests, or runtime behavior changes. > > **Overview** > Updates `@metamask/smart-transactions-controller` **Unreleased** changelog only: the MetaMask#9400 note about using `TransactionController:failTransaction` instead of `TransactionController:updateTransaction` when a smart transaction is cancelled moves from `### Fixed` to `### Changed` and is labeled **`BREAKING:`**, with explicit guidance that consumers must delegate the new allowed action. > > The separate `### Fixed` section is removed and the `@metamask/messenger` bump stays under `### Changed`. **No runtime or API code changes** in this PR—only semver/documentation so the next release is treated as major for the messenger requirement change from MetaMask#9400. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3bdac95. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
## Explanation Releases two packages needed to fix stuck/underpriced bridge smart transactions in the clients (extension + mobile). ### `@metamask/transaction-controller` — `68.2.2` → `68.3.0` (minor) - **Added:** `failTransaction` method + `TransactionController:failTransaction` messenger action ([MetaMask#9400](MetaMask#9400)) — fails a tx through the standard path, emitting `transactionFailed`/`transactionStatusUpdated`/`transactionFinished`. - **Fixed:** Only apply user-saved (advanced) gas fees to dApp transactions; ignored for internal (`isInternal`) txs such as swaps and bridges ([MetaMask#9401](MetaMask#9401)). - **Changed:** Bump `@metamask/messenger` `^1.2.0` → `^2.0.0` ([MetaMask#9392](MetaMask#9392)). ### `@metamask/smart-transactions-controller` — `24.2.4` → `25.0.0` (major) - **Changed (BREAKING):** Fail the associated regular transaction via `TransactionController:failTransaction` instead of `updateTransaction` when a smart transaction is cancelled ([MetaMask#9400](MetaMask#9400)). Consumers must now grant the STX messenger access to `TransactionController:failTransaction`. - **Changed:** Bump `@metamask/messenger` `^1.2.0` → `^2.0.0` ([MetaMask#9392](MetaMask#9392)). All other changed packages are `intentionally-skip` (owned by other teams); dependent version ranges were updated automatically. ## References - Fixes stuck-pending bridge STX (root cause in MetaMask#9400) and underpriced bridge gas (MetaMask#9401). ## Checklist - [x] I've followed the [release process](https://github.com/MetaMask/core/blob/main/docs/processes/releasing.md) - [x] Changelogs reviewed and categorized (no `Uncategorized`; breaking change marked) - [x] Version bumps follow SemVer (STX major for the breaking messenger change) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches transaction failure signaling and gas on internal bridge flows; STX **25.0.0** is a breaking messenger permission change for extension/mobile integrators. > > **Overview** > **Monorepo release `1102.0.0`** that publishes **`@metamask/transaction-controller` `68.3.0`** and **`@metamask/smart-transactions-controller` `25.0.0`**, and rolls **`@metamask/transaction-controller` `^68.2.2` → `^68.3.0`** through dependent packages and **`yarn.lock`**. > > **`transaction-controller` `68.3.0`** (documented in its changelog for this release): adds **`failTransaction`** / **`TransactionController:failTransaction`** so out-of-band failures emit normal lifecycle events; fixes **advanced gas fees** so they apply only to dApp txs, not **`isInternal`** bridge/swap txs. > > **`smart-transactions-controller` `25.0.0` (breaking):** on smart-tx cancel, calls **`failTransaction`** instead of **`updateTransaction`**; hosts must delegate **`TransactionController:failTransaction`** on the STX messenger. > > Other touched packages in the diff are mainly **version/changelog dependency bumps** tied to this release, not new feature code in those files. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c423a08. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Explanation
When the transaction relay cancels a smart transaction (e.g.
FAILED_GAS_TOO_LOW— the smart tx never lands on chain),SmartTransactionsControllermarks the associated regular transaction as failed via theTransactionController:updateTransactionaction.updateTransactiononly patches state — it does not emit any transaction lifecycle events. As a result, consumers that react totransactionFailed/transactionStatusUpdatedare never notified:BridgeStatusControllermarks a bridge item failed only from thetransactionStatusUpdated/transactionFailedevent. Since that event never fires for a cancelled smart tx, the bridge history item staysPENDINGforever — the transaction shows as failed, but the bridge UI remains stuck on "Pending".Transaction Finalized) are likely under-reported for the same reason.This was observed in production with bridge smart transactions repeatedly stuck as pending (source tx
status: failed, bridgestatus.status: PENDING), while the relay'sbatchStatusreportedminedTx: cancelled/cancellationReason: too_cheap.Fix
Route the cancellation failure through the standard fail path so the lifecycle events fire.
transaction-controller: add a publicfailTransaction(transactionId, error)method and correspondingTransactionController:failTransactionmessenger action. It fails the transaction through the internal#failTransaction, emittingtransactionFailed,transactionStatusUpdated, andtransactionFinished.smart-transactions-controller:markRegularTransactionsAsFailednow callsfailTransactioninstead ofupdateTransaction.References
Changelog
@metamask/transaction-controllerfailTransactionmethod andTransactionController:failTransactionmessenger action.@metamask/smart-transactions-controllerTransactionController:failTransaction(wasupdateTransaction) so transaction lifecycle events are emitted and downstream consumers no longer leave cancelled smart transactions stuck as pending.TransactionController:failTransaction(previouslyTransactionController:updateTransaction).Checklist
Note
Medium Risk
Touches core transaction lifecycle and messenger permissions for smart-transactions; behavior change is intentional but clients must update delegated actions or cancellation failures will not propagate.
Overview
When a smart transaction is cancelled, the linked regular transaction is now failed through
TransactionController:failTransactioninstead ofupdateTransaction, sotransactionFailed,transactionStatusUpdated, andtransactionFinishedare emitted. That fixes cases where the tx looked failed but bridge history and similar subscribers stayed pending because they only react to those events.@metamask/transaction-controllerexposes publicfailTransaction(transactionId, error)and theTransactionController:failTransactionmessenger action, delegating to the existing internal failure path.@metamask/smart-transactions-controllerupdatesmarkRegularTransactionsAsFailedand messenger allowances to call it with aSmartTransactionFailederror (same matching rules for tx id / hashes; still skips already-failed txs).Integrators: grant
TransactionController:failTransactionto the smart transactions controller messenger (replacingTransactionController:updateTransactionfor this flow).Reviewed by Cursor Bugbot for commit 1cdd855. Bugbot is set up for automated code reviews on this repo. Configure here.