feat: offer only signers that can sign for Quantus
All checks were successful
deploy / build (push) Successful in 2m25s
deploy / deploy-web (push) Successful in 8s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
2026-09-16 19:51:02 +03:00
parent 9d7b002464
commit b66b0a65c8
9 changed files with 101 additions and 121 deletions

View File

@@ -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))
),
[]
);

5
pnpm-lock.yaml generated
View File

@@ -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)

View File

@@ -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

View File

@@ -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() {
<Route path="*" element={<Navigate to="/explorer" replace />} />
</Routes>
</AppShell>
<VaultTxModal />
<ConnectionState />
</div>
)

View File

@@ -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(
<App />
</TooltipProvider>
</BrowserRouter>
<MultisigExternalSignerModal />
</PolkaHubProvider>
</ThemeProvider>
</StrictMode>

View File

@@ -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<string, string> = {
"pjs-wallet": "Browser Extensions",
[ledgerProviderId]: "Ledger",
[readOnlyProviderId]: "Read Only",
[polkadotVaultProviderId]: "Polkadot Vault",
[walletConnectProviderId]: "Wallet Connect",
}
const knownGroupsColors: Record<string, string> = {
"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<string, string> = {
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

View File

@@ -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<PropsWithChildren> = ({ children }) => {
export const PolkahubModalBasedManagers = () => (
<>
<ul className="flex gap-2 flex-wrap items-center justify-center">
<ManageLedger />
<ManageVault />
<WalletConnectButton />
</ul>
<ul className="flex gap-2 flex-wrap items-center justify-center">
<ManageReadOnly />
<ManageProxy />
<ManageMultisig />
</ul>
</>
)

View File

@@ -8,10 +8,8 @@ import {
import { FC } from "react"
const groupLabels: Record<string, string> = {
ledger: "Ledger",
"pjs-wallet": "Browser Extensions",
readonly: "Read Only",
"polkadot-vault": "Vault",
walletconnect: "Wallet Connect",
}
export const SelectAccount: FC<{

View File

@@ -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 = <T>() =>
pipe(
@@ -44,23 +32,8 @@ const removeSuspense = <T>() =>
filter<T | SUSPENSE, T>((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<any> | null)[]) => v.filter((v) => v != null))),
{
getIdentity: (address) =>