3 Commits
Author SHA1 Message Date
grenadeandClaude Opus 5 29033e8bb8 feat: post-quantum only; no derivation, no MetaMask shim, no Ethereum addresses
Lock Threads / lock (push) Has been cancelled
Moves to the quantus.3 forks of keyring, util-crypto, networks and ui-keyring,
which hold only ML-DSA keys and have deleted the classical primitives
(quantus/common#6). What in the extension depended on them goes too:

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 21:28:01 +03:00
grenadeandClaude Opus 5 851500a5fb fix: manifest author and homepage are blackbeard's, not polkadot{.js}'s
The rebrand renamed the extension but left the manifest's author as
"polkadot.{js}" and its homepage as the upstream GitHub repo. Browsers show
both on the add-on's details page. Package.json authorship stays upstream's:
that is credit for the code, not the extension's identity.

Refs #2, #15

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 20:50:20 +03:00
grenadeandClaude Opus 5 e0aa78360a feat: rebrand as blackbeard
Lock Threads / lock (push) Has been cancelled
The extension presented itself as "polkadot{.js} extension". It is now
blackbeard, with the sigil and gold of blackbeard.observer.

Visible:
- manifest name, short_name, action title and page titles are "blackbeard",
  and the description says what it is: a post-quantum wallet for the Quantus
  Network;
- icons (16–128), favicon and the header logo are rendered from observer's
  sigil, centred on a square;
- the accent colour moves from polkadot orange to the sigil's gold;
- the phishing page and header fallback no longer name polkadot{.js};
- the welcome screen no longer claims "your information never leaves this
  machine". That stopped being true when balances arrived. It now says what
  is sent where, and that both can be pointed elsewhere or turned off;
- the README describes this extension rather than upstream's.

Behavioural, and part of the same change:
- Dapps find the extension as window.injectedWeb3['blackbeard']. As
  'polkadot-js' it overwrote, or was overwritten by, the real polkadot{.js}
  extension when both were installed.
- Page/content-script message channels follow short_name, so they are
  blackbeard-*. Sharing polkadot{.js}'s names meant each extension's content
  script answered the other's page messages.
- The Firefox add-on id is extension@blackbeard.observer instead of upstream's
  {7e3ce1f0-…}, so Firefox stops treating this as polkadot{.js}. Storage
  there starts empty; wallets need re-importing.
- Storage keys are fixed rather than derived from EXTENSION_PREFIX, and equal
  to what existing installs already use. chrome.storage is per-extension, and
  deriving keys from the name would have orphaned every stored account and
  wallet on rename.

Package names stay @polkadot/extension-*: they aren't user-visible, and
renaming them touches every import here and in the forks.

Verified in Firefox against the built extension:
- it installs as "blackbeard" with the new id;
- a plain page sees exactly one injected key, blackbeard;
- its enable() request reaches the background over the renamed channels and
  opens the connection-request screen, with the sigil and gold.

Closes #15

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 19:34:06 +03:00
58 changed files with 227 additions and 1528 deletions
+62 -78
View File
@@ -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.
![interface screenshots](docs/extension-overview.png)
- **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.
+4 -4
View File
@@ -53,13 +53,13 @@
},
"resolutions": {
"@polkadot/api": "^16.5.6",
"@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz",
"@polkadot/networks": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz",
"@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.3/keyring-14.0.3-quantus.3.tgz",
"@polkadot/networks": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.3/networks-14.0.3-quantus.3.tgz",
"@polkadot/rpc-provider": "^16.5.6",
"@polkadot/types": "^16.5.6",
"@polkadot/ui-keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz",
"@polkadot/ui-keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.3/ui-keyring-3.16.7-quantus.3.tgz",
"@polkadot/util": "^14.0.3",
"@polkadot/util-crypto": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz",
"@polkadot/util-crypto": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.3/util-crypto-14.0.3-quantus.3.tgz",
"@polkadot/x-fetch": "^14.0.3",
"@quantus/crypto": "^0.3.0",
"safe-buffer": "^5.2.1",
@@ -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');
}
}
+1 -18
View File
@@ -4,10 +4,7 @@
import type { KeypairType } from '@polkadot/util-crypto/types';
/** Every keypair type this extension can hold and sign with. */
const SIGNABLE: KeypairType[] = ['dilithium65', 'dilithium87', 'ecdsa', 'ed25519', 'ethereum', 'sr25519'];
/** The types that support deriving a child account from a parent's key. */
const DERIVABLE: KeypairType[] = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
const SIGNABLE: KeypairType[] = ['dilithium65', 'dilithium87'];
/**
* Whether an account of this type should be offered to dapps.
@@ -22,17 +19,3 @@ const DERIVABLE: KeypairType[] = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
export function canInject (type?: KeypairType): boolean {
return !!type && SIGNABLE.includes(type);
}
/**
* Whether a child account can be derived from a parent of this type.
*
* False for ML-DSA, and permanently so. Quantus derives every account from the
* mnemonic independently along `m/44'/189189'/<account>'/0'/<scheme>'`, and a
* pair holds no material a child could come from — `pair.derive()` refuses
* outright (quantus/common#4). Adding another account therefore needs the
* recovery phrase, not an unlocked parent, which is why the derive-from-parent
* flow has no Quantus meaning.
*/
export function canDerive (type?: KeypairType): boolean {
return !!type && DERIVABLE.includes(type);
}
+1 -1
View File
@@ -1,5 +1,5 @@
// Copyright 2019-2026 @polkadot/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { canDerive, canInject } from './canDerive.js';
export { canInject } from './canDerive.js';
export { isExtrinsicRequest } from './isExtrinsicRequest.js';
@@ -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}
+28 -11
View File
@@ -23,6 +23,12 @@ interface Props {
className?: string;
}
function unsafeKeyMessage (e: Error): string | null {
return /not quantum-safe/.test(e.message)
? e.message
: null;
}
function Upload ({ className }: Props): React.ReactElement {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
@@ -30,7 +36,10 @@ function Upload ({ className }: Props): React.ReactElement {
const [isBusy, setIsBusy] = useState(false);
const [accountsInfo, setAccountsInfo] = useState<ResponseJsonGetAccountInfo[]>([]);
const [password, setPassword] = useState<string>('');
const [isFileError, setFileError] = useState(false);
// The reason a file can't be restored, when there is one worth showing: a
// backup of a classical key is well-formed, just not something this
// extension will hold (quantus/common#6), and saying "invalid" would hide that.
const [fileError, setFileError] = useState<string | null>(null);
const [requirePassword, setRequirePassword] = useState(false);
const [isPasswordError, setIsPasswordError] = useState(false);
// don't use the info from the file directly
@@ -51,6 +60,7 @@ function Upload ({ className }: Props): React.ReactElement {
const _onChangeFile = useCallback(
(file: Uint8Array): void => {
setAccountsInfo(() => []);
setFileError(null);
let json: KeyringPair$Json | KeyringPairs$Json | undefined;
@@ -59,7 +69,7 @@ function Upload ({ className }: Props): React.ReactElement {
setFile(json);
} catch (e) {
console.error(e);
setFileError(true);
setFileError(t('Invalid Json file'));
}
if (json === undefined) {
@@ -79,12 +89,12 @@ function Upload ({ className }: Props): React.ReactElement {
setRequirePassword(true);
jsonGetAccountInfo(json)
.then((accountInfo) => setAccountsInfo((old) => [...old, accountInfo]))
.catch((e) => {
setFileError(true);
.catch((e: Error) => {
setFileError(unsafeKeyMessage(e) || t('Invalid Json file'));
console.error(e);
});
}
}, []
}, [t]
);
const _onRestore = useCallback(
@@ -103,10 +113,17 @@ function Upload ({ className }: Props): React.ReactElement {
.then(() => {
onAction('/');
})
.catch((e) => {
.catch((e: Error) => {
console.error(e);
setIsBusy(false);
setIsPasswordError(true);
// A batch file is only opened on restore, so this is where one
// holding a classical key is refused.
const unsafe = unsafeKeyMessage(e);
unsafe
? setFileError(unsafe)
: setIsPasswordError(true);
});
},
[file, onAction, password, requirePassword]
@@ -131,16 +148,16 @@ function Upload ({ className }: Props): React.ReactElement {
))}
<InputFileWithLabel
accept={acceptedFormats}
isError={isFileError}
isError={!!fileError}
label={t('backup file')}
onChange={_onChangeFile}
withLabel
/>
{isFileError && (
{fileError && (
<Warning
isDanger
>
{t('Invalid Json file')}
{fileError}
</Warning>
)}
{requirePassword && (
@@ -164,7 +181,7 @@ function Upload ({ className }: Props): React.ReactElement {
<Button
className='restoreButton'
isBusy={isBusy}
isDisabled={isFileError || isPasswordError}
isDisabled={!!fileError || isPasswordError}
onClick={_onRestore}
>
{t('Restore')}
+6 -4
View File
@@ -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
View File
@@ -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
+1 -9
View File
@@ -3,7 +3,7 @@
/* eslint-disable no-redeclare */
import type { AccountBalances, AccountJson, AllowedPath, AuthorizeRequest, ConnectedTabsUrlResponse, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, ResponseWalletPreview, SeedLengths, SigningRequest, SubscriptionMessageTypes, WalletInfo, WormholeBalance } from '@polkadot/extension-base/background/types';
import type { AccountBalances, AccountJson, AllowedPath, AuthorizeRequest, ConnectedTabsUrlResponse, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseAuthorizeList, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, ResponseWalletPreview, SeedLengths, SigningRequest, SubscriptionMessageTypes, WalletInfo, WormholeBalance } from '@polkadot/extension-base/background/types';
import type { Message } from '@polkadot/extension-base/types';
import type { Chain } from '@polkadot/extension-chains/types';
import type { MetadataDef } from '@polkadot/extension-inject/types';
@@ -288,14 +288,6 @@ export async function validateSeed (suri: string, type?: KeypairType): Promise<{
return sendMessage('pri(seed.validate)', { suri, type });
}
export async function validateDerivationPath (parentAddress: string, suri: string, parentPassword: string): Promise<ResponseDeriveValidate> {
return sendMessage('pri(derivation.validate)', { parentAddress, parentPassword, suri });
}
export async function deriveAccount (parentAddress: string, suri: string, parentPassword: string, name: string, password: string, genesisHash: HexString | null): Promise<boolean> {
return sendMessage('pri(derivation.create)', { genesisHash, name, parentAddress, parentPassword, password, suri });
}
export async function windowOpen (path: AllowedPath): Promise<boolean> {
return sendMessage('pri(window.open)', path);
}
@@ -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' : ''}`}>
+3 -15
View File
@@ -1,11 +1,11 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { faCodeBranch, faEye, faFileExport, faFileUpload, faKey, faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { faEye, faFileExport, faFileUpload, faKey, faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useContext } from 'react';
import React, { useCallback } from 'react';
import { AccountContext, Link, Menu, MenuDivider, MenuItem } from '../components/index.js';
import { Link, Menu, MenuDivider, MenuItem } from '../components/index.js';
import { useIsPopup, useTranslation } from '../hooks/index.js';
import { windowOpen } from '../messaging.js';
import { styled } from '../styled.js';
@@ -19,7 +19,6 @@ const jsonPath = '/account/restore-json';
function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { master } = useContext(AccountContext);
const isPopup = useIsPopup();
const _openJson = useCallback(
@@ -40,17 +39,6 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
</Link>
</MenuItem>
<MenuDivider />
{!!master && (
<>
<MenuItem className='menuItem'>
<Link to={`/account/derive/${master.address}`}>
<FontAwesomeIcon icon={faCodeBranch} />
<span>{t('Derive from an account')}</span>
</Link>
</MenuItem>
<MenuDivider />
</>
)}
<MenuItem className='menuItem'>
<Link to={'/account/export-all'}>
<FontAwesomeIcon icon={faFileExport} />
@@ -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}`;
}
+6 -6
View File
@@ -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": [
+7 -7
View File
@@ -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"
}
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

-4
View File
@@ -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

+1 -1
View File
@@ -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.": ""
}
+1 -1
View File
@@ -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>
+4 -4
View File
@@ -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"] {
+4 -1
View File
@@ -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
});
}
-1
View File
@@ -17,7 +17,6 @@
"@polkadot/extension-base/*": ["extension-base/src/*.ts"],
"@polkadot/extension-chains": ["extension-chains/src/index.ts"],
"@polkadot/extension-chains/*": ["extension-chains/src/*.ts"],
"@polkadot/extension-compat-metamask": ["extension-compat-metamask/src/index.ts"],
"@polkadot/extension-dapp": ["extension-dapp/src/index.ts"],
"@polkadot/extension-dapp/*": ["extension-dapp/src/*.ts"],
"@polkadot/extension-inject": ["extension-inject/src/index.ts"],
-1
View File
@@ -9,7 +9,6 @@
{ "path": "./packages/extension-base/tsconfig.build.json" },
{ "path": "./packages/extension-base/tsconfig.spec.json" },
{ "path": "./packages/extension-chains/tsconfig.build.json" },
{ "path": "./packages/extension-compat-metamask/tsconfig.build.json" },
{ "path": "./packages/extension-dapp/tsconfig.build.json" },
{ "path": "./packages/extension-dapp/tsconfig.spec.json" },
{ "path": "./packages/extension-inject/tsconfig.build.json" },
+22 -22
View File
@@ -1153,29 +1153,29 @@ __metadata:
languageName: unknown
linkType: soft
"@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz":
version: 14.0.3-quantus.2
resolution: "@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz"
"@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.3/keyring-14.0.3-quantus.3.tgz":
version: 14.0.3-quantus.3
resolution: "@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.3/keyring-14.0.3-quantus.3.tgz"
dependencies:
"@polkadot/util": "npm:14.0.3"
"@polkadot/util-crypto": "npm:14.0.3-quantus.2"
"@quantus/crypto": "npm:^0.1.0"
"@polkadot/util-crypto": "npm:14.0.3-quantus.3"
"@quantus/crypto": "npm:^0.3.0"
tslib: "npm:^2.8.0"
peerDependencies:
"@polkadot/util": 14.0.3
"@polkadot/util-crypto": 14.0.3
checksum: 10/e3885d6a75b4c006887d24dadbfc25fff562baae84d42fd2125878af60daecbf789deed12c975edfef0884a3caff4dd469dcdcd8b63eacbd1a5fbf8bbfe48815
checksum: 10/2264245c4fc2505c852d5a874bda2fcd86a1269cb6545f7bb578a6efdf58beea3deccf28d27ba6e41f9fdf9a8ea50cf230c626b69afe775a7629e069149164f7
languageName: node
linkType: hard
"@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz":
version: 14.0.3-quantus.2
resolution: "@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz"
"@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.3/networks-14.0.3-quantus.3.tgz":
version: 14.0.3-quantus.3
resolution: "@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.3/networks-14.0.3-quantus.3.tgz"
dependencies:
"@polkadot/util": "npm:14.0.3"
"@substrate/ss58-registry": "npm:^1.51.0"
tslib: "npm:^2.8.0"
checksum: 10/3d2f0a08cde4be418d073f63587bed451a6ce4c91a8872a5e0c4191ff3e62d57a7942c1a1b6c183ed9c22742f5b95b7496fe4e32e3565baf2093d879ac51a55e
checksum: 10/04228325c8472901fc04e5570e6d3450539c4ee51bbbae5c6eea945c721dc86b4bd9a581503e9f65c0132317ecc3c915b7f2c4cc2774b169f49d6eb2524bd8c2
languageName: node
linkType: hard
@@ -1364,14 +1364,14 @@ __metadata:
languageName: node
linkType: hard
"@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz":
version: 3.16.7-quantus.2
resolution: "@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz"
"@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.3/ui-keyring-3.16.7-quantus.3.tgz":
version: 3.16.7-quantus.3
resolution: "@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.3/ui-keyring-3.16.7-quantus.3.tgz"
dependencies:
"@polkadot/keyring": "npm:14.0.3-quantus.2"
"@polkadot/keyring": "npm:14.0.3-quantus.3"
"@polkadot/ui-settings": "npm:3.16.7"
"@polkadot/util": "npm:^14.0.3"
"@polkadot/util-crypto": "npm:14.0.3-quantus.2"
"@polkadot/util-crypto": "npm:14.0.3-quantus.3"
mkdirp: "npm:^3.0.1"
rxjs: "npm:^7.8.1"
store: "npm:^2.0.12"
@@ -1380,7 +1380,7 @@ __metadata:
"@polkadot/keyring": "*"
"@polkadot/ui-settings": "*"
"@polkadot/util": "*"
checksum: 10/bd62eb7fb2ad152c8f6844938af34fa75fa6b359d5414cbaca0d77c80b808f8a4e51f62b223cbbb1cbe8feefab43b42db86dd978af92dcce3bab5236cdb3a885
checksum: 10/bc68714552d1702344fca5378713ba804987931e5f55a1b1dbd14905e7608bf918bac92f5d909895c7b44ff08afc61e95a104a29b81bf9aabfba181cae6cffb0
languageName: node
linkType: hard
@@ -1413,25 +1413,25 @@ __metadata:
languageName: node
linkType: hard
"@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz":
version: 14.0.3-quantus.2
resolution: "@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz"
"@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.3/util-crypto-14.0.3-quantus.3.tgz":
version: 14.0.3-quantus.3
resolution: "@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.3/util-crypto-14.0.3-quantus.3.tgz"
dependencies:
"@noble/curves": "npm:^1.3.0"
"@noble/hashes": "npm:^1.3.3"
"@polkadot/networks": "npm:14.0.3-quantus.2"
"@polkadot/networks": "npm:14.0.3-quantus.3"
"@polkadot/util": "npm:14.0.3"
"@polkadot/wasm-crypto": "npm:^7.5.3"
"@polkadot/wasm-util": "npm:^7.5.3"
"@polkadot/x-bigint": "npm:14.0.3"
"@polkadot/x-randomvalues": "npm:14.0.3"
"@quantus/crypto": "npm:^0.1.0"
"@quantus/crypto": "npm:^0.3.0"
"@scure/base": "npm:^1.1.7"
"@scure/sr25519": "npm:^0.2.0"
tslib: "npm:^2.8.0"
peerDependencies:
"@polkadot/util": 14.0.3
checksum: 10/78f04147ddfe8ae0bca4cba2f235715615a140de9ab95f10d6e4bbabee4a2865e6f8a84f70668722ab688094a8519eeb019ed5122620b355a3ea40b0d8cfdeb8
checksum: 10/ffbbb1a45a45abf51376276674e223b1adbbb9355b8881811c558d428ca84e6d07370d11ff07329cf5c32032b4c710a955b84c02462b6c4f0dc52b5f1117e17e
languageName: node
linkType: hard