Allow Ledger generic app accounts to sign on any chain (#1613)

* feat(ui): allow generic Ledger app to derive from any chain via polkadot slip44

* feat: allow Ledger generic app accounts to sign on any chain

* fix(extension-ui): guard generic ledger init and stabilize i18n deps
This commit is contained in:
Francis O'Brien 2026-03-30 21:44:34 +01:00 committed by GitHub
parent 2198396137
commit bc320ec762
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 140 additions and 84 deletions

View File

@ -201,7 +201,7 @@ export interface RequestAccountCreateHardware {
accountIndex: number; accountIndex: number;
address: string; address: string;
addressOffset: number; addressOffset: number;
genesisHash: HexString; genesisHash?: HexString | null;
hardwareType: string; hardwareType: string;
name: string; name: string;
type: KeypairType; type: KeypairType;

View File

@ -12,6 +12,9 @@ import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { settings } from '@polkadot/ui-settings';
import { SettingsContext } from '../../components/index.js';
import * as messaging from '../../messaging.js'; import * as messaging from '../../messaging.js';
import { flushAllPromises } from '../../testHelpers.js'; import { flushAllPromises } from '../../testHelpers.js';
import Account from './Account.js'; import Account from './Account.js';
@ -34,11 +37,13 @@ describe('Account component', () => {
let wrapper: ReactWrapper; let wrapper: ReactWrapper;
const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5'; const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return
const mountAccountComponent = (additionalAccountProperties: Record<string, unknown>): ReactWrapper => mount( const mountAccountComponent = (additionalAccountProperties: Record<string, unknown>, ledgerApp: 'generic' | 'migration' | 'chainSpecific' = 'chainSpecific'): ReactWrapper => mount(
<MemoryRouter> <MemoryRouter>
<Account <SettingsContext.Provider value={{ ...settings.get(), ledgerApp }}>
{...{ address: VALID_ADDRESS, ...additionalAccountProperties }} <Account
/> {...{ address: VALID_ADDRESS, ...additionalAccountProperties }}
/>
</SettingsContext.Provider>
</MemoryRouter>); </MemoryRouter>);
it('shows Export option if account is not external', async () => { it('shows Export option if account is not external', async () => {
@ -79,7 +84,7 @@ describe('Account component', () => {
}); });
it('does not show genesis hash selection dropsown if account is hardware', async () => { it('does not show genesis hash selection dropsown if account is hardware', async () => {
wrapper = mountAccountComponent({ isExternal: true, isHardware: true }); wrapper = mountAccountComponent({ isExternal: true, isHardware: true }, 'chainSpecific');
wrapper.find('.settings').first().simulate('click'); wrapper.find('.settings').first().simulate('click');
await act(flushAllPromises); await act(flushAllPromises);
@ -88,4 +93,15 @@ describe('Account component', () => {
expect(wrapper.find('a.menuItem').at(1).text()).toBe('Forget Account'); expect(wrapper.find('a.menuItem').at(1).text()).toBe('Forget Account');
expect(wrapper.find('.genesisSelection').exists()).toBe(false); expect(wrapper.find('.genesisSelection').exists()).toBe(false);
}); });
it('shows genesis hash selection for hardware account in generic mode', async () => {
wrapper = mountAccountComponent({ isExternal: true, isHardware: true }, 'generic');
wrapper.find('.settings').first().simulate('click');
await act(flushAllPromises);
expect(wrapper.find('a.menuItem').length).toBe(2);
expect(wrapper.find('a.menuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('a.menuItem').at(1).text()).toBe('Forget Account');
expect(wrapper.find('.genesisSelection').exists()).toBe(true);
});
}); });

View File

@ -8,7 +8,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
import { canDerive } from '@polkadot/extension-base/utils'; import { canDerive } from '@polkadot/extension-base/utils';
import { AccountContext, Address, Checkbox, Dropdown, Link, MenuDivider } from '../../components/index.js'; import { AccountContext, Address, Checkbox, Dropdown, Link, MenuDivider, SettingsContext } from '../../components/index.js';
import { useGenesisHashOptions, useTranslation } from '../../hooks/index.js'; import { useGenesisHashOptions, useTranslation } from '../../hooks/index.js';
import { editAccount, tieAccount } from '../../messaging.js'; import { editAccount, tieAccount } from '../../messaging.js';
import { Name } from '../../partials/index.js'; import { Name } from '../../partials/index.js';
@ -33,8 +33,10 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
const [editedName, setName] = useState<string | undefined | null>(name); const [editedName, setName] = useState<string | undefined | null>(name);
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const genesisOptions = useGenesisHashOptions(); const genesisOptions = useGenesisHashOptions();
const { ledgerApp } = useContext(SettingsContext);
const { selectedAccounts = [], setSelectedAccounts } = useContext(AccountContext); const { selectedAccounts = [], setSelectedAccounts } = useContext(AccountContext);
const isSelected = useMemo(() => selectedAccounts?.includes(address) || false, [address, selectedAccounts]); const isSelected = useMemo(() => selectedAccounts?.includes(address) || false, [address, selectedAccounts]);
const canEditGenesis = !isHardware || ledgerApp === 'generic';
useEffect(() => { useEffect(() => {
setChecked(isSelected); setChecked(isSelected);
@ -50,7 +52,7 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
const _onChangeGenesis = useCallback( const _onChangeGenesis = useCallback(
(genesisHash?: HexString | null): void => { (genesisHash?: HexString | null): void => {
tieAccount(address, genesisHash ?? null) tieAccount(address, genesisHash || null)
.catch(console.error); .catch(console.error);
}, },
[address] [address]
@ -105,7 +107,7 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
> >
{t('Forget Account')} {t('Forget Account')}
</Link> </Link>
{!isHardware && ( {canEditGenesis && (
<> <>
<MenuDivider /> <MenuDivider />
<div className='menuItem'> <div className='menuItem'>
@ -120,7 +122,7 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
</> </>
)} )}
</> </>
), [_onChangeGenesis, _toggleEdit, address, genesisHash, genesisOptions, isExternal, isHardware, t, type]); ), [_onChangeGenesis, _toggleEdit, address, canEditGenesis, genesisHash, genesisOptions, isExternal, t, type]);
return ( return (
<div className={className}> <div className={className}>
@ -138,6 +140,7 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
className='address' className='address'
genesisHash={genesisHash} genesisHash={genesisHash}
isExternal={isExternal} isExternal={isExternal}
isHardware={isHardware}
isHidden={isHidden} isHidden={isHidden}
name={editedName} name={editedName}
parentName={parentName} parentName={parentName}

View File

@ -5,11 +5,11 @@ 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, useContext, useEffect, useRef, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { settings } from '@polkadot/ui-settings'; import { settings } from '@polkadot/ui-settings';
import { ActionContext, Address, Button, ButtonArea, Dropdown, Switch, VerticalSpace, Warning } from '../components/index.js'; import { ActionContext, Address, Button, ButtonArea, Dropdown, SettingsContext, Switch, VerticalSpace, Warning } from '../components/index.js';
import { useLedger, useTranslation } from '../hooks/index.js'; import { useLedger, useTranslation } from '../hooks/index.js';
import { createAccountHardware } from '../messaging.js'; import { createAccountHardware } from '../messaging.js';
import { Header, Name } from '../partials/index.js'; import { Header, Name } from '../partials/index.js';
@ -23,10 +23,12 @@ interface AccOption {
interface NetworkOption { interface NetworkOption {
text: string; text: string;
value: string | null; value: string;
} }
const AVAIL: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; const AVAIL: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
const SELECT_NETWORK = '';
const ALLOW_ANY_NETWORK = '__allow_any_network__';
interface Props { interface Props {
className?: string; className?: string;
@ -41,8 +43,10 @@ function ImportLedger ({ className }: Props): React.ReactElement {
const [genesis, setGenesis] = useState<HexString | null>(null); const [genesis, setGenesis] = useState<HexString | null>(null);
const [isEthereum, setIsEthereum] = useState(false); const [isEthereum, setIsEthereum] = useState(false);
const onAction = useContext(ActionContext); const onAction = useContext(ActionContext);
const { ledgerApp } = useContext(SettingsContext);
const [name, setName] = useState<string | null>(null); const [name, setName] = useState<string | null>(null);
const isChainSpecific = settings.ledgerApp === 'chainSpecific'; const isGenericLedgerApp = ledgerApp === 'generic';
const isChainSpecific = ledgerApp === 'chainSpecific';
const { address, error: ledgerError, isLoading: ledgerLoading, isLocked: ledgerLocked, refresh, type, warning: ledgerWarning } = useLedger(genesis, accountIndex, addressOffset, isEthereum); const { address, error: ledgerError, isLoading: ledgerLoading, isLocked: ledgerLocked, refresh, type, warning: ledgerWarning } = useLedger(genesis, accountIndex, addressOffset, isEthereum);
useEffect(() => { useEffect(() => {
@ -67,20 +71,32 @@ function ImportLedger ({ className }: Props): React.ReactElement {
value value
}))); })));
const networkOps = useRef( const onNetworkChange = useCallback((value: string) => {
[{ if (value === ALLOW_ANY_NETWORK) {
text: t('Select network'), setGenesis(null);
value: ''
}, return;
...ledgerChains.map(({ displayName, genesisHash }): NetworkOption => ({ }
text: displayName,
value: genesisHash[0] setGenesis(value ? value as HexString : null);
}))] }, []);
const networkOps = useMemo(
() => [
...(isGenericLedgerApp
? [{ text: t('Allow use on any chain'), value: ALLOW_ANY_NETWORK }]
: [{ text: t('Select network'), value: SELECT_NETWORK }]),
...ledgerChains.map(({ displayName, genesisHash }): NetworkOption => ({
text: displayName,
value: genesisHash[0]
}))
],
[isGenericLedgerApp, t]
); );
const _onSave = useCallback( const _onSave = useCallback(
() => { () => {
if (address && genesis && name && type) { if (address && name && type) {
setIsBusy(true); setIsBusy(true);
createAccountHardware(address, 'ledger', accountIndex, addressOffset, name, genesis, type) createAccountHardware(address, 'ledger', accountIndex, addressOffset, name, genesis, type)
@ -127,11 +143,11 @@ function ImportLedger ({ className }: Props): React.ReactElement {
<Dropdown <Dropdown
className='network' className='network'
label={t('Network')} label={t('Network')}
onChange={setGenesis} onChange={onNetworkChange}
options={networkOps.current} options={networkOps}
value={genesis} value={genesis ?? (isGenericLedgerApp ? ALLOW_ANY_NETWORK : SELECT_NETWORK)}
/> />
{!!genesis && !!address && !ledgerError && ( {!!address && !ledgerError && (
<Name <Name
onChange={setName} onChange={setName}
value={name || ''} value={name || ''}
@ -186,7 +202,7 @@ function ImportLedger ({ className }: Props): React.ReactElement {
: ( : (
<Button <Button
isBusy={ledgerLoading || isBusy} isBusy={ledgerLoading || isBusy}
isDisabled={!!error || !!ledgerError || !address || !genesis} isDisabled={!!error || !!ledgerError || !address || (!isGenericLedgerApp && !genesis)}
onClick={_onSave} onClick={_onSave}
> >
{t('Import Account')} {t('Import Account')}

View File

@ -43,12 +43,17 @@ function isRawPayload (payload: SignerPayloadJSON | SignerPayloadRaw): payload i
return !!(payload as SignerPayloadRaw).data; return !!(payload as SignerPayloadRaw).data;
} }
export default function Request ({ account: { accountIndex, addressOffset, genesisHash, isExternal, isHardware, type }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null { export default function Request ({ account: { accountIndex, addressOffset, genesisHash: accountGenesisHash, isExternal, isHardware, type }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const onAction = useContext(ActionContext); const onAction = useContext(ActionContext);
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null }); const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const chain = useMetadata(genesisHash); // Use payload genesis for transaction-signing flow. Account genesis can be null
// for allow-any accounts and should not drive payload decoding/signing setup.
const payloadGenesisHash = !isRawPayload(request.payload)
? request.payload.genesisHash
: null;
const chain = useMetadata(payloadGenesisHash);
useEffect((): void => { useEffect((): void => {
// When the chain and request are ready, configure the chain's registry. // When the chain and request are ready, configure the chain's registry.
@ -153,12 +158,12 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
isExternal={isExternal} isExternal={isExternal}
/> />
</div> </div>
{isExternal && !isHardware && genesisHash {isExternal && !isHardware && accountGenesisHash
? ( ? (
<Qr <Qr
address={address} address={address}
cmd={CMD_SIGN_MESSAGE} cmd={CMD_SIGN_MESSAGE}
genesisHash={genesisHash} genesisHash={accountGenesisHash}
onSignature={_onSignature} onSignature={_onSignature}
payload={data} payload={data}
/> />
@ -171,7 +176,7 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
) )
} }
<VerticalSpace /> <VerticalSpace />
{isExternal && !isHardware && !genesisHash && ( {isExternal && !isHardware && !accountGenesisHash && (
<> <>
<Warning isDanger>{t('"Allow use on any network" is not supported to show a QR code. You must associate this account with a network.')}</Warning> <Warning isDanger>{t('"Allow use on any network" is not supported to show a QR code. You must associate this account with a network.')}</Warning>
<VerticalSpace /> <VerticalSpace />

View File

@ -56,35 +56,37 @@ function getState (): StateBase {
}; };
} }
function retrieveLedger (genesis: string, ledgerApp: string): LedgerGeneric | Ledger { function retrieveLedger (genesis: string | null, ledgerApp: string): LedgerGeneric | Ledger {
let ledger: LedgerGeneric | Ledger | null = null;
const { isLedgerCapable } = getState(); const { isLedgerCapable } = getState();
assert(isLedgerCapable, 'Incompatible browser, only Chrome is supported'); assert(isLedgerCapable, 'Incompatible browser, only Chrome is supported');
const transport = getTransportType();
if (ledgerApp === 'generic') {
// Generic app always uses Polkadot's slip44, regardless of chain genesis.
return new LedgerGeneric(transport, 'polkadot', knownLedger['polkadot']);
}
// Shouldn't happen but guard to satisfy the compiler
assert(genesis, 'Genesis hash is required to connect to the Ledger in non-generic mode');
const def = getNetwork(genesis); const def = getNetwork(genesis);
assert(def, 'There is no known Ledger app available for this chain'); assert(def, 'There is no known Ledger app available for this chain');
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');
const transport = getTransportType(); if (ledgerApp === 'migration') {
return new LedgerGeneric(transport, def.network, knownLedger[def.network]);
if (ledgerApp === 'generic') {
// All chains use the `slip44` from polkadot in their derivation path in ledger.
// This interface is specific to the underlying PolkadotGenericApp.
ledger = new LedgerGeneric(transport, def.network, knownLedger['polkadot']);
} else if (ledgerApp === 'migration') {
ledger = new LedgerGeneric(transport, def.network, knownLedger[def.network]);
} else if (ledgerApp === 'chainSpecific') {
ledger = new Ledger(transport, def.network);
} else {
// This will never get touched since it will always hit the above two. This satisfies the compiler.
ledger = new LedgerGeneric(transport, def.network, knownLedger['polkadot']);
} }
return ledger; if (ledgerApp === 'chainSpecific') {
return new Ledger(transport, def.network);
}
// This will never get touched since it will always hit the above two. This satisfies the compiler.
return new LedgerGeneric(transport, 'polkadot', knownLedger['polkadot']);
} }
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 {
@ -97,47 +99,59 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
const [type, setType] = useState<KeypairType | null>(null); const [type, setType] = useState<KeypairType | null>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const { ledgerApp } = useContext(SettingsContext); const { ledgerApp } = useContext(SettingsContext);
const tRef = useRef(t);
// Holds the ledger from the previous effect run so we can close its // Holds the ledger from the previous effect run so we can close its
// transport when the network changes and a new instance is created. // transport when the network changes and a new instance is created.
const prevLedgerRef = useRef<LedgerGeneric | Ledger | null>(null); const prevLedgerRef = useRef<LedgerGeneric | Ledger | null>(null);
const handleGetAddressError = (e: Error, genesis: string) => { // Keep a stable reference so effects and callbacks don't depend on
// i18n function identity changes.
tRef.current = t;
const handleGetAddressError = useCallback((e: Error, genesis: string, ledgerApp: string) => {
setIsLoading(false); setIsLoading(false);
const { network } = getNetwork(genesis) || { network: 'unknown network' }; const { network } = getNetwork(genesis) || { network: ledgerApp === 'generic' ? 'Polkadot' : 'unknown network' };
const warningMessage = e.message.includes('Code: 26628') const warningMessage = e.message.includes('Code: 26628')
? t('Is your ledger locked?') ? tRef.current('Is your ledger locked?')
: null; : null;
const errorMessage = e.message.includes('App does not seem to be open') const errorMessage = e.message.includes('App does not seem to be open')
? t('App "{{network}}" does not seem to be open', { replace: { network } }) ? tRef.current('App "{{network}}" does not seem to be open', { replace: { network } })
: e.message; : e.message;
setIsLocked(true); setIsLocked(true);
setWarning(warningMessage); setWarning(warningMessage);
setError(t( setError(tRef.current(
'Ledger error: {{errorMessage}}', 'Ledger error: {{errorMessage}}',
{ replace: { errorMessage } } { replace: { errorMessage } }
)); ));
console.error(e); console.error(e);
setAddress(null); setAddress(null);
setType(null); setType(null);
}; }, []);
const { ledger, ledgerInitError } = useMemo(() => { const { ledger, ledgerInitError } = useMemo(() => {
if (refreshCount > 0 || genesis) { // undefined means no ledger connection attempt.
if (!genesis) { // null means connect using the generic app without a specific chain genesis.
return { ledger: null, ledgerInitError: null }; // Prevents hook instances that only check isLedgerCapable or isLedgerEnabled
} // from attempting a connection when ledgerApp === 'generic'.
if (genesis === undefined) {
try { return { ledger: null, ledgerInitError: null };
return { ledger: retrieveLedger(genesis, ledgerApp), ledgerInitError: null };
} catch (error) {
return { ledger: null, ledgerInitError: (error as Error).message };
}
} }
return { ledger: null, ledgerInitError: null }; // Generic app connects without a specific chain genesis (always derives from polkadot).
const canConnect = ledgerApp === 'generic' || refreshCount > 0 || !!genesis;
if (!canConnect || (ledgerApp !== 'generic' && !genesis)) {
return { ledger: null, ledgerInitError: null };
}
try {
return { ledger: retrieveLedger(genesis ?? null, ledgerApp), ledgerInitError: null };
} catch (error) {
return { ledger: null, ledgerInitError: (error as Error).message };
}
}, [genesis, ledgerApp, refreshCount]); }, [genesis, ledgerApp, refreshCount]);
useEffect(() => { useEffect(() => {
@ -147,7 +161,7 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
useEffect(() => { useEffect(() => {
let isStale = false; let isStale = false;
if (!ledger || !genesis) { if (!ledger || (ledgerApp !== 'generic' && !genesis)) {
if (prevLedgerRef.current) { if (prevLedgerRef.current) {
prevLedgerRef.current.disconnect().catch(console.error); prevLedgerRef.current.disconnect().catch(console.error);
prevLedgerRef.current = null; prevLedgerRef.current = null;
@ -172,7 +186,7 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
const onAddressError = (e: Error): void => { const onAddressError = (e: Error): void => {
if (!isStale) { if (!isStale) {
handleGetAddressError(e, genesis); handleGetAddressError(e, genesis ?? '', ledgerApp);
} }
}; };
@ -190,10 +204,14 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
} }
const chosenNetwork = chains.find(({ genesisHash }) => genesisHash === genesis as HexString); const chosenNetwork = chains.find(({ genesisHash }) => genesisHash === genesis as HexString);
// Use the chain's SS58 prefix when known; fall back to 42 (substrate default). // Use the chain's SS58 prefix when known; fall back to 42 (substrate default).
const ss58Prefix = chosenNetwork?.ss58Format ?? 42; const ss58Prefix = chosenNetwork?.ss58Format ?? 42;
if (ledgerApp === 'migration') {
// Migration app is only expected on known, mapped chains.
assert(chosenNetwork, tRef.current('This network is not available, please report an issue to update the known chains'));
}
if (ledgerApp === 'generic' || ledgerApp === 'migration') { if (ledgerApp === 'generic' || ledgerApp === 'migration') {
if (isEthereum) { if (isEthereum) {
(ledger as LedgerGeneric).getAddressEcdsa(false, accountIndex, addressOffset) (ledger as LedgerGeneric).getAddressEcdsa(false, accountIndex, addressOffset)
@ -206,14 +224,15 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
}); });
}).catch(onAddressError); }).catch(onAddressError);
} else { } else {
(ledger as LedgerGeneric).getAddress(ss58Prefix, false, accountIndex, addressOffset).then((res) => { (ledger as LedgerGeneric).getAddress(ss58Prefix, false, accountIndex, addressOffset)
runIfCurrent(() => { .then((res) => {
setIsLoading(false); runIfCurrent(() => {
setIsLocked(false); setIsLoading(false);
setAddress(res.address); setIsLocked(false);
setType('ed25519'); setAddress(res.address);
}); setType('ed25519');
}).catch(onAddressError); });
}).catch(onAddressError);
} }
} else if (ledgerApp === 'chainSpecific') { } else if (ledgerApp === 'chainSpecific') {
(ledger as Ledger).getAddress(false, accountIndex, addressOffset) (ledger as Ledger).getAddress(false, accountIndex, addressOffset)
@ -238,10 +257,7 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
return () => { return () => {
isStale = true; isStale = true;
}; };
// If the dependency array is exhaustive, with t, the translation function, it }, [accountIndex, addressOffset, genesis, handleGetAddressError, ledger, ledgerApp, isEthereum]);
// triggers a useless re-render when ledger device is connected.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountIndex, addressOffset, genesis, ledger, ledgerApp, isEthereum]);
const ledgerRef = useRef(ledger); const ledgerRef = useRef(ledger);

View File

@ -143,7 +143,7 @@ export async function createAccountExternal (name: string, address: string, gene
return sendMessage('pri(accounts.create.external)', { address, genesisHash, name }); return sendMessage('pri(accounts.create.external)', { address, genesisHash, name });
} }
export async function createAccountHardware (address: string, hardwareType: string, accountIndex: number, addressOffset: number, name: string, genesisHash: HexString, type: KeypairType): Promise<boolean> { export async function createAccountHardware (address: string, hardwareType: string, accountIndex: number, addressOffset: number, name: string, genesisHash: HexString | null, type: KeypairType): Promise<boolean> {
return sendMessage('pri(accounts.create.hardware)', { accountIndex, address, addressOffset, genesisHash, hardwareType, name, type }); return sendMessage('pri(accounts.create.hardware)', { accountIndex, address, addressOffset, genesisHash, hardwareType, name, type });
} }