feat: post-quantum only; no derivation, no MetaMask shim, no Ethereum addresses
Some checks failed
Lock Threads / lock (push) Has been cancelled
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:
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# @polkadot/extension-metamask-compat
|
||||
|
||||
An optional metamask-compatible layer
|
||||
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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]);
|
||||
@@ -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' };
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../extension-inject/tsconfig.build.json" }
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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')}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
@@ -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" },
|
||||
|
||||
44
yarn.lock
44
yarn.lock
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user