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
7.9 KiB
blackbeard
A post-quantum browser wallet for the Quantus Network, forked from the 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:
- ML-DSA only. Accounts are ML-DSA-65 and ML-DSA-87 (the two variants of the runtime's
DilithiumSignatureScheme), derived asquantus-cliand 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), 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
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::UsedNullifiersso 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; #1 is the epic.
Installation
There is no store listing. Build from source (below), or use the zips a build writes to the repo root.
- Firefox:
about:debugging#/runtime/this-firefox→ Load Temporary Add-on… →master-ff-build.zip. Temporary add-ons are unloaded on restart; a permanent install of an unsigned add-on needs Developer Edition or Nightly withxpinstall.signatures.requiredset tofalse. - Chrome / Chromium / Brave: unzip
master-chrome-build.zipinto a folder, thenchrome://extensions→ Developer mode → Load unpacked → that folder. Notpackages/extension/build, which holds whichever browser was built last.
Privacy settings
Everything the extension asks the network is a setting, and each can be pointed at your own infrastructure or turned off:
- 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
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
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.
The packages keep their upstream @polkadot/extension-* names:
- extension — injection, background and manifests (the main entry)
- extension-base — the background: keyring, wallets, signing, balances
- extension-ui — the popup
- extension-dapp — a convenience wrapper for dapps using any injected extension
- extension-inject — the injection interface
Dapp developers
Use @polkadot/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
The extension injection interfaces are generic, i.e. it is designed to allow any extension developer to easily inject extensions (that conforms to a specific interface) and at the same time, it allows for any dapp developer to easily enable the interfaces from multiple extensions at the same time. It is not an all-or-nothing approach, but rather it is an ecosystem where the user can choose which extensions fit their style best.
From a dapp developer perspective, the only work needed is to include the @polkadot/extension-dapp package and call the appropriate enabling function to retrieve all the extensions and their associated interfaces.
From an extension developer perspective, the only work required is to enable the extension via the razor-thin @polkadot/extension-inject wrapper. Any dapp using the above interfaces will have access to the extension via this interface.
When there is more than one extension, each will populate an entry via the injection interface and each will be made available to the dapp. The Injected interface, as returned via enable, contains the following information for any compliant extension -
interface Injected {
// the interface for Accounts, as detailed below
readonly accounts: Accounts;
// the standard Signer interface for the API, as detailed below
readonly signer: Signer;
// not injected as of yet, subscribable provider for polkadot-js API injection,
// this can be passed to the API itself upon construction in the dapp
// readonly provider?: Provider
}
interface Account = {
// ss-58 encoded address
readonly address: string;
// the genesisHash for this account (empty if applicable to all)
readonly genesisHash?: string;
// (optional) name for display
readonly name?: string;
};
// exposes accounts
interface Accounts {
// retrieves the list of accounts for right now
get: () => Promise<Account[]>;
// (optional) subscribe to all accounts, updating as they change
subscribe?: (cb: (accounts: Account[]) => any) => () => void
}
// a signer that communicates with the extension via sendMessage
interface Signer extends SignerInterface {
// no specific signer extensions, exposes the `sign` interface for use by
// the polkadot-js API, confirming the Signer interface for this API
}
Injection information
The information contained in this section may change and evolve. It is therefore recommended that all access is done via the @polkadot/extension-dapp (for dapps) and extension-inject (for extensions) packages, which removes the need to work with the lower-level targets.
The extension injects injectedWeb3 into the global window object, exposing the following: (This is meant to be generic across extensions, allowing any dapp to utilize multiple signers, and pull accounts from multiples, as they are available.)
window.injectedWeb3 = {
// this is the name for this extension, there could be multiples injected,
// each with their own keys, here `blackbeard` is for this extension
'blackbeard': {
// semver for the package
version: '0.1.0',
// this is called to enable the injection, and returns an injected
// object containing the accounts, signer and provider interfaces
// (or it will reject if not authorized)
enable (originName: string): Promise<Injected>
}
}
Recovery phrases and passwords
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.