Extrinsic signing no longer goes through ExtrinsicPayload.sign(pair). That helper
calls pair.sign(encoded, { withType: true }) and has nowhere to put a signing
context — and an ML-DSA pair refuses to sign without one, so before this the
extension simply threw.
The two lines it would have run are reproduced here with the context added:
encode the payload, BLAKE2b-256 it if it exceeds 256 bytes (Substrate's own rule
from unchecked_extrinsic.rs, which signer and runtime must apply identically),
then sign. Forking @polkadot/types to thread an option through was the
alternative, and is a far larger commitment for the same result.
The context comes from the payload's specVersion, because that is the only place
it can come from: spec >= 148 verifies under QUANTUS_EXTRINSIC, earlier specs
under the empty context, and only the caller knows which runtime it is talking
to. Getting it wrong produces a signature that is cryptographically valid,
rejected by the chain, and indistinguishable from a correct one without asking a
node — which is why both directions are pinned by tests rather than assumed.
Raw bytes use the **empty** context, deliberately not the extrinsic one. A
signature made for a dapp login must never be replayable as a transfer, and the
context is bound into the signature itself. u8aWrapBytes stays, but it is not
what provides that guarantee — it is polkadot-js's own <Bytes> wrapping, kept so
a verifier written against that convention still sees what it expects. For ML-DSA
the context is authoritative: it cannot be stripped or forgotten by a verifier
the way an in-band wrapper can.
No Quantus-specific bytes context is invented. Nothing else in the ecosystem
defines one — the chain and the SDK name only QUANTUS_EXTRINSIC — and making one
up would produce signatures no other Quantus tool could verify.
Raw signing returns sig||pk, which is not a convenience: the account id is a
one-way Poseidon2 hash of the public key, so a verifier holding only an address
cannot recover the key. The key has to travel with the signature. Pinned by a
test that verifies against the signer's address and nothing else.
79 tests pass; typecheck, lint and build:chrome clean.
Refs quantus/extension#4, quantus/common#5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
polkadot{.js} extension
A very simple scaffolding browser extension that injects a @polkadot/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.
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.
Installation
- On Chrome, install via Chrome web store
- On Firefox, install via Firefox add-ons
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
Firefox installation from source instructions.
- Uncompress
master-ff-src.zip - Run
corepack enableMore information - Install dependencies via
yarn install - Build all packages via
yarn build
- The
/packages/extension/builddirectory will contain the exact code used in the add-on, and should exactly match the uncompressedmaster-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:
- Chrome:
- Build via
yarn build:chrome
- NOTE: You may need to enable corepack by running
corepack enable
- 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
- Build via
- Firefox
- Build via
yarn build:ff
- NOTE: You may need to enable corepack by running
corepack enable
- 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
- Build via
- 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, when loaded, will show these accounts as <account name> (extension)
Development
The repo is split into a number of packages -
- extension - All the injection and background processing logic (the main entry)
- extension-ui - The UI components for the extension, to build up the popup
- 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 - A convenience wrapper that allows extension developers to inject their extension for use by any dapp
It also contains a manifest_chrome.json file which contains the manifest configuration for Chrome and another manifest_firefox.json with the configuration for Firefox, for compatibility reasons, and a dummy manifest.json file that's only used by the build.
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.
This approach is used to support multiple external signers in for instance apps. You can read more about the convenience wrapper @polkadot/extension-dapp along with usage samples.
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 `polkadot-js` is for this extension
'polkadot-js': {
// 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>
}
}
Mnemonics, Passwords, and Imports/Exports
Using the mnemonic and password from the extension
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, 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.
