Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29033e8bb8
|
||
|
|
851500a5fb
|
||
|
|
e0aa78360a
|
@@ -1,71 +1,74 @@
|
||||
# polkadot{.js} extension
|
||||
# blackbeard
|
||||
|
||||
A very simple scaffolding browser extension that injects a [@polkadot/api](https://github.com/polkadot-js/api) Signer into a page, along with any associated accounts, allowing for use by any dapp. This is an extensible POC implementation of a Polkadot/Substrate browser signer.
|
||||
A post-quantum browser wallet for the [Quantus Network](https://quantus.com), forked from the
|
||||
[polkadot{.js} extension](https://github.com/polkadot-js/extension). It keeps upstream's shape — accounts
|
||||
in a popup, a signer injected into dapps — with the cryptography, encoding and account model Quantus
|
||||
actually needs:
|
||||
|
||||
As it stands, it does one thing: it _only_ manages accounts and allows the signing of transactions with those accounts. It does not inject providers for use by dapps at this early point, nor does it perform wallet functions where it constructs and submits txs to the network.
|
||||
- **ML-DSA only.** Accounts are ML-DSA-65 and ML-DSA-87 (the two variants of the runtime's
|
||||
`DilithiumSignatureScheme`), derived as `quantus-cli` and the mobile wallet derive them.
|
||||
- **The runtime is the codec.** Calls and extrinsics are encoded and decoded from the chain's own
|
||||
metadata ([@quantus/codec](https://git.lair.cafe/quantus/wasm)), fetched from a node and checked
|
||||
against its genesis — never from a hardcoded type registry, which drifts from a runtime that changes.
|
||||
- **Wallets, not keypairs.** A recovery phrase is one wallet, shown as every account it unlocks:
|
||||
ML-DSA-65, ML-DSA-87 and wormhole tabs, per account index. The phrase is stored encrypted with the
|
||||
wallet password so further accounts can be derived later.
|
||||
- **Wormhole balances, read-only.** Deposits come from a [blackbeard observer](https://blackbeard.observer)
|
||||
and are checked against the chain's own transfer counts; spent status is decided locally from
|
||||
nullifiers precomputed while the password is to hand, checked against whole buckets of
|
||||
`Wormhole::UsedNullifiers` so no node learns which ones are yours. Sending from a wormhole address
|
||||
is not in the extension yet (quantus/extension#13).
|
||||
|
||||
Work is tracked at [git.lair.cafe/quantus/extension/issues](https://git.lair.cafe/quantus/extension/issues);
|
||||
#1 is the epic.
|
||||
|
||||
## Installation
|
||||
|
||||
- On Chrome, install via [Chrome web store](https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd)
|
||||
- On Firefox, install via [Firefox add-ons](https://addons.mozilla.org/en-US/firefox/addon/polkadot-js-extension/)
|
||||
There is no store listing. Build from source (below), or use the zips a build writes to the repo root.
|
||||
|
||||

|
||||
- **Firefox:** `about:debugging#/runtime/this-firefox` → **Load Temporary Add-on…** → `master-ff-build.zip`.
|
||||
Temporary add-ons are unloaded on restart; a permanent install of an unsigned add-on needs Developer
|
||||
Edition or Nightly with `xpinstall.signatures.required` set to `false`.
|
||||
- **Chrome / Chromium / Brave:** unzip `master-chrome-build.zip` into a folder, then `chrome://extensions`
|
||||
→ **Developer mode** → **Load unpacked** → that folder. Not `packages/extension/build`, which holds
|
||||
whichever browser was built last.
|
||||
|
||||
## Documentation and examples
|
||||
Find out more about how to use the extension as a Dapp developer, cookbook, as well as answers to most frequent questions in the [Polkadot-js extension documentation](https://polkadot.js.org/docs/extension/)
|
||||
## Privacy settings
|
||||
|
||||
## Firefox installation from source instructions.
|
||||
Everything the extension asks the network is a setting, and each can be pointed at your own
|
||||
infrastructure or turned off:
|
||||
|
||||
1. Uncompress `master-ff-src.zip`
|
||||
2. Run `corepack enable` [More information](https://github.com/nodejs/corepack?tab=readme-ov-file#corepack-enable--name)
|
||||
3. Install dependencies via `yarn install`
|
||||
4. Build all packages via `yarn build`
|
||||
- The `/packages/extension/build` directory will contain the exact code used in the add-on, and should exactly match the uncompressed `master-ff-build`.
|
||||
|
||||
NOTE: If you would like to regenerate the compressed `master-ff-build.zip`, and `master-ff-src.zip` files run: `yarn build:zip:ff`
|
||||
|
||||
## Development version
|
||||
|
||||
Steps to build the extension and view your changes in a browser:
|
||||
|
||||
1. Chrome:
|
||||
1. Build via `yarn build:chrome`
|
||||
- NOTE: You may need to enable corepack by running `corepack enable`
|
||||
2. Install the extension
|
||||
- go to `chrome://extensions/`
|
||||
- ensure you have the Development flag set
|
||||
- "Load unpacked" and point to `packages/extension/build`
|
||||
- if developing, after making changes - refresh the extension
|
||||
2. Firefox
|
||||
1. Build via `yarn build:ff`
|
||||
- NOTE: You may need to enable corepack by running `corepack enable`
|
||||
2. Install the extension
|
||||
- go to `about:debugging#addons`
|
||||
- check "Enable add-on debugging"
|
||||
- click on "Load Temporary Add-on" and point to `packages/extension/build/manifest.json`
|
||||
- if developing, after making changes - reload the extension
|
||||
3. When visiting `https://polkadot.js.org/apps/` it will inject the extension
|
||||
|
||||
Once added, you can create an account (via a generated seed) or import via an existing seed. The [apps UI](https://github.com/polkadot-js/apps/), when loaded, will show these accounts as `<account name> (extension)`
|
||||
- **Read balances from** — the node used for balances and wormhole chain state.
|
||||
- **Look up wormhole deposits with** — the observer asked which deposits went to your wormhole
|
||||
addresses. It learns that those addresses belong to one person; nothing it is sent can name your exits.
|
||||
|
||||
## Development
|
||||
|
||||
The repo is split into a number of packages -
|
||||
```sh
|
||||
yarn install
|
||||
yarn build # both browsers; writes master-chrome-build.zip and master-ff-build.zip
|
||||
yarn test # background specs (the upstream React specs are not run: see quantus/extension#14)
|
||||
yarn lint
|
||||
```
|
||||
|
||||
- [extension](packages/extension/) - All the injection and background processing logic (the main entry)
|
||||
- [extension-ui](packages/extension-ui/) - The UI components for the extension, to build up the popup
|
||||
- [extension-dapp](packages/extension-dapp/) - A convenience wrapper to work with the injected objects, simplifying data extraction for any dapp that wishes to integrate the extension (or any extension that supports the interface)
|
||||
- [extension-inject](packages/extension-inject/) - A convenience wrapper that allows extension developers to inject their extension for use by any dapp
|
||||
The Quantus forks it builds against (`@polkadot/util-crypto`, `@polkadot/keyring`, `@polkadot/ui-keyring`,
|
||||
`@polkadot/networks`) and `@quantus/crypto` / `@quantus/codec` come from the Gitea package registry;
|
||||
see `.yarnrc.yml`. `scripts/tier1` submits real extrinsics against a node, and `scripts/tier2` is a
|
||||
dapp harness for exercising the injected signer in a browser.
|
||||
|
||||
It also contains a [`manifest_chrome.json`](packages/extension/manifest_chrome.json) file which contains the manifest configuration for Chrome and another [`manifest_firefox.json`](packages/extension/manifest_firefox.json) with the configuration for Firefox, for compatibility reasons, and a dummy `manifest.json` file that's only used by the build.
|
||||
The packages keep their upstream `@polkadot/extension-*` names:
|
||||
|
||||
- [extension](packages/extension/) — injection, background and manifests (the main entry)
|
||||
- [extension-base](packages/extension-base/) — the background: keyring, wallets, signing, balances
|
||||
- [extension-ui](packages/extension-ui/) — the popup
|
||||
- [extension-dapp](packages/extension-dapp/) — a convenience wrapper for dapps using any injected extension
|
||||
- [extension-inject](packages/extension-inject/) — the injection interface
|
||||
|
||||
## Dapp developers
|
||||
|
||||
The actual in-depth technical breakdown is given in the next section for any dapp developer wishing to work with the raw objects injected into the window. However, convenience wrappers are provided that allow for any dapp to use this extension (or any other extension that conforms to the interface) without having to manage any additional info.
|
||||
|
||||
The documentation for Dapp development is available [in the polkadot-js doc](https://polkadot.js.org/docs/extension).
|
||||
|
||||
This approach is used to support multiple external signers in for instance [apps](https://github.com/polkadot-js/apps/). You can read more about the convenience wrapper [@polkadot/extension-dapp](packages/extension-dapp/) along with usage samples.
|
||||
Use [@polkadot/extension-dapp](packages/extension-dapp/), or any library that enumerates
|
||||
`window.injectedWeb3`; this extension appears there as `blackbeard`. Signing requests are encoded and
|
||||
shown using the chain's own metadata, so a dapp does not need to supply any.
|
||||
|
||||
## API interface
|
||||
|
||||
@@ -121,8 +124,8 @@ The extension injects `injectedWeb3` into the global `window` object, exposing t
|
||||
```js
|
||||
window.injectedWeb3 = {
|
||||
// this is the name for this extension, there could be multiples injected,
|
||||
// each with their own keys, here `polkadot-js` is for this extension
|
||||
'polkadot-js': {
|
||||
// each with their own keys, here `blackbeard` is for this extension
|
||||
'blackbeard': {
|
||||
// semver for the package
|
||||
version: '0.1.0',
|
||||
|
||||
@@ -134,30 +137,11 @@ window.injectedWeb3 = {
|
||||
}
|
||||
```
|
||||
|
||||
## Mnemonics, Passwords, and Imports/Exports
|
||||
|
||||
### Using the mnemonic and password from the extension
|
||||
## Recovery phrases and passwords
|
||||
|
||||
When you create a keypair via the extension, it supplies a 12-word mnemonic seed and asks you to create a password. This password only encrypts the private key on disk so that the password is required to spend funds in `polkadot-js/apps` or to import the account from backup. The password does not protect the mnemonic phrase. That is, if an attacker were to acquire the mnemonic phrase, they would be able to use it to spend funds without the password.
|
||||
|
||||
### Importing mnemonics from other key generation utilities
|
||||
|
||||
Some key-generation tools, e.g. [Subkey](https://www.substrate.io/kb/integrate/subkey), support hard and soft key derivation as well as passwords that encrypt the mnemonic phrase such that the mnemonic phrase itself is insufficient to spend funds.
|
||||
|
||||
The extension supports these advanced features. When you import an account from a seed, you can add these derivation paths or password to the end of the mnemonic in the following format:
|
||||
|
||||
```
|
||||
<mnemonic phrase>//<hard>/<soft>///<password>
|
||||
```
|
||||
|
||||
That is, hard-derivation paths are prefixed with `//`, soft paths with `/`, and the password with `///`.
|
||||
|
||||
The extension will still ask you to enter a password for this account. As before, this password only encrypts the private key on disk. It is not required to be the same password as the one that encrypts the mnemonic phrase.
|
||||
|
||||
Accounts can also be derived from existing accounts – `Derive New Account` option in account's dropdown menu should be selected. After providing the password of the parent account, along with name and password of the derived account, enter derivation path in the following format:
|
||||
|
||||
```
|
||||
//<hard>/<soft>
|
||||
```
|
||||
|
||||
The path will be added to the mnemonic phrase of the parent account.
|
||||
A wallet's password encrypts the stored recovery phrase and each account's key; the phrase itself is
|
||||
not protected by it, so anyone with the phrase controls every account the wallet shows. Import accepts
|
||||
a BIP39 phrase, or a `0x`-prefixed 32-byte seed for the dev accounts. There are no derivation paths
|
||||
to type: account indices are added from the wallet's menu, and derive the same accounts
|
||||
`quantus-cli` and the mobile wallet derive for that index.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
|
||||
import type { KeyringJson, KeyringStore } from '@polkadot/ui-keyring/types';
|
||||
|
||||
import { EXTENSION_PREFIX } from '../defaults.js';
|
||||
import BaseStore from './Base.js';
|
||||
|
||||
export default class AccountsStore extends BaseStore<KeyringJson> implements KeyringStore {
|
||||
constructor () {
|
||||
super(
|
||||
EXTENSION_PREFIX && EXTENSION_PREFIX !== 'polkadot{.js}'
|
||||
? `${EXTENSION_PREFIX}accounts`
|
||||
: null
|
||||
);
|
||||
// Fixed, not derived from EXTENSION_PREFIX. chrome.storage is already per
|
||||
// extension, and deriving the key from the name meant renaming the
|
||||
// extension (quantus/extension#15) would orphan everything stored.
|
||||
super(null);
|
||||
}
|
||||
|
||||
public override async set (key: string, value: KeyringJson, update?: () => void): Promise<void> {
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
|
||||
import type { MetadataDef } from '@polkadot/extension-inject/types';
|
||||
|
||||
import { EXTENSION_PREFIX } from '../defaults.js';
|
||||
import BaseStore from './Base.js';
|
||||
|
||||
export default class MetadataStore extends BaseStore<MetadataDef> {
|
||||
constructor () {
|
||||
super(
|
||||
EXTENSION_PREFIX && EXTENSION_PREFIX !== 'polkadot{.js}'
|
||||
? `${EXTENSION_PREFIX}metadata`
|
||||
: 'metadata'
|
||||
);
|
||||
// Fixed, not derived from EXTENSION_PREFIX. chrome.storage is already per
|
||||
// extension, and deriving the key from the name meant renaming the
|
||||
// extension (quantus/extension#15) would orphan everything stored.
|
||||
super('metadata');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import type { WormholeNullifiersJson } from '../background/types.js';
|
||||
|
||||
import { EXTENSION_PREFIX } from '../defaults.js';
|
||||
import BaseStore from './Base.js';
|
||||
|
||||
/**
|
||||
@@ -15,10 +14,9 @@ import BaseStore from './Base.js';
|
||||
*/
|
||||
export default class NullifiersStore extends BaseStore<WormholeNullifiersJson> {
|
||||
constructor () {
|
||||
super(
|
||||
EXTENSION_PREFIX && EXTENSION_PREFIX !== 'polkadot{.js}'
|
||||
? `${EXTENSION_PREFIX}quantus:nullifiers`
|
||||
: 'quantus:nullifiers'
|
||||
);
|
||||
// Fixed, not derived from EXTENSION_PREFIX. chrome.storage is already per
|
||||
// extension, and deriving the key from the name meant renaming the
|
||||
// extension (quantus/extension#15) would orphan everything stored.
|
||||
super('quantus:nullifiers');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import type { WalletJson } from '../background/types.js';
|
||||
|
||||
import { EXTENSION_PREFIX } from '../defaults.js';
|
||||
import BaseStore from './Base.js';
|
||||
|
||||
/**
|
||||
@@ -15,10 +14,9 @@ import BaseStore from './Base.js';
|
||||
*/
|
||||
export default class WalletsStore extends BaseStore<WalletJson> {
|
||||
constructor () {
|
||||
super(
|
||||
EXTENSION_PREFIX && EXTENSION_PREFIX !== 'polkadot{.js}'
|
||||
? `${EXTENSION_PREFIX}quantus:wallet`
|
||||
: 'quantus:wallet'
|
||||
);
|
||||
// Fixed, not derived from EXTENSION_PREFIX. chrome.storage is already per
|
||||
// extension, and deriving the key from the name meant renaming the
|
||||
// extension (quantus/extension#15) would orphan everything stored.
|
||||
super('quantus:wallet');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -27,7 +27,7 @@ function PhishingDetected ({ className }: Props): React.ReactElement<Props> {
|
||||
<Header text={t('Phishing detected')} />
|
||||
<div className={className}>
|
||||
<p>
|
||||
{t('You have been redirected because the Polkadot{.js} extension believes that this website could compromise the security of your accounts and your tokens.')}
|
||||
{t('You have been redirected because blackbeard believes that this website could compromise the security of your accounts and your tokens.')}
|
||||
</p>
|
||||
<p className='websiteAddress'>
|
||||
{decodedWebsite}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -31,12 +31,14 @@ function Welcome ({ className }: Props): React.ReactElement<Props> {
|
||||
<p>{t('Before we start, just a couple of notes regarding use:')}</p>
|
||||
<Box>
|
||||
<List>
|
||||
<li>{t('We do not send any clicks, pageviews or events to a central server')}</li>
|
||||
<li>{t('We do not use any trackers or analytics')}</li>
|
||||
<li>{t("We don't collect keys, addresses or any information - your information never leaves this machine")}</li>
|
||||
<li>{t('No clicks, pageviews or events are sent anywhere, and there are no trackers or analytics.')}</li>
|
||||
<li>{t('Keys and recovery phrases never leave this browser.')}</li>
|
||||
<li>{t('To show balances, your addresses are sent to a Quantus node; to show wormhole balances, your wormhole addresses are sent to a blackbeard observer. Either can be pointed at your own server, or turned off, in settings.')}</li>
|
||||
</List>
|
||||
</Box>
|
||||
<p>{t('... we are not in the information collection business (even anonymized).')}</p>
|
||||
{/* Upstream said "your information never leaves this machine". Once
|
||||
balances were shown that stopped being true, and a welcome screen
|
||||
is the wrong place to overstate privacy. quantus/extension#15 */}
|
||||
</div>
|
||||
<VerticalSpace />
|
||||
<ButtonArea>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="15 15 140 140" style="enable-background:new 0 0 170 170;zoom: 1;" xml:space="preserve"><style type="text/css">.bg0{fill:#FF8C00} .st0{fill:#FFFFFF}</style><g><circle class="bg0" cx="85" cy="85" r="70"></circle><g><path class="st0" d="M85,34.7c-20.8,0-37.8,16.9-37.8,37.8c0,4.2,0.7,8.3,2,12.3c0.9,2.7,3.9,4.2,6.7,3.3c2.7-0.9,4.2-3.9,3.3-6.7 c-1.1-3.1-1.6-6.4-1.5-9.7C58.1,57.6,69.5,46,83.6,45.3c15.7-0.8,28.7,11.7,28.7,27.2c0,14.5-11.4,26.4-25.7,27.2 c0,0-5.3,0.3-7.9,0.7c-1.3,0.2-2.3,0.4-3,0.5c-0.3,0.1-0.6-0.2-0.5-0.5l0.9-4.4L81,73.4c0.6-2.8-1.2-5.6-4-6.2 c-2.8-0.6-5.6,1.2-6.2,4c0,0-11.8,55-11.9,55.6c-0.6,2.8,1.2,5.6,4,6.2c2.8,0.6,5.6-1.2,6.2-4c0.1-0.6,1.7-7.9,1.7-7.9 c1.2-5.6,5.8-9.7,11.2-10.4c1.2-0.2,5.9-0.5,5.9-0.5c19.5-1.5,34.9-17.8,34.9-37.7C122.8,51.6,105.8,34.7,85,34.7z M87.7,121.7 c-3.4-0.7-6.8,1.4-7.5,4.9c-0.7,3.4,1.4,6.8,4.9,7.5c3.4,0.7,6.8-1.4,7.5-4.9C93.3,125.7,91.2,122.4,87.7,121.7z"></path></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40">
|
||||
<!-- blackbeard.observer's sigil, centred on a square for icons -->
|
||||
<rect width="40" height="40" rx="6" fill="#0d0b09"/>
|
||||
<g transform="translate(3 0)">
|
||||
<path d="M17 1 L32 7 V20 C32 29 25 35 17 39 C9 35 2 29 2 20 V7 Z" fill="none" stroke="#bd8829" stroke-width="1.6"/>
|
||||
<path d="M17 8 L24 12 V21 C24 26 20.5 29.5 17 31.5 C13.5 29.5 10 26 10 21 V12 Z" fill="#bd882922"/>
|
||||
<path d="M17 11 V28" stroke="#d8453a" stroke-width="1.6"/>
|
||||
<path d="M13 15 L17 11 L21 15" fill="none" stroke="#d8453a" stroke-width="1.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 628 B |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { faArrowLeft, faCog, faExpand, faPlusCircle, faSearch } from '@fortaweso
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import logo from '../assets/pjs.svg';
|
||||
import logo from '../assets/sigil.svg';
|
||||
import { ActionContext } from '../components/index.js';
|
||||
import InputFilter from '../components/InputFilter.js';
|
||||
import Link from '../components/Link.js';
|
||||
@@ -130,7 +130,7 @@ function Header ({ children, className = '', onFilter, showAdd, showBackArrow, s
|
||||
/>
|
||||
)
|
||||
}
|
||||
<span className='logoText'>{text || 'polkadot{.js}'}</span>
|
||||
<span className='logoText'>{text || 'blackbeard'}</span>
|
||||
</div>
|
||||
{showSearch && (
|
||||
<div className={`searchBarWrapper ${isSearchOpen ? 'selected' : ''}`}>
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"author": "polkadot.{js}",
|
||||
"description": "Manage your Polkadot accounts outside of dapps. Injects the accounts and allows signing transactions for a specific account.",
|
||||
"homepage_url": "https://github.com/polkadot-js/extension",
|
||||
"name": "polkadot{.js} extension",
|
||||
"short_name": "polkadot{.js}",
|
||||
"author": "blackbeard",
|
||||
"description": "A post-quantum wallet for the Quantus Network: ML-DSA-65 and ML-DSA-87 accounts, wormhole balances, and signing for dapps.",
|
||||
"homepage_url": "https://git.lair.cafe/quantus/extension",
|
||||
"name": "blackbeard",
|
||||
"short_name": "blackbeard",
|
||||
"manifest_version": 3,
|
||||
"permissions": [
|
||||
"storage",
|
||||
@@ -15,7 +15,7 @@
|
||||
"type": "module"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "polkadot{.js}",
|
||||
"default_title": "blackbeard",
|
||||
"default_popup": "index.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"author": "polkadot.{js}",
|
||||
"description": "Manage your Polkadot accounts outside of dapps. Injects the accounts and allows signing transactions for a specific account.",
|
||||
"homepage_url": "https://github.com/polkadot-js/extension",
|
||||
"name": "polkadot{.js} extension",
|
||||
"short_name": "polkadot{.js}",
|
||||
"author": "blackbeard",
|
||||
"description": "A post-quantum wallet for the Quantus Network: ML-DSA-65 and ML-DSA-87 accounts, wormhole balances, and signing for dapps.",
|
||||
"homepage_url": "https://git.lair.cafe/quantus/extension",
|
||||
"name": "blackbeard",
|
||||
"short_name": "blackbeard",
|
||||
"manifest_version": 3,
|
||||
"permissions": [
|
||||
"storage",
|
||||
@@ -17,12 +17,12 @@
|
||||
"type": "module"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "polkadot{.js}",
|
||||
"default_title": "blackbeard",
|
||||
"default_popup": "index.html"
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{7e3ce1f0-15fb-4fb1-99c6-25774749ec6d}",
|
||||
"id": "extension@blackbeard.observer",
|
||||
"strict_min_version": "108.0.2"
|
||||
}
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 510 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="15 15 140 140" style="enable-background:new 0 0 170 170;zoom: 1;" xml:space="preserve"><style type="text/css">
|
||||
.bg0{fill:#FF8C00}
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style><g><circle class="bg0" cx="85" cy="85" r="70"></circle><g><path class="st0" d="M85,34.7c-20.8,0-37.8,16.9-37.8,37.8c0,4.2,0.7,8.3,2,12.3c0.9,2.7,3.9,4.2,6.7,3.3c2.7-0.9,4.2-3.9,3.3-6.7 c-1.1-3.1-1.6-6.4-1.5-9.7C58.1,57.6,69.5,46,83.6,45.3c15.7-0.8,28.7,11.7,28.7,27.2c0,14.5-11.4,26.4-25.7,27.2 c0,0-5.3,0.3-7.9,0.7c-1.3,0.2-2.3,0.4-3,0.5c-0.3,0.1-0.6-0.2-0.5-0.5l0.9-4.4L81,73.4c0.6-2.8-1.2-5.6-4-6.2 c-2.8-0.6-5.6,1.2-6.2,4c0,0-11.8,55-11.9,55.6c-0.6,2.8,1.2,5.6,4,6.2c2.8,0.6,5.6-1.2,6.2-4c0.1-0.6,1.7-7.9,1.7-7.9 c1.2-5.6,5.8-9.7,11.2-10.4c1.2-0.2,5.9-0.5,5.9-0.5c19.5-1.5,34.9-17.8,34.9-37.7C122.8,51.6,105.8,34.7,85,34.7z M87.7,121.7 c-3.4-0.7-6.8,1.4-7.5,4.9c-0.7,3.4,1.4,6.8,4.9,7.5c3.4,0.7,6.8-1.4,7.5-4.9C93.3,125.7,91.2,122.4,87.7,121.7z"></path></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>polkadot{.js}</title>
|
||||
<title>blackbeard</title>
|
||||
<link href="fonts/fonts.css" rel="stylesheet">
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
|
||||
@@ -192,5 +192,9 @@
|
||||
"Check newer deposits": "",
|
||||
"Whether a wormhole deposit is spent is decided by its nullifier, which only this wallet can compute. {{count}} deposits to \"{{name}}\" arrived after it last did. Enter the wallet password to compute theirs; they are stored with the wallet and removed if you forget it.": "",
|
||||
"Compute and check": "",
|
||||
"Open the wallet's wormhole tab first, so there is a balance to extend.": ""
|
||||
"Open the wallet's wormhole tab first, so there is a balance to extend.": "",
|
||||
"You have been redirected because blackbeard believes that this website could compromise the security of your accounts and your tokens.": "",
|
||||
"No clicks, pageviews or events are sent anywhere, and there are no trackers or analytics.": "",
|
||||
"Keys and recovery phrases never leave this browser.": "",
|
||||
"To show balances, your addresses are sent to a Quantus node; to show wormhole balances, your wormhole addresses are sent to a blackbeard observer. Either can be pointed at your own server, or turned off, in settings.": ""
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>polkadot{.js}</title>
|
||||
<title>blackbeard</title>
|
||||
<link href="fonts/fonts.css" rel="stylesheet">
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
@@ -28,15 +28,15 @@ div#root {
|
||||
--lineHeight: 26px;
|
||||
/* shared colors */
|
||||
--accountDotsIconColor: #8E8E8E;
|
||||
--buttonBackground: #E86F00;
|
||||
--buttonBackground: #A8761F;
|
||||
--buttonBackgroundDangerHover: #D93B3B;
|
||||
--buttonBackgroundHover: #ED9329;
|
||||
--buttonBackgroundHover: #BD8829;
|
||||
--buttonTextColor: #FFFFFF;
|
||||
--connectedDotColor: seagreen;
|
||||
--errorColor: #E42F2F;
|
||||
--iconWarningColor: #FF7D01;
|
||||
--iconWarningColor: #BD8829;
|
||||
--identiconBackground: #F4F5F8;
|
||||
--primaryColor: #FF7D01;
|
||||
--primaryColor: #BD8829;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
|
||||
@@ -12,7 +12,10 @@ import { packageInfo } from './packageInfo.js';
|
||||
|
||||
function inject () {
|
||||
injectExtension(enable, {
|
||||
name: 'polkadot-js',
|
||||
// What dapps find us under in window.injectedWeb3. Not 'polkadot-js': with
|
||||
// that extension also installed, both wrote the same key and one replaced
|
||||
// the other. quantus/extension#15
|
||||
name: 'blackbeard',
|
||||
version: packageInfo.version
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||