Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dfd109991
|
||
|
|
87a0b3926c
|
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
`);
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user