Skip to content

fix(ledger): fall back to blind signing when clear-sign resolution fails#41948

Merged
owencraston merged 6 commits into
mainfrom
fix/41602-ledger-monad-double-confirm
Apr 30, 2026
Merged

fix(ledger): fall back to blind signing when clear-sign resolution fails#41948
owencraston merged 6 commits into
mainfrom
fix/41602-ledger-monad-double-confirm

Conversation

@dawnseeker8

@dawnseeker8 dawnseeker8 commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Description

Ledger hardware wallet users on Monad (and other chains / contracts without a matching Ledger plugin) were hitting a misleading "blind signing is not enabled" error when trying to complete gas-sponsored swaps, even though blind signing was enabled on the device.

Root cause: LedgerOffscreenHandler.signTransaction called app.clearSignTransaction, which requires a Ledger plugin / contract resolver for the target chain+contract. For Monad's delegation / swap contracts no plugin exists, so clearSignTransaction throws. That throw was surfaced to the UI as a generic hardware-wallet error that the confirmation flow rendered as the "blind signing" copy.

Fix: Wrap clearSignTransaction in a try/catch. On non-user-rejection failures, fall back to signTransaction(hdPath, tx, null) so the device performs a raw blind sign (which works because the user already has blind signing enabled). Clear signing is still attempted first, so ERC20 / NFT flows on supported chains are unchanged.

User rejections on device (status 0x6985 / CONDITIONS_OF_USE_NOT_SATISFIED) are detected and re-thrown instead of triggering a fallback, so we never silently re-prompt after an explicit on-device rejection.

The change is scoped entirely to the offscreen Ledger handler — no UI, keyring, or controller changes were needed.

Changelog

CHANGELOG entry: Fixed a bug where Ledger users could not complete swaps on networks without Ledger plugin support (e.g. Monad), which previously surfaced as a misleading "blind signing is not enabled" error.

Related issues

Fixes: #41602

Manual testing steps

  1. Connect a Ledger device to the extension; unlock and open the Ethereum app.
  2. Enable Blind signing (Settings → Blind signing → Enabled) on the device.
  3. Add the Monad network (or any network without a Ledger plugin mapping) and fund an account with USDC.
  4. Initiate a USDC → MON swap and keep gas sponsorship enabled so the flow requires two transactions (approval + swap).
  5. Confirm both transactions on the Ledger.
  6. Verify:
    • No "blind signing is not enabled" / generic "Something went wrong" modal.
    • Both transactions are broadcast successfully.
    • On-device rejection of either transaction still cancels cleanly (no silent retry / extra prompts).
  7. Regression check: perform a standard ERC20 approve + swap on Ethereum mainnet and confirm clear-signing UI still renders normal token details on the Ledger screen (fallback is not triggered).

Screenshots/Recordings

Before

Users received a "Something went wrong — blind signing is not enabled" modal on the second confirmation of the Monad swap, even with blind signing enabled on device.

After

Both confirmations complete; Ledger shows the blind-sign payload on unsupported contracts and the clear-signed payload on supported contracts.

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs and MetaMask Extension Coding Standards.
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable (added coverage in app/offscreen/hardware-wallets/ledger.test.ts for both the fallback path and the on-device rejection path)
  • I've documented my code using JSDoc format if applicable
  • I've applied the right labels on the PR (see labeling guidelines)

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Made with Cursor


Note

Medium Risk
Changes the Ledger transaction-signing flow to retry with blind signing when clear-sign resolution fails, which could affect how transactions are presented/signed on-device for some contracts/chains. Scoped to the offscreen Ledger handler and covered by new unit tests, but it touches a critical signing path.

Overview
Improves Ledger signTransaction reliability on chains/contracts without Ledger plugin support by catching clearSignTransaction failures and retrying via signTransaction(hdPath, tx, null) (blind sign), with a warning log.

Adds explicit detection of on-device user rejection (0x6985 / CONDITIONS_OF_USE_NOT_SATISFIED) to avoid the fallback and instead surface the rejection error. Updates ledger.test.ts to cover getAppConfiguration, the new fallback behavior, and the no-fallback rejection case.

Reviewed by Cursor Bugbot for commit 78e9e99. Bugbot is set up for automated code reviews on this repo. Configure here.

- Use ensureApp() for getAppConfiguration; avoid undefined arbitraryDataEnabled
- Reset transport on partial makeApp failure and transport-without-ethApp state
- Log full app configuration when METAMASK_DEBUG (UI + offscreen)
- Document investigation notes and add getAppConfiguration unit test
…ution fails

Ledger's `clearSignTransaction` requires a plugin / contract resolver for
the target chain and contract. On networks / contracts without one (e.g.
Monad gas-sponsored swaps routed through the delegation contract), the
call throws and the user sees a generic "Something went wrong" /
"blind signing is not enabled" error even though blind signing is
enabled on the device.

Wrap `clearSignTransaction` in a try/catch and, on non-user-rejection
failures, fall back to `signTransaction(hdPath, tx, null)` so the device
performs a raw blind sign. Clear signing is still attempted first, so
ERC20 / NFT flows on supported chains are unaffected.

User rejections on device (status 0x6985 /
CONDITIONS_OF_USE_NOT_SATISFIED) are detected and re-thrown so we never
silently re-prompt after an explicit rejection.

Fixes #41602.

Made-with: Cursor
…ests

The two fallback tests added for GH 41602 were triggering unmocked
console.warn / console.error, which polluted the global unit-test console
baseline. Align them with the convention used by the rest of this file:
spy on the specific channel with a local mockImplementation, assert the
expected log shape, and restore at the end.

Also tightens the rejection-path assertion to verify that statusCode
0x6985 survives serialization back to the UI rather than only checking
success: false.

Made-with: Cursor
…g logging

- Removed unnecessary debug logging and related imports from the Ledger offscreen handler.
- Simplified the `makeApp` method to ensure transport is only opened when necessary.
- Updated `getAppConfiguration` to throw an error if no transport is available, improving error handling.
- Cleaned up comments and documentation for clarity.

This refactor enhances code readability and maintainability while addressing issues related to transport management and error handling.
@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-be-trade BE Trade team label Apr 20, 2026
@dawnseeker8 dawnseeker8 marked this pull request as ready for review April 20, 2026 13:01
@dawnseeker8 dawnseeker8 requested a review from a team as a code owner April 20, 2026 13:01
@metamaskbotv2

metamaskbotv2 Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

✨ Files requiring CODEOWNER review ✨

🔑 @MetaMask/accounts-engineers (2 files, +129 -10)
  • 📁 app/
    • 📁 offscreen/
      • 📁 hardware-wallets/
        • 📄 ledger.test.ts +81 -0
        • 📄 ledger.ts +48 -10

@sonarqubecloud

Copy link
Copy Markdown

@metamaskbotv2

metamaskbotv2 Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor
Builds ready [ff1ed6a]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24667931544 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -77%
  • loadNewAccount/total: -77%
  • bridgeUserActions/bridge_load_page: -19%
  • bridgeUserActions/bridge_load_asset_picker: -33%
  • bridgeUserActions/bridge_search_token: -24%
  • bridgeUserActions/total: -28%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.6s
  • 🟡 confirmTx/FCP: p75 2.6s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -21%
  • startupStandardHome/domContentLoaded: -11%
  • startupStandardHome/firstPaint: +11%
  • startupStandardHome/backgroundConnect: +10%
  • startupStandardHome/firstReactRender: -14%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -13%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -20%
  • startupStandardHome/load: -16%
  • startupStandardHome/domContentLoaded: -15%
  • startupStandardHome/firstPaint: -15%
  • startupStandardHome/backgroundConnect: -37%
  • startupStandardHome/firstReactRender: -23%
  • startupStandardHome/loadScripts: -15%
  • startupStandardHome/setupStore: -13%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/domInteractive: -53%
  • startupStandardHome/backgroundConnect: +14%
  • startupStandardHome/initialActions: +33%
  • startupStandardHome/numNetworkReqs: -32%
  • startupStandardHome/uiStartup: -14%
  • startupStandardHome/domInteractive: -56%
  • startupStandardHome/initialActions: +14%
  • startupStandardHome/setupStore: -57%
  • startupStandardHome/numNetworkReqs: -34%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -83%
  • onboardingImportWallet/metricsToWalletReadyScreen: -14%
  • onboardingImportWallet/doneButtonToHomeScreen: -79%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +45%
  • onboardingImportWallet/total: -40%
  • onboardingNewWallet/srpButtonToPwForm: -77%
  • onboardingNewWallet/skipBackupToMetricsScreen: -68%
  • onboardingNewWallet/doneButtonToAssetList: -28%
  • onboardingNewWallet/total: -29%
  • assetDetails/assetClickToPriceChart: -60%
  • assetDetails/total: -60%
  • solanaAssetDetails/assetClickToPriceChart: -69%
  • solanaAssetDetails/total: -69%
  • importSrpHome/openAccountMenuAfterLogin: -71%
  • importSrpHome/homeAfterImportWithNewWallet: -73%
  • importSrpHome/total: -64%
  • sendTransactions/selectTokenToSendFormLoaded: -21%
  • sendTransactions/reviewTransactionToConfirmationPage: +43%
  • sendTransactions/total: +40%
  • swap/openSwapPageFromHome: -97%
  • swap/fetchAndDisplaySwapQuotes: +31%
  • swap/total: +11%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.5s
  • 🟡 importSrpHome/FCP: p75 2.5s
  • 🟡 sendTransactions/FCP: p75 2.6s
  • 🟡 swap/FCP: p75 2.6s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 58 Bytes (0%)
  • ui: 8.84 KiB (0.1%)
  • common: 23 Bytes (0%)

@dawnseeker8

Copy link
Copy Markdown
Contributor Author

Close as duplicated to #41899

@github-actions github-actions Bot locked and limited conversation to collaborators Apr 20, 2026
@dawnseeker8

Copy link
Copy Markdown
Contributor Author

Reopen for QA to test before we make final decisions on #41899

@nikolastoimenovski-consensys

Copy link
Copy Markdown

Tested on the latest build with Mon, Sei and other networks that don't support gas sponsorship. Tested swap and bridge with 1 and 2 approvals, with and without dapp, works as expected. No regression issues have been found. Adding "qa passed" label.

@gantunesr

Copy link
Copy Markdown
Member

Looks good. Did not test

@owencraston owencraston added this pull request to the merge queue Apr 30, 2026
Merged via the queue into main with commit f9dcf04 Apr 30, 2026
212 checks passed
@owencraston owencraston deleted the fix/41602-ledger-monad-double-confirm branch April 30, 2026 14:54
@metamaskbot metamaskbot added the release-13.30.0 Issue or pull request that will be included in release 13.30.0 label Apr 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

QA Passed release-13.30.0 Issue or pull request that will be included in release 13.30.0 size-M team-be-trade BE Trade team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: [Ledger] Gas sponsorship - The swap fails when 2 transaction confirmations are needed on Mon network

6 participants