@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 +87 -0
- package/package.json +22 -0
- package/src/framing.js +18 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +136 -0
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
|
+
}
|