From b66b0a65c875491ccaff4e5b0c2bc70f9f1bb699 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 16 Sep 2026 19:51:02 +0300 Subject: [PATCH] feat: offer only signers that can sign for Quantus The Quantus runtime accepts only ML-DSA signatures. qapi offered every signer polkahub knows, and none of those could sign here: - Polkadot Vault, Ledger and WalletConnect produce sr25519, ed25519 or ecdsa signatures (and WalletConnect was configured only for Polkadot, Kusama and Paseo); - the external multisig signer hands signing to multisig.usepapi.app; - the browser-extension list offered polkadot{.js}, Nova, Talisman, SubWallet, PolkaGate and Fearless. These providers, their management buttons, and the Vault and multisig modals are removed. What remains: browser extensions, read-only (watch) addresses, and proxies, which sign through whichever signer they wrap. @polkahub/pjs-wallet is patched (pinned to 0.9.1, like the substrate-bindings patches) so the extension list is blackbeard alone, with its sigil as the logo. Its provider only ever sees, offers or connects blackbeard: anything else injected into window.injectedWeb3 is ignored, not merely unlabelled. Verified against a local build in Firefox with blackbeard installed. The Account Providers panel shows blackbeard (with the sigil), Address and Proxy, and nothing else, including after fake polkadot-js and talisman entries were injected into the page and the provider's 2 s re-scan had run. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f --- patches/@polkahub__pjs-wallet@0.9.1.patch | 89 +++++++++++++++++ pnpm-lock.yaml | 5 +- pnpm-workspace.yaml | 1 + src/App.tsx | 2 - src/main.tsx | 3 +- src/pages/Accounts/AccountList.tsx | 11 --- src/pages/Accounts/Providers.tsx | 10 -- .../Extrinsics/SubmitTx/SubmitTxForm.tsx | 4 +- src/state/polkahub.ts | 97 ++----------------- 9 files changed, 101 insertions(+), 121 deletions(-) create mode 100644 patches/@polkahub__pjs-wallet@0.9.1.patch diff --git a/patches/@polkahub__pjs-wallet@0.9.1.patch b/patches/@polkahub__pjs-wallet@0.9.1.patch new file mode 100644 index 0000000..c18483d --- /dev/null +++ b/patches/@polkahub__pjs-wallet@0.9.1.patch @@ -0,0 +1,89 @@ +diff --git a/dist/ManagePjsWallet.js b/dist/ManagePjsWallet.js +index ec891b30a0bda8eb60b0ec647262ad334c04db89..03c93af0448018b58ef8abc2395e56b330d64038 100644 +--- a/dist/ManagePjsWallet.js ++++ b/dist/ManagePjsWallet.js +@@ -4,38 +4,16 @@ import { cn, SourceButton } from '@polkahub/ui-components'; + import { state, useStateObservable } from '@react-rxjs/core'; + import { CircleQuestionMark } from 'lucide-react'; + import { filter, switchMap } from 'rxjs'; +-import nova from './assets/nova.webp.js'; +-import pjs from './assets/pjs.webp.js'; +-import subwallet from './assets/subwallet.webp.js'; +-import talisman from './assets/talisman.webp.js'; +-import polkagate from './assets/polkagate.webp.js'; +-import fearless from './assets/fearless.webp.js'; ++import blackbeard from './assets/blackbeard.svg.js'; + import { pjsWalletProviderId } from './provider.js'; + ++// Only extensions that sign ML-DSA are listed: the Quantus runtime accepts no ++// other signature, so offering polkadot{.js}, Talisman and the rest would ++// connect wallets that can never sign here. Patched in qapi. + const knownExtensions = { +- "polkadot-js": { +- name: "Polkadot JS", +- logo: pjs +- }, +- "nova-wallet": { +- name: "Nova Wallet", +- logo: nova +- }, +- talisman: { +- name: "Talisman", +- logo: talisman +- }, +- "subwallet-js": { +- name: "Subwallet", +- logo: subwallet +- }, +- polkagate: { +- name: "Polkagate", +- logo: polkagate +- }, +- "fearless-wallet": { +- name: "Fearless Wallet", +- logo: fearless ++ blackbeard: { ++ name: "blackbeard", ++ logo: blackbeard + } + }; + const [pjsWalletPlugin$, useExternalizedPlugin] = externalizePlugin(pjsWalletProviderId); +diff --git a/dist/assets/blackbeard.svg.js b/dist/assets/blackbeard.svg.js +new file mode 100644 +index 0000000000000000000000000000000000000000..4dfb32200a16b6c32b258d0519cd6c820a3bf9ab +--- /dev/null ++++ b/dist/assets/blackbeard.svg.js +@@ -0,0 +1,4 @@ ++// blackbeard's sigil, the Quantus post-quantum wallet (quantus/extension#15) ++var blackbeard = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCIgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIj4KICA8IS0tIGJsYWNrYmVhcmQub2JzZXJ2ZXIncyBzaWdpbCwgY2VudHJlZCBvbiBhIHNxdWFyZSBmb3IgaWNvbnMgLS0+CiAgPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiByeD0iNiIgZmlsbD0iIzBkMGIwOSIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMgMCkiPgogICAgPHBhdGggZD0iTTE3IDEgTDMyIDcgVjIwIEMzMiAyOSAyNSAzNSAxNyAzOSBDOSAzNSAyIDI5IDIgMjAgVjcgWiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjYmQ4ODI5IiBzdHJva2Utd2lkdGg9IjEuNiIvPgogICAgPHBhdGggZD0iTTE3IDggTDI0IDEyIFYyMSBDMjQgMjYgMjAuNSAyOS41IDE3IDMxLjUgQzEzLjUgMjkuNSAxMCAyNiAxMCAyMSBWMTIgWiIgZmlsbD0iI2JkODgyOTIyIi8+CiAgICA8cGF0aCBkPSJNMTcgMTEgVjI4IiBzdHJva2U9IiNkODQ1M2EiIHN0cm9rZS13aWR0aD0iMS42Ii8+CiAgICA8cGF0aCBkPSJNMTMgMTUgTDE3IDExIEwyMSAxNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDg0NTNhIiBzdHJva2Utd2lkdGg9IjEuNiIvPgogIDwvZz4KPC9zdmc+Cg=="; ++ ++export { blackbeard as default }; +diff --git a/dist/provider.js b/dist/provider.js +index a0a44532893fc9b9b3d9c449c40362e60cfa3b95..d8f90bf491ea5fc4f708d456d2c3037255bc6dc8 100644 +--- a/dist/provider.js ++++ b/dist/provider.js +@@ -6,6 +6,10 @@ import { getSs58AddressInfo } from 'polkadot-api'; + import { concat, timer, map, filter, take, interval, switchMap, defer, retry, fromEventPattern, startWith, catchError, tap, ignoreElements, NEVER, shareReplay, firstValueFrom, scan } from 'rxjs'; + + const pjsWalletProviderId = "pjs-wallet"; ++// Patched in qapi: only extensions that can sign for a post-quantum chain are ++// offered or connected. Anything else injected into the page is ignored. ++const PQ_EXTENSIONS = ["blackbeard"]; ++const getPqExtensions = () => getInjectedExtensions().filter((name) => PQ_EXTENSIONS.includes(name)); + const createPjsWalletProvider = (opts) => { + const { persist, accountFormat } = { + persist: localStorageProvider("pjs-wallet-plugin"), +@@ -15,11 +19,11 @@ const createPjsWalletProvider = (opts) => { + const availableExtensions$ = state( + concat( + timer(0, 100).pipe( +- map(getInjectedExtensions), ++ map(getPqExtensions), + filter((v) => v.length > 0), + take(1) + ), +- interval(2e3).pipe(map(getInjectedExtensions)) ++ interval(2e3).pipe(map(getPqExtensions)) + ), + [] + ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf2b7dd..0ad3d7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,7 @@ patchedDependencies: '@polkadot-api/signers-common': 365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37 '@polkadot-api/substrate-bindings@0.21.0': 44a839dff72a3f1f9334c05ea6c72f29757e5d7168e8fee564f12de216710537 '@polkadot-api/substrate-bindings@0.21.1': 44a839dff72a3f1f9334c05ea6c72f29757e5d7168e8fee564f12de216710537 + '@polkahub/pjs-wallet@0.9.1': f359348c980dcf8a9f7077a067ffbc79e23c166172a4fa1be36de5340072104d react18-json-view: 962696fac5fe7ad3e94fd36dad07521c6c3eafd52f219ab335d52438387f6b66 importers: @@ -5741,7 +5742,7 @@ snapshots: - supports-color - utf-8-validate - '@polkahub/pjs-wallet@0.9.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@polkahub/pjs-wallet@0.9.1(patch_hash=f359348c980dcf8a9f7077a067ffbc79e23c166172a4fa1be36de5340072104d)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@polkadot-api/pjs-signer': 0.8.0(rxjs@7.8.2) '@polkahub/context': 0.9.1(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -9288,7 +9289,7 @@ snapshots: '@polkahub/context': 0.9.1(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@polkahub/ledger': 0.9.1(@ledgerhq/hw-transport@6.35.5)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@polkahub/multisig': 0.9.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@polkahub/pjs-wallet': 0.9.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@polkahub/pjs-wallet': 0.9.1(patch_hash=f359348c980dcf8a9f7077a067ffbc79e23c166172a4fa1be36de5340072104d)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@polkahub/plugin': 0.9.1(@react-rxjs/core@0.10.8(react@19.2.8)(rxjs@7.8.2))(esbuild@0.28.1)(react@19.2.8) '@polkahub/proxy': 0.9.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@polkahub/read-only': 0.9.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0c3647a..8aedb0b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,4 +18,5 @@ patchedDependencies: # and patched too so a minor bump does not silently drop the fix. '@polkadot-api/substrate-bindings@0.21.0': patches/@polkadot-api__substrate-bindings@0.21.0.patch '@polkadot-api/substrate-bindings@0.21.1': patches/@polkadot-api__substrate-bindings@0.21.1.patch + '@polkahub/pjs-wallet@0.9.1': patches/@polkahub__pjs-wallet@0.9.1.patch react18-json-view: patches/react18-json-view.patch diff --git a/src/App.tsx b/src/App.tsx index 8ffbaf1..b9ff199 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ -import { VaultTxModal } from "polkahub" import { lazy } from "react" import { Navigate, Route, Routes } from "react-router-dom" import { ConnectionState } from "./components/ConnectionState" @@ -35,7 +34,6 @@ export default function App() { } /> - ) diff --git a/src/main.tsx b/src/main.tsx index 0561f70..2892b43 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import { dynamicBuilder$, getDefaultChain } from "@/state/chains/chain.state" import { RemoveSubscribe, Subscribe } from "@react-rxjs/core" -import { MultisigExternalSignerModal, PolkaHubProvider } from "polkahub" +import { PolkaHubProvider } from "polkahub" import { StrictMode } from "react" import { createRoot } from "react-dom/client" import { BrowserRouter } from "react-router-dom" @@ -43,7 +43,6 @@ createRoot(document.getElementById("root")!).render( - diff --git a/src/pages/Accounts/AccountList.tsx b/src/pages/Accounts/AccountList.tsx index b1b69f8..2c30499 100644 --- a/src/pages/Accounts/AccountList.tsx +++ b/src/pages/Accounts/AccountList.tsx @@ -16,12 +16,9 @@ import { CompatibilityLevel, SS58String } from "polkadot-api" import { toHex } from "polkadot-api/utils" import { Account, - ledgerProviderId, PjsWalletAccount, - polkadotVaultProviderId, readOnlyProviderId, useAvailableAccounts, - walletConnectProviderId, } from "polkahub" import { FC } from "react" import { @@ -34,15 +31,11 @@ import { const knownGroupsNames: Record = { "pjs-wallet": "Browser Extensions", - [ledgerProviderId]: "Ledger", [readOnlyProviderId]: "Read Only", - [polkadotVaultProviderId]: "Polkadot Vault", - [walletConnectProviderId]: "Wallet Connect", } const knownGroupsColors: Record = { "pjs-wallet": "var(--color-lime-700)", [readOnlyProviderId]: "var(--color-orange-700)", - [walletConnectProviderId]: "var(--color-blue-700)", } export const AccountList = () => { @@ -96,10 +89,6 @@ const AccountCard: FC<{ } export const knownExtensions: Record = { blackbeard: "blackbeard", - "polkadot-js": "Polkadot JS", - "nova-wallet": "Nova Wallet", - talisman: "Talisman", - "subwallet-js": "Subwallet", } const getExtensionName = (id: string) => knownExtensions[id] ?? id const getGroupName = (id: string) => knownGroupsNames[id] ?? id diff --git a/src/pages/Accounts/Providers.tsx b/src/pages/Accounts/Providers.tsx index 0e541ef..542a94c 100644 --- a/src/pages/Accounts/Providers.tsx +++ b/src/pages/Accounts/Providers.tsx @@ -8,15 +8,11 @@ import { } from "@polkahub/ui-components" import { ChevronLeft } from "lucide-react" import { - ManageLedger, - ManageMultisig, ManageProxy, ManageReadOnly, - ManageVault, ModalContext, PjsWalletButtons, usePolkaHubModalState, - WalletConnectButton, } from "polkahub" import { FC, PropsWithChildren } from "react" @@ -71,15 +67,9 @@ const ManageToDialog: FC = ({ children }) => { export const PolkahubModalBasedManagers = () => ( <> -
    - - - -
    -
) diff --git a/src/pages/Extrinsics/SubmitTx/SubmitTxForm.tsx b/src/pages/Extrinsics/SubmitTx/SubmitTxForm.tsx index 86cc05c..58c25e2 100644 --- a/src/pages/Extrinsics/SubmitTx/SubmitTxForm.tsx +++ b/src/pages/Extrinsics/SubmitTx/SubmitTxForm.tsx @@ -8,10 +8,8 @@ import { import { FC } from "react" const groupLabels: Record = { - ledger: "Ledger", + "pjs-wallet": "Browser Extensions", readonly: "Read Only", - "polkadot-vault": "Vault", - walletconnect: "Wallet Connect", } export const SelectAccount: FC<{ diff --git a/src/state/polkahub.ts b/src/state/polkahub.ts index 54662a4..126e08d 100644 --- a/src/state/polkahub.ts +++ b/src/state/polkahub.ts @@ -1,20 +1,13 @@ import { polkadot_people } from "@polkadot-api/descriptors" import { liftSuspense, state, SUSPENSE, withDefault } from "@react-rxjs/core" import { AccountId, Binary, HexString, SS58String } from "polkadot-api" -import { WsEvent } from "polkadot-api/ws" import { Account, - createLedgerProvider, - createMultisigProvider, createPjsWalletProvider, - createPolkadotVaultProvider, createPolkaHub, createProxyProvider, createReadOnlyProvider, createSelectedAccountPlugin, - createWalletConnectProvider, - knownChains, - multisigExternalSigner, Plugin, } from "polkahub" import { @@ -23,20 +16,15 @@ import { firstValueFrom, map, pipe, - startWith, switchMap, } from "rxjs" import { chainProperties$ } from "./chain-props.state" import { canSetStorage$, client$, - currentWsStatus$, - getChainSource, - selectedChain$, unsafeApi$, } from "./chains/chain.state" import { identity$, isVerified } from "./identity.state" -import { WebsocketSource } from "./chains/websocket" const removeSuspense = () => pipe( @@ -44,23 +32,8 @@ const removeSuspense = () => filter((v): v is T => v !== SUSPENSE), ) -const getNetworkInfo = () => - firstValueFrom( - chainProperties$.pipe( - removeSuspense(), - filter((v) => v != null), - map(({ tokenDecimals, tokenSymbol }) => ({ - decimals: tokenDecimals!, - tokenSymbol: tokenSymbol!, - })), - ), - ) - const selectedAccountPlugin = createSelectedAccountPlugin() const pjsWalletProvider = createPjsWalletProvider() -const polkadotVaultProvider = createPolkadotVaultProvider({ - getNetworkInfo, -}) const readOnlyProvider$ = canSetStorage$.pipe( liftSuspense(), filter((v) => v !== SUSPENSE), @@ -70,21 +43,6 @@ const readOnlyProvider$ = canSetStorage$.pipe( }), ), ) -const ledgerAccountProvider = createLedgerProvider(async () => { - const module = await import("@ledgerhq/hw-transport-webusb") - return module.default.create() -}, getNetworkInfo) -const walletConnectProvider = createWalletConnectProvider( - import.meta.env.VITE_REOWN_PROJECT_ID, - [ - knownChains.polkadot, - knownChains.polkadotAh, - knownChains.kusama, - knownChains.kusamaAh, - knownChains.paseo, - knownChains.paseoAh, - ], -) const proxyProvider = createProxyProvider((address) => firstValueFrom( unsafeApi$.pipe( @@ -95,61 +53,18 @@ const proxyProvider = createProxyProvider((address) => ), ), ) -const multisigProvider$ = selectedChain$.pipe( - switchMap((chain) => - combineLatest([getChainSource(chain), currentWsStatus$]), - ), - map(([source, wsStatus]) => { - if (source.type === "websocket" && source.forkMethod !== "none") { - return null - } - - if ( - source.type === "chainSpec" && - ![ - "polkadot", - "westend", - "kusama", - "paseo", - "polkadot_asset_hub", - "westend_asset_hub", - "kusama_asset_hub", - "paseo_asset_hub", - ].includes(source.id) - ) { - return null - } - - const getWsUri = (source: WebsocketSource) => { - if (wsStatus?.type === WsEvent.CONNECTED) return wsStatus.uri - return typeof source.endpoint === "string" - ? source.endpoint - : source.endpoint[0] - } - - const chain = - source.type === "chainSpec" ? `sm-${source.id}` : `ws-${getWsUri(source)}` - - return createMultisigProvider( - multisigExternalSigner( - (info, callData) => - `https://multisig.usepapi.app/?chain=${chain}&calldata=${callData}&signatories=${info.signatories.join("_")}&threshold=${info.threshold}`, - ), - ) - }), - startWith(null), -) - +// Signers that cannot sign for Quantus are not offered. Polkadot Vault, Ledger +// and WalletConnect produce sr25519/ed25519/ecdsa signatures, and the external +// multisig signer hands signing to multisig.usepapi.app; the runtime accepts +// only ML-DSA. What remains: browser extensions (blackbeard, the only one that +// signs ML-DSA), read-only accounts, and proxies, which sign through whichever +// of those they wrap. export const polkaHub = createPolkaHub( combineLatest([ [selectedAccountPlugin], [pjsWalletProvider], - [polkadotVaultProvider], readOnlyProvider$, - [ledgerAccountProvider], - [walletConnectProvider], [proxyProvider], - multisigProvider$, ]).pipe(map((v: (Plugin | null)[]) => v.filter((v) => v != null))), { getIdentity: (address) =>