@qorechain/wallet-adapter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @qorechain/wallet-adapter
2
+
3
+ Add **QoreChain** to any Cosmos wallet — Keplr, Leap, Cosmostation — and send its
4
+ **PQC-required** transactions, with **no wallet-side changes**.
5
+
6
+ QoreChain's ante chain rejects any Cosmos tx that lacks a FIPS-204 **ML-DSA-87**
7
+ hybrid signature (in a tx-body extension option) alongside the account's classical
8
+ secp256k1 signature. Stock wallets can't produce ML-DSA signatures — so this
9
+ adapter does. The design is what makes it drop-in:
10
+
11
+ > The wallet only ever produces a **standard `SIGN_MODE_DIRECT` signature** over
12
+ > the final transaction body. The adapter bakes the ML-DSA-87 extension **into
13
+ > that body before the wallet signs it**. So `wallet.signDirect(...)` works
14
+ > exactly as it does for any Cosmos chain — it has no idea PQC is involved.
15
+
16
+ The ML-DSA part uses [`@qorechain/pqc`](../qorechain-pqc) — the same FIPS-204
17
+ implementation the chain itself was migrated to, so the signatures are
18
+ byte-compatible and verify in the chain's ante. (Before that migration this was
19
+ impossible: the chain ran a non-standard Dilithium variant no JS lib could match.)
20
+
21
+ ## Why it works — the protocol
22
+
23
+ Mirrors the chain's own `qorechaind tx pqc cosign`:
24
+
25
+ ```
26
+ B0 = TxBody{messages, memo, timeoutHeight} // no extension
27
+ sigP = ML-DSA-87.sign( frame(B0, authInfoBytes) ) // adapter does this
28
+ body = TxBody{ ...B0, extensionOptions:[ PQCHybridSignature{1, sigP} ] }
29
+ sigC = wallet.signDirect( SignDoc{ body, authInfo, chainId, accountNumber } )
30
+ tx = TxRaw{ body, authInfo, [sigC] }
31
+ ```
32
+
33
+ where `frame(b0, auth) = BE32(len b0) ‖ b0 ‖ BE32(len auth) ‖ auth`, the extension
34
+ type URL is `/qorechain.pqc.v1.PQCHybridSignature`, and algorithm `1` = ML-DSA-87.
35
+
36
+ **Verified end-to-end:** an adapter-built tx (ML-DSA-87 via `@noble/post-quantum`
37
+ + classical via a cosmjs signer standing in for Keplr) **committed with code 0**
38
+ against a live 7-validator QoreChain — the PQC ante accepted it.
39
+
40
+ ## Usage (Keplr)
41
+
42
+ ```js
43
+ import {
44
+ QoreChainSigner, qoreChainInfo, derivePqcKeyFromWallet,
45
+ } from '@qorechain/wallet-adapter';
46
+
47
+ // 1. Register the chain with the wallet (one click for the user).
48
+ await window.keplr.experimentalSuggestChain(qoreChainInfo({ rpc, rest }));
49
+ await window.keplr.enable('qorechain-diana');
50
+ const signer = window.keplr.getOfflineSigner('qorechain-diana');
51
+ const [account] = await signer.getAccounts();
52
+
53
+ // 2. Derive the user's ML-DSA-87 key, bound to their wallet (no mnemonic export).
54
+ const pqc = await derivePqcKeyFromWallet(window.keplr, 'qorechain-diana', account.address);
55
+ // (first time only) register the PQC public key on-chain via MsgRegisterPQCKey —
56
+ // that message is classical-exempt, so the wallet can sign it normally.
57
+
58
+ // 3. Sign + broadcast a PQC-required tx. The wallet signs an ordinary SignDoc.
59
+ const adapter = new QoreChainSigner({
60
+ wallet: window.keplr, chainId: 'qorechain-diana', address: account.address,
61
+ pubkeySecp256k1: account.pubkey, accountNumber, pqc,
62
+ });
63
+ const txBytes = await adapter.signHybrid({ messages, fee, sequence });
64
+ await fetch(`${rpc}`, { method:'POST', body: JSON.stringify({
65
+ jsonrpc:'2.0', id:1, method:'broadcast_tx_sync', params:{ tx: toBase64(txBytes) } }) });
66
+ ```
67
+
68
+ ## Wallet support
69
+
70
+ | Wallet | How | Status |
71
+ |---|---|---|
72
+ | **Keplr** | `experimentalSuggestChain` + `signDirect` | ✅ supported |
73
+ | **Leap / Cosmostation** | same `signDirect` interface | ✅ supported (any wallet exposing `signDirect`) |
74
+ | **MetaMask** | uses QoreChain's **EVM** path (chainId 9800) — structurally PQC-exempt | ✅ works natively, no adapter needed |
75
+ | **Phantom** | QoreChain's Solana-compatible RPC (`:8899`) | ⚠️ read/native-send; PQC SVM execute needs the same hybrid pattern |
76
+
77
+ ## API
78
+
79
+ - `frame(b0, auth)` — QoreChain hybrid sign-bytes framing.
80
+ - `encodePqcHybridSignature(algId, sig)` — proto encoder for the extension.
81
+ - `derivePqcKeyFromWallet(wallet, chainId, address)` — deterministic ML-DSA-87 key from a wallet signature.
82
+ - `QoreChainSigner#signHybrid({ messages, fee, sequence, memo?, timeoutHeight? })` → `TxRaw` bytes.
83
+ - `qoreChainInfo({ chainId?, rpc, rest })` — Keplr chain descriptor.
84
+
85
+ ## License
86
+
87
+ Apache-2.0
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@qorechain/wallet-adapter",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in adapter to add QoreChain to any Cosmos wallet (Keplr, Leap, Cosmostation, …) and sign its PQC-required transactions. The wallet signs an ordinary SIGN_MODE_DIRECT SignDoc; the adapter layers a standard FIPS-204 ML-DSA-87 hybrid signature into the tx body, so no wallet code changes are needed.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": { ".": "./src/index.js" },
9
+ "files": ["src", "README.md"],
10
+ "scripts": { "test": "node --test" },
11
+ "keywords": ["qorechain", "keplr", "cosmos", "wallet", "post-quantum", "ml-dsa", "pqc", "dilithium"],
12
+ "license": "Apache-2.0",
13
+ "publishConfig": { "access": "public" },
14
+ "dependencies": {
15
+ "@qorechain/pqc": "^0.1.0",
16
+ "cosmjs-types": "^0.9.0"
17
+ },
18
+ "peerDependencies": {
19
+ "@cosmjs/proto-signing": "^0.32.0",
20
+ "@cosmjs/stargate": "^0.32.0"
21
+ }
22
+ }
package/src/framing.js ADDED
@@ -0,0 +1,18 @@
1
+ // Pure, dependency-free QoreChain PQC tx-extension framing (the chain-matching bits).
2
+ export const HYBRID_SIG_TYPE_URL = '/qorechain.pqc.v1.PQCHybridSignature';
3
+ export const ALGORITHM_ML_DSA_87 = 1; // chain AlgorithmDilithium5 == FIPS-204 ML-DSA-87
4
+
5
+ export function frame(b0, auth) {
6
+ const out = new Uint8Array(4 + b0.length + 4 + auth.length);
7
+ const dv = new DataView(out.buffer);
8
+ dv.setUint32(0, b0.length, false);
9
+ out.set(b0, 4);
10
+ dv.setUint32(4 + b0.length, auth.length, false);
11
+ out.set(auth, 8 + b0.length);
12
+ return out;
13
+ }
14
+
15
+ export function encodePqcHybridSignature(algorithmId, sig) {
16
+ const varint = (n) => { const b = []; while (n > 0x7f) { b.push((n & 0x7f) | 0x80); n >>>= 7; } b.push(n); return b; };
17
+ return Uint8Array.from([0x08, ...varint(algorithmId), 0x12, ...varint(sig.length), ...sig]);
18
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export const HYBRID_SIG_TYPE_URL: string;
2
+ export const ALGORITHM_ML_DSA_87: number;
3
+ export function frame(b0: Uint8Array, auth: Uint8Array): Uint8Array;
4
+ export function encodePqcHybridSignature(algorithmId: number, sig: Uint8Array): Uint8Array;
5
+ export function derivePqcKeyFromWallet(wallet: any, chainId: string, address: string, domain?: string): Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>;
6
+ export function qoreChainInfo(opts?: { chainId?: string; rpc?: string; rest?: string }): any;
7
+ export class QoreChainSigner {
8
+ constructor(opts: { wallet: any; chainId: string; address: string; pubkeySecp256k1: Uint8Array; accountNumber: number | bigint; pqc: { publicKey: Uint8Array; secretKey: Uint8Array } });
9
+ signHybrid(opts: { messages: any[]; fee: any; memo?: string; sequence: number | bigint; timeoutHeight?: bigint }): Promise<Uint8Array>;
10
+ }
package/src/index.js ADDED
@@ -0,0 +1,136 @@
1
+ // @qorechain/wallet-adapter
2
+ //
3
+ // Add QoreChain to any Cosmos wallet (Keplr, Leap, Cosmostation, …) and sign its
4
+ // PQC-required transactions WITHOUT any wallet-side changes.
5
+ //
6
+ // QoreChain's ante chain requires every Cosmos tx to carry a FIPS-204 ML-DSA-87
7
+ // hybrid signature in a tx-body extension option, in addition to the account's
8
+ // classical secp256k1 signature. The trick that makes this wallet-compatible:
9
+ // the wallet only ever produces a *standard SIGN_MODE_DIRECT* signature over the
10
+ // final body — and the PQC extension is baked into that body BEFORE the wallet
11
+ // signs it. So `wallet.signDirect(...)` works unmodified; the adapter does the
12
+ // ML-DSA part with @qorechain/pqc (standard, interoperable since the chain was
13
+ // migrated to the FIPS standards).
14
+ //
15
+ // Protocol (mirrors the chain's `tx pqc cosign`):
16
+ // B0 = TxBody{messages, memo, timeoutHeight} (no extension)
17
+ // sigP = ML-DSA-87.sign( frame(B0, authInfoBytes) ) // frame = below
18
+ // body = TxBody{...B0, extensionOptions:[PQCHybridSignature{1, sigP}]}
19
+ // sigC = wallet.signDirect( SignDoc{body, authInfo, chainId, accountNumber} )
20
+ // tx = TxRaw{ body, authInfo, [sigC] }
21
+ //
22
+ // where frame(b0, auth) = BE32(len b0) ‖ b0 ‖ BE32(len auth) ‖ auth.
23
+
24
+ import { mldsa, shake256 } from '@qorechain/pqc';
25
+ import { frame, encodePqcHybridSignature, HYBRID_SIG_TYPE_URL, ALGORITHM_ML_DSA_87 } from './framing.js';
26
+ export { frame, encodePqcHybridSignature, HYBRID_SIG_TYPE_URL, ALGORITHM_ML_DSA_87 };
27
+ import { TxBody, AuthInfo, TxRaw, SignerInfo, ModeInfo, Fee } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
28
+ import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing.js';
29
+ import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
30
+ import { PubKey } from 'cosmjs-types/cosmos/crypto/secp256k1/keys.js';
31
+
32
+
33
+ // Deterministically derive an ML-DSA-87 keypair bound to the wallet, without ever
34
+ // touching the mnemonic: the wallet ADR-36-signs a fixed message, and SHAKE-256 of
35
+ // that signature seeds the keygen. The same wallet always yields the same PQC key.
36
+ export async function derivePqcKeyFromWallet(wallet, chainId, address, domain = 'qorechain:pqc:v1') {
37
+ const { signature } = await wallet.signArbitrary(chainId, address, domain);
38
+ const sigBytes = typeof signature === 'string' ? Uint8Array.from(Buffer.from(signature, 'base64')) : signature;
39
+ const seed = shake256(sigBytes, 32);
40
+ return mldsa.keygen(seed); // { publicKey, secretKey } — deterministic from seed
41
+ }
42
+
43
+ export class QoreChainSigner {
44
+ // wallet: a Keplr-like object exposing signDirect(chainId, signer, signDoc) and
45
+ // (optionally) signArbitrary(...). pqc: { publicKey, secretKey } ML-DSA-87.
46
+ constructor({ wallet, chainId, address, pubkeySecp256k1, accountNumber, pqc }) {
47
+ Object.assign(this, { wallet, chainId, address, pubkeySecp256k1, accountNumber, pqc });
48
+ }
49
+
50
+ // Build + hybrid-sign + return TxRaw bytes ready to broadcast.
51
+ async signHybrid({ messages, fee, memo = '', sequence, timeoutHeight = 0n }) {
52
+ // 1. AuthInfo: single DIRECT signer (secp256k1) + fee.
53
+ const pubAny = {
54
+ typeUrl: '/cosmos.crypto.secp256k1.PubKey',
55
+ value: PubKey.encode(PubKey.fromPartial({ key: this.pubkeySecp256k1 })).finish(),
56
+ };
57
+ const authInfo = AuthInfo.fromPartial({
58
+ signerInfos: [SignerInfo.fromPartial({
59
+ publicKey: pubAny,
60
+ modeInfo: ModeInfo.fromPartial({ single: { mode: SignMode.SIGN_MODE_DIRECT } }),
61
+ sequence: BigInt(sequence),
62
+ })],
63
+ fee: Fee.fromPartial(fee),
64
+ });
65
+ const authInfoBytes = AuthInfo.encode(authInfo).finish();
66
+
67
+ // 2. B0 = body without the PQC extension.
68
+ const b0 = TxBody.encode(TxBody.fromPartial({ messages, memo, timeoutHeight })).finish();
69
+
70
+ // 3. ML-DSA-87 sign the framed (B0, authInfo).
71
+ const pqcSig = mldsa.sign(this.pqc.secretKey, frame(b0, authInfoBytes));
72
+
73
+ // 4. body WITH the PQC hybrid extension.
74
+ const bodyWithExt = TxBody.encode(TxBody.fromPartial({
75
+ messages, memo, timeoutHeight,
76
+ extensionOptions: [{ typeUrl: HYBRID_SIG_TYPE_URL, value: encodePqcHybridSignature(ALGORITHM_ML_DSA_87, pqcSig) }],
77
+ })).finish();
78
+
79
+ // 5. Classical secp256k1 signature from the wallet over the final SignDoc.
80
+ const signDoc = SignDoc.fromPartial({
81
+ bodyBytes: bodyWithExt, authInfoBytes, chainId: this.chainId, accountNumber: BigInt(this.accountNumber),
82
+ });
83
+ const { signature } = await this.wallet.signDirect(this.chainId, this.address, {
84
+ bodyBytes: bodyWithExt, authInfoBytes, chainId: this.chainId, accountNumber: BigInt(this.accountNumber),
85
+ });
86
+ const classicalSig = typeof signature.signature === 'string'
87
+ ? Uint8Array.from(Buffer.from(signature.signature, 'base64')) : signature.signature;
88
+
89
+ // 6. Assemble TxRaw.
90
+ return TxRaw.encode(TxRaw.fromPartial({
91
+ bodyBytes: bodyWithExt, authInfoBytes, signatures: [classicalSig],
92
+ })).finish();
93
+ }
94
+ }
95
+
96
+ // Keplr chain-registration descriptor for QoreChain (pass to keplr.experimentalSuggestChain).
97
+ export function qoreChainInfo({ chainId = 'qorechain-diana', rpc, rest } = {}) {
98
+ return {
99
+ chainId, chainName: 'QoreChain', rpc, rest,
100
+ bip44: { coinType: 118 },
101
+ bech32Config: {
102
+ bech32PrefixAccAddr: 'qor', bech32PrefixAccPub: 'qorpub',
103
+ bech32PrefixValAddr: 'qorvaloper', bech32PrefixValPub: 'qorvaloperpub',
104
+ bech32PrefixConsAddr: 'qorvalcons', bech32PrefixConsPub: 'qorvalconspub',
105
+ },
106
+ currencies: [{ coinDenom: 'QOR', coinMinimalDenom: 'uqor', coinDecimals: 6 }],
107
+ feeCurrencies: [{
108
+ coinDenom: 'QOR', coinMinimalDenom: 'uqor', coinDecimals: 6,
109
+ // matches the chain's minimum-gas-prices (0.001uqor); avg/high give headroom
110
+ gasPriceStep: { low: 0.001, average: 0.0025, high: 0.004 },
111
+ }],
112
+ stakeCurrency: { coinDenom: 'QOR', coinMinimalDenom: 'uqor', coinDecimals: 6 },
113
+ features: ['cosmwasm'],
114
+ };
115
+ }
116
+
117
+ // EIP-3085 `wallet_addEthereumChain` params for QoreChain's EVM, for MetaMask &
118
+ // any EIP-1193 wallet. IMPORTANT: the EVM native currency is the 18-decimal
119
+ // `aqor` view of QOR (1 QOR = 1e18 aqor = 1e6 uqor; the EVM lane scales the
120
+ // 6-decimal bank denom by 1e12). nativeCurrency.decimals MUST be 18 here — do
121
+ // NOT copy the 6 from the Cosmos `uqor` currency, or balances render 1e12x off.
122
+ export function qoreEvmChainParams({ evmChainId = 9800, rpcUrl, wsUrl, explorerUrl, testnet = true } = {}) {
123
+ if (!rpcUrl) throw new Error('qoreEvmChainParams: rpcUrl is required');
124
+ return {
125
+ chainId: '0x' + Number(evmChainId).toString(16), // 9800 -> 0x2648, 9801 -> 0x2649
126
+ chainName: testnet ? 'QoreChain Testnet' : 'QoreChain',
127
+ nativeCurrency: { name: 'QORE', symbol: 'QOR', decimals: 18 },
128
+ rpcUrls: wsUrl ? [rpcUrl, wsUrl] : [rpcUrl],
129
+ blockExplorerUrls: explorerUrl ? [explorerUrl] : [],
130
+ };
131
+ }
132
+
133
+ // One-call helper: prompt an EIP-1193 wallet (MetaMask) to add QoreChain's EVM.
134
+ export async function addQoreEvmToWallet(provider, opts) {
135
+ return provider.request({ method: 'wallet_addEthereumChain', params: [qoreEvmChainParams(opts)] });
136
+ }