`SubstrateAdapter` implements the chain-adapter port for Quantus. `prepare` builds the call from a chain-neutral intent, decodes it back from its own bytes through the runtime metadata into a summary a person can check (a 32-byte account renders as the chain's SS58 address, an amount in the token's units), prices it by assembling a full extrinsic with a zeroed signature of the right size and asking `TransactionPaymentApi_query_info`, and returns the signer payload for the session to sign. `submit` refuses if the runtime spec moved or the payload no longer matches, assembles with the session's signature, and streams statuses; a dispatch error is reported at inclusion, named `Pallet::Variant` where the chain names it, so nobody waits for finality to learn a transfer failed. Reversible transfers: `reversible_for` routes through `ReversibleTransfers::schedule_transfer_with_delay`; a high-security account is routed through the pallet's default-delay call whether it asked or not, since the chain's extension refuses a plain transfer from one. `pending_reversible` lists what is waiting with the execution block from the scheduler's lookup, whose task name is the tx id itself, and `CancelReversible` cancels. Three things the dev node taught, each now written into the code: - Mortality must be anchored at the *best* block. Finality on this proof-of-work chain trails the tip by a hundred blocks, and an era born at the finalized head names a birth block whose hash the runtime resolves differently: "Transaction has a bad signature", for both schemes, while immortal transactions from the same keys passed. Probed all four scheme-by-era combinations to isolate it. - Everything the UI shows is read at the best block, with the block number and the finalized height alongside so confirmations can be shown. subxt's `at_latest()` is the finalized block; the pending list read there showed the state of a hundred blocks ago while the chain's events showed every schedule landing. - subxt's runtime-update subscription died on this node with "header not found" after a reorg and took the connection with it. It is advisory now: on error the runtime version is polled every ten heads and a change forces a reconnect, which refetches metadata. Live, against `quantus-node --dev` with the funded dev accounts: a 3 DEV transfer from crystal_alice to crystal_bob is included and moves bob's balance; a reversible transfer with a 200-block window is scheduled, shows as pending with its execution block, and is cancelled; an overspend fails as `Token(FundsUnavailable)`. Fee for a plain transfer: 0.010157927 DEV. In the app: `transfer_prepare` (amount parsed from its decimal form in Rust), `transfer_submit` (signs through the session, emits `tx:status`), `transfer_discard`, `reversible_pending`, `reversible_cancel_prepare`. Closes #22 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
blackbeard wallet
A desktop wallet for post-quantum chains, Quantus first.
Linux, Windows and macOS; installable on Fedora with dnf.
Shape
The wallet is a Rust cargo workspace with a Tauri shell and a React webview. That split is the security model, not a convenience: every key, signature, decoded transaction and amount lives on the Rust side, and the webview only renders what a Tauri command hands it. Nothing that could serialise a secret is reachable from JavaScript.
crates/
wallet-entities/ domain types and the IPC wire protocol; exported to TypeScript
wallet-core/ wallet logic and the ports it is written against
wallet-data/ adapters behind those ports: keystore, cache, RPC, provider APIs
wallet-app/ the Tauri binary; commands are one-line shims into core
ui/ Vite + React + TypeScript webview
The ports in wallet-core are what make "any post-quantum chain" a profile
rather than a rewrite:
KeySchemeturns a seed and a path into a signer. ML-DSA-65 and ML-DSA-87 now; whatever the next chain uses later.ChainAdapterknows one chain: balances, building and submitting a transaction, watching it. The Substrate adapter is driven by runtime metadata plus a small per-chain profile (ss58 prefix, decimals, signature enum, transaction extensions, signing-context rule, coin type).SwapProvideris a swap venue. NEAR Intents first.FiatRampbuilds a provider session through a relay on our infrastructure, which holds the provider secrets, and opens it in the system browser.
The wallet is post-quantum only. Classical chains appear only as transit legs of a swap or ramp; the wallet never derives a secp256k1 or ed25519 key.
Why these choices
Quantus signs extrinsics with ML-DSA, hashes public keys with Poseidon2 for
the account id, and shields mining rewards behind a Plonky2 wormhole whose
prover is minutes of native CPU. All of that exists as Rust crates and nothing
else, so the core is Rust. Tauri is the house desktop convention
(~/git/architecture/generic.md §4) and the only shell that packages rpm, deb,
msi and dmg with a Rust-owned IPC boundary out of the box.
NEAR's post-quantum work (their MPC fork with threshold ML-DSA-87) is not yet able to sign a Quantus extrinsic, so the swap module is built and tested against chains NEAR Intents already lists, with Quantus as a registry entry that lights up when the bridge does.
Building
pnpm --dir ui install
cargo tauri dev --config crates/wallet-app/tauri.conf.json # from crates/wallet-app: cargo tauri dev
cargo test --workspace # also regenerates ui/src/api/generated
The CI gate is cargo fmt --check, cargo clippy -D warnings, cargo test,
and the webview's typecheck, lint and build. Run them before claiming done.
Work
Work is filed before it is done. Epics carry the epic label and an external
definition of done; their steps carry child, in dependency order, at
https://git.lair.cafe/blackbeard/wallet/issues. A commit that finishes a
child closes it (Closes #N); when an investigation contradicts an issue, the
correction is a comment on the issue, dated, not a surprise in a diff.