forked from polkadot-js/apps
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: hybrid signature and mnemonic import #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ruseinov
wants to merge
5
commits into
master
Choose a base branch
from
v0.2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| [submodule "quip-protocol-rs"] | ||
| path = quip-protocol-rs | ||
| url = git@gitlab.com:quip.network/quip-protocol-rs.git |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Quip hybrid-signature integration for the polkadot-js apps fork. | ||
| # | ||
| # The Quip transaction signer (sr25519 + ML-DSA-44 hybrid) lives in the | ||
| # `quip-protocol-rs` git submodule, pinned to a specific commit. Its browser | ||
| # WASM is a generated, git-ignored artifact, so it must be built locally before | ||
| # the dev signer (packages/apps/src/initQuipSigner.ts) can load it. | ||
| # | ||
| # Usage: | ||
| # make quip-signer # init submodule + (re)build the hybrid-signer WASM | ||
| # make start # build WASM if missing, then run the dev server with | ||
| # # the Quip hybrid signer enabled | ||
| # | ||
| # Requires `wasm-pack` (cargo install wasm-pack) and the Rust toolchain. | ||
|
|
||
| QUIP_SUBMODULE := quip-protocol-rs | ||
| WASM_OUT := $(QUIP_SUBMODULE)/js/quip-transaction-crypto-wasm/quip_transaction_crypto_wasm_bg.wasm | ||
|
|
||
| .PHONY: all quip-signer quip-submodule start | ||
|
|
||
| # Default target builds everything needed for hybrid-sig support. | ||
| all: quip-signer | ||
|
|
||
| # Check out the submodule at its pinned commit (idempotent). | ||
| quip-submodule: | ||
| git submodule update --init $(QUIP_SUBMODULE) | ||
|
|
||
| # Build the git-ignored hybrid-signer WASM inside the submodule. The submodule's | ||
| # own `wasm-signer` target runs wasm-pack and writes the artifacts into | ||
| # quip-protocol-rs/js/quip-transaction-crypto-wasm/, which is exactly where | ||
| # initQuipSigner.ts imports them from. Always rebuilds. | ||
| quip-signer: quip-submodule | ||
| $(MAKE) -C $(QUIP_SUBMODULE) wasm-signer | ||
| @echo "Hybrid signer WASM ready. Enable it in the apps with QUIP_DEV_SIGNER=1 (or ?quipSigner)." | ||
|
|
||
| # Build the WASM only when it is missing (so `make start` doesn't recompile the | ||
| # crate every run). Run `make quip-signer` explicitly to force a rebuild. | ||
| $(WASM_OUT): | ||
| $(MAKE) quip-signer | ||
|
|
||
| # Start the apps dev server (webpack-serve on :3000) with the Quip hybrid signer | ||
| # injected. Builds the signer WASM first if it isn't present yet. | ||
| start: $(WASM_OUT) | ||
| QUIP_DEV_SIGNER=1 yarn start |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // Copyright 2017-2026 @polkadot/apps authors & contributors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| const ENABLED_VALUES = new Set(['1', 'true', 'yes', 'on']); | ||
| const STORAGE_KEY = 'quip:devSigner'; | ||
|
|
||
| const DEV_SEEDS = [ | ||
| { | ||
| name: 'Quip Alice', | ||
| seedHex: '0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a' | ||
| }, | ||
| { | ||
| name: 'Quip Bob', | ||
| seedHex: '0x398f0c28f98885e046333d4a41c19cee4c37368a9832c6502f6cfd182e2aef89' | ||
| }, | ||
| { | ||
| name: 'Quip Alice Stash', | ||
| seedHex: '0x3c881bc4d45926680c64a7f9315eeda3dd287f8d598f3653d7c107799c5422b3' | ||
| } | ||
| ]; | ||
|
|
||
| interface QuipDevProvider { | ||
| importMnemonic: ( | ||
| name: string, | ||
| mnemonic: string, | ||
| genesisHash?: string | null | ||
| ) => Promise<{ address: string }>; | ||
| } | ||
|
|
||
| /** | ||
| * Cross-package handle published on `window` so the page-accounts UI can import | ||
| * Quip accounts without `page-accounts` importing back into the `apps` package | ||
| * (which would be a circular dependency). | ||
| */ | ||
| export interface QuipSignerUiApi { | ||
| importMnemonic: (name: string, mnemonic: string) => Promise<string>; | ||
| } | ||
|
|
||
| declare global { | ||
| // eslint-disable-next-line no-var | ||
| var quipSigner: QuipSignerUiApi | undefined; | ||
| } | ||
|
|
||
| let isInjected = false; | ||
| let quipProvider: QuipDevProvider | null = null; | ||
|
|
||
| function isEnabledValue (value: string | null | undefined): boolean { | ||
| return !!value && ENABLED_VALUES.has(value.toLowerCase()); | ||
| } | ||
|
|
||
| function isEnabledByQuery (): boolean { | ||
| const params = new URLSearchParams(window.location.search); | ||
|
|
||
| return ['quipSigner', 'quip-signer', 'quipDevSigner'].some((key) => { | ||
| if (!params.has(key)) { | ||
| return false; | ||
| } | ||
|
|
||
| const value = params.get(key); | ||
|
|
||
| return value === '' || value === null || isEnabledValue(value); | ||
| }); | ||
| } | ||
|
|
||
| function isEnabledByStorage (): boolean { | ||
| try { | ||
| return isEnabledValue(window.localStorage.getItem(STORAGE_KEY)); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| function shouldInjectQuipSigner (): boolean { | ||
| return isEnabledValue(process.env.QUIP_DEV_SIGNER) || | ||
| isEnabledByQuery() || | ||
| isEnabledByStorage(); | ||
| } | ||
|
|
||
| export async function initQuipSigner (): Promise<void> { | ||
| if (isInjected || !shouldInjectQuipSigner()) { | ||
| return; | ||
| } | ||
|
|
||
| isInjected = true; | ||
|
|
||
| const [signerModule, wasmModule] = await Promise.all([ | ||
| import('../../../quip-protocol-rs/js/quip-signer/src/index.js'), | ||
| import('../../../quip-protocol-rs/js/quip-transaction-crypto-wasm/quip_transaction_crypto_wasm.js') | ||
| ]); | ||
|
|
||
| await wasmModule.default(); | ||
|
|
||
| // Quip's hybrid signature (3828 bytes) is larger than polkadot-js's hardcoded | ||
| // 256-byte fake signature, which breaks `paymentInfo`/fee estimation. Patch | ||
| // signFake to size the fake from the registry before any tx flow runs. | ||
| signerModule.patchExtrinsicSignFake(); | ||
|
|
||
| const { accounts, provider } = await signerModule.DevSeedProvider.fromSeeds(wasmModule, DEV_SEEDS); | ||
|
|
||
| signerModule.injectQuip({ | ||
| accounts, | ||
| signer: new signerModule.QuipSigner(provider) | ||
| }); | ||
|
|
||
| quipProvider = provider; | ||
| globalThis.quipSigner = { importMnemonic: importQuipMnemonic }; | ||
|
|
||
| console.info(`Quip dev signer injected ${accounts.length} account${accounts.length === 1 ? '' : 's'}`); | ||
| } | ||
|
|
||
| /** Whether the Quip dev signer has been injected this session. */ | ||
| export function isQuipSignerActive (): boolean { | ||
| return quipProvider !== null; | ||
| } | ||
|
|
||
| /** | ||
| * Imports a Quip account from a BIP39 phrase (or `0x` seed hex), registering | ||
| * its seed with the injected Quip signer and adding it to the keyring so it | ||
| * appears in the UI and signs through the injected signer. | ||
| */ | ||
| export async function importQuipMnemonic (name: string, mnemonic: string): Promise<string> { | ||
| if (!quipProvider) { | ||
| throw new Error('Quip dev signer is not active'); | ||
| } | ||
|
|
||
| const { address } = await quipProvider.importMnemonic(name, mnemonic, null); | ||
|
|
||
| const { keyring } = await import('@polkadot/ui-keyring'); | ||
|
|
||
| // Use the same path the keyring uses for extension accounts: `loadInjected` | ||
| // sets `meta.isInjected` (so react-signer routes signing through the injected | ||
| // Quip signer) and updates the live account subject so the UI refreshes | ||
| // without a reload. `addExternal` would instead set `isExternal`, which | ||
| // routes to the QR signer. `loadInjected` is not in the public typings. | ||
| (keyring as unknown as { | ||
| loadInjected: (address: string, meta: Record<string, unknown>, type?: string) => void; | ||
| }).loadInjected(address, { name, source: 'quip' }); | ||
|
|
||
| return address; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
packages/apps/src/initQuipSigner.ts:84 —
isInjectedis set totruebefore the async dynamic imports/wasm init. If any of those steps throw, the session will be permanently marked as injected and later calls toinitQuipSigner()won’t retry even though nothing was actually injected.Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.