From 62b7395023d937d9b9f1fde90e271b03aa9232a2 Mon Sep 17 00:00:00 2001 From: Francis O'Brien <42175565+F-OBrien@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:30:08 +0000 Subject: [PATCH] Improve Ledger reliability and signing (WebHID, lifecycle, mismatch guard) (#1611) * fix: prefer WebHID over WebUSB for Ledger transport to fix Flex/Nano X connectivity * fix(ui): release Ledger transport with explicit disconnect lifecycle * fix(ui): align Ledger app mode with reactive settings context * refactor(ui): keep useLedger render phase pure for init errors * fix(ui): apply popup-aware routing to Ledger connection links * fix(ui): harden Ledger signing error handling and mismatch UX - Remove chain.name being set as specName from merkleizeMetadata payload - Use reactive SettingsContext ledgerApp instead of settings.get() snapshot - Guard no-metadata signing path with setIsBusy(false) + early return - Detect signer mismatch by normalizing addresses and block signing when mismatched --- packages/extension-ui/package.json | 2 +- .../src/Popup/Signing/LedgerSign.tsx | 78 +++++-- packages/extension-ui/src/hooks/useLedger.ts | 207 ++++++++++++------ .../extension-ui/src/partials/MenuAdd.tsx | 8 +- .../public/locales/en/translation.json | 4 +- yarn.lock | 24 +- 6 files changed, 214 insertions(+), 109 deletions(-) diff --git a/packages/extension-ui/package.json b/packages/extension-ui/package.json index 06049f10..2a4f9832 100644 --- a/packages/extension-ui/package.json +++ b/packages/extension-ui/package.json @@ -29,7 +29,7 @@ "@polkadot/extension-chains": "0.62.7-1-x", "@polkadot/extension-dapp": "0.62.7-1-x", "@polkadot/extension-inject": "0.62.7-1-x", - "@polkadot/hw-ledger": "^13.5.9", + "@polkadot/hw-ledger": "^14.0.3", "@polkadot/keyring": "^13.5.9", "@polkadot/networks": "^13.5.9", "@polkadot/react-identicon": "^3.16.4", diff --git a/packages/extension-ui/src/Popup/Signing/LedgerSign.tsx b/packages/extension-ui/src/Popup/Signing/LedgerSign.tsx index 55a7f45c..29376ec2 100644 --- a/packages/extension-ui/src/Popup/Signing/LedgerSign.tsx +++ b/packages/extension-ui/src/Popup/Signing/LedgerSign.tsx @@ -11,13 +11,13 @@ import type { HexString } from '@polkadot/util/types'; import { faSync } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useState } from 'react'; -import settings from '@polkadot/ui-settings'; -import { assert, objectSpread, u8aToHex } from '@polkadot/util'; +import { assert, objectSpread, stringShorten, u8aToHex } from '@polkadot/util'; +import { decodeAddress } from '@polkadot/util-crypto'; import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata'; -import { Button, Warning } from '../../components/index.js'; +import { Button, SettingsContext, Warning } from '../../components/index.js'; import { useLedger, useMetadata, useTranslation } from '../../hooks/index.js'; import { styled } from '../../styled.js'; @@ -42,7 +42,6 @@ function getMetadataProof (chain: Chain, payload: SignerPayloadJSON) { const merkleizedMetadata = merkleizeMetadata(m, { base58Prefix: chain.ss58Format, decimals: chain.tokenDecimals, - specName: chain.name.toLowerCase(), specVersion: chain.specVersion, tokenSymbol: chain.tokenSymbol }); @@ -56,25 +55,48 @@ function getMetadataProof (chain: Chain, payload: SignerPayloadJSON) { }; } +function matchesExpectedSigner (derivedAddress: string, expectedAddress: string, isEthereum: boolean): boolean { + if (isEthereum) { + return derivedAddress.toLowerCase() === expectedAddress.toLowerCase(); + } + + try { + return u8aToHex(decodeAddress(derivedAddress)) === u8aToHex(decodeAddress(expectedAddress)); + } catch { + return derivedAddress === expectedAddress; + } +} + function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHash, isEthereum = false, onSignature, payloadExt, payloadJson, setError }: Props): React.ReactElement { const [isBusy, setIsBusy] = useState(false); const { t } = useTranslation(); + const { ledgerApp } = useContext(SettingsContext); const chain = useMetadata(genesisHash); - const { error: ledgerError, isLoading: ledgerLoading, isLocked: ledgerLocked, ledger, refresh, type: _ledgerType, warning: ledgerWarning } = useLedger(genesisHash, accountIndex, addressOffset, isEthereum); - - useEffect(() => { - if (ledgerError) { - setError(ledgerError); - } - }, [chain, ledgerError, setError]); + const { address, error: ledgerError, isLoading: ledgerLoading, isLocked: ledgerLocked, ledger, refresh, type: _ledgerType, warning: ledgerWarning } = useLedger(genesisHash, accountIndex, addressOffset, isEthereum); + const expectedAddress = payloadJson?.address; + const signerMismatchError = address && expectedAddress && !matchesExpectedSigner(address, expectedAddress, isEthereum) + ? t('Address mismatch: derived {{derived}}, expected {{expected}}. Check that the correct Ledger device is connected and the correct app is selected.', { + replace: { + derived: stringShorten(address, 8), + expected: stringShorten(expectedAddress, 8) + } + }) + : null; + const signerMismatchHint = signerMismatchError + ? t('Possible cause: the Ledger App setting differs from the app originally used to derive this account.') + : null; const _onRefresh = useCallback(() => { - refresh(); setError(null); + refresh(); }, [refresh, setError]); const _onSignLedger = useCallback( (): void => { + if (signerMismatchError) { + return; + } + if (!ledger || !payloadJson || !onSignature || !chain || !payloadExt) { if (!chain) { setError('No chain information found. You may need to update/upload the metadata.'); @@ -87,11 +109,14 @@ function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHas setError(null); setIsBusy(true); - const currApp = settings.get().ledgerApp; + const currApp = ledgerApp; if (currApp === 'generic' || currApp === 'migration') { if (!chain?.definition.rawMetadata) { setError('No metadata found for this chain. You must upload the metadata to the extension in order to use Ledger.'); + setIsBusy(false); + + return; } const { raw, txMetadata } = getMetadataProof(chain, payloadJson); @@ -152,9 +177,12 @@ function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHas }); } }, - [accountIndex, addressOffset, chain, ledger, onSignature, payloadJson, payloadExt, setError, isEthereum] + [accountIndex, addressOffset, chain, ledger, ledgerApp, onSignature, payloadJson, payloadExt, setError, signerMismatchError, isEthereum] ); + const activeError = ledgerError || error; + const hasActiveError = !!activeError; + return (
{!!ledgerWarning && ( @@ -162,17 +190,22 @@ function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHas {ledgerWarning} )} - {error && ( + {hasActiveError && ( - {error} + {activeError} )} - { - - {`You are using the Ledger ${settings.ledgerApp.toUpperCase()} App. If you would like to switch it, please go to "MANAGE LEDGER APP" in the extension's settings.`} + {signerMismatchError && !hasActiveError && ( + + {signerMismatchError} - } - {(ledgerLocked || error) + )} + {signerMismatchHint && !hasActiveError && ( + + {signerMismatchHint} + + )} + {(ledgerLocked || hasActiveError) ? (