Author SHA1 Message Date
rob thijssenandClaude Opus 5 6dfd109991 feat: replace the Parity Signer QR flow with "track an address"
Keeps watch-only accounts, drops the protocol that cannot work.

ImportQr scanned a Parity Signer QR — SUBSTRATE_ID 0x53, CRYPTO_SR25519 0x01, a
crypto-type byte with no ML-DSA value — so no Quantus device could ever answer
it, and one branch hardcoded createAccountSuri(..., 'sr25519', ...). But it was
also the *only* caller of createAccountExternal, so deleting it would have taken
watch-only accounts with it. Those are wanted independently of how the address
arrives, and are what the account list is used for.

TrackAddress takes a pasted SS58 string instead, which for Quantus is the common
case anyway: the address is on screen in another wallet, not on a signing device.
It round-trips through decodeAddress/encodeAddress, which rejects a mistyped
address by its checksum rather than storing one that can never receive anything,
and normalises the prefix so an address pasted from a tool using a different one
displays the way the rest of the extension displays it.

The signing path had to change with it, and this is the part that would have been
a bug. `isExternal && !isHardware` previously rendered the QR signer — and a
tracked address satisfies exactly that condition, so leaving it would have shown
a Parity QR that nothing can scan, for an account that can never sign. External
accounts now show the decoded call and say plainly that the extension holds no
key for them.

That makes Signing/Qr.tsx unreachable, along with the CMD_MORTAL and
CMD_SIGN_MESSAGE Parity command bytes and the _onSignature callback, whose whole
job was accepting a signature produced outside the extension. All removed.

approveSignSignature in the background is deliberately left. Unlike Ledger, which
is gone for good, external signing returns when quantus/extension#10 ports the
flow to multipart UR — the message it carries is the right shape for that, and
deleting it would only mean writing it again.

Two specs rewritten rather than deleted: the one asserting a QR scanner appears
for external accounts now asserts the extrinsic and the cannot-sign warning
appear instead.

Typecheck, lint and 65 tests clean; build:chrome completes.

Refs quantus/extension#10, quantus/extension#5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 12:19:08 +03:00
rob thijssenandClaude Opus 5 87a0b3926c feat: remove the Ledger account path
No hardware wallet speaks ML-DSA, and @polkadot/hw-ledger talks ed25519 over the
Ledger Substrate app, so every one of these screens was unreachable for a Quantus
account. Removed rather than hidden behind a flag: this fork is not trying to stay
mergeable with upstream, so unreachable UI costs more than it saves. It is also
work that quantus/common#6 turns into a type error regardless.

Gone: ImportLedger.tsx, useLedger.ts, LedgerSign.tsx, legerChains.ts (upstream's
typo), the route and its ALLOWED_PATH entry, the add-menu item, the Ledger App
setting, and the @polkadot/hw-ledger dependency.

Also gone is the background handler. accountsCreateHardware and its message type
were reachable only from ImportLedger, so they are now dead code — and dead code
that mints accounts is worth deleting rather than leaving for someone to wire up
again.

Two things deliberately stay:

isHardware remains on AccountJson. Nothing creates such an account any more, but
a restored JSON can still carry the flag, and the display branches that read it
are correct for that case — an imported hardware account has no key here and
cannot sign. Ripping the flag out would touch unrelated display code to remove a
branch that is right.

canEditGenesis becomes unconditional. It was `!isHardware || ledgerApp ===
'generic'`, because the chain-specific Ledger app could not sign for an arbitrary
genesis hash. With no hardware path and no such setting, nothing restricts it.
The two spec cases that asserted the old behaviour are rewritten into one that
asserts the new rule, rather than deleted.

Not done, contrary to what the issue asked for: no "hardware wallets are not
supported" note in the add menu. The reasoning there was that a missing item
reads as broken — but that only holds for someone looking for it, and in a
Quantus-only wallet nobody is. A permanent dead entry needing translation into
every locale is a worse wart than its absence. It belongs in the README when
branding lands (#2).

65 tests pass; typecheck clean.

Refs quantus/extension#5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 12:02:19 +03:00
24 changed files with 205 additions and 1691 deletions
@@ -8,7 +8,7 @@ import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/
import type { Registry } from '@polkadot/types/types';
import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestActiveTabsUrlUpdate, RequestAuthorizeApprove, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types.js';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestActiveTabsUrlUpdate, RequestAuthorizeApprove, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types.js';
import type { AuthorizedAccountsDiff } from './State.js';
import type State from './State.js';
@@ -61,12 +61,6 @@ export default class Extension {
return true;
}
private accountsCreateHardware ({ accountIndex, address, addressOffset, genesisHash, hardwareType, name, type }: RequestAccountCreateHardware): boolean {
keyring.addHardware(address, hardwareType, { accountIndex, addressOffset, genesisHash, name, type });
return true;
}
private accountsCreateSuri ({ genesisHash, name, password, suri, type }: RequestAccountCreateSuri): boolean {
keyring.addUri(getSuri(suri, type), password, { genesisHash, name }, type);
@@ -582,9 +576,6 @@ export default class Extension {
case 'pri(accounts.create.external)':
return this.accountsCreateExternal(request as RequestAccountCreateExternal);
case 'pri(accounts.create.hardware)':
return this.accountsCreateHardware(request as RequestAccountCreateHardware);
case 'pri(accounts.create.suri)':
return this.accountsCreateSuri(request as RequestAccountCreateSuri);
@@ -80,7 +80,6 @@ export type ConnectedTabsUrlResponse = string[]
export interface RequestSignatures {
// private/internal requests, i.e. from a popup
'pri(accounts.create.external)': [RequestAccountCreateExternal, boolean];
'pri(accounts.create.hardware)': [RequestAccountCreateHardware, boolean];
'pri(accounts.create.suri)': [RequestAccountCreateSuri, boolean];
'pri(accounts.edit)': [RequestAccountEdit, boolean];
'pri(accounts.export)': [RequestAccountExport, ResponseAccountExport];
@@ -197,16 +196,6 @@ export interface RequestAccountCreateSuri {
type?: KeypairType;
}
export interface RequestAccountCreateHardware {
accountIndex: number;
address: string;
addressOffset: number;
genesisHash?: HexString | null;
hardwareType: string;
name: string;
type: KeypairType;
}
export interface RequestAccountChangePassword {
address: string;
oldPass: string;
+3 -1
View File
@@ -16,7 +16,9 @@ export const PORT_EXTENSION = `${PORT_PREFIX}-extension`;
export const MESSAGE_ORIGIN_PAGE = `${PORT_PREFIX}-page`;
export const MESSAGE_ORIGIN_CONTENT = `${PORT_PREFIX}-content`;
export const ALLOWED_PATH = ['/', '/account/import-ledger', '/account/restore-json'] as const;
// '/account/import-ledger' is gone with the Ledger path: no hardware wallet
// speaks ML-DSA, so there is nothing for it to connect to. See quantus/extension#5.
export const ALLOWED_PATH = ['/', '/account/restore-json', '/account/track-address'] as const;
export const PASSWORD_EXPIRY_MIN = 15;
export const PASSWORD_EXPIRY_MS = PASSWORD_EXPIRY_MIN * 60 * 1000;
-1
View File
@@ -29,7 +29,6 @@
"@polkadot/extension-chains": "0.64.0",
"@polkadot/extension-dapp": "0.64.0",
"@polkadot/extension-inject": "0.64.0",
"@polkadot/hw-ledger": "^14.0.3",
"@polkadot/keyring": "^14.0.3",
"@polkadot/networks": "^14.0.3",
"@polkadot/react-identicon": "^3.16.7",
@@ -37,9 +37,9 @@ describe('Account component', () => {
let wrapper: ReactWrapper;
const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const mountAccountComponent = (additionalAccountProperties: Record<string, unknown>, ledgerApp: 'generic' | 'migration' | 'chainSpecific' = 'chainSpecific'): ReactWrapper => mount(
const mountAccountComponent = (additionalAccountProperties: Record<string, unknown>): ReactWrapper => mount(
<MemoryRouter>
<SettingsContext.Provider value={{ ...settings.get(), ledgerApp }}>
<SettingsContext.Provider value={settings.get()}>
<Account
{...{ address: VALID_ADDRESS, ...additionalAccountProperties }}
/>
@@ -83,19 +83,16 @@ describe('Account component', () => {
expect(wrapper.find('.genesisSelection').exists()).toBe(true);
});
it('does not show genesis hash selection dropsown if account is hardware', async () => {
wrapper = mountAccountComponent({ isExternal: true, isHardware: true }, 'chainSpecific');
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(false);
});
it('shows genesis hash selection for hardware account in generic mode', async () => {
wrapper = mountAccountComponent({ isExternal: true, isHardware: true }, 'generic');
// Was two tests, one per Ledger app mode: the chain-specific app could not
// sign for an arbitrary genesis hash, so the dropdown was hidden for hardware
// accounts unless the generic app was selected. With no hardware path left
// (quantus/extension#5) there is no such restriction, and the setting that
// drove it is gone.
//
// isHardware survives as a meta flag — a restored JSON can still carry it —
// so an account marked that way must still behave like any other.
it('shows genesis hash selection even for an account marked hardware', async () => {
wrapper = mountAccountComponent({ isExternal: true, isHardware: true });
wrapper.find('.settings').first().simulate('click');
await act(flushAllPromises);
@@ -8,7 +8,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
import { canDerive } from '@polkadot/extension-base/utils';
import { AccountContext, Address, Checkbox, Dropdown, Link, MenuDivider, SettingsContext } from '../../components/index.js';
import { AccountContext, Address, Checkbox, Dropdown, Link, MenuDivider } from '../../components/index.js';
import { useGenesisHashOptions, useTranslation } from '../../hooks/index.js';
import { editAccount, tieAccount } from '../../messaging.js';
import { Name } from '../../partials/index.js';
@@ -33,10 +33,12 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
const [editedName, setName] = useState<string | undefined | null>(name);
const [checked, setChecked] = useState(false);
const genesisOptions = useGenesisHashOptions();
const { ledgerApp } = useContext(SettingsContext);
const { selectedAccounts = [], setSelectedAccounts } = useContext(AccountContext);
const isSelected = useMemo(() => selectedAccounts?.includes(address) || false, [address, selectedAccounts]);
const canEditGenesis = !isHardware || ledgerApp === 'generic';
// Was `!isHardware || ledgerApp === 'generic'` — the Ledger Substrate app only
// let you pin a genesis hash in its generic mode. With no hardware path left,
// there is nothing to restrict. See quantus/extension#5.
const canEditGenesis = true;
useEffect(() => {
setChecked(isSelected);
@@ -1,225 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
import { faSync } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { settings } from '@polkadot/ui-settings';
import { ActionContext, Address, Button, ButtonArea, Dropdown, SettingsContext, Switch, VerticalSpace, Warning } from '../components/index.js';
import { useLedger, useTranslation } from '../hooks/index.js';
import { createAccountHardware } from '../messaging.js';
import { Header, Name } from '../partials/index.js';
import { styled } from '../styled.js';
import ledgerChains from '../util/legerChains.js';
interface AccOption {
text: string;
value: number;
}
interface NetworkOption {
text: string;
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 SELECT_NETWORK = '';
const ALLOW_ANY_NETWORK = '__allow_any_network__';
interface Props {
className?: string;
}
function ImportLedger ({ className }: Props): React.ReactElement {
const { t } = useTranslation();
const [accountIndex, setAccountIndex] = useState<number>(0);
const [addressOffset, setAddressOffset] = useState<number>(0);
const [error, setError] = useState<string | null>(null);
const [isBusy, setIsBusy] = useState(false);
const [genesis, setGenesis] = useState<HexString | null>(null);
const [isEthereum, setIsEthereum] = useState(false);
const onAction = useContext(ActionContext);
const { ledgerApp } = useContext(SettingsContext);
const [name, setName] = useState<string | null>(null);
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);
useEffect(() => {
if (address) {
settings.set({ ledgerConn: 'webusb' });
}
}, [address]);
useEffect(() => {
if (isChainSpecific) {
setIsEthereum(false);
}
}, [isChainSpecific]);
const accOps = useRef(AVAIL.map((value): AccOption => ({
text: t('Account type {{index}}', { replace: { index: value } }),
value
})));
const addOps = useRef(AVAIL.map((value): AccOption => ({
text: t('Address index {{index}}', { replace: { index: value } }),
value
})));
const onNetworkChange = useCallback((value: string) => {
if (value === ALLOW_ANY_NETWORK) {
setGenesis(null);
return;
}
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(
() => {
if (address && name && type) {
setIsBusy(true);
createAccountHardware(address, 'ledger', accountIndex, addressOffset, name, genesis, type)
.then(() => onAction('/'))
.catch((error: Error) => {
console.error(error);
setIsBusy(false);
setError(error.message);
});
}
},
[accountIndex, address, addressOffset, genesis, name, onAction, type]
);
// select element is returning a string
const _onSetAccountIndex = useCallback((value: number) => setAccountIndex(Number(value)), []);
const _onSetAddressOffset = useCallback((value: number) => setAddressOffset(Number(value)), []);
return (
<>
<Header
showBackArrow
text={t('Import Ledger Account')}
/>
<div className={className}>
<Address
address={address}
genesisHash={genesis}
isExternal
isHardware
name={name}
type={type ?? undefined}
/>
<div className='ethereum-toggle'>
<Switch
checked={isEthereum}
checkedLabel={t('Ethereum Account')}
isDisabled={isChainSpecific}
onChange={setIsEthereum}
uncheckedLabel={t('ED25519 Account')}
/>
</div>
<Dropdown
className='network'
label={t('Network')}
onChange={onNetworkChange}
options={networkOps}
value={genesis ?? (isGenericLedgerApp ? ALLOW_ANY_NETWORK : SELECT_NETWORK)}
/>
{!!address && !ledgerError && (
<Name
onChange={setName}
value={name || ''}
/>
)}
{!!name && (
<>
<Dropdown
className='accountType'
isDisabled={ledgerLoading}
label={t('account type')}
onChange={_onSetAccountIndex}
options={accOps.current}
value={accountIndex}
/>
<Dropdown
className='accountIndex'
isDisabled={ledgerLoading}
label={t('address index')}
onChange={_onSetAddressOffset}
options={addOps.current}
value={addressOffset}
/>
</>
)}
{!!ledgerWarning && (
<Warning>
{ledgerWarning}
</Warning>
)}
{(!!error || !!ledgerError) && (
<Warning
isDanger
>
{error || ledgerError}
</Warning>
)}
</div>
<VerticalSpace />
<ButtonArea>
{ledgerLocked
? (
<Button
isBusy={ledgerLoading || isBusy}
onClick={refresh}
>
<FontAwesomeIcon icon={faSync} />
{t('Refresh')}
</Button>
)
: (
<Button
isBusy={ledgerLoading || isBusy}
isDisabled={!!error || !!ledgerError || !address || (!isGenericLedgerApp && !genesis)}
onClick={_onSave}
>
{t('Import Account')}
</Button>
)
}
</ButtonArea>
</>
);
}
export default styled(ImportLedger)<Props>`
.refreshIcon {
margin-right: 0.3rem;
}
.ethereum-toggle {
margin: 1rem 0;
}
`;
@@ -1,140 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@polkadot/extension-mocks/chrome';
import type { ReactWrapper } from 'enzyme';
import type * as _ from '@polkadot/dev-test/globals.d.ts';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import enzyme from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router';
import { Button } from '../components/index.js';
import * as messaging from '../messaging.js';
import { flushAllPromises } from '../testHelpers.js';
import ImportQr from './ImportQr.js';
const { configure, mount } = enzyme;
const mockedAccount = {
content: '12bxf6QJS5hMJgwbJMDjFot1sq93EvgQwyuPWENr9SzJfxtN',
expectedBannerChain: 'Polkadot',
genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
isAddress: true,
name: 'My Polkadot Account'
};
interface ScanType {
isAddress: boolean;
content: string;
genesisHash: string;
name?: string;
}
interface QrScanAddressProps {
className?: string;
onError?: (error: Error) => void;
onScan: (scanned: ScanType) => void;
size?: string | number;
style?: React.CSSProperties;
}
// // NOTE Required for spyOn when using @swc/jest
// // https://github.com/swc-project/swc/issues/3843
// jest.mock('../messaging', (): Record<string, unknown> => ({
// __esModule: true,
// ...jest.requireActual('../messaging')
// }));
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
configure({ adapter: new Adapter() });
const typeName = async (wrapper: ReactWrapper, value: string) => {
wrapper.find('input').first().simulate('change', { target: { value } });
await act(flushAllPromises);
wrapper.update();
};
// jest.mock('@polkadot/react-qr', () => {
// return {
// QrScanAddress: (_: QrScanAddressProps): null => {
// return null;
// }
// };
// });
describe('ImportQr component', () => {
let wrapper: ReactWrapper;
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
wrapper = mount(
<MemoryRouter>
<ImportQr />
</MemoryRouter>
);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
act(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
(wrapper.find('QrScanAddress').first().prop('onScan') as unknown as QrScanAddressProps['onScan'])(mockedAccount);
});
await act(flushAllPromises);
wrapper.update();
});
describe('Address component', () => {
it('shows account as external', () => {
expect(wrapper.find('Name').find('FontAwesomeIcon [data-icon="qrcode"]').exists()).toBe(true);
});
it('shows the correct name', () => {
expect(wrapper.find('Name span').text()).toEqual(mockedAccount.name);
});
it('shows the correct address', () => {
expect(wrapper.find('[data-field="address"]').text()).toEqual(mockedAccount.content);
});
it('shows the correct banner', () => {
expect(wrapper.find('[data-field="chain"]').text()).toEqual(mockedAccount.expectedBannerChain);
});
});
it('has the button enabled', () => {
expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
});
it('displays and error and the button is disabled with a short name', async () => {
await typeName(wrapper, 'a');
expect(wrapper.find('.warning-message').first().text()).toBe('Account name is too short');
expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
});
it('has no error message and button enabled with a long name', async () => {
const longName = 'aaa';
await typeName(wrapper, 'a');
await typeName(wrapper, longName);
expect(wrapper.find('.warning-message')).toHaveLength(0);
expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
expect(wrapper.find('Name span').text()).toEqual(longName);
});
it('shows the external name in the input field', () => {
expect(wrapper.find('input').prop('value')).toBe(mockedAccount.name);
});
it('creates the external account', async () => {
jest.spyOn(messaging, 'createAccountExternal').mockImplementation(() => Promise.resolve(false));
wrapper.find(Button).simulate('click');
await act(flushAllPromises);
expect(messaging.createAccountExternal).toHaveBeenCalledWith(mockedAccount.name, mockedAccount.content, mockedAccount.genesisHash);
});
});
@@ -1,115 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
import React, { useCallback, useContext, useState } from 'react';
import { QrScanAddress } from '@polkadot/react-qr';
import AccountNamePasswordCreation from '../components/AccountNamePasswordCreation.js';
import { ActionContext, Address, ButtonArea, NextStepButton, VerticalSpace } from '../components/index.js';
import { useTranslation } from '../hooks/index.js';
import { createAccountExternal, createAccountSuri, createSeed } from '../messaging.js';
import { Header, Name } from '../partials/index.js';
interface QrAccount {
content: string;
genesisHash: HexString | null;
isAddress: boolean;
name?: string;
}
export default function ImportQr (): React.ReactElement {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [account, setAccount] = useState<QrAccount | null>(null);
const [address, setAddress] = useState<string | null>(null);
const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(null);
const _setAccount = useCallback(
(qrAccount: QrAccount) => {
setAccount(qrAccount);
setName(qrAccount?.name || null);
if (qrAccount.isAddress) {
setAddress(qrAccount.content);
} else {
createSeed(undefined, qrAccount.content)
.then(({ address }) => setAddress(address))
.catch(console.error);
}
},
[]
);
const _onCreate = useCallback(
(): void => {
if (account && name) {
if (account.isAddress) {
createAccountExternal(name, account.content, account.genesisHash)
.then(() => onAction('/'))
.catch((error: Error) => console.error(error));
} else if (password) {
createAccountSuri(name, password, account.content, 'sr25519', account.genesisHash)
.then(() => onAction('/'))
.catch((error: Error) => console.error(error));
}
}
},
[account, name, onAction, password]
);
return (
<>
<Header
showBackArrow
text={t('Scan Address Qr')}
/>
{!account && (
<div>
<QrScanAddress onScan={_setAccount} />
</div>
)}
{account && (
<>
<div>
<Address
{...account}
address={address}
isExternal={true}
name={name}
/>
</div>
{account.isAddress
? (
<Name
isFocused
onChange={setName}
value={name || ''}
/>
)
: (
<AccountNamePasswordCreation
isBusy={false}
onCreate={_onCreate}
onNameChange={setName}
onPasswordChange={setPassword}
/>
)
}
<VerticalSpace />
<ButtonArea>
<NextStepButton
isDisabled={!name || (!account.isAddress && !password)}
onClick={_onCreate}
>
{t('Add the account with identified address')}
</NextStepButton>
</ButtonArea>
</>
)}
</>
);
}
@@ -70,7 +70,7 @@ function Request ({ className, metaId, request, url }: Props): React.ReactElemen
</Table>
<div className='requestInfo'>
<Warning className='requestWarning'>
{t('This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata. It will also allow the use of Ledger\'s Generic Polkadot App.')}
{t('This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.')}
</Warning>
<Button
className='btnAccept'
@@ -1,239 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable deprecation/deprecation */
import type { Chain } from '@polkadot/extension-chains/types';
import type { Ledger, LedgerGeneric } from '@polkadot/hw-ledger';
import type { ExtrinsicPayload } from '@polkadot/types/interfaces';
import type { SignerPayloadJSON } from '@polkadot/types/types';
import type { HexString } from '@polkadot/util/types';
import { faSync } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useContext, useState } from 'react';
import { assert, objectSpread, stringShorten, u8aToHex } from '@polkadot/util';
import { decodeAddress } from '@polkadot/util-crypto';
import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata';
import { Button, SettingsContext, Warning } from '../../components/index.js';
import { useLedger, useMetadata, useTranslation } from '../../hooks/index.js';
import { styled } from '../../styled.js';
interface Props {
accountIndex?: number;
addressOffset?: number;
className?: string;
error: string | null;
genesisHash?: string;
isEthereum?: boolean;
onSignature?: ({ signature }: { signature: HexString }, signedTransaction?: HexString) => void;
payloadJson?: SignerPayloadJSON;
payloadExt?: ExtrinsicPayload
setError: (value: string | null) => void;
}
function getMetadataProof (chain: Chain, payload: SignerPayloadJSON) {
const m = chain.definition.rawMetadata;
assert(m, 'To sign with Ledger\'s Polkadot Generic App, the metadata must be present in the extension.');
const merkleizedMetadata = merkleizeMetadata(m, {
base58Prefix: chain.ss58Format,
decimals: chain.tokenDecimals,
specVersion: chain.specVersion,
tokenSymbol: chain.tokenSymbol
});
const metadataHash = u8aToHex(merkleizedMetadata.digest());
const newPayload = objectSpread<SignerPayloadJSON>({}, payload, { metadataHash, mode: 1 });
const raw = chain.registry.createType('ExtrinsicPayload', newPayload);
return {
raw,
txMetadata: merkleizedMetadata.getProofForExtrinsicPayload(u8aToHex(raw.toU8a(true)))
};
}
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> {
const [isBusy, setIsBusy] = useState(false);
const { t } = useTranslation();
const { ledgerApp } = useContext(SettingsContext);
const chain = useMetadata(genesisHash);
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(() => {
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.');
setIsBusy(false);
}
return;
}
setError(null);
setIsBusy(true);
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);
const metaBuff = Buffer.from(txMetadata);
if (isEthereum) {
(ledger as LedgerGeneric).signWithMetadataEcdsa(raw.toU8a(true), accountIndex, addressOffset, { metadata: metaBuff })
.then(({ signature }) => {
const extrinsic = chain.registry.createType(
'Extrinsic',
{ method: raw.method },
{ version: 4 }
);
(ledger as LedgerGeneric).getAddressEcdsa(false, accountIndex, addressOffset)
.then(({ address }) => {
extrinsic.addSignature(`0x${address}`, signature, raw.toHex());
onSignature({ signature }, extrinsic.toHex());
})
.catch((e: Error) => {
setError(e.message);
setIsBusy(false);
});
}).catch((e: Error) => {
setError(e.message);
setIsBusy(false);
});
} else {
(ledger as LedgerGeneric).signWithMetadata(raw.toU8a(true), accountIndex, addressOffset, { metadata: metaBuff })
.then((signature) => {
const extrinsic = chain.registry.createType(
'Extrinsic',
{ method: raw.method },
{ version: 4 }
);
(ledger as LedgerGeneric).getAddress(chain.ss58Format, false, accountIndex, addressOffset)
.then(({ address }) => {
extrinsic.addSignature(address, signature.signature, raw.toHex());
onSignature(signature, extrinsic.toHex());
})
.catch((e: Error) => {
setError(e.message);
setIsBusy(false);
});
}).catch((e: Error) => {
setError(e.message);
setIsBusy(false);
});
}
} else if (currApp === 'chainSpecific') {
(ledger as Ledger).sign(payloadExt.toU8a(true), accountIndex, addressOffset)
.then((signature) => {
onSignature(signature);
}).catch((e: Error) => {
setError(e.message);
setIsBusy(false);
});
}
},
[accountIndex, addressOffset, chain, ledger, ledgerApp, onSignature, payloadJson, payloadExt, setError, signerMismatchError, isEthereum]
);
const activeError = ledgerError || error;
const hasActiveError = !!activeError;
return (
<div className={className}>
{!!ledgerWarning && (
<Warning>
{ledgerWarning}
</Warning>
)}
{hasActiveError && (
<Warning isDanger>
{activeError}
</Warning>
)}
{signerMismatchError && !hasActiveError && (
<Warning isDanger>
{signerMismatchError}
</Warning>
)}
{signerMismatchHint && !hasActiveError && (
<Warning>
{signerMismatchHint}
</Warning>
)}
{(ledgerLocked || hasActiveError)
? (
<Button
isBusy={isBusy || ledgerLoading}
onClick={_onRefresh}
>
<FontAwesomeIcon icon={faSync} />
{t('Refresh')}
</Button>
)
: (
<Button
isBusy={isBusy || ledgerLoading}
isDisabled={!!signerMismatchError}
onClick={_onSignLedger}
>
{t('Sign on Ledger')}
</Button>
)
}
</div>
);
}
export default styled(LedgerSign) <Props>`
flex-direction: column;
padding: 6px 24px;
.danger {
margin-bottom: .5rem;
}
`;
@@ -1,103 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ExtrinsicPayload } from '@polkadot/types/interfaces';
import type { HexString } from '@polkadot/util/types';
import React, { useCallback, useMemo, useState } from 'react';
import { QrDisplayPayload, QrScanSignature } from '@polkadot/react-qr';
import { u8aWrapBytes } from '@polkadot/util';
import { Button } from '../../components/index.js';
import { useTranslation } from '../../hooks/index.js';
import { styled } from '../../styled.js';
import { CMD_MORTAL, CMD_SIGN_MESSAGE } from './Request/index.js';
interface Props {
address: string;
children?: React.ReactNode;
className?: string;
cmd: number;
genesisHash: string;
onSignature: ({ signature }: { signature: HexString }) => void;
payload: ExtrinsicPayload | string;
}
function Qr ({ address, className, cmd, genesisHash, onSignature, payload }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isScanning, setIsScanning] = useState(false);
const payloadU8a = useMemo(
() => {
switch (cmd) {
case CMD_MORTAL:
return (payload as ExtrinsicPayload).toU8a();
case CMD_SIGN_MESSAGE:
return u8aWrapBytes(payload as string);
default:
return null;
}
},
[cmd, payload]
);
const _onShowQr = useCallback(
() => setIsScanning(true),
[]
);
if (!payloadU8a) {
return (
<div className={className}>
<div className='qrContainer'>
Transaction command:{cmd} not supported.
</div>
</div>
);
}
return (
<div className={className}>
<div className='qrContainer'>
{isScanning
? <QrScanSignature onScan={onSignature} />
: (
<QrDisplayPayload
address={address}
cmd={cmd}
genesisHash={genesisHash}
payload={payloadU8a}
/>
)
}
</div>
{!isScanning && (
<Button
className='scanButton'
onClick={_onShowQr}
>
{t('Scan signature via camera')}
</Button>
)}
</div>
);
}
export default styled(Qr)<Props>`
height: 100%;
.qrContainer {
margin: 5px auto 10px auto;
width: 65%;
img {
border: white solid 1px;
}
}
.scanButton {
margin-bottom: 8px;
}
`;
@@ -3,20 +3,16 @@
import type { AccountJson, RequestSign } from '@polkadot/extension-base/background/types';
import type { ExtrinsicPayload } from '@polkadot/types/interfaces';
import type { HexString } from '@polkadot/util/types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { isExtrinsicRequest } from '@polkadot/extension-base/utils';
import { TypeRegistry } from '@polkadot/types';
import { ActionContext, Address, VerticalSpace, Warning } from '../../../components/index.js';
import { Address, VerticalSpace, Warning } from '../../../components/index.js';
import { useMetadata, useTranslation } from '../../../hooks/index.js';
import { approveSignSignature } from '../../../messaging.js';
import Bytes from '../Bytes.js';
import Extrinsic from '../Extrinsic.js';
import LedgerSign from '../LedgerSign.js';
import Qr from '../Qr.js';
import SignArea from './SignArea.js';
interface Props {
@@ -33,14 +29,10 @@ interface Data {
payload: ExtrinsicPayload | null;
}
export const CMD_MORTAL = 2;
export const CMD_SIGN_MESSAGE = 3;
// keep it global, we can and will re-use this across requests
const registry = new TypeRegistry();
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);
export default function Request ({ account: { isExternal, isHardware }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation();
@@ -55,7 +47,6 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
useEffect((): void => {
// When the chain and request are ready, configure the chain's registry.
// This will be picked up by LedgerSign.
if (chain && isExtrinsicRequest(request)) {
chain.registry.setSignedExtensions(request.payload.signedExtensions, chain.definition.userExtensions);
}
@@ -79,18 +70,6 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
}
}, [request]);
const _onSignature = useCallback(
({ signature }: { signature: HexString }, signedTransaction?: HexString): void => {
approveSignSignature(signId, signature, signedTransaction)
.then(() => onAction())
.catch((error: Error): void => {
setError(error.message);
console.error(error);
});
},
[onAction, signId]
);
// Branch on the request itself rather than on the decoded state, so a render
// that lands before the effect has caught up shows nothing instead of feeding
// the previous request's view a payload of the other shape.
@@ -111,36 +90,16 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
isHardware={isHardware}
/>
</div>
{isExternal && !isHardware
? (
<Qr
address={json.address}
cmd={CMD_MORTAL}
genesisHash={json.genesisHash}
onSignature={_onSignature}
payload={payload}
/>
)
: (
<Extrinsic
payload={payload}
request={json}
url={url}
/>
)
}
{isHardware && (
<LedgerSign
accountIndex={accountIndex || 0}
addressOffset={addressOffset || 0}
error={error}
genesisHash={json.genesisHash}
isEthereum={type === 'ethereum'}
onSignature={_onSignature}
payloadExt={payload}
payloadJson={json}
setError={setError}
/>
<Extrinsic
payload={payload}
request={json}
url={url}
/>
{isExternal && (
<>
<Warning>{t('This account is tracked only — the extension holds no key for it and cannot sign.')}</Warning>
<VerticalSpace />
</>
)}
<SignArea
buttonText={buttonText}
@@ -167,32 +126,13 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
isExternal={isExternal}
/>
</div>
{isExternal && !isHardware && accountGenesisHash
? (
<Qr
address={address}
cmd={CMD_SIGN_MESSAGE}
genesisHash={accountGenesisHash}
onSignature={_onSignature}
payload={data}
/>
)
: (
<Bytes
bytes={data}
url={url}
/>
)
}
<Bytes
bytes={data}
url={url}
/>
<VerticalSpace />
{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>
<VerticalSpace />
</>
)}
{isHardware && <>
<Warning>{t('Message signing is not supported for hardware wallets.')}</Warning>
{isExternal && <>
<Warning>{t('This account is tracked only — the extension holds no key for it and cannot sign.')}</Warning>
<VerticalSpace />
</>}
<SignArea
@@ -21,7 +21,6 @@ import Request from './Request/index.js';
import Extrinsic from './Extrinsic.js';
import Signing from './index.js';
import { westendMetadata } from './metadataMock.js';
import Qr from './Qr.js';
import TransactionIndex from './TransactionIndex.js';
const { configure, mount } = enzyme;
@@ -211,7 +210,13 @@ describe('Signing requests', () => {
});
describe('External account', () => {
it('shows Qr scanner for external accounts', async () => {
// Was "shows Qr scanner for external accounts". That flow round-tripped a
// signature through a Parity Signer QR, a protocol no Quantus device speaks
// (quantus/extension#10). An external account here is now a tracked address:
// the extension holds no key for it and there is no signer to hand off to, so
// the right behaviour is to show the call and say so rather than display a
// QR nothing can scan.
it('shows the extrinsic and a cannot-sign warning for external accounts', async () => {
signRequests = [{
account: {
address: '5Cf1CGZas62RWwce3d2EPqUvSoi1txaXKd9M5w9bEFSsQtRe',
@@ -251,8 +256,8 @@ describe('Signing requests', () => {
url: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fwestend-rpc.polkadot.io#/accounts'
}];
await mountComponent();
expect(wrapper.find(Extrinsic)).toHaveLength(0);
expect(wrapper.find(Qr)).toHaveLength(1);
expect(wrapper.find(Extrinsic)).toHaveLength(1);
expect(wrapper.text()).toContain('tracked only');
});
});
@@ -0,0 +1,135 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useContext, useState } from 'react';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import { ActionContext, Address, ButtonArea, NextStepButton, VerticalSpace, Warning } from '../components/index.js';
import { useTranslation } from '../hooks/index.js';
import { createAccountExternal } from '../messaging.js';
import { Header, Name } from '../partials/index.js';
import { styled } from '../styled.js';
interface Props {
className?: string;
}
// Replaces ImportQr as the way to add an account the extension holds no key for.
//
// That flow scanned a Parity Signer QR, a protocol no Quantus device speaks
// (quantus/extension#10) — but it was also the *only* caller of
// createAccountExternal, so removing it would have taken watch-only accounts with
// it. Those are wanted independently of how the address arrives, and for Quantus
// pasting is the common case anyway: the address is on screen in another wallet,
// not on a signing device.
function TrackAddress ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [address, setAddress] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [name, setName] = useState<string | null>(null);
const _onChangeAddress = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
const value = event.target.value;
const trimmed = value.trim();
if (!trimmed) {
setAddress(null);
setError(null);
return;
}
try {
// Round-tripping through decode/encode does two jobs: it rejects a
// mistyped address by its checksum rather than storing something that
// will never receive anything, and it normalises the prefix so an
// address pasted from a tool using a different one is stored the way the
// rest of the extension will display it.
setAddress(encodeAddress(decodeAddress(trimmed)));
setError(null);
} catch {
setAddress(null);
setError(t('This is not a valid address'));
}
},
[t]
);
const _onCreate = useCallback(
(): void => {
if (address && name) {
// genesisHash null — "any network". A tracked address is just an
// address; nothing here knows which chain the holder uses it on.
createAccountExternal(name, address, null)
.then(() => onAction('/'))
.catch((error: Error) => {
console.error(error);
setError((error).message);
});
}
},
[address, name, onAction]
);
return (
<>
<Header
showBackArrow
text={t('Track an address')}
/>
<div className={className}>
<Address
address={address}
isExternal={true}
name={name}
/>
<textarea
autoFocus
className='address'
onChange={_onChangeAddress}
placeholder={t('Paste the address you want to track')}
rows={3}
/>
{error && (
<Warning isDanger>{error}</Warning>
)}
<Name
onChange={setName}
value={name || ''}
/>
<Warning>
{t('This account has no key in the extension, so it cannot sign. You will see its balance and can use it as a recipient.')}
</Warning>
</div>
<VerticalSpace />
<ButtonArea>
<NextStepButton
isDisabled={!address || !name}
onClick={_onCreate}
>
{t('Add the address')}
</NextStepButton>
</ButtonArea>
</>
);
}
export default React.memo(styled(TrackAddress)<Props>`
.address {
background: var(--readonlyInputBackground);
border-color: var(--inputBorderColor);
border-radius: var(--borderRadius);
border-style: solid;
border-width: 1px;
color: var(--textColor);
font-family: inherit;
font-size: var(--inputLabelFontSize);
margin-top: 16px;
padding: 12px;
resize: none;
width: 100%;
}
`);
+2 -4
View File
@@ -29,10 +29,9 @@ import AssetHubMigration from './AssetHubMigration.js';
import Export from './Export.js';
import ExportAll from './ExportAll.js';
import Forget from './Forget.js';
import ImportLedger from './ImportLedger.js';
import ImportQr from './ImportQr.js';
import PhishingDetected from './PhishingDetected.js';
import RestoreJson from './RestoreJson.js';
import TrackAddress from './TrackAddress.js';
import Welcome from './Welcome.js';
const startSettings = settings.get();
@@ -161,8 +160,7 @@ export default function Popup (): React.ReactElement {
<Route path='/account/forget/:address'>{wrapWithErrorBoundary(<Forget />, 'forget-address')}</Route>
<Route path='/account/export/:address'>{wrapWithErrorBoundary(<Export />, 'export-address')}</Route>
<Route path='/account/export-all'>{wrapWithErrorBoundary(<ExportAll />, 'export-all-address')}</Route>
<Route path='/account/import-ledger'>{wrapWithErrorBoundary(<ImportLedger />, 'import-ledger')}</Route>
<Route path='/account/import-qr'>{wrapWithErrorBoundary(<ImportQr />, 'import-qr')}</Route>
<Route path='/account/track-address'>{wrapWithErrorBoundary(<TrackAddress />, 'track-address')}</Route>
<Route path='/account/import-seed'>{wrapWithErrorBoundary(<ImportSeed />, 'import-seed')}</Route>
<Route path='/account/restore-json'>{wrapWithErrorBoundary(<RestoreJson />, 'restore-json')}</Route>
<Route path='/account/derive/:address/locked'>{wrapWithErrorBoundary(<Derive isLocked />, 'derived-address-locked')}</Route>
-1
View File
@@ -4,7 +4,6 @@
export { default as useGenesisHashOptions } from './useGenesisHashOptions.js';
export { default as useIsMounted } from './useIsMounted.js';
export { default as useIsPopup } from './useIsPopup.js';
export { default as useLedger } from './useLedger.js';
export { default as useMetadata } from './useMetadata.js';
export { default as useOutsideClick } from './useOutsideClick.js';
export { default as useToast } from './useToast.js';
@@ -1,287 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
// This is to ensure the legacy `class Ledger` doesn't throw linting errors.
//
/* eslint-disable deprecation/deprecation */
import type { Network } from '@polkadot/networks/types';
import type { HexString } from '@polkadot/util/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Ledger, LedgerGeneric } from '@polkadot/hw-ledger';
import { knownLedger } from '@polkadot/networks/defaults';
import { settings } from '@polkadot/ui-settings';
import { assert } from '@polkadot/util';
import { SettingsContext } from '../components/contexts.js';
import chains from '../util/chains.js';
import ledgerChains from '../util/legerChains.js';
import useTranslation from './useTranslation.js';
interface StateBase {
isLedgerCapable: boolean;
isLedgerEnabled: boolean;
}
interface State extends StateBase {
address: string | null;
error: string | null;
isLoading: boolean;
isLocked: boolean;
ledger: LedgerGeneric | Ledger | null;
refresh: () => void;
type: KeypairType | null;
warning: string | null;
}
function getNetwork (genesisHash: string): Network | undefined {
return ledgerChains.find(({ genesisHash: [hash] }) => hash === genesisHash);
}
function getTransportType (): 'hid' | 'webusb' {
// prefer WebHID over WebUSB.
return ('hid' in navigator) ? 'hid' : 'webusb';
}
function getState (): StateBase {
const w = window as unknown as { USB?: unknown; hid?: unknown };
const isLedgerCapable = !!(w.hid || w.USB);
return {
isLedgerCapable,
isLedgerEnabled: isLedgerCapable && settings.ledgerConn !== 'none'
};
}
function retrieveLedger (genesis: string | null, ledgerApp: string): LedgerGeneric | Ledger {
const { isLedgerCapable } = getState();
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);
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');
if (ledgerApp === 'migration') {
return new LedgerGeneric(transport, def.network, knownLedger[def.network]);
}
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 {
const [isLoading, setIsLoading] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [refreshCount, setRefreshCount] = useState(0);
const [warning, setWarning] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [address, setAddress] = useState<string | null>(null);
const [type, setType] = useState<KeypairType | null>(null);
const { t } = useTranslation();
const { ledgerApp } = useContext(SettingsContext);
const tRef = useRef(t);
// 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);
// 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);
const { network } = getNetwork(genesis) || { network: ledgerApp === 'generic' ? 'Polkadot' : 'unknown network' };
const warningMessage = e.message.includes('Code: 26628')
? tRef.current('Is your ledger locked?')
: null;
const errorMessage = e.message.includes('App does not seem to be open')
? tRef.current('App "{{network}}" does not seem to be open', { replace: { network } })
: e.message;
setIsLocked(true);
setWarning(warningMessage);
setError(tRef.current(
'Ledger error: {{errorMessage}}',
{ replace: { errorMessage } }
));
console.error(e);
setAddress(null);
setType(null);
}, []);
const { ledger, ledgerInitError } = useMemo(() => {
// undefined means no ledger connection attempt.
// null means connect using the generic app without a specific chain genesis.
// Prevents hook instances that only check isLedgerCapable or isLedgerEnabled
// from attempting a connection when ledgerApp === 'generic'.
if (genesis === undefined) {
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]);
useEffect(() => {
setError(ledgerInitError);
}, [ledgerInitError]);
useEffect(() => {
let isStale = false;
if (!ledger || (ledgerApp !== 'generic' && !genesis)) {
if (prevLedgerRef.current) {
prevLedgerRef.current.disconnect().catch(console.error);
prevLedgerRef.current = null;
}
setIsLoading(false);
setIsLocked(false);
setWarning(null);
setAddress(null);
setType(null);
return () => {
isStale = true;
};
}
const runIfCurrent = (fn: () => void): void => {
if (!isStale) {
fn();
}
};
const onAddressError = (e: Error): void => {
if (!isStale) {
handleGetAddressError(e, genesis ?? '', ledgerApp);
}
};
setIsLoading(true);
setError(null);
setWarning(null);
const prevLedger = prevLedgerRef.current;
prevLedgerRef.current = ledger;
const fetchLedgerAddress = () => {
if (isStale) {
return;
}
const chosenNetwork = chains.find(({ genesisHash }) => genesisHash === genesis as HexString);
// Use the chain's SS58 prefix when known; fall back to 42 (substrate default).
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 (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;
};
}, [accountIndex, addressOffset, genesis, handleGetAddressError, 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(() => {
if (ledger) {
// Prevent the address-fetch effect from disconnecting this instance again.
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 });
}
-4
View File
@@ -143,10 +143,6 @@ export async function createAccountExternal (name: string, address: string, gene
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 | null, type: KeypairType): Promise<boolean> {
return sendMessage('pri(accounts.create.hardware)', { accountIndex, address, addressOffset, genesisHash, hardwareType, name, type });
}
export async function createAccountSuri (name: string, password: string, suri: string, type?: KeypairType, genesisHash?: HexString | null): Promise<boolean> {
return sendMessage('pri(accounts.create.suri)', { genesisHash, name, password, suri, type });
}
+6 -53
View File
@@ -1,13 +1,12 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { faUsb } from '@fortawesome/free-brands-svg-icons';
import { faCodeBranch, faFileExport, faFileUpload, faKey, faPlusCircle, faQrcode } from '@fortawesome/free-solid-svg-icons';
import { faCodeBranch, faEye, faFileExport, faFileUpload, faKey, faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useContext } from 'react';
import { AccountContext, Link, MediaContext, Menu, MenuDivider, MenuItem } from '../components/index.js';
import { useIsPopup, useLedger, useTranslation } from '../hooks/index.js';
import { AccountContext, Link, Menu, MenuDivider, MenuItem } from '../components/index.js';
import { useIsPopup, useTranslation } from '../hooks/index.js';
import { windowOpen } from '../messaging.js';
import { styled } from '../styled.js';
@@ -17,13 +16,10 @@ interface Props {
}
const jsonPath = '/account/restore-json';
const ledgerPath = '/account/import-ledger';
function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { master } = useContext(AccountContext);
const mediaAllowed = useContext(MediaContext);
const { isLedgerCapable, isLedgerEnabled } = useLedger();
const isPopup = useIsPopup();
const _openJson = useCallback(
@@ -32,12 +28,6 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
}, []
);
const _onOpenLedgerConnect = useCallback(
(): void => {
windowOpen(ledgerPath).catch(console.error);
}, []
);
return (
<Menu
className={className}
@@ -84,48 +74,11 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
</MenuItem>
<MenuDivider />
<MenuItem className='menuItem'>
<Link
isDisabled={!mediaAllowed}
title={!mediaAllowed
? t('Camera access must be first enabled in the settings')
: ''
}
to='/account/import-qr'
>
<FontAwesomeIcon icon={faQrcode} />
<span>{t('Attach external QR-signer account')}</span>
<Link to='/account/track-address'>
<FontAwesomeIcon icon={faEye} />
<span>{t('Track an address')}</span>
</Link>
</MenuItem>
<MenuItem className='menuItem ledger'>
{isLedgerEnabled
? (
<Link
isDisabled={!isLedgerCapable}
onClick={isPopup ? _onOpenLedgerConnect : undefined}
title={ (!isLedgerCapable && t('Ledger devices can only be connected with Chrome browser')) || ''}
to={isPopup ? undefined : ledgerPath}
>
<FontAwesomeIcon
icon={faUsb}
rotation={270}
/>
<span>{ t('Attach ledger account')}</span>
</Link>
)
: (
<Link
onClick={isPopup ? _onOpenLedgerConnect : undefined}
to={isPopup ? undefined : ledgerPath}
>
<FontAwesomeIcon
icon={faUsb}
rotation={270}
/>
<span>{ t('Connect Ledger device')}</span>
</Link>
)
}
</MenuItem>
</Menu>
);
}
@@ -39,7 +39,6 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement<Prop
const isPopup = useIsPopup();
const languageOptions = useMemo(() => getLanguageOptions(), []);
const onAction = useContext(ActionContext);
const _ledgerAppOptions = settings.availableLedgerApp;
useEffect(() => {
settings.set({ camera: camera ? 'on' : 'off' });
@@ -83,12 +82,6 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement<Prop
}, []
);
const _onChangeLedgerApp = useCallback(
(value: string): void => {
settings.set({ ledgerApp: value });
}, []
);
const _goToAuthList = useCallback(
() => {
onAction('auth-list');
@@ -123,18 +116,6 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement<Prop
value={`${prefix}`}
/>
</MenuItem>
<MenuItem
className='setting'
title={t('Ledger App')}
>
<Dropdown
className='dropdown'
label=''
onChange={_onChangeLedgerApp}
options={_ledgerAppOptions}
value={settings.ledgerApp}
/>
</MenuItem>
<MenuItem
className='setting'
title={t('Language')}
@@ -222,9 +203,6 @@ export default React.memo(styled(MenuSettings)<Props>`
font-size: 15px;
margin-bottom: 0;
&.ledger {
margin-top: 0.2rem;
}
label {
color: var(--textColor);
@@ -1,6 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { selectableNetworks } from '@polkadot/networks';
export default selectableNetworks.filter((network) => network.hasLedgerSupport);
@@ -199,5 +199,12 @@
"I Understand": "",
"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.": ""
"Possible cause: the Ledger App setting differs from the app originally used to derive this account.": "",
"This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.": "",
"Track an address": "",
"This account is tracked only — the extension holds no key for it and cannot sign.": "",
"This is not a valid address": "",
"Paste the address you want to track": "",
"This account has no key in the extension, so it cannot sign. You will see its balance and can use it as a recipient.": "",
"Add the address": ""
}
+4 -366
View File
@@ -408,108 +408,6 @@ __metadata:
languageName: node
linkType: hard
"@ledgerhq/devices@npm:8.4.7, @ledgerhq/devices@npm:^8.4.4":
version: 8.4.7
resolution: "@ledgerhq/devices@npm:8.4.7"
dependencies:
"@ledgerhq/errors": "npm:^6.22.0"
"@ledgerhq/logs": "npm:^6.13.0"
rxjs: "npm:^7.8.1"
semver: "npm:^7.3.5"
checksum: 10/1d49aa692831915f1a540cba2d7faba3a08e5a2d1db831e3849c7791526d6ebb85fab679585f6940d5a923abf91f2113d675445172e37b38d742c3f18952e0fc
languageName: node
linkType: hard
"@ledgerhq/errors@npm:^6.19.1, @ledgerhq/errors@npm:^6.22.0":
version: 6.22.0
resolution: "@ledgerhq/errors@npm:6.22.0"
checksum: 10/6bea0c5fbb07c4f20fbc5083856646708c195c6d138dcbeed1f08f88c97536fd5059959a724332c45f17b8944aa8d8e68d850f43b33bf5b2f30d901c593e2930
languageName: node
linkType: hard
"@ledgerhq/hw-transport-node-hid-noevents@npm:^6.30.8":
version: 6.30.8
resolution: "@ledgerhq/hw-transport-node-hid-noevents@npm:6.30.8"
dependencies:
"@ledgerhq/devices": "npm:8.4.7"
"@ledgerhq/errors": "npm:^6.22.0"
"@ledgerhq/hw-transport": "npm:^6.31.7"
"@ledgerhq/logs": "npm:^6.13.0"
node-hid: "npm:2.1.2"
checksum: 10/b6e5795636431d06befd64cded66f300b8c81bfa8d590d56f728662e77074cb26898903be6371c1f3a720473173f6c5d223778b2ac8ad84e7b450ed64168f901
languageName: node
linkType: hard
"@ledgerhq/hw-transport-node-hid-singleton@npm:^6.31.5":
version: 6.31.8
resolution: "@ledgerhq/hw-transport-node-hid-singleton@npm:6.31.8"
dependencies:
"@ledgerhq/devices": "npm:8.4.7"
"@ledgerhq/errors": "npm:^6.22.0"
"@ledgerhq/hw-transport": "npm:^6.31.7"
"@ledgerhq/hw-transport-node-hid-noevents": "npm:^6.30.8"
"@ledgerhq/logs": "npm:^6.13.0"
node-hid: "npm:2.1.2"
usb: "npm:2.9.0"
checksum: 10/d79272bcb79d863f9b7934a4175e4564faea71257ec419675942e964f95ad9ae159632594f63f86e55c68572c7377848de9f3f4ecf684134beddf182f1a462cb
languageName: node
linkType: hard
"@ledgerhq/hw-transport-webhid@npm:^6.29.4":
version: 6.30.3
resolution: "@ledgerhq/hw-transport-webhid@npm:6.30.3"
dependencies:
"@ledgerhq/devices": "npm:8.4.7"
"@ledgerhq/errors": "npm:^6.22.0"
"@ledgerhq/hw-transport": "npm:^6.31.7"
"@ledgerhq/logs": "npm:^6.13.0"
checksum: 10/4f9ea31952fd07e366fab0acd020cabdf2346e03aaa6971805aeec8820225ffac66a57aaf375137f599ba0e9fd42f7b3febd923dd3b420b70a421588054b2135
languageName: node
linkType: hard
"@ledgerhq/hw-transport-webusb@npm:^6.29.4":
version: 6.29.7
resolution: "@ledgerhq/hw-transport-webusb@npm:6.29.7"
dependencies:
"@ledgerhq/devices": "npm:8.4.7"
"@ledgerhq/errors": "npm:^6.22.0"
"@ledgerhq/hw-transport": "npm:^6.31.7"
"@ledgerhq/logs": "npm:^6.13.0"
checksum: 10/2beb65383939df07c0df18e2b8d97c41e1fffbba4af9bccf26639d3814c617f47293f5a16b268340b5e64fe1f45f4b5a65ecc4edba3c502984d2fa92bed25c2b
languageName: node
linkType: hard
"@ledgerhq/hw-transport@npm:6.31.4":
version: 6.31.4
resolution: "@ledgerhq/hw-transport@npm:6.31.4"
dependencies:
"@ledgerhq/devices": "npm:^8.4.4"
"@ledgerhq/errors": "npm:^6.19.1"
"@ledgerhq/logs": "npm:^6.12.0"
events: "npm:^3.3.0"
checksum: 10/cf101e5b818e95e59031241d556dbec24658f54104910e414be493bc4b90b0aea50f5d4b3339a237dd0b12845bb2683c845f3a82f2ea9da4e077b68d1e1f7e48
languageName: node
linkType: hard
"@ledgerhq/hw-transport@npm:^6.31.4, @ledgerhq/hw-transport@npm:^6.31.7":
version: 6.31.7
resolution: "@ledgerhq/hw-transport@npm:6.31.7"
dependencies:
"@ledgerhq/devices": "npm:8.4.7"
"@ledgerhq/errors": "npm:^6.22.0"
"@ledgerhq/logs": "npm:^6.13.0"
events: "npm:^3.3.0"
checksum: 10/a8c44980fb1d795f23fe62edc6a7b910efcb2dda6b626f6ac3a6b2efd4c855d6591e9cd6fa3d7d65c89eea2418ee4b960878cc6be0d73e7e2a958d3af378f44a
languageName: node
linkType: hard
"@ledgerhq/logs@npm:^6.12.0, @ledgerhq/logs@npm:^6.13.0":
version: 6.13.0
resolution: "@ledgerhq/logs@npm:6.13.0"
checksum: 10/3ac984bdc533505359cd1d8bc74175547fcfa51fef6294a9fd832e8ea88d462a385be5cfe68ccfa1b77cd68b840243e6f79c2be66fe28f123ae57c58625edf5f
languageName: node
linkType: hard
"@leichtgewicht/ip-codec@npm:^2.0.1":
version: 2.0.5
resolution: "@leichtgewicht/ip-codec@npm:2.0.5"
@@ -1184,7 +1082,6 @@ __metadata:
"@polkadot/extension-dapp": "npm:0.64.0"
"@polkadot/extension-inject": "npm:0.64.0"
"@polkadot/extension-mocks": "npm:0.64.0"
"@polkadot/hw-ledger": "npm:^14.0.3"
"@polkadot/keyring": "npm:^14.0.3"
"@polkadot/networks": "npm:^14.0.3"
"@polkadot/react-identicon": "npm:^3.16.7"
@@ -1253,35 +1150,6 @@ __metadata:
languageName: unknown
linkType: soft
"@polkadot/hw-ledger-transports@npm:14.0.3":
version: 14.0.3
resolution: "@polkadot/hw-ledger-transports@npm:14.0.3"
dependencies:
"@ledgerhq/hw-transport": "npm:^6.31.4"
"@ledgerhq/hw-transport-node-hid-singleton": "npm:^6.31.5"
"@ledgerhq/hw-transport-webhid": "npm:^6.29.4"
"@ledgerhq/hw-transport-webusb": "npm:^6.29.4"
"@polkadot/util": "npm:14.0.3"
tslib: "npm:^2.8.0"
dependenciesMeta:
"@ledgerhq/hw-transport-node-hid-singleton":
optional: true
checksum: 10/5cf48a41079e06ea4c46df4c4aaf2efcca786b19f9eecb0c7c0ba505a1f5a98f730518cff41e3b0fa27f09d6dc74329e2f2b982a2dd8f1c7e59db9d65b90eabe
languageName: node
linkType: hard
"@polkadot/hw-ledger@npm:^14.0.3":
version: 14.0.3
resolution: "@polkadot/hw-ledger@npm:14.0.3"
dependencies:
"@polkadot/hw-ledger-transports": "npm:14.0.3"
"@polkadot/util": "npm:14.0.3"
"@zondax/ledger-substrate": "npm:1.1.1"
tslib: "npm:^2.8.0"
checksum: 10/1f4746a3fd7f311211c451ba26ff1cd11bcfc729c27bc9516e619e5df2d57a9e79e94b163d2e00448864680c7afa0d20b933932134bafa709af091db1e6d9b10
languageName: node
linkType: hard
"@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.1/keyring-14.0.3-quantus.1.tgz":
version: 14.0.3-quantus.1
resolution: "@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.1/keyring-14.0.3-quantus.1.tgz"
@@ -2649,13 +2517,6 @@ __metadata:
languageName: node
linkType: hard
"@types/w3c-web-usb@npm:^1.0.6":
version: 1.0.10
resolution: "@types/w3c-web-usb@npm:1.0.10"
checksum: 10/6ac6786a0788f0846a48b103ab06ca5fde5eb95674217b522420a2f6157bee3e181a961c1b7011940f497c55f4f5cc46129657d881fdd8112b48764089679ad6
languageName: node
linkType: hard
"@types/ws@npm:8.5.3":
version: 8.5.3
resolution: "@types/ws@npm:8.5.3"
@@ -3267,26 +3128,6 @@ __metadata:
languageName: node
linkType: hard
"@zondax/ledger-js@npm:^1.2.0":
version: 1.2.0
resolution: "@zondax/ledger-js@npm:1.2.0"
dependencies:
"@ledgerhq/hw-transport": "npm:6.31.4"
checksum: 10/5852b340efd421f96d4e52d0a93d4e8464604c070939dd42fade5a495010a08d730b03e1b7ab3ff0f26f13075d8a0a5226a748a41986f2ed087fca4471f24e4e
languageName: node
linkType: hard
"@zondax/ledger-substrate@npm:1.1.1":
version: 1.1.1
resolution: "@zondax/ledger-substrate@npm:1.1.1"
dependencies:
"@ledgerhq/hw-transport": "npm:6.31.4"
"@zondax/ledger-js": "npm:^1.2.0"
axios: "npm:^1.8.4"
checksum: 10/039ffef55b31870f82c6b0f20b518089ad9ce0295d695f880640cb586256adacd4a326c10157ccaf82e897abde7358f9e4e02c069dac672ec90881e21d250722
languageName: node
linkType: hard
"@zxcvbn-ts/core@npm:^3.0.4":
version: 3.0.4
resolution: "@zxcvbn-ts/core@npm:3.0.4"
@@ -3909,17 +3750,6 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.8.4":
version: 1.10.0
resolution: "axios@npm:1.10.0"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: 10/d43c80316a45611fd395743e15d16ea69a95f2b7f7095f2bb12cb78f9ca0a905194a02e52a3bf4e0db9f85fd1186d6c690410644c10ecd8bb0a468e57c2040e4
languageName: node
linkType: hard
"b4a@npm:^1.6.4":
version: 1.6.7
resolution: "b4a@npm:1.6.7"
@@ -4602,16 +4432,7 @@ __metadata:
languageName: node
linkType: hard
"bindings@npm:^1.5.0":
version: 1.5.0
resolution: "bindings@npm:1.5.0"
dependencies:
file-uri-to-path: "npm:1.0.0"
checksum: 10/593d5ae975ffba15fbbb4788fe5abd1e125afbab849ab967ab43691d27d6483751805d98cb92f7ac24a2439a8a8678cd0131c535d5d63de84e383b0ce2786133
languageName: node
linkType: hard
"bl@npm:^4.0.3, bl@npm:^4.1.0":
"bl@npm:^4.1.0":
version: 4.1.0
resolution: "bl@npm:4.1.0"
dependencies:
@@ -5131,13 +4952,6 @@ __metadata:
languageName: node
linkType: hard
"chownr@npm:^1.1.1":
version: 1.1.4
resolution: "chownr@npm:1.1.4"
checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d
languageName: node
linkType: hard
"chownr@npm:^3.0.0":
version: 3.0.0
resolution: "chownr@npm:3.0.0"
@@ -5924,13 +5738,6 @@ __metadata:
languageName: node
linkType: hard
"detect-libc@npm:^2.0.0":
version: 2.0.4
resolution: "detect-libc@npm:2.0.4"
checksum: 10/136e995f8c5ffbc515955b0175d441b967defd3d5f2268e89fa695e9c7170d8bed17993e31a34b04f0fad33d844a3a598e0fd519a8e9be3cad5f67662d96fee0
languageName: node
linkType: hard
"detect-node@npm:^2.0.4":
version: 2.1.0
resolution: "detect-node@npm:2.1.0"
@@ -7192,7 +6999,7 @@ __metadata:
languageName: node
linkType: hard
"events@npm:^3.2.0, events@npm:^3.3.0":
"events@npm:^3.2.0":
version: 3.3.0
resolution: "events@npm:3.3.0"
checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be
@@ -7227,13 +7034,6 @@ __metadata:
languageName: node
linkType: hard
"expand-template@npm:^2.0.3":
version: 2.0.3
resolution: "expand-template@npm:2.0.3"
checksum: 10/588c19847216421ed92befb521767b7018dc88f88b0576df98cb242f20961425e96a92cbece525ef28cc5becceae5d544ae0f5b9b5e2aa05acb13716ca5b3099
languageName: node
linkType: hard
"exponential-backoff@npm:^3.1.1":
version: 3.1.2
resolution: "exponential-backoff@npm:3.1.2"
@@ -7454,13 +7254,6 @@ __metadata:
languageName: node
linkType: hard
"file-uri-to-path@npm:1.0.0":
version: 1.0.0
resolution: "file-uri-to-path@npm:1.0.0"
checksum: 10/b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144
languageName: node
linkType: hard
"filename-reserved-regex@npm:^2.0.0":
version: 2.0.0
resolution: "filename-reserved-regex@npm:2.0.0"
@@ -7591,7 +7384,7 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.6":
"follow-redirects@npm:^1.0.0":
version: 1.15.9
resolution: "follow-redirects@npm:1.15.9"
peerDependenciesMeta:
@@ -7656,13 +7449,6 @@ __metadata:
languageName: node
linkType: hard
"fs-constants@npm:^1.0.0":
version: 1.0.0
resolution: "fs-constants@npm:1.0.0"
checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d
languageName: node
linkType: hard
"fs-extra@npm:^11.1.1":
version: 11.3.0
resolution: "fs-extra@npm:11.3.0"
@@ -7942,13 +7728,6 @@ __metadata:
languageName: node
linkType: hard
"github-from-package@npm:0.0.0":
version: 0.0.0
resolution: "github-from-package@npm:0.0.0"
checksum: 10/2a091ba07fbce22205642543b4ea8aaf068397e1433c00ae0f9de36a3607baf5bcc14da97fbb798cfca6393b3c402031fca06d8b491a44206d6efef391c58537
languageName: node
linkType: hard
"github-url-to-object@npm:^4.0.4":
version: 4.0.6
resolution: "github-url-to-object@npm:4.0.6"
@@ -10108,7 +9887,7 @@ __metadata:
languageName: node
linkType: hard
"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6":
"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6":
version: 1.2.8
resolution: "minimist@npm:1.2.8"
checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f
@@ -10191,13 +9970,6 @@ __metadata:
languageName: node
linkType: hard
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
version: 0.5.3
resolution: "mkdirp-classic@npm:0.5.3"
checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac
languageName: node
linkType: hard
"mkdirp@npm:^0.5.1":
version: 0.5.6
resolution: "mkdirp@npm:0.5.6"
@@ -10313,13 +10085,6 @@ __metadata:
languageName: node
linkType: hard
"napi-build-utils@npm:^2.0.0":
version: 2.0.0
resolution: "napi-build-utils@npm:2.0.0"
checksum: 10/69adcdb828481737f1ec64440286013f6479d5b264e24d5439ba795f65293d0bb6d962035de07c65fae525ed7d2fcd0baab6891d8e3734ea792fec43918acf83
languageName: node
linkType: hard
"napi-postinstall@npm:^0.3.0":
version: 0.3.0
resolution: "napi-postinstall@npm:0.3.0"
@@ -10422,33 +10187,6 @@ __metadata:
languageName: node
linkType: hard
"node-abi@npm:^3.3.0":
version: 3.75.0
resolution: "node-abi@npm:3.75.0"
dependencies:
semver: "npm:^7.3.5"
checksum: 10/dd87627fa4f447bda9c69dc1ec0da82e3b466790844b5bd7f7787db093dfea21dcc405588e13b5207c266472c1fda9b95060da8d70644aeb5fc31ec81dc2007c
languageName: node
linkType: hard
"node-addon-api@npm:^3.0.2":
version: 3.2.1
resolution: "node-addon-api@npm:3.2.1"
dependencies:
node-gyp: "npm:latest"
checksum: 10/681b52dfa3e15b0a8e5cf283cc0d8cd5fd2a57c559ae670fcfd20544cbb32f75de7648674110defcd17ab2c76ebef630aa7d2d2f930bc7a8cc439b20fe233518
languageName: node
linkType: hard
"node-addon-api@npm:^6.0.0":
version: 6.1.0
resolution: "node-addon-api@npm:6.1.0"
dependencies:
node-gyp: "npm:latest"
checksum: 10/8eea1d4d965930a177a0508695beb0d89b4c1d80bf330646a035357a1e8fc31e0d09686e2374996e96e757b947a7ece319f98ede3146683f162597c0bcb4df90
languageName: node
linkType: hard
"node-domexception@npm:^1.0.0":
version: 1.0.0
resolution: "node-domexception@npm:1.0.0"
@@ -10488,17 +10226,6 @@ __metadata:
languageName: node
linkType: hard
"node-gyp-build@npm:^4.5.0":
version: 4.8.4
resolution: "node-gyp-build@npm:4.8.4"
bin:
node-gyp-build: bin.js
node-gyp-build-optional: optional.js
node-gyp-build-test: build-test.js
checksum: 10/6a7d62289d1afc419fc8fc9bd00aa4e554369e50ca0acbc215cb91446148b75ff7e2a3b53c2c5b2c09a39d416d69f3d3237937860373104b5fe429bf30ad9ac5
languageName: node
linkType: hard
"node-gyp@npm:latest":
version: 11.2.0
resolution: "node-gyp@npm:11.2.0"
@@ -10519,20 +10246,6 @@ __metadata:
languageName: node
linkType: hard
"node-hid@npm:2.1.2":
version: 2.1.2
resolution: "node-hid@npm:2.1.2"
dependencies:
bindings: "npm:^1.5.0"
node-addon-api: "npm:^3.0.2"
node-gyp: "npm:latest"
prebuild-install: "npm:^7.1.1"
bin:
hid-showdevices: src/show-devices.js
checksum: 10/7b424d9f049221b439cfa3a092f415b608bcd316b819ff1fb0bf5234d766348d9fdaeaafa5ccece514c045825a936cb5617080168066d16b3aaeb0e9808e38f9
languageName: node
linkType: hard
"node-releases@npm:^2.0.19":
version: 2.0.19
resolution: "node-releases@npm:2.0.19"
@@ -11223,28 +10936,6 @@ __metadata:
languageName: node
linkType: hard
"prebuild-install@npm:^7.1.1":
version: 7.1.3
resolution: "prebuild-install@npm:7.1.3"
dependencies:
detect-libc: "npm:^2.0.0"
expand-template: "npm:^2.0.3"
github-from-package: "npm:0.0.0"
minimist: "npm:^1.2.3"
mkdirp-classic: "npm:^0.5.3"
napi-build-utils: "npm:^2.0.0"
node-abi: "npm:^3.3.0"
pump: "npm:^3.0.0"
rc: "npm:^1.2.7"
simple-get: "npm:^4.0.0"
tar-fs: "npm:^2.0.0"
tunnel-agent: "npm:^0.6.0"
bin:
prebuild-install: bin.js
checksum: 10/1b7e4c00d2750b532a4fc2a83ffb0c5fefa1b6f2ad071896ead15eeadc3255f5babd816949991af083cf7429e375ae8c7d1c51f73658559da36f948a020a3a11
languageName: node
linkType: hard
"precinct@npm:^8.1.0":
version: 8.3.1
resolution: "precinct@npm:8.3.1"
@@ -11403,13 +11094,6 @@ __metadata:
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: 10/f0bb4a87cfd18f77bc2fba23ae49c3b378fb35143af16cc478171c623eebe181678f09439707ad80081d340d1593cd54a33a0113f3ccb3f4bc9451488780ee23
languageName: node
linkType: hard
"psl@npm:^1.1.33":
version: 1.15.0
resolution: "psl@npm:1.15.0"
@@ -13361,31 +13045,6 @@ __metadata:
languageName: node
linkType: hard
"tar-fs@npm:^2.0.0":
version: 2.1.3
resolution: "tar-fs@npm:2.1.3"
dependencies:
chownr: "npm:^1.1.1"
mkdirp-classic: "npm:^0.5.2"
pump: "npm:^3.0.0"
tar-stream: "npm:^2.1.4"
checksum: 10/37fdfd3aa73f4f49c0821ef75f67647ecafd5370d2e311d9ace6ff3825ff4355014055c3d43407c6a655adf6c5bfb0cbcf93412161dad5af7110eb7d7a0c2eae
languageName: node
linkType: hard
"tar-stream@npm:^2.1.4":
version: 2.2.0
resolution: "tar-stream@npm:2.2.0"
dependencies:
bl: "npm:^4.0.3"
end-of-stream: "npm:^1.4.1"
fs-constants: "npm:^1.0.0"
inherits: "npm:^2.0.3"
readable-stream: "npm:^3.1.1"
checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a
languageName: node
linkType: hard
"tar@npm:^7.4.3":
version: 7.4.3
resolution: "tar@npm:7.4.3"
@@ -13729,15 +13388,6 @@ __metadata:
languageName: node
linkType: hard
"tunnel-agent@npm:^0.6.0":
version: 0.6.0
resolution: "tunnel-agent@npm:0.6.0"
dependencies:
safe-buffer: "npm:^5.0.1"
checksum: 10/7f0d9ed5c22404072b2ae8edc45c071772affd2ed14a74f03b4e71b4dd1a14c3714d85aed64abcaaee5fec2efc79002ba81155c708f4df65821b444abb0cfade
languageName: node
linkType: hard
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
version: 0.4.0
resolution: "type-check@npm:0.4.0"
@@ -14117,18 +13767,6 @@ __metadata:
languageName: node
linkType: hard
"usb@npm:2.9.0":
version: 2.9.0
resolution: "usb@npm:2.9.0"
dependencies:
"@types/w3c-web-usb": "npm:^1.0.6"
node-addon-api: "npm:^6.0.0"
node-gyp: "npm:latest"
node-gyp-build: "npm:^4.5.0"
checksum: 10/ab68d4247a24a463328e293256aec5682c12163278ab474b7243a9dba6283a44bb74f9e0ca96f1d00197bb271f294549c3a4debdbd3452a02750a4a65ff29ee6
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"