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
This commit is contained in:
Francis O'Brien 2026-03-26 19:30:08 +00:00 committed by GitHub
parent 73fef6ffbf
commit 62b7395023
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 214 additions and 109 deletions

View File

@ -29,7 +29,7 @@
"@polkadot/extension-chains": "0.62.7-1-x", "@polkadot/extension-chains": "0.62.7-1-x",
"@polkadot/extension-dapp": "0.62.7-1-x", "@polkadot/extension-dapp": "0.62.7-1-x",
"@polkadot/extension-inject": "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/keyring": "^13.5.9",
"@polkadot/networks": "^13.5.9", "@polkadot/networks": "^13.5.9",
"@polkadot/react-identicon": "^3.16.4", "@polkadot/react-identicon": "^3.16.4",

View File

@ -11,13 +11,13 @@ import type { HexString } from '@polkadot/util/types';
import { faSync } from '@fortawesome/free-solid-svg-icons'; import { faSync } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 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, stringShorten, u8aToHex } from '@polkadot/util';
import { assert, objectSpread, u8aToHex } from '@polkadot/util'; import { decodeAddress } from '@polkadot/util-crypto';
import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata'; 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 { useLedger, useMetadata, useTranslation } from '../../hooks/index.js';
import { styled } from '../../styled.js'; import { styled } from '../../styled.js';
@ -42,7 +42,6 @@ function getMetadataProof (chain: Chain, payload: SignerPayloadJSON) {
const merkleizedMetadata = merkleizeMetadata(m, { const merkleizedMetadata = merkleizeMetadata(m, {
base58Prefix: chain.ss58Format, base58Prefix: chain.ss58Format,
decimals: chain.tokenDecimals, decimals: chain.tokenDecimals,
specName: chain.name.toLowerCase(),
specVersion: chain.specVersion, specVersion: chain.specVersion,
tokenSymbol: chain.tokenSymbol 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<Props> { function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHash, isEthereum = false, onSignature, payloadExt, payloadJson, setError }: Props): React.ReactElement<Props> {
const [isBusy, setIsBusy] = useState(false); const [isBusy, setIsBusy] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { ledgerApp } = useContext(SettingsContext);
const chain = useMetadata(genesisHash); const chain = useMetadata(genesisHash);
const { error: ledgerError, isLoading: ledgerLoading, isLocked: ledgerLocked, ledger, refresh, type: _ledgerType, warning: ledgerWarning } = useLedger(genesisHash, accountIndex, addressOffset, isEthereum); const { address, error: ledgerError, isLoading: ledgerLoading, isLocked: ledgerLocked, ledger, refresh, type: _ledgerType, warning: ledgerWarning } = useLedger(genesisHash, accountIndex, addressOffset, isEthereum);
const expectedAddress = payloadJson?.address;
useEffect(() => { const signerMismatchError = address && expectedAddress && !matchesExpectedSigner(address, expectedAddress, isEthereum)
if (ledgerError) { ? t('Address mismatch: derived {{derived}}, expected {{expected}}. Check that the correct Ledger device is connected and the correct app is selected.', {
setError(ledgerError); replace: {
} derived: stringShorten(address, 8),
}, [chain, ledgerError, setError]); 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(() => { const _onRefresh = useCallback(() => {
refresh();
setError(null); setError(null);
refresh();
}, [refresh, setError]); }, [refresh, setError]);
const _onSignLedger = useCallback( const _onSignLedger = useCallback(
(): void => { (): void => {
if (signerMismatchError) {
return;
}
if (!ledger || !payloadJson || !onSignature || !chain || !payloadExt) { if (!ledger || !payloadJson || !onSignature || !chain || !payloadExt) {
if (!chain) { if (!chain) {
setError('No chain information found. You may need to update/upload the metadata.'); 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); setError(null);
setIsBusy(true); setIsBusy(true);
const currApp = settings.get().ledgerApp; const currApp = ledgerApp;
if (currApp === 'generic' || currApp === 'migration') { if (currApp === 'generic' || currApp === 'migration') {
if (!chain?.definition.rawMetadata) { if (!chain?.definition.rawMetadata) {
setError('No metadata found for this chain. You must upload the metadata to the extension in order to use Ledger.'); 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); 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 ( return (
<div className={className}> <div className={className}>
{!!ledgerWarning && ( {!!ledgerWarning && (
@ -162,17 +190,22 @@ function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHas
{ledgerWarning} {ledgerWarning}
</Warning> </Warning>
)} )}
{error && ( {hasActiveError && (
<Warning isDanger> <Warning isDanger>
{error} {activeError}
</Warning> </Warning>
)} )}
{ {signerMismatchError && !hasActiveError && (
<Warning> <Warning isDanger>
{`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}
</Warning> </Warning>
} )}
{(ledgerLocked || error) {signerMismatchHint && !hasActiveError && (
<Warning>
{signerMismatchHint}
</Warning>
)}
{(ledgerLocked || hasActiveError)
? ( ? (
<Button <Button
isBusy={isBusy || ledgerLoading} isBusy={isBusy || ledgerLoading}
@ -185,6 +218,7 @@ function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHas
: ( : (
<Button <Button
isBusy={isBusy || ledgerLoading} isBusy={isBusy || ledgerLoading}
isDisabled={!!signerMismatchError}
onClick={_onSignLedger} onClick={_onSignLedger}
> >
{t('Sign on Ledger')} {t('Sign on Ledger')}

View File

@ -9,13 +9,14 @@ import type { Network } from '@polkadot/networks/types';
import type { HexString } from '@polkadot/util/types'; import type { HexString } from '@polkadot/util/types';
import type { KeypairType } from '@polkadot/util-crypto/types'; import type { KeypairType } from '@polkadot/util-crypto/types';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Ledger, LedgerGeneric } from '@polkadot/hw-ledger'; import { Ledger, LedgerGeneric } from '@polkadot/hw-ledger';
import { knownLedger } from '@polkadot/networks/defaults'; import { knownLedger } from '@polkadot/networks/defaults';
import { settings } from '@polkadot/ui-settings'; import { settings } from '@polkadot/ui-settings';
import { assert } from '@polkadot/util'; import { assert } from '@polkadot/util';
import { SettingsContext } from '../components/contexts.js';
import chains from '../util/chains.js'; import chains from '../util/chains.js';
import ledgerChains from '../util/legerChains.js'; import ledgerChains from '../util/legerChains.js';
import useTranslation from './useTranslation.js'; import useTranslation from './useTranslation.js';
@ -40,8 +41,14 @@ function getNetwork (genesisHash: string): Network | undefined {
return ledgerChains.find(({ genesisHash: [hash] }) => hash === genesisHash); return ledgerChains.find(({ genesisHash: [hash] }) => hash === genesisHash);
} }
function getTransportType (): 'hid' | 'webusb' {
// prefer WebHID over WebUSB.
return ('hid' in navigator) ? 'hid' : 'webusb';
}
function getState (): StateBase { function getState (): StateBase {
const isLedgerCapable = !!(window as unknown as { USB?: unknown }).USB; const w = window as unknown as { USB?: unknown; hid?: unknown };
const isLedgerCapable = !!(w.hid || w.USB);
return { return {
isLedgerCapable, isLedgerCapable,
@ -49,11 +56,9 @@ function getState (): StateBase {
}; };
} }
function retrieveLedger (genesis: string): LedgerGeneric | Ledger { function retrieveLedger (genesis: string, ledgerApp: string): LedgerGeneric | Ledger {
let ledger: LedgerGeneric | Ledger | null = null; let ledger: LedgerGeneric | Ledger | null = null;
const currApp = settings.get().ledgerApp;
const { isLedgerCapable } = getState(); const { isLedgerCapable } = getState();
assert(isLedgerCapable, 'Incompatible browser, only Chrome is supported'); assert(isLedgerCapable, 'Incompatible browser, only Chrome is supported');
@ -64,21 +69,19 @@ function retrieveLedger (genesis: string): LedgerGeneric | Ledger {
assert(def.slip44, 'Slip44 is not available for this network, please report an issue to update this chains slip44'); assert(def.slip44, 'Slip44 is not available for this network, please report an issue to update this chains slip44');
// All chains use the `slip44` from polkadot in their derivation path in ledger. const transport = getTransportType();
// This interface is specific to the underlying PolkadotGenericApp.
ledger = new LedgerGeneric('webusb', def.network, knownLedger['polkadot']);
if (currApp === 'generic') { if (ledgerApp === 'generic') {
// All chains use the `slip44` from polkadot in their derivation path in ledger. // All chains use the `slip44` from polkadot in their derivation path in ledger.
// This interface is specific to the underlying PolkadotGenericApp. // This interface is specific to the underlying PolkadotGenericApp.
ledger = new LedgerGeneric('webusb', def.network, knownLedger['polkadot']); ledger = new LedgerGeneric(transport, def.network, knownLedger['polkadot']);
} else if (currApp === 'migration') { } else if (ledgerApp === 'migration') {
ledger = new LedgerGeneric('webusb', def.network, knownLedger[def.network]); ledger = new LedgerGeneric(transport, def.network, knownLedger[def.network]);
} else if (currApp === 'chainSpecific') { } else if (ledgerApp === 'chainSpecific') {
ledger = new Ledger('webusb', def.network); ledger = new Ledger(transport, def.network);
} else { } else {
// This will never get touched since it will always hit the above two. This satisfies the compiler. // This will never get touched since it will always hit the above two. This satisfies the compiler.
ledger = new LedgerGeneric('webusb', def.network, knownLedger['polkadot']); ledger = new LedgerGeneric(transport, def.network, knownLedger['polkadot']);
} }
return ledger; return ledger;
@ -87,12 +90,16 @@ function retrieveLedger (genesis: string): LedgerGeneric | Ledger {
export default function useLedger (genesis?: string | null, accountIndex = 0, addressOffset = 0, isEthereum = false): State { export default function useLedger (genesis?: string | null, accountIndex = 0, addressOffset = 0, isEthereum = false): State {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLocked, setIsLocked] = useState(false); const [isLocked, setIsLocked] = useState(false);
const [refreshLock, setRefreshLock] = useState(false); const [refreshCount, setRefreshCount] = useState(0);
const [warning, setWarning] = useState<string | null>(null); const [warning, setWarning] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [address, setAddress] = useState<string | null>(null); const [address, setAddress] = useState<string | null>(null);
const [type, setType] = useState<KeypairType | null>(null); const [type, setType] = useState<KeypairType | null>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const { ledgerApp } = useContext(SettingsContext);
// Holds the ledger from the previous effect run so we can close its
// transport when the network changes and a new instance is created.
const prevLedgerRef = useRef<LedgerGeneric | Ledger | null>(null);
const handleGetAddressError = (e: Error, genesis: string) => { const handleGetAddressError = (e: Error, genesis: string) => {
setIsLoading(false); setIsLoading(false);
@ -117,90 +124,148 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
setType(null); setType(null);
}; };
const ledger = useMemo(() => { const { ledger, ledgerInitError } = useMemo(() => {
setError(null); if (refreshCount > 0 || genesis) {
setIsLocked(false);
setRefreshLock(false);
// this trick allows to refresh the ledger on demand
// when it is shown as locked and the user has actually
// unlocked it, which we can't know.
if (refreshLock || genesis) {
if (!genesis) { if (!genesis) {
return null; return { ledger: null, ledgerInitError: null };
} }
try { try {
return retrieveLedger(genesis); return { ledger: retrieveLedger(genesis, ledgerApp), ledgerInitError: null };
} catch (error) { } catch (error) {
setError((error as Error).message); return { ledger: null, ledgerInitError: (error as Error).message };
} }
} }
return null; return { ledger: null, ledgerInitError: null };
}, [genesis, refreshLock]); }, [genesis, ledgerApp, refreshCount]);
useEffect(() => { useEffect(() => {
setError(ledgerInitError);
}, [ledgerInitError]);
useEffect(() => {
let isStale = false;
if (!ledger || !genesis) { if (!ledger || !genesis) {
if (prevLedgerRef.current) {
prevLedgerRef.current.disconnect().catch(console.error);
prevLedgerRef.current = null;
}
setIsLoading(false);
setIsLocked(false);
setWarning(null);
setAddress(null); setAddress(null);
setType(null); setType(null);
return; return () => {
isStale = true;
};
} }
const runIfCurrent = (fn: () => void): void => {
if (!isStale) {
fn();
}
};
const onAddressError = (e: Error): void => {
if (!isStale) {
handleGetAddressError(e, genesis);
}
};
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
setWarning(null); setWarning(null);
// This is used with a genesisHash only when importing the Ledger account const prevLedger = prevLedgerRef.current;
// and when signing with Ledger. In both cases, the genesisHash is known and
// will be in this array.
const chosenNetwork = chains.find(({ genesisHash }) => genesisHash === genesis as HexString);
// Just in case, but this shouldn't be triggered prevLedgerRef.current = ledger;
assert(chosenNetwork, t('This network is not available, please report an issue to update the known chains'));
const currApp = settings.get().ledgerApp;
if (currApp === 'generic' || currApp === 'migration') { const fetchLedgerAddress = () => {
if (isEthereum) { if (isStale) {
(ledger as LedgerGeneric).getAddressEcdsa(false, accountIndex, addressOffset) return;
.then((res) => {
setIsLoading(false);
setAddress(`0x${res.address}`);
setType('ethereum');
}).catch((e: Error) => {
handleGetAddressError(e, genesis);
});
} else {
(ledger as LedgerGeneric).getAddress(chosenNetwork.ss58Format, false, accountIndex, addressOffset)
.then((res) => {
setIsLoading(false);
setAddress(res.address);
setType('ed25519');
}).catch((e: Error) => {
handleGetAddressError(e, genesis);
});
} }
} else if (currApp === 'chainSpecific') {
(ledger as Ledger).getAddress(false, accountIndex, addressOffset) const chosenNetwork = chains.find(({ genesisHash }) => genesisHash === genesis as HexString);
.then((res) => {
setIsLoading(false); // Use the chain's SS58 prefix when known; fall back to 42 (substrate default).
setAddress(res.address); const ss58Prefix = chosenNetwork?.ss58Format ?? 42;
setType('ed25519');
}).catch((e: Error) => { if (ledgerApp === 'generic' || ledgerApp === 'migration') {
handleGetAddressError(e, genesis); if (isEthereum) {
}); (ledger as LedgerGeneric).getAddressEcdsa(false, accountIndex, addressOffset)
.then((res) => {
runIfCurrent(() => {
setIsLoading(false);
setIsLocked(false);
setAddress(`0x${res.address}`);
setType('ethereum');
});
}).catch(onAddressError);
} else {
(ledger as LedgerGeneric).getAddress(ss58Prefix, false, accountIndex, addressOffset).then((res) => {
runIfCurrent(() => {
setIsLoading(false);
setIsLocked(false);
setAddress(res.address);
setType('ed25519');
});
}).catch(onAddressError);
}
} else if (ledgerApp === 'chainSpecific') {
(ledger as Ledger).getAddress(false, accountIndex, addressOffset)
.then((res) => {
runIfCurrent(() => {
setIsLoading(false);
setIsLocked(false);
setAddress(res.address);
setType('ed25519');
});
}).catch(onAddressError);
}
};
// Disconnect the previous instance before opening the new one (network or ledger app change).
if (prevLedger && prevLedger !== ledger) {
prevLedger.disconnect().catch(console.error).finally(fetchLedgerAddress);
} else {
fetchLedgerAddress();
} }
return () => {
isStale = true;
};
// If the dependency array is exhaustive, with t, the translation function, it // If the dependency array is exhaustive, with t, the translation function, it
// triggers a useless re-render when ledger device is connected. // triggers a useless re-render when ledger device is connected.
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountIndex, addressOffset, genesis, ledger, isEthereum]); }, [accountIndex, addressOffset, genesis, ledger, ledgerApp, isEthereum]);
const ledgerRef = useRef(ledger);
// Keep in sync so the unmount cleanup always closes the latest instance.
ledgerRef.current = ledger;
// On unmount, release the transport so the next session can open it.
useEffect(() => {
return () => {
ledgerRef.current?.disconnect().catch(console.error);
};
}, []);
const refresh = useCallback(() => { const refresh = useCallback(() => {
setRefreshLock(true); if (ledger) {
setError(null); // Prevent the address-fetch effect from disconnecting this instance again.
setWarning(null); prevLedgerRef.current = null;
}, []); ledger.disconnect()
.catch(console.error)
.finally(() => setRefreshCount((c) => c + 1));
} else {
setRefreshCount((c) => c + 1);
}
}, [ledger]);
return ({ ...getState(), address, error, isLoading, isLocked, ledger, refresh, type, warning }); return ({ ...getState(), address, error, isLoading, isLocked, ledger, refresh, type, warning });
} }

View File

@ -101,8 +101,9 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
? ( ? (
<Link <Link
isDisabled={!isLedgerCapable} isDisabled={!isLedgerCapable}
onClick={isPopup ? _onOpenLedgerConnect : undefined}
title={ (!isLedgerCapable && t('Ledger devices can only be connected with Chrome browser')) || ''} title={ (!isLedgerCapable && t('Ledger devices can only be connected with Chrome browser')) || ''}
to={ledgerPath} to={isPopup ? undefined : ledgerPath}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faUsb} icon={faUsb}
@ -112,7 +113,10 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
</Link> </Link>
) )
: ( : (
<Link onClick={_onOpenLedgerConnect}> <Link
onClick={isPopup ? _onOpenLedgerConnect : undefined}
to={isPopup ? undefined : ledgerPath}
>
<FontAwesomeIcon <FontAwesomeIcon
icon={faUsb} icon={faUsb}
rotation={270} rotation={270}

View File

@ -197,5 +197,7 @@
"You are starting a Parachain": "", "You are starting a Parachain": "",
"For all other operations, your balances are already on Asset Hub.": "", "For all other operations, your balances are already on Asset Hub.": "",
"I Understand": "", "I Understand": "",
"Do not teleport balances to the Relay Chain unless:": "" "Do not teleport balances to the Relay Chain unless:": "",
"Address mismatch: derived {{derived}}, expected {{expected}}. Check that the correct Ledger device is connected and the correct app is selected.": "",
"Possible cause: the Ledger App setting differs from the app originally used to derive this account.": ""
} }

View File

@ -1184,7 +1184,7 @@ __metadata:
"@polkadot/extension-dapp": "npm:0.62.7-1-x" "@polkadot/extension-dapp": "npm:0.62.7-1-x"
"@polkadot/extension-inject": "npm:0.62.7-1-x" "@polkadot/extension-inject": "npm:0.62.7-1-x"
"@polkadot/extension-mocks": "npm:0.62.7-1-x" "@polkadot/extension-mocks": "npm:0.62.7-1-x"
"@polkadot/hw-ledger": "npm:^13.5.9" "@polkadot/hw-ledger": "npm:^14.0.3"
"@polkadot/keyring": "npm:^13.5.9" "@polkadot/keyring": "npm:^13.5.9"
"@polkadot/networks": "npm:^13.5.9" "@polkadot/networks": "npm:^13.5.9"
"@polkadot/react-identicon": "npm:^3.16.4" "@polkadot/react-identicon": "npm:^3.16.4"
@ -1253,32 +1253,32 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@polkadot/hw-ledger-transports@npm:13.5.9": "@polkadot/hw-ledger-transports@npm:14.0.3":
version: 13.5.9 version: 14.0.3
resolution: "@polkadot/hw-ledger-transports@npm:13.5.9" resolution: "@polkadot/hw-ledger-transports@npm:14.0.3"
dependencies: dependencies:
"@ledgerhq/hw-transport": "npm:^6.31.4" "@ledgerhq/hw-transport": "npm:^6.31.4"
"@ledgerhq/hw-transport-node-hid-singleton": "npm:^6.31.5" "@ledgerhq/hw-transport-node-hid-singleton": "npm:^6.31.5"
"@ledgerhq/hw-transport-webhid": "npm:^6.29.4" "@ledgerhq/hw-transport-webhid": "npm:^6.29.4"
"@ledgerhq/hw-transport-webusb": "npm:^6.29.4" "@ledgerhq/hw-transport-webusb": "npm:^6.29.4"
"@polkadot/util": "npm:13.5.9" "@polkadot/util": "npm:14.0.3"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
dependenciesMeta: dependenciesMeta:
"@ledgerhq/hw-transport-node-hid-singleton": "@ledgerhq/hw-transport-node-hid-singleton":
optional: true optional: true
checksum: 10/2ea7ee7873d6a3208342a5c5919c4e1ca6fe78ee014f3442a0bb094243cb28d179cfefe550e81a6b64a26e7af8daaa2b45f2b5615fbd6796c266ff0d57c1076c checksum: 10/5cf48a41079e06ea4c46df4c4aaf2efcca786b19f9eecb0c7c0ba505a1f5a98f730518cff41e3b0fa27f09d6dc74329e2f2b982a2dd8f1c7e59db9d65b90eabe
languageName: node languageName: node
linkType: hard linkType: hard
"@polkadot/hw-ledger@npm:^13.5.9": "@polkadot/hw-ledger@npm:^14.0.3":
version: 13.5.9 version: 14.0.3
resolution: "@polkadot/hw-ledger@npm:13.5.9" resolution: "@polkadot/hw-ledger@npm:14.0.3"
dependencies: dependencies:
"@polkadot/hw-ledger-transports": "npm:13.5.9" "@polkadot/hw-ledger-transports": "npm:14.0.3"
"@polkadot/util": "npm:13.5.9" "@polkadot/util": "npm:14.0.3"
"@zondax/ledger-substrate": "npm:1.1.1" "@zondax/ledger-substrate": "npm:1.1.1"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/92f598cd8a761e7bce0e6eeb00279ace512dbb34265dab55f23c0ea58ec854c894c64d50765a768d9e30c7bc8825779e33a254f0abb35595c749ed19b7dfe50c checksum: 10/1f4746a3fd7f311211c451ba26ff1cd11bcfc729c27bc9516e619e5df2d57a9e79e94b163d2e00448864680c7afa0d20b933932134bafa709af091db1e6d9b10
languageName: node languageName: node
linkType: hard linkType: hard