feat: post-quantum only; no derivation, no MetaMask shim, no Ethereum addresses
Some checks failed
Lock Threads / lock (push) Has been cancelled

Moves to the quantus.3 forks of keyring, util-crypto, networks and ui-keyring,
which hold only ML-DSA keys and have deleted the classical primitives
(quantus/common#6). What in the extension depended on them goes too:

- the derive-from-parent flow (Popup/Derive, pri(derivation.*), the
  AccountsContext master, the menu entries): ML-DSA pairs have no //hard/soft
  derivation, and Quantus derives every account from the mnemonic instead
- extension-compat-metamask, which injected Ethereum accounts
- the Ethereum branches in Address and the //m/44'/60' suri suffix
- canDerive and nextDerivationPath, now unused

Restoring a JSON backup of a classical key shows the keyring's reason ("ed25519
keys are not quantum-safe and cannot be held here...") rather than "Invalid
Json file", for single and batch files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
2026-09-16 21:28:01 +03:00
parent 851500a5fb
commit 29033e8bb8
34 changed files with 102 additions and 1393 deletions

View File

@@ -53,13 +53,13 @@
},
"resolutions": {
"@polkadot/api": "^16.5.6",
"@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz",
"@polkadot/networks": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz",
"@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.3/keyring-14.0.3-quantus.3.tgz",
"@polkadot/networks": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.3/networks-14.0.3-quantus.3.tgz",
"@polkadot/rpc-provider": "^16.5.6",
"@polkadot/types": "^16.5.6",
"@polkadot/ui-keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz",
"@polkadot/ui-keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.3/ui-keyring-3.16.7-quantus.3.tgz",
"@polkadot/util": "^14.0.3",
"@polkadot/util-crypto": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz",
"@polkadot/util-crypto": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.3/util-crypto-14.0.3-quantus.3.tgz",
"@polkadot/x-fetch": "^14.0.3",
"@quantus/crypto": "^0.3.0",
"safe-buffer": "^5.2.1",

View File

@@ -149,81 +149,11 @@ describe('Extension', () => {
});
});
describe('account derivation', () => {
let address: string;
// An ed25519 parent, explicitly — which is what upstream's harness got by
// default, hence the vectors below. Naming it matters now: the default here
// is ML-DSA, as in production, and derivation is a property of the key type.
// Quantus derives every account from the mnemonic independently along
// m/44'/189189'/<account>'/0'/<scheme>', so an ML-DSA pair holds nothing a
// child could come from. See quantus/common#4 and `canDerive`.
beforeEach(async () => {
address = await createAccount('ed25519');
});
it('refuses to derive from an ML-DSA parent', async () => {
const mldsa = await createAccount();
await expect(extension.handle('id', 'pri(derivation.validate)', {
parentAddress: mldsa,
parentPassword: password,
suri: '//path'
}, {} as chrome.runtime.Port)).rejects.toThrow();
});
it('pri(derivation.validate) passes for valid suri', async () => {
const result = await extension.handle('id', 'pri(derivation.validate)', {
parentAddress: address,
parentPassword: password,
suri: '//path'
}, {} as chrome.runtime.Port);
expect(result).toEqual({
address: '5FP3TT3EruYBNh8YM8yoxsreMx7uZv1J1zNX7fFhoC5enwmN',
suri: '//path'
});
});
it('pri(derivation.validate) throws for invalid suri', async () => {
await expect(extension.handle('id', 'pri(derivation.validate)', {
parentAddress: address,
parentPassword: password,
suri: 'invalid-path'
}, {} as chrome.runtime.Port)).rejects.toThrow(/is not a valid derivation path/);
});
it('pri(derivation.validate) throws for invalid password', async () => {
await expect(extension.handle('id', 'pri(derivation.validate)', {
parentAddress: address,
parentPassword: 'invalid-password',
suri: '//path'
}, {} as chrome.runtime.Port)).rejects.toThrow(/invalid password/);
});
it('pri(derivation.create) adds a derived account', async () => {
const before = keyring.getAccounts().length;
await extension.handle('id', 'pri(derivation.create)', {
name: 'child',
parentAddress: address,
parentPassword: password,
password,
suri: '//path'
}, {} as chrome.runtime.Port);
expect(keyring.getAccounts()).toHaveLength(before + 1);
});
it('pri(derivation.create) saves parent address in meta', async () => {
await extension.handle('id', 'pri(derivation.create)', {
name: 'child',
parentAddress: address,
parentPassword: password,
password,
suri: '//path'
}, {} as chrome.runtime.Port);
expect(keyring.getAccount('5FP3TT3EruYBNh8YM8yoxsreMx7uZv1J1zNX7fFhoC5enwmN')?.meta.parentAddress).toEqual(address);
});
// Upstream derived child accounts from a parent pair along a //hard/soft
// path. ML-DSA keys have no such derivation; Quantus derives every account
// from the mnemonic instead, so the messages are gone (quantus/common#6).
it('has no account derivation', async () => {
await expect(extension.handle('id', 'pri(derivation.validate)' as 'pri(ping)', {} as never, {} as chrome.runtime.Port)).rejects.toThrow(/Unable to handle message/);
});
describe('account management', () => {

View File

@@ -5,10 +5,9 @@
import type { Runtime } from '@quantus/codec';
import type { MetadataDef } from '@polkadot/extension-inject/types';
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types';
import type { KeyringPair, KeyringPair$Json } from '@polkadot/keyring/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, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestActiveTabsUrlUpdate, RequestAuthorizeApprove, RequestBalancesSubscribe, RequestBalancesUnsubscribe, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, RequestWalletAddAccount, RequestWalletCreate, RequestWalletForget, RequestWalletPreview, RequestWalletRename, RequestWormholeBalance, RequestWormholeUnlock, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, ResponseWalletPreview, SigningRequest, WalletInfo, WormholeBalance } from '../types.js';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestActiveTabsUrlUpdate, RequestAuthorizeApprove, RequestBalancesSubscribe, RequestBalancesUnsubscribe, RequestBatchRestore, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, RequestWalletAddAccount, RequestWalletCreate, RequestWalletForget, RequestWalletPreview, RequestWalletRename, RequestWormholeBalance, RequestWormholeUnlock, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, ResponseWalletPreview, SigningRequest, WalletInfo, WormholeBalance } from '../types.js';
import type { AuthorizedAccountsDiff } from './State.js';
import type State from './State.js';
@@ -29,13 +28,6 @@ type CachedUnlocks = Record<string, number>;
const SEED_DEFAULT_LENGTH = 12;
const SEED_LENGTHS = [12, 15, 18, 21, 24];
const ETH_DERIVE_DEFAULT = "/m/44'/60'/0'/0/0";
function getSuri (seed: string, type?: KeypairType): string {
return type === 'ethereum'
? `${seed}${ETH_DERIVE_DEFAULT}`
: seed;
}
export default class Extension {
readonly #balances = new Balances();
@@ -67,7 +59,7 @@ export default class Extension {
}
private accountsCreateSuri ({ genesisHash, name, password, suri, type }: RequestAccountCreateSuri): boolean {
keyring.addUri(getSuri(suri, type), password, { genesisHash, name }, type);
keyring.addUri(suri, password, { genesisHash, name }, type);
return true;
}
@@ -418,7 +410,7 @@ export default class Extension {
const seed = _seed || mnemonicGenerate(length);
return {
address: keyring.createFromUri(getSuri(seed, type), {}, type).address,
address: keyring.createFromUri(seed, {}, type).address,
seed
};
}
@@ -435,7 +427,7 @@ export default class Extension {
}
return {
address: keyring.createFromUri(getSuri(suri, type), {}, type).address,
address: keyring.createFromUri(suri, {}, type).address,
suri
};
}
@@ -586,44 +578,6 @@ export default class Extension {
return true;
}
private derive (parentAddress: string, suri: string, password: string, metadata: KeyringPair$Meta): KeyringPair {
const parentPair = keyring.getPair(parentAddress);
try {
parentPair.decodePkcs8(password);
} catch {
throw new Error('invalid password');
}
try {
return parentPair.derive(suri, metadata);
} catch {
throw new Error(`"${suri}" is not a valid derivation path`);
}
}
private derivationValidate ({ parentAddress, parentPassword, suri }: RequestDeriveValidate): ResponseDeriveValidate {
const childPair = this.derive(parentAddress, suri, parentPassword, {});
return {
address: childPair.address,
suri
};
}
private derivationCreate ({ genesisHash, name, parentAddress, parentPassword, password, suri }: RequestDeriveCreate): boolean {
const childPair = this.derive(parentAddress, suri, parentPassword, {
genesisHash,
name,
parentAddress,
suri
});
keyring.addPair(childPair, password);
return true;
}
private async removeAuthorization (url: string): Promise<ResponseAuthorizeList> {
const remAuth = await this.#state.removeAuthorization(url);
@@ -771,12 +725,6 @@ export default class Extension {
case 'pri(connectedTabsUrl.get)':
return this.getConnectedTabsUrl();
case 'pri(derivation.create)':
return this.derivationCreate(request as RequestDeriveCreate);
case 'pri(derivation.validate)':
return this.derivationValidate(request as RequestDeriveValidate);
case 'pri(json.restore)':
return this.jsonRestore(request as RequestJsonRestore);

View File

@@ -51,7 +51,6 @@ export type AccountWithChildren = AccountJson & {
export interface AccountsContext {
accounts: AccountJson[];
hierarchy: AccountWithChildren[];
master?: AccountJson;
selectedAccounts?: AccountJson['address'][];
setSelectedAccounts?: (address: AccountJson['address'][]) => void;
}
@@ -111,8 +110,6 @@ export interface RequestSignatures {
'pri(authorize.update)': [RequestUpdateAuthorizedAccounts, void];
'pri(activeTabsUrl.update)': [RequestActiveTabsUrlUpdate, void];
'pri(connectedTabsUrl.get)': [null, ConnectedTabsUrlResponse];
'pri(derivation.create)': [RequestDeriveCreate, boolean];
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
'pri(json.restore)': [RequestJsonRestore, void];
'pri(json.batchRestore)': [RequestBatchRestore, void];
'pri(json.account.info)': [KeyringPair$Json, ResponseJsonGetAccountInfo];
@@ -345,21 +342,6 @@ export interface RequestAccountValidate {
password: string;
}
export interface RequestDeriveCreate {
name: string;
genesisHash?: HexString | null;
suri: string;
parentAddress: string;
parentPassword: string;
password: string;
}
export interface RequestDeriveValidate {
suri: string;
parentAddress: string;
parentPassword: string;
}
export interface RequestAccountExport {
address: string;
password: string;
@@ -499,11 +481,6 @@ export interface ResponseSigning {
signedTransaction?: HexString;
}
export interface ResponseDeriveValidate {
address: string;
suri: string;
}
export interface ResponseSeedCreate {
address: string;
seed: string;

View File

@@ -4,10 +4,7 @@
import type { KeypairType } from '@polkadot/util-crypto/types';
/** Every keypair type this extension can hold and sign with. */
const SIGNABLE: KeypairType[] = ['dilithium65', 'dilithium87', 'ecdsa', 'ed25519', 'ethereum', 'sr25519'];
/** The types that support deriving a child account from a parent's key. */
const DERIVABLE: KeypairType[] = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
const SIGNABLE: KeypairType[] = ['dilithium65', 'dilithium87'];
/**
* Whether an account of this type should be offered to dapps.
@@ -22,17 +19,3 @@ const DERIVABLE: KeypairType[] = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
export function canInject (type?: KeypairType): boolean {
return !!type && SIGNABLE.includes(type);
}
/**
* Whether a child account can be derived from a parent of this type.
*
* False for ML-DSA, and permanently so. Quantus derives every account from the
* mnemonic independently along `m/44'/189189'/<account>'/0'/<scheme>'`, and a
* pair holds no material a child could come from — `pair.derive()` refuses
* outright (quantus/common#4). Adding another account therefore needs the
* recovery phrase, not an unlocked parent, which is why the derive-from-parent
* flow has no Quantus meaning.
*/
export function canDerive (type?: KeypairType): boolean {
return !!type && DERIVABLE.includes(type);
}

View File

@@ -1,5 +1,5 @@
// Copyright 2019-2026 @polkadot/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { canDerive, canInject } from './canDerive.js';
export { canInject } from './canDerive.js';
export { isExtrinsicRequest } from './isExtrinsicRequest.js';

View File

@@ -4,7 +4,7 @@
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { Keyring } from '@polkadot/keyring';
import { canDerive, canInject } from './canDerive.js';
import { canInject } from './canDerive.js';
describe('quantus defaults', (): void => {
it('the default type produces a Quantus account', (): void => {
@@ -24,18 +24,10 @@ describe('quantus defaults', (): void => {
expect(zero).not.toEqual(one);
});
// The trap: canDerive gated dapp injection upstream. If that had stayed, no
// Quantus account would reach any dapp.
it('ML-DSA accounts are injected but not derivable', (): void => {
it('ML-DSA accounts are injected, and nothing else is', (): void => {
expect(canInject('dilithium65')).toEqual(true);
expect(canInject('dilithium87')).toEqual(true);
expect(canDerive('dilithium65')).toEqual(false);
expect(canDerive('dilithium87')).toEqual(false);
});
it('leaves the curve types alone', (): void => {
expect(canInject('sr25519')).toEqual(true);
expect(canDerive('sr25519')).toEqual(true);
expect(canInject('sr25519' as never)).toEqual(false);
expect(canInject(undefined)).toEqual(false);
});
});

View File

@@ -1,3 +0,0 @@
# @polkadot/extension-metamask-compat
An optional metamask-compatible layer

View File

@@ -1,35 +0,0 @@
{
"author": "Jaco Greeff <jacogr@gmail.com>",
"bugs": "https://github.com/polkadot-js/extension/issues",
"description": "Metamask compatibility layer",
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/polkadot-js/extension/tree/master/packages/extension-compat-metamask#readme",
"license": "Apache-2.0",
"name": "@polkadot/extension-compat-metamask",
"repository": {
"directory": "packages/extension-compat-metamask",
"type": "git",
"url": "https://github.com/polkadot-js/extension.git"
},
"sideEffects": [
"./packageDetect.js",
"./packageDetect.cjs"
],
"type": "module",
"version": "0.64.0",
"main": "index.js",
"dependencies": {
"@metamask/detect-provider": "^2.0.0",
"@polkadot/extension-inject": "0.64.0",
"@polkadot/types": "^16.5.6",
"@polkadot/util": "^14.0.3",
"tslib": "^2.8.1",
"web3": "^4.7.0"
},
"peerDependencies": {
"@polkadot/api": "*",
"@polkadot/util": "*"
}
}

View File

@@ -1,102 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-compat-metamask authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Injected, InjectedAccount, InjectedWindow } from '@polkadot/extension-inject/types';
import type { SignerPayloadRaw, SignerResult } from '@polkadot/types/types';
import type { HexString } from '@polkadot/util/types';
import detectEthereumProvider from '@metamask/detect-provider';
import Web3 from 'web3';
import { assert } from '@polkadot/util';
export { packageInfo } from './packageInfo.js';
interface RequestArguments {
method: string;
params?: unknown[];
}
interface EthRpcSubscription {
unsubscribe: () => void
}
interface EthereumProvider {
request: (args: RequestArguments) => Promise<unknown>;
isMetaMask: boolean;
on: (name: string, cb: (value: unknown) => void) => EthRpcSubscription;
}
interface Web3Window extends InjectedWindow {
// this is injected by metaMask
ethereum: unknown;
}
function isMetaMaskProvider (prov: unknown): EthereumProvider {
assert(prov && (prov as EthereumProvider).isMetaMask, 'Injected provider is not MetaMask');
return (prov as EthereumProvider);
}
// transform the Web3 accounts into a simple address/name array
function transformAccounts (accounts: string[]): InjectedAccount[] {
return accounts.map((address, i) => ({
address,
name: `MetaMask Address #${i}`,
type: 'ethereum'
}));
}
// add a compat interface of metaMaskSource to window.injectedWeb3
function injectMetaMaskWeb3 (win: Web3Window): void {
// decorate the compat interface
win.injectedWeb3['Web3Source'] = {
enable: async (): Promise<Injected> => {
const providerRaw = await detectEthereumProvider({ mustBeMetaMask: true });
const provider = isMetaMaskProvider(providerRaw);
await provider.request({ method: 'eth_requestAccounts' });
return {
accounts: {
get: async (): Promise<InjectedAccount[]> => {
const response = (await provider.request({ method: 'eth_requestAccounts' })) as string[];
return transformAccounts(response);
},
subscribe: (cb: (accounts: InjectedAccount[]) => void): (() => void) => {
const sub = provider.on('accountsChanged', (accounts): void => {
cb(transformAccounts(accounts as string[]));
});
// TODO: add onchainchanged
return (): void => {
sub.unsubscribe();
};
}
},
signer: {
signRaw: async (raw: SignerPayloadRaw): Promise<SignerResult> => {
const signature = (await provider.request({ method: 'eth_sign', params: [raw.address, Web3.utils.sha3(raw.data)] })) as HexString;
return { id: 0, signature };
}
}
};
},
version: '0' // TODO: win.ethereum.version
};
}
export default function initMetaMask (): Promise<boolean> {
return new Promise((resolve): void => {
const win = window as Window & Web3Window;
if (win.ethereum) {
injectMetaMaskWeb3(win);
resolve(true);
} else {
resolve(false);
}
});
}

View File

@@ -1,7 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-compat-metamask authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Since we inject into pages, we skip this
// import './detectPackage';
export * from './bundle.js';

View File

@@ -1,12 +0,0 @@
// Copyright 2017-2026 @polkadot/extension-compat-metamask authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev
// (packageInfo imports will be kept as-is, user-editable)
import { packageInfo as injectInfo } from '@polkadot/extension-inject/packageInfo';
import { detectPackage } from '@polkadot/util';
import { packageInfo } from './packageInfo.js';
detectPackage(packageInfo, null, [injectInfo]);

View File

@@ -1,6 +0,0 @@
// Copyright 2017-2026 @polkadot/extension-compat-metamask authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/extension-compat-metamask', path: 'auto', type: 'auto', version: '0.64.0' };

View File

@@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src"
},
"references": [
{ "path": "../extension-inject/tsconfig.build.json" }
]
}

View File

@@ -47,20 +47,19 @@ describe('Account component', () => {
</MemoryRouter>);
it('shows Export option if account is not external', async () => {
wrapper = mountAccountComponent({ isExternal: false, type: 'ed25519' });
wrapper = mountAccountComponent({ isExternal: false, type: 'dilithium65' });
wrapper.find('.settings').first().simulate('click');
await act(flushAllPromises);
expect(wrapper.find('a.menuItem').length).toBe(4);
expect(wrapper.find('a.menuItem').length).toBe(3);
expect(wrapper.find('a.menuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('a.menuItem').at(1).text()).toBe('Derive New Account');
expect(wrapper.find('a.menuItem').at(2).text()).toBe('Export Account');
expect(wrapper.find('a.menuItem').at(3).text()).toBe('Forget Account');
expect(wrapper.find('a.menuItem').at(1).text()).toBe('Export Account');
expect(wrapper.find('a.menuItem').at(2).text()).toBe('Forget Account');
expect(wrapper.find('.genesisSelection').exists()).toBe(true);
});
it('does not show Export option if account is external', async () => {
wrapper = mountAccountComponent({ isExternal: true, type: 'ed25519' });
wrapper = mountAccountComponent({ isExternal: true, type: 'dilithium65' });
wrapper.find('.settings').first().simulate('click');
await act(flushAllPromises);
@@ -70,19 +69,6 @@ describe('Account component', () => {
expect(wrapper.find('.genesisSelection').exists()).toBe(true);
});
it('shows Derive option if account is of ethereum type', async () => {
wrapper = mountAccountComponent({ isExternal: false, type: 'ethereum' });
wrapper.find('.settings').first().simulate('click');
await act(flushAllPromises);
expect(wrapper.find('a.menuItem').length).toBe(4);
expect(wrapper.find('a.menuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('a.menuItem').at(1).text()).toBe('Derive New Account');
expect(wrapper.find('a.menuItem').at(2).text()).toBe('Export Account');
expect(wrapper.find('a.menuItem').at(3).text()).toBe('Forget Account');
expect(wrapper.find('.genesisSelection').exists()).toBe(true);
});
// 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

View File

@@ -6,8 +6,6 @@ import type { HexString } from '@polkadot/util/types';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { canDerive } from '@polkadot/extension-base/utils';
import { AccountContext, Address, Checkbox, Dropdown, Link, MenuDivider } from '../../components/index.js';
import { useGenesisHashOptions, useTranslation } from '../../hooks/index.js';
import { editAccount, tieAccount } from '../../messaging.js';
@@ -27,7 +25,7 @@ interface EditState {
toggleActions: number;
}
function Account ({ address, className, genesisHash, isExternal, isHardware, isHidden, name, parentName, showVisibilityAction, suri, type, withCheckbox = false, withMenu = true }: Props): React.ReactElement<Props> {
function Account ({ address, className, genesisHash, isExternal, isHardware, isHidden, name, parentName, showVisibilityAction, suri, withCheckbox = false, withMenu = true }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [{ isEditing, toggleActions }, setEditing] = useState<EditState>({ isEditing: false, toggleActions: 0 });
const [editedName, setName] = useState<string | undefined | null>(name);
@@ -84,14 +82,6 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
>
{t('Rename')}
</Link>
{!isExternal && canDerive(type) && (
<Link
className='menuItem'
to={`/account/derive/${address}/locked`}
>
{t('Derive New Account')}
</Link>
)}
<MenuDivider />
{!isExternal && (
<Link
@@ -124,7 +114,7 @@ function Account ({ address, className, genesisHash, isExternal, isHardware, isH
</>
)}
</>
), [_onChangeGenesis, _toggleEdit, address, canEditGenesis, genesisHash, genesisOptions, isExternal, t, type]);
), [_onChangeGenesis, _toggleEdit, address, canEditGenesis, genesisHash, genesisOptions, isExternal, t]);
return (
<div className={className}>

View File

@@ -31,17 +31,17 @@ const twoRequests = [
];
const oneAccount = [
{ address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', name: 'A', type: 'sr25519' }
{ address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', name: 'A', type: 'dilithium65' }
] as AccountJson[];
const twoAccountsOnehidden = [
...oneAccount,
{ address: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', isHidden: true, name: 'B', type: 'sr25519' }
{ address: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', isHidden: true, name: 'B', type: 'dilithium65' }
] as AccountJson[];
const threeAccountsOnehidden = [
...twoAccountsOnehidden,
{ address: '5D2TPhGEy2FhznvzaNYW9AkuMBbg3cyRemnPsBvBY4ZhkZXA', name: 'BB', parentAddress: twoAccountsOnehidden[1].address, type: 'sr25519' }
{ address: '5D2TPhGEy2FhznvzaNYW9AkuMBbg3cyRemnPsBvBY4ZhkZXA', name: 'BB', parentAddress: twoAccountsOnehidden[1].address, type: 'dilithium65' }
] as AccountJson[];
describe('Authorize', () => {

View File

@@ -1,106 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useRef, useState } from 'react';
import arrow from '../../assets/arrow-down.svg';
import { Address } from '../../components/index.js';
import { useOutsideClick } from '../../hooks/index.js';
import { styled } from '../../styled.js';
interface Props {
allAddresses: [string, string | null][];
className?: string;
onSelect: (address: string) => void;
selectedAddress: string;
selectedGenesis: string | null;
}
function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress, selectedGenesis }: Props): React.ReactElement<Props> {
const [isDropdownVisible, setDropdownVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const _hideDropdown = useCallback(() => setDropdownVisible(false), []);
const _toggleDropdown = useCallback(() => setDropdownVisible(!isDropdownVisible), [isDropdownVisible]);
const _selectParent = useCallback((newParent: string) => () => onSelect(newParent), [onSelect]);
useOutsideClick([ref], _hideDropdown);
return (
<div className={className}>
<div
onClick={_toggleDropdown}
ref={ref}
>
<Address
address={selectedAddress}
className='address'
genesisHash={selectedGenesis}
/>
</div>
<div className={`dropdown ${isDropdownVisible ? 'visible' : ''}`}>
{allAddresses.map(([address, genesisHash]) => (
<div
data-parent-option
key={address}
onClick={_selectParent(address)}
>
<Address
address={address}
className='address'
genesisHash={genesisHash}
/>
</div>
))}
</div>
</div>
);
}
export default styled(AddressDropdown)<Props>`
margin-bottom: 16px;
cursor: pointer;
& > div:first-child > .address::after {
content: '';
position: absolute;
top: 66%;
transform: translateY(-50%);
right: 11px;
width: 30px;
height: 30px;
background: url(${arrow}) center no-repeat;
background-color: var(--inputBackground);
pointer-events: none;
border-radius: 4px;
border: 1px solid var(--boxBorderColor);
}
.address .copyIcon {
visibility: hidden;
}
.dropdown {
position: absolute;
visibility: hidden;
width: 510px;
z-index: 100;
background: var(--bodyColor);
max-height: 0;
overflow: auto;
padding: 5px;
border: 1px solid var(--boxBorderColor);
box-sizing: border-box;
border-radius: 4px;
margin-top: -8px;
&.visible{
visibility: visible;
max-height: 200px;
}
& > div {
cursor: pointer;
}
}
`;

View File

@@ -1,110 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useEffect, useState } from 'react';
import { Button, InputWithLabel } from '../../components/index.js';
import { useTranslation } from '../../hooks/index.js';
import { styled } from '../../styled.js';
interface Props {
className?: string;
defaultPath: string;
isError: boolean;
onChange: (suri: string) => void;
parentAddress: string;
parentPassword: string;
withSoftPath: boolean;
}
function DerivationPath ({ className, defaultPath, isError, onChange, withSoftPath }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [path, setPath] = useState<string>(defaultPath);
const [isDisabled, setIsDisabled] = useState(true);
useEffect(() => {
setPath(defaultPath);
}, [defaultPath]);
const _onExpand = useCallback(() => setIsDisabled(!isDisabled), [isDisabled]);
const _onChange = useCallback((newPath: string): void => {
setPath(newPath);
onChange(newPath);
}, [onChange]);
return (
<div className={className}>
<div className='container'>
<div className={`pathInput ${isDisabled ? 'locked' : ''}`}>
<InputWithLabel
data-input-suri
disabled={isDisabled}
isError={isError || !path}
label={
isDisabled
? t('Derivation Path (unlock to edit)')
: t('Derivation Path')
}
onChange={_onChange}
placeholder={withSoftPath
? t('//hard/soft')
: t('//hard')
}
value={path}
/>
</div>
<Button
className='lockButton'
onClick={_onExpand}
>
<FontAwesomeIcon
className='lockIcon'
icon={isDisabled ? faLock : faLockOpen}
/>
</Button>
</div>
</div>
);
}
export default React.memo(styled(DerivationPath)<Props>`
> .container {
display: flex;
flex-direction: row;
}
.lockButton {
background: none;
height: 14px;
margin: 36px 2px 0 10px;
padding: 3px;
width: 11px;
&:not(:disabled):hover {
background: none;
}
&:active, &:focus {
outline: none;
}
&::-moz-focus-inner {
border: 0;
}
}
.lockIcon {
color: var(--iconNeutralColor)
}
.pathInput {
width: 100%;
&.locked input {
opacity: 50%;
}
}
`);

View File

@@ -1,331 +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 type { AccountJson, ResponseDeriveValidate } from '@polkadot/extension-base/background/types';
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, Route } from 'react-router';
import { AccountContext, ActionContext } from '../../components/index.js';
import * as messaging from '../../messaging.js';
import { flushAllPromises } from '../../testHelpers.js';
import { buildHierarchy } from '../../util/buildHierarchy.js';
import AddressDropdown from './AddressDropdown.js';
import Derive from './index.js';
const { configure, mount } = enzyme;
// // 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')
// }));
// For this file, there are a lot of them
/* eslint-disable @typescript-eslint/no-unsafe-argument */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
configure({ adapter: new Adapter() });
const parentPassword = 'pass';
const westendGenesis = '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e';
const defaultDerivation = '//0';
const derivedAddress = '5GYQRJj3NUznYDzCduENRcocMsyxmb6tjb5xW87ZMErBe9R7';
const accounts = [
{ address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', name: 'A', type: 'sr25519' },
{ address: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', genesisHash: westendGenesis, name: 'B', type: 'sr25519' },
{ address: '5D2TPhGEy2FhznvzaNYW9AkuMBbg3cyRemnPsBvBY4ZhkZXA', name: 'BB', parentAddress: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', type: 'sr25519' },
{ address: '5GhGENSJBWQZ8d8mARKgqEkiAxiW3hHeznQDW2iG4XzNieb6', isExternal: true, name: 'C', type: 'sr25519' },
{ address: '0xd5D81CD4236a43F48A983fc5B895975c511f634D', name: 'Ethereum', type: 'ethereum' },
{ address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', isExternal: false, name: 'D', type: 'ed25519' },
{ address: '5HRKYp5anSNGtqC7cq9ftiaq4y8Mk7uHk7keaXUrQwZqDWLJ', name: 'DD', parentAddress: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', type: 'ed25519' }
] as AccountJson[];
describe('Derive', () => {
const mountComponent = async (locked = false, account = 1): Promise<{
wrapper: ReactWrapper;
onActionStub: ReturnType<typeof jest.fn>;
}> => {
const onActionStub = jest.fn();
const wrapper = mount(
<MemoryRouter initialEntries={ [`/account/derive/${accounts[account].address}`] }>
<ActionContext.Provider value={onActionStub}>
<AccountContext.Provider
value={{
accounts,
hierarchy: buildHierarchy(accounts)
}}
>
<Route path='/account/derive/:address'>
<Derive isLocked={locked} />
</Route>
</AccountContext.Provider>
</ActionContext.Provider>
</MemoryRouter>
);
await act(flushAllPromises);
return { onActionStub, wrapper };
};
let wrapper: ReactWrapper;
let onActionStub: ReturnType<typeof jest.fn>;
const type = async (input: ReactWrapper, value: string): Promise<void> => {
input.simulate('change', { target: { value } });
await act(flushAllPromises);
input.update();
};
const enterName = (name: string): Promise<void> => type(wrapper.find('input').first(), name);
const password = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').first(), password);
const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
describe('Parent selection screen', () => {
beforeEach(async () => {
const mountedComponent = await mountComponent();
wrapper = mountedComponent.wrapper;
onActionStub = mountedComponent.onActionStub;
});
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(messaging, 'validateAccount').mockImplementation(async (_, pass) => pass === parentPassword);
// silencing the following expected console.error
console.error = jest.fn();
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(messaging, 'validateDerivationPath').mockImplementation(async (_, path) => {
if (path === '//') {
throw new Error('wrong suri');
}
return { address: derivedAddress, suri: defaultDerivation } as ResponseDeriveValidate;
});
it('Button is disabled and password field visible, path field is hidden', () => {
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.exists()).toBe(true);
expect(button.prop('disabled')).toBe(true);
expect(wrapper.find('.pathInput').exists()).toBe(false);
});
it('Password field is visible and not in error state', () => {
const passwordField = wrapper.find('[data-input-password]').first();
expect(passwordField.exists()).toBe(true);
expect(passwordField.prop('isError')).toBe(false);
});
it('No error is visible when first loading the page', () => {
expect(wrapper.find('Warning')).toHaveLength(0);
});
it('An error is visible, input higlighted and the button disabled when password is incorrect', async () => {
await type(wrapper.find('input[type="password"]'), 'wrong_pass');
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
await act(flushAllPromises);
wrapper.update();
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(true);
expect(wrapper.find('[data-input-password]').first().prop('isError')).toBe(true);
expect(wrapper.find('.warning-message')).toHaveLength(1);
expect(wrapper.find('.warning-message').first().text()).toEqual('Wrong password');
});
it('The error disappears when typing a new password and "Create derived account" is enabled', async () => {
await type(wrapper.find('input[type="password"]'), 'wrong_pass');
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
await act(flushAllPromises);
wrapper.update();
await type(wrapper.find('input[type="password"]'), 'new_attempt');
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(false);
expect(wrapper.find('[data-input-password]').first().prop('isError')).toBe(false);
expect(wrapper.find('.warning-message')).toHaveLength(0);
});
it('Button is enabled when password is set', async () => {
await type(wrapper.find('input[type="password"]'), parentPassword);
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(false);
expect(wrapper.find('.warning-message')).toHaveLength(0);
});
it('Derivation path gets visible, is set and locked', async () => {
await type(wrapper.find('input[type="password"]'), 'wrong_pass');
expect(wrapper.find('.pathInput.locked input').prop('disabled')).toBe(true);
expect(wrapper.find('.pathInput.locked input').prop('value')).toBe('//1');
});
it('Derivation path can be unlocked', async () => {
await type(wrapper.find('input[type="password"]'), 'wrong_pass');
wrapper.find('FontAwesomeIcon.lockIcon').simulate('click');
await act(flushAllPromises);
wrapper.update();
expect(wrapper.find('.pathInput').exists()).toBe(true);
expect(wrapper.find('.pathInput input').prop('disabled')).toBe(false);
});
it('Derivation path placeholder contains //hard/soft', async () => {
await type(wrapper.find('input[type="password"]'), parentPassword);
const pathInput = wrapper.find('[data-input-suri] input');
expect(pathInput.first().prop('placeholder')).toEqual('//hard/soft');
});
it('An error is visible and the button is disabled when suri is incorrect', async () => {
await type(wrapper.find('input[type="password"]'), parentPassword);
await type(wrapper.find('[data-input-suri] input'), '//');
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
await act(flushAllPromises);
wrapper.update();
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(true);
expect(wrapper.find('.warning-message')).toHaveLength(1);
expect(wrapper.find('.warning-message').first().text()).toEqual('Invalid derivation path');
});
it('An error is visible and the button is disabled when suri contains `///`', async () => {
await type(wrapper.find('input[type="password"]'), parentPassword);
await type(wrapper.find('[data-input-suri] input'), '///');
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(true);
expect(wrapper.find('.warning-message')).toHaveLength(1);
// eslint-disable-next-line quotes
expect(wrapper.find('.warning-message').first().text()).toEqual("`///password` not supported for derivation");
});
it('No error is shown when suri contains soft derivation `/` with sr25519', async () => {
await type(wrapper.find('input[type="password"]'), parentPassword);
await type(wrapper.find('[data-input-suri] input'), '//somehard/soft');
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(false);
expect(wrapper.find('.warning-message')).toHaveLength(0);
});
it('The error disappears and "Create derived account" is enabled when typing a new suri', async () => {
await type(wrapper.find('input[type="password"]'), parentPassword);
await type(wrapper.find('[data-input-suri] input'), '//');
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
await act(flushAllPromises);
wrapper.update();
await type(wrapper.find('[data-input-suri] input'), 'new');
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(false);
expect(wrapper.find('Warning')).toHaveLength(0);
});
it('takes selected address from URL as parent account', () => {
expect(wrapper.find('[data-field="name"]').first().text()).toBe('B');
});
it('selects internal root accounts as other options, no external and no Ethereum account', () => {
const options = wrapper.find('[data-parent-option] [data-field="name"]').map((el) => el.text());
expect(options).toEqual(['A', 'B', 'D', 'Ethereum']);
});
it('redirects to derive from next account when other option is selected', () => {
wrapper.find('[data-parent-option]').first().simulate('click');
expect(onActionStub).toHaveBeenCalledWith(`/account/derive/${accounts[0].address}`);
});
});
describe('Locked parent selection', () => {
beforeAll(async () => {
const mountedComponent = (await mountComponent(true));
wrapper = mountedComponent.wrapper;
onActionStub = mountedComponent.onActionStub;
});
it('address dropdown does not exist', () => {
expect(wrapper.exists(AddressDropdown)).toBe(false);
});
it('parent is taken from URL', () => {
expect(wrapper.find('[data-field="name"]').first().text()).toBe('B');
});
describe('Second phase', () => {
it('correctly creates the derived account', async () => {
const newAccount = {
name: 'newName',
password: 'somePassword'
};
const deriveMock = jest.spyOn(messaging, 'deriveAccount');
await type(wrapper.find('input[type="password"]'), parentPassword);
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
await act(flushAllPromises);
wrapper.update();
await enterName(newAccount.name).then(password(newAccount.password)).then(repeat(newAccount.password));
wrapper.find('[data-button-action="add new root"] button').simulate('click');
await act(flushAllPromises);
wrapper.update();
expect(deriveMock).toHaveBeenCalledWith(accounts[1].address, defaultDerivation, parentPassword, newAccount.name, newAccount.password, westendGenesis);
expect(onActionStub).toHaveBeenCalledWith('/');
});
});
});
describe('Ed25519 Parent', () => {
beforeEach(async () => {
const mountedComponent = await mountComponent(false, 5);
wrapper = mountedComponent.wrapper;
onActionStub = mountedComponent.onActionStub;
await type(wrapper.find('input[type="password"]'), parentPassword);
});
it('Derivation path placeholder only contains //hard', () => {
const pathInput = wrapper.find('[data-input-suri] input');
expect(pathInput.first().prop('placeholder')).toEqual('//hard');
});
it('An error is shown when suri contains soft derivation `/` with ed25519', async () => {
const pathInput = wrapper.find('[data-input-suri] input');
await type(pathInput, '//somehard/soft');
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(true);
expect(wrapper.find('[data-input-suri]').first().prop('isError')).toBe(true);
expect(wrapper.find('.warning-message')).toHaveLength(1);
expect(wrapper.find('.warning-message').first().text()).toEqual('Soft derivation is only allowed for sr25519 accounts');
});
});
});

View File

@@ -1,195 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { canDerive } from '@polkadot/extension-base/utils';
import { AccountContext, ActionContext, Address, ButtonArea, InputWithLabel, Label, NextStepButton, VerticalSpace, Warning } from '../../components/index.js';
import { useTranslation } from '../../hooks/index.js';
import { validateAccount, validateDerivationPath } from '../../messaging.js';
import { nextDerivationPath } from '../../util/nextDerivationPath.js';
import AddressDropdown from './AddressDropdown.js';
import DerivationPath from './DerivationPath.js';
interface Props {
className?: string;
isLocked?: boolean;
parentAddress: string;
parentGenesis: string | null;
onDerivationConfirmed: (derivation: { account: { address: string; suri: string }; parentPassword: string }) => void;
}
// match any single slash
const singleSlashRegex = /([^/]|^)\/([^/]|$)/;
export default function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false);
const { accounts, hierarchy } = useContext(AccountContext);
const defaultPath = useMemo(() => nextDerivationPath(accounts, parentAddress), [accounts, parentAddress]);
const [suriPath, setSuriPath] = useState<null | string>(defaultPath);
const [parentPassword, setParentPassword] = useState<string>('');
const [isProperParentPassword, setIsProperParentPassword] = useState(false);
const [pathError, setPathError] = useState('');
const passwordInputRef = useRef<HTMLDivElement>(null);
const allowSoftDerivation = useMemo(() => {
const parent = accounts.find(({ address }) => address === parentAddress);
return parent?.type === 'sr25519';
}, [accounts, parentAddress]);
// reset the password field if the parent address changes
useEffect(() => {
setParentPassword('');
}, [parentAddress]);
useEffect(() => {
// forbid the use of password since Keyring ignores it
if (suriPath?.includes('///')) {
setPathError(t('`///password` not supported for derivation'));
}
if (!allowSoftDerivation && suriPath && singleSlashRegex.test(suriPath)) {
setPathError(t('Soft derivation is only allowed for sr25519 accounts'));
}
}, [allowSoftDerivation, suriPath, t]);
const allAddresses = useMemo(
() => hierarchy
.filter(({ isExternal }) => !isExternal)
.filter(({ type }) => canDerive(type))
.map(({ address, genesisHash }): [string, string | null] => [address, genesisHash || null]),
[hierarchy]
);
const _onParentPasswordEnter = useCallback(
(parentPassword: string): void => {
setParentPassword(parentPassword);
setIsProperParentPassword(!!parentPassword);
},
[]
);
const _onSuriPathChange = useCallback(
(path: string): void => {
setSuriPath(path);
setPathError('');
},
[]
);
const _onParentChange = useCallback(
(address: string) => onAction(`/account/derive/${address}`),
[onAction]
);
const _onSubmit = useCallback(
async (): Promise<void> => {
if (suriPath && parentAddress && parentPassword) {
setIsBusy(true);
const isUnlockable = await validateAccount(parentAddress, parentPassword);
if (isUnlockable) {
try {
const account = await validateDerivationPath(parentAddress, suriPath, parentPassword);
onDerivationConfirmed({ account, parentPassword });
} catch (error) {
setIsBusy(false);
setPathError(t('Invalid derivation path'));
console.error(error);
}
} else {
setIsBusy(false);
setIsProperParentPassword(false);
}
}
},
[parentAddress, parentPassword, onDerivationConfirmed, suriPath, t]
);
useEffect(() => {
setParentPassword('');
setIsProperParentPassword(false);
passwordInputRef.current?.querySelector('input')?.focus();
}, [_onParentPasswordEnter]);
return (
<>
<div className={className}>
{isLocked
? (
<Address
address={parentAddress}
genesisHash={parentGenesis}
/>
)
: (
<Label label={t('Choose Parent Account:')}>
<AddressDropdown
allAddresses={allAddresses}
onSelect={_onParentChange}
selectedAddress={parentAddress}
selectedGenesis={parentGenesis}
/>
</Label>
)
}
<div ref={passwordInputRef}>
<InputWithLabel
data-input-password
isError={!!parentPassword && !isProperParentPassword}
isFocused
label={t('enter the password for the account you want to derive from')}
onChange={_onParentPasswordEnter}
type='password'
value={parentPassword}
/>
{!!parentPassword && !isProperParentPassword && (
<Warning
isBelowInput
isDanger
>
{t('Wrong password')}
</Warning>
)}
</div>
{isProperParentPassword && (
<>
<DerivationPath
defaultPath={defaultPath}
isError={!!pathError}
onChange={_onSuriPathChange}
parentAddress={parentAddress}
parentPassword={parentPassword}
withSoftPath={allowSoftDerivation}
/>
{(!!pathError) && (
<Warning
isBelowInput
isDanger
>
{pathError}
</Warning>
)}
</>
)}
</div>
<VerticalSpace />
<ButtonArea>
<NextStepButton
data-button-action='create derived account'
isBusy={isBusy}
isDisabled={!isProperParentPassword || !!pathError}
onClick={_onSubmit}
>
{t('Create a derived account')}
</NextStepButton>
</ButtonArea>
</>
);
}

View File

@@ -1,104 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useContext, useMemo, useState } from 'react';
import { useParams } from 'react-router';
import { AccountContext, AccountNamePasswordCreation, ActionContext, Address } from '../../components/index.js';
import { useTranslation } from '../../hooks/index.js';
import { deriveAccount } from '../../messaging.js';
import { HeaderWithSteps } from '../../partials/index.js';
import SelectParent from './SelectParent.js';
interface Props {
isLocked?: boolean;
}
interface AddressState {
address: string;
}
interface PathState extends AddressState {
suri: string;
}
interface ConfirmState {
account: PathState;
parentPassword: string;
}
function Derive ({ isLocked }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const { accounts } = useContext(AccountContext);
const { address: parentAddress } = useParams<AddressState>();
const [isBusy, setIsBusy] = useState(false);
const [account, setAccount] = useState<null | PathState>(null);
const [name, setName] = useState<string | null>(null);
const [parentPassword, setParentPassword] = useState<string | null>(null);
const parentGenesis = useMemo(
() => accounts.find((a) => a.address === parentAddress)?.genesisHash || null,
[accounts, parentAddress]
);
const _onCreate = useCallback((name: string, password: string) => {
if (!account || !name || !password || !parentPassword) {
return;
}
setIsBusy(true);
deriveAccount(parentAddress, account.suri, parentPassword, name, password, parentGenesis)
.then(() => onAction('/'))
.catch((error): void => {
setIsBusy(false);
console.error(error);
});
}, [account, onAction, parentAddress, parentGenesis, parentPassword]);
const _onDerivationConfirmed = useCallback(({ account, parentPassword }: ConfirmState) => {
setAccount(account);
setParentPassword(parentPassword);
}, []);
const _onBackClick = useCallback(() => {
setAccount(null);
}, []);
return (
<>
<HeaderWithSteps
step={account ? 2 : 1}
text={t('Add new account')}
/>
{!account && (
<SelectParent
isLocked={isLocked}
onDerivationConfirmed={_onDerivationConfirmed}
parentAddress={parentAddress}
parentGenesis={parentGenesis}
/>
)}
{account && (
<>
<div>
<Address
address={account.address}
genesisHash={parentGenesis}
name={name}
/>
</div>
<AccountNamePasswordCreation
buttonLabel={t('Create derived account')}
isBusy={isBusy}
onBackClick={_onBackClick}
onCreate={_onCreate}
onNameChange={setName}
/>
</>
)}
</>
);
}
export default React.memo(Derive);

View File

@@ -23,6 +23,12 @@ interface Props {
className?: string;
}
function unsafeKeyMessage (e: Error): string | null {
return /not quantum-safe/.test(e.message)
? e.message
: null;
}
function Upload ({ className }: Props): React.ReactElement {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
@@ -30,7 +36,10 @@ function Upload ({ className }: Props): React.ReactElement {
const [isBusy, setIsBusy] = useState(false);
const [accountsInfo, setAccountsInfo] = useState<ResponseJsonGetAccountInfo[]>([]);
const [password, setPassword] = useState<string>('');
const [isFileError, setFileError] = useState(false);
// The reason a file can't be restored, when there is one worth showing: a
// backup of a classical key is well-formed, just not something this
// extension will hold (quantus/common#6), and saying "invalid" would hide that.
const [fileError, setFileError] = useState<string | null>(null);
const [requirePassword, setRequirePassword] = useState(false);
const [isPasswordError, setIsPasswordError] = useState(false);
// don't use the info from the file directly
@@ -51,6 +60,7 @@ function Upload ({ className }: Props): React.ReactElement {
const _onChangeFile = useCallback(
(file: Uint8Array): void => {
setAccountsInfo(() => []);
setFileError(null);
let json: KeyringPair$Json | KeyringPairs$Json | undefined;
@@ -59,7 +69,7 @@ function Upload ({ className }: Props): React.ReactElement {
setFile(json);
} catch (e) {
console.error(e);
setFileError(true);
setFileError(t('Invalid Json file'));
}
if (json === undefined) {
@@ -79,12 +89,12 @@ function Upload ({ className }: Props): React.ReactElement {
setRequirePassword(true);
jsonGetAccountInfo(json)
.then((accountInfo) => setAccountsInfo((old) => [...old, accountInfo]))
.catch((e) => {
setFileError(true);
.catch((e: Error) => {
setFileError(unsafeKeyMessage(e) || t('Invalid Json file'));
console.error(e);
});
}
}, []
}, [t]
);
const _onRestore = useCallback(
@@ -103,10 +113,17 @@ function Upload ({ className }: Props): React.ReactElement {
.then(() => {
onAction('/');
})
.catch((e) => {
.catch((e: Error) => {
console.error(e);
setIsBusy(false);
setIsPasswordError(true);
// A batch file is only opened on restore, so this is where one
// holding a classical key is refused.
const unsafe = unsafeKeyMessage(e);
unsafe
? setFileError(unsafe)
: setIsPasswordError(true);
});
},
[file, onAction, password, requirePassword]
@@ -131,16 +148,16 @@ function Upload ({ className }: Props): React.ReactElement {
))}
<InputFileWithLabel
accept={acceptedFormats}
isError={isFileError}
isError={!!fileError}
label={t('backup file')}
onChange={_onChangeFile}
withLabel
/>
{isFileError && (
{fileError && (
<Warning
isDanger
>
{t('Invalid Json file')}
{fileError}
</Warning>
)}
{requirePassword && (
@@ -164,7 +181,7 @@ function Upload ({ className }: Props): React.ReactElement {
<Button
className='restoreButton'
isBusy={isBusy}
isDisabled={isFileError || isPasswordError}
isDisabled={!!fileError || isPasswordError}
onClick={_onRestore}
>
{t('Restore')}

View File

@@ -8,7 +8,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Route, Switch, useHistory } from 'react-router';
import { PHISHING_PAGE_REDIRECT } from '@polkadot/extension-base/defaults';
import { canDerive } from '@polkadot/extension-base/utils';
import { settings } from '@polkadot/ui-settings';
import { AccountContext, ActionContext, AuthorizeReqContext, BalanceContext, MediaContext, MetadataReqContext, SettingsContext, SigningReqContext, WalletContext } from '../components/contexts.js';
@@ -22,7 +21,6 @@ import AccountManagement from './AuthManagement/AccountManagement.js';
import AuthList from './AuthManagement/index.js';
import Authorize from './Authorize/index.js';
import CreateAccount from './CreateAccount/index.js';
import Derive from './Derive/index.js';
import ImportSeed from './ImportSeed/index.js';
import Metadata from './Metadata/index.js';
import Signing from './Signing/index.js';
@@ -58,12 +56,10 @@ async function requestMediaAccess (cameraOn: boolean): Promise<boolean> {
function initAccountContext ({ accounts, selectedAccounts, setSelectedAccounts }: Omit<AccountsContext, 'hierarchy' | 'master'>): AccountsContext {
const hierarchy = buildHierarchy(accounts);
const master = hierarchy.find(({ isExternal, type }) => !isExternal && canDerive(type));
return {
accounts,
hierarchy,
master,
selectedAccounts,
setSelectedAccounts
};
@@ -175,8 +171,6 @@ export default function Popup (): React.ReactElement {
<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>
<Route path='/account/derive/:address'>{wrapWithErrorBoundary(<Derive />, 'derive-address')}</Route>
<Route path='/url/manage/:url'>{wrapWithErrorBoundary(<AccountManagement />, 'manage-url')}</Route>
<Route path={`${PHISHING_PAGE_REDIRECT}/:website`}>{wrapWithErrorBoundary(<PhishingDetected />, 'phishing-page-redirect')}</Route>
<Route

View File

@@ -51,7 +51,7 @@ interface AccountTestGenesisJson extends AccountTestJson {
genesisHash: HexString;
}
const externalAccount = { address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', expectedIconTheme: 'polkadot', isExternal: true, name: 'External Account', type: 'sr25519' } as AccountJson;
const externalAccount = { address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', expectedIconTheme: 'polkadot', isExternal: true, name: 'External Account', type: 'dilithium65' } as AccountJson;
const hardwareAccount = {
address: 'HDE6uFdw53SwUyfKSsjwZNmS2sziWMPuY6uJhGHcFzLYRaJ',
expectedIconTheme: 'polkadot',
@@ -60,14 +60,13 @@ const hardwareAccount = {
isExternal: true,
isHardware: true,
name: 'Hardware Account',
type: 'sr25519'
type: 'dilithium65'
} as AccountJson;
const accounts = [
{ address: '5HSDXAC3qEMkSzZK377sTD1zJhjaPiX5tNWppHx2RQMYkjaJ', expectedIconTheme: 'polkadot', name: 'ECDSA Account', type: 'ecdsa' },
{ address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', expectedIconTheme: 'polkadot', name: 'Ed Account', type: 'ed25519' },
{ address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q', expectedIconTheme: 'polkadot', name: 'Parent Sr Account', type: 'sr25519' },
{ address: '0xd5D81CD4236a43F48A983fc5B895975c511f634D', expectedIconTheme: 'ethereum', name: 'Ethereum', type: 'ethereum' },
{ address: '5HSDXAC3qEMkSzZK377sTD1zJhjaPiX5tNWppHx2RQMYkjaJ', expectedIconTheme: 'polkadot', name: 'ECDSA Account', type: 'dilithium87' },
{ address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', expectedIconTheme: 'polkadot', name: 'Ed Account', type: 'dilithium65' },
{ address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q', expectedIconTheme: 'polkadot', name: 'Parent Sr Account', type: 'dilithium65' },
{ ...externalAccount },
{ ...hardwareAccount }
] as AccountTestJson[];
@@ -82,7 +81,7 @@ const westEndAccount = {
expectedNetworkLabel: 'Westend',
genesisHash: '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e',
name: 'acc',
type: 'ed25519'
type: 'dilithium65'
} as AccountTestGenesisJson;
const accountsWithGenesisHash = [
@@ -93,7 +92,7 @@ const accountsWithGenesisHash = [
expectedIconTheme: 'polkadot',
expectedNetworkLabel: 'Polkadot',
genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
type: 'sr25519'
type: 'dilithium65'
},
// with Kusama genesis Hash
{
@@ -102,7 +101,7 @@ const accountsWithGenesisHash = [
expectedIconTheme: 'polkadot',
expectedNetworkLabel: 'Kusama',
genesisHash: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe',
type: 'sr25519'
type: 'dilithium65'
},
// with Edgeware genesis Hash
{
@@ -111,7 +110,7 @@ const accountsWithGenesisHash = [
expectedIconTheme: 'substrate',
expectedNetworkLabel: 'Edgeware',
genesisHash: '0x742a2ca70c2fda6cee4f8df98d64c4c670a052d9568058982dad9d5a7a135c5b',
type: 'sr25519'
type: 'dilithium65'
}
] as AccountTestGenesisJson[];
@@ -330,7 +329,7 @@ describe('Address', () => {
name: 'Luke',
parentName: 'Dark Vador',
suri: '//42',
type: 'sr25519'
type: 'dilithium65'
} as AccountJson;
beforeAll(async () => {

View File

@@ -64,13 +64,6 @@ function findSubstrateAccount (accounts: AccountJson[], publicKey: Uint8Array):
) || null;
}
// find an account in our list
function findAccountByAddress (accounts: AccountJson[], _address: string): AccountJson | null {
return accounts.find(({ address }): boolean =>
address === _address
) || null;
}
// recodes an supplied address using the prefix/genesisHash, include the actual saved account & chain
function recodeAddress (address: string, accounts: AccountWithChildren[], chain: Chain | null, settings: SettingsStruct): Recoded {
// decode and create a shortcut for the encoded address
@@ -82,9 +75,7 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain:
// always allow the actual settings to override the display
return {
account,
formatted: account?.type === 'ethereum'
? address
: encodeAddress(publicKey, prefix),
formatted: encodeAddress(publicKey, prefix),
genesisHash: account?.genesisHash,
prefix,
type: account?.type || DEFAULT_TYPE
@@ -94,12 +85,12 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain:
const ACCOUNTS_SCREEN_HEIGHT = 550;
const defaultRecoded = { account: null, formatted: null, prefix: 42, type: DEFAULT_TYPE };
function Address ({ actions, address, children, className, genesisHash, isExternal, isHardware, isHidden, name, parentName, showVisibilityAction = false, suri, toggleActions, type: givenType }: Props): React.ReactElement<Props> {
function Address ({ actions, address, children, className, genesisHash, isExternal, isHardware, isHidden, name, parentName, showVisibilityAction = false, suri, toggleActions }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
const balances = useContext(BalanceContext);
const settings = useContext(SettingsContext);
const [{ account, formatted, genesisHash: recodedGenesis, prefix, type }, setRecoded] = useState<Recoded>(defaultRecoded);
const [{ account, formatted, genesisHash: recodedGenesis, prefix }, setRecoded] = useState<Recoded>(defaultRecoded);
const chain = useMetadata(genesisHash || recodedGenesis, true);
const [showActionsMenu, setShowActionsMenu] = useState(false);
@@ -115,18 +106,10 @@ function Address ({ actions, address, children, className, genesisHash, isExtern
return setRecoded(defaultRecoded);
}
const account = findAccountByAddress(accounts, address);
setRecoded(
(
chain?.definition.chainType === 'ethereum' ||
account?.type === 'ethereum' ||
(!account && givenType === 'ethereum')
)
? { account, formatted: address, type: 'ethereum' }
: recodeAddress(address, accounts, chain, settings)
);
}, [accounts, address, chain, givenType, settings]);
// No Ethereum branch: this extension holds no Ethereum keys, and Quantus
// addresses are always SS58. quantus/common#6
setRecoded(recodeAddress(address, accounts, chain, settings));
}, [accounts, address, chain, settings]);
useEffect(() => {
if (!showActionsMenu) {
@@ -144,11 +127,7 @@ function Address ({ actions, address, children, className, genesisHash, isExtern
setShowActionsMenu(false);
}, [toggleActions]);
const theme = (
type === 'ethereum'
? 'ethereum'
: (chain?.icon || 'polkadot')
) as IconTheme;
const theme = (chain?.icon || 'polkadot') as IconTheme;
const _onClick = useCallback(
() => setShowActionsMenu(!showActionsMenu),

View File

@@ -11,7 +11,7 @@ import { settings } from '@polkadot/ui-settings';
const noop = (): void => undefined;
const AccountContext = React.createContext<AccountsContext>({ accounts: [], hierarchy: [], master: undefined });
const AccountContext = React.createContext<AccountsContext>({ accounts: [], hierarchy: [] });
const ActionContext = React.createContext<(to?: string) => void>(noop);
const AuthorizeReqContext = React.createContext<AuthorizeRequest[]>([]);
// Empty until a node has answered, and empty again if one never does — an

View File

@@ -3,7 +3,7 @@
/* eslint-disable no-redeclare */
import type { AccountBalances, AccountJson, AllowedPath, AuthorizeRequest, ConnectedTabsUrlResponse, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, ResponseWalletPreview, SeedLengths, SigningRequest, SubscriptionMessageTypes, WalletInfo, WormholeBalance } from '@polkadot/extension-base/background/types';
import type { AccountBalances, AccountJson, AllowedPath, AuthorizeRequest, ConnectedTabsUrlResponse, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseAuthorizeList, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, ResponseWalletPreview, SeedLengths, SigningRequest, SubscriptionMessageTypes, WalletInfo, WormholeBalance } from '@polkadot/extension-base/background/types';
import type { Message } from '@polkadot/extension-base/types';
import type { Chain } from '@polkadot/extension-chains/types';
import type { MetadataDef } from '@polkadot/extension-inject/types';
@@ -288,14 +288,6 @@ export async function validateSeed (suri: string, type?: KeypairType): Promise<{
return sendMessage('pri(seed.validate)', { suri, type });
}
export async function validateDerivationPath (parentAddress: string, suri: string, parentPassword: string): Promise<ResponseDeriveValidate> {
return sendMessage('pri(derivation.validate)', { parentAddress, parentPassword, suri });
}
export async function deriveAccount (parentAddress: string, suri: string, parentPassword: string, name: string, password: string, genesisHash: HexString | null): Promise<boolean> {
return sendMessage('pri(derivation.create)', { genesisHash, name, parentAddress, parentPassword, password, suri });
}
export async function windowOpen (path: AllowedPath): Promise<boolean> {
return sendMessage('pri(window.open)', path);
}

View File

@@ -1,11 +1,11 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { faCodeBranch, faEye, faFileExport, faFileUpload, faKey, faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { faEye, faFileExport, faFileUpload, faKey, faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useContext } from 'react';
import React, { useCallback } from 'react';
import { AccountContext, Link, Menu, MenuDivider, MenuItem } from '../components/index.js';
import { 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';
@@ -19,7 +19,6 @@ const jsonPath = '/account/restore-json';
function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { master } = useContext(AccountContext);
const isPopup = useIsPopup();
const _openJson = useCallback(
@@ -40,17 +39,6 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
</Link>
</MenuItem>
<MenuDivider />
{!!master && (
<>
<MenuItem className='menuItem'>
<Link to={`/account/derive/${master.address}`}>
<FontAwesomeIcon icon={faCodeBranch} />
<span>{t('Derive from an account')}</span>
</Link>
</MenuItem>
<MenuDivider />
</>
)}
<MenuItem className='menuItem'>
<Link to={'/account/export-all'}>
<FontAwesomeIcon icon={faFileExport} />

View File

@@ -1,32 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type * as _ from '@polkadot/dev-test/globals.d.ts';
import { nextDerivationPath } from './nextDerivationPath.js';
describe('Generate Derivation Path', () => {
const acc = (address: string, parentAddress?: string): {
address: string;
parentAddress?: string;
} => ({
address,
parentAddress
});
it('generates path for first masters child', () => {
expect(nextDerivationPath([acc('a')], 'a')).toEqual('//0');
});
it('generates path for third masters child', () => {
expect(nextDerivationPath([acc('a'), acc('b', 'a'), acc('c', 'a')], 'a')).toEqual('//2');
});
it('generates path for masters child when another root exists', () => {
expect(nextDerivationPath([acc('a'), acc('b', 'a'), acc('c', 'a'), acc('d')], 'a')).toEqual('//2');
});
it('generates path for masters grandchild', () => {
expect(nextDerivationPath([acc('a'), acc('b', 'a'), acc('c', 'b'), acc('d', 'b')], 'b')).toEqual('//2');
});
});

View File

@@ -1,10 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountJson } from '@polkadot/extension-base/background/types';
export function nextDerivationPath (accounts: AccountJson[], parentAddress: string): string {
const siblingsCount = accounts.filter((account) => account.parentAddress === parentAddress).length;
return `//${siblingsCount}`;
}

View File

@@ -17,7 +17,6 @@
"@polkadot/extension-base/*": ["extension-base/src/*.ts"],
"@polkadot/extension-chains": ["extension-chains/src/index.ts"],
"@polkadot/extension-chains/*": ["extension-chains/src/*.ts"],
"@polkadot/extension-compat-metamask": ["extension-compat-metamask/src/index.ts"],
"@polkadot/extension-dapp": ["extension-dapp/src/index.ts"],
"@polkadot/extension-dapp/*": ["extension-dapp/src/*.ts"],
"@polkadot/extension-inject": ["extension-inject/src/index.ts"],

View File

@@ -9,7 +9,6 @@
{ "path": "./packages/extension-base/tsconfig.build.json" },
{ "path": "./packages/extension-base/tsconfig.spec.json" },
{ "path": "./packages/extension-chains/tsconfig.build.json" },
{ "path": "./packages/extension-compat-metamask/tsconfig.build.json" },
{ "path": "./packages/extension-dapp/tsconfig.build.json" },
{ "path": "./packages/extension-dapp/tsconfig.spec.json" },
{ "path": "./packages/extension-inject/tsconfig.build.json" },

View File

@@ -1153,29 +1153,29 @@ __metadata:
languageName: unknown
linkType: soft
"@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz":
version: 14.0.3-quantus.2
resolution: "@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz"
"@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.3/keyring-14.0.3-quantus.3.tgz":
version: 14.0.3-quantus.3
resolution: "@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.3/keyring-14.0.3-quantus.3.tgz"
dependencies:
"@polkadot/util": "npm:14.0.3"
"@polkadot/util-crypto": "npm:14.0.3-quantus.2"
"@quantus/crypto": "npm:^0.1.0"
"@polkadot/util-crypto": "npm:14.0.3-quantus.3"
"@quantus/crypto": "npm:^0.3.0"
tslib: "npm:^2.8.0"
peerDependencies:
"@polkadot/util": 14.0.3
"@polkadot/util-crypto": 14.0.3
checksum: 10/e3885d6a75b4c006887d24dadbfc25fff562baae84d42fd2125878af60daecbf789deed12c975edfef0884a3caff4dd469dcdcd8b63eacbd1a5fbf8bbfe48815
checksum: 10/2264245c4fc2505c852d5a874bda2fcd86a1269cb6545f7bb578a6efdf58beea3deccf28d27ba6e41f9fdf9a8ea50cf230c626b69afe775a7629e069149164f7
languageName: node
linkType: hard
"@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz":
version: 14.0.3-quantus.2
resolution: "@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz"
"@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.3/networks-14.0.3-quantus.3.tgz":
version: 14.0.3-quantus.3
resolution: "@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.3/networks-14.0.3-quantus.3.tgz"
dependencies:
"@polkadot/util": "npm:14.0.3"
"@substrate/ss58-registry": "npm:^1.51.0"
tslib: "npm:^2.8.0"
checksum: 10/3d2f0a08cde4be418d073f63587bed451a6ce4c91a8872a5e0c4191ff3e62d57a7942c1a1b6c183ed9c22742f5b95b7496fe4e32e3565baf2093d879ac51a55e
checksum: 10/04228325c8472901fc04e5570e6d3450539c4ee51bbbae5c6eea945c721dc86b4bd9a581503e9f65c0132317ecc3c915b7f2c4cc2774b169f49d6eb2524bd8c2
languageName: node
linkType: hard
@@ -1364,14 +1364,14 @@ __metadata:
languageName: node
linkType: hard
"@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz":
version: 3.16.7-quantus.2
resolution: "@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz"
"@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.3/ui-keyring-3.16.7-quantus.3.tgz":
version: 3.16.7-quantus.3
resolution: "@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.3/ui-keyring-3.16.7-quantus.3.tgz"
dependencies:
"@polkadot/keyring": "npm:14.0.3-quantus.2"
"@polkadot/keyring": "npm:14.0.3-quantus.3"
"@polkadot/ui-settings": "npm:3.16.7"
"@polkadot/util": "npm:^14.0.3"
"@polkadot/util-crypto": "npm:14.0.3-quantus.2"
"@polkadot/util-crypto": "npm:14.0.3-quantus.3"
mkdirp: "npm:^3.0.1"
rxjs: "npm:^7.8.1"
store: "npm:^2.0.12"
@@ -1380,7 +1380,7 @@ __metadata:
"@polkadot/keyring": "*"
"@polkadot/ui-settings": "*"
"@polkadot/util": "*"
checksum: 10/bd62eb7fb2ad152c8f6844938af34fa75fa6b359d5414cbaca0d77c80b808f8a4e51f62b223cbbb1cbe8feefab43b42db86dd978af92dcce3bab5236cdb3a885
checksum: 10/bc68714552d1702344fca5378713ba804987931e5f55a1b1dbd14905e7608bf918bac92f5d909895c7b44ff08afc61e95a104a29b81bf9aabfba181cae6cffb0
languageName: node
linkType: hard
@@ -1413,25 +1413,25 @@ __metadata:
languageName: node
linkType: hard
"@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz":
version: 14.0.3-quantus.2
resolution: "@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz"
"@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.3/util-crypto-14.0.3-quantus.3.tgz":
version: 14.0.3-quantus.3
resolution: "@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.3/util-crypto-14.0.3-quantus.3.tgz"
dependencies:
"@noble/curves": "npm:^1.3.0"
"@noble/hashes": "npm:^1.3.3"
"@polkadot/networks": "npm:14.0.3-quantus.2"
"@polkadot/networks": "npm:14.0.3-quantus.3"
"@polkadot/util": "npm:14.0.3"
"@polkadot/wasm-crypto": "npm:^7.5.3"
"@polkadot/wasm-util": "npm:^7.5.3"
"@polkadot/x-bigint": "npm:14.0.3"
"@polkadot/x-randomvalues": "npm:14.0.3"
"@quantus/crypto": "npm:^0.1.0"
"@quantus/crypto": "npm:^0.3.0"
"@scure/base": "npm:^1.1.7"
"@scure/sr25519": "npm:^0.2.0"
tslib: "npm:^2.8.0"
peerDependencies:
"@polkadot/util": 14.0.3
checksum: 10/78f04147ddfe8ae0bca4cba2f235715615a140de9ab95f10d6e4bbabee4a2865e6f8a84f70668722ab688094a8519eeb019ed5122620b355a3ea40b0d8cfdeb8
checksum: 10/ffbbb1a45a45abf51376276674e223b1adbbb9355b8881811c558d428ca84e6d07370d11ff07329cf5c32032b4c710a955b84c02462b6c4f0dc52b5f1117e17e
languageName: node
linkType: hard