@qorechain/wallet-adapter 0.1.3 → 0.1.5

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 CHANGED
@@ -72,15 +72,24 @@ await fetch(`${rpc}`, { method:'POST', body: JSON.stringify({
72
72
  | **Keplr** | `experimentalSuggestChain` + `signDirect` | ✅ supported |
73
73
  | **Leap / Cosmostation** | same `signDirect` interface | ✅ supported (any wallet exposing `signDirect`) |
74
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 |
75
+ | **Phantom** | derive a unified account from a Phantom signature (`walletFromSeed`) | connect → qor1/0x/svm, receive on any, spend on any (incl. hybrid PQC) |
76
76
 
77
77
  ## API
78
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.
79
+ Wallet generation & unified addresses:
80
+ - `generateQoreWallet(strength?)` / `walletFromMnemonic(mnemonic)` / `walletFromSeed(seed32)` a unified wallet `{ mnemonic, privateKey, pubkey, cosmos, evm, svm, pqc }`.
81
+ - `addressesFrom20(bytes20)` / `qoreAddresses({cosmos|evm|hex})` — the three encodings of a known account.
82
+
83
+ eth-native Cosmos signing (chain ≥ v3.1.83):
84
+ - `signClassicalEth({ key, chainId, accountNumber, sequence, messages, fee, memo?, timeoutHeight? })` → `TxRaw` bytes (classical, e.g. PQC key registration).
85
+ - `signHybridEth({ ... })` → `TxRaw` bytes (eth_secp256k1 + ML-DSA-87 hybrid).
86
+ - `ETHSECP256K1_PUBKEY_TYPE` — the eth pubkey type URL.
87
+
88
+ Keplr / any-signDirect adapter + PQC framing:
82
89
  - `QoreChainSigner#signHybrid({ messages, fee, sequence, memo?, timeoutHeight? })` → `TxRaw` bytes.
83
- - `qoreChainInfo({ chainId?, rpc, rest })` — Keplr chain descriptor.
90
+ - `derivePqcKeyFromWallet(wallet, chainId, address)` — deterministic ML-DSA-87 key from a wallet signature.
91
+ - `frame(b0, auth)` — QoreChain hybrid sign-bytes framing; `encodePqcHybridSignature(algId, sig)` — proto encoder for the extension.
92
+ - `qoreChainInfo({ chainId?, rpc, rest })` — Keplr chain descriptor; `qoreEvmChainParams(...)` / `addQoreEvmToWallet(provider, opts)` — MetaMask (EIP-3085) EVM descriptor.
84
93
 
85
94
  ## License
86
95
 
@@ -112,3 +121,52 @@ under which the chain reads one `x/bank` balance. The account can sign EVM txs
112
121
 
113
122
  `addressesFrom20(bytes20)` / `qoreAddresses({cosmos|evm|hex})` derive the three
114
123
  encodings from a known account (for explorers / backends).
124
+
125
+ ### Derive from a seed (Phantom & other non-mnemonic flows)
126
+
127
+ `walletFromSeed(seed32)` builds the same unified wallet from any 32 bytes (the
128
+ seed becomes the secp256k1 key). Because a wallet's signature over a fixed message
129
+ is deterministic, you can derive **one canonical QoreChain account from a Phantom
130
+ (ed25519) signature** — a Phantom user connects once and gets three usable
131
+ QoreChain addresses they fully control:
132
+
133
+ ```js
134
+ import { walletFromSeed } from "@qorechain/wallet-adapter";
135
+ import { shake256 } from "@qorechain/pqc";
136
+
137
+ const msg = new TextEncoder().encode("QoreChain unified account derivation v1");
138
+ const { signature } = await window.solana.signMessage(msg); // Phantom, deterministic
139
+ const w = await walletFromSeed(shake256(signature, 32)); // → qor1 / 0x / svm + pqc
140
+ ```
141
+
142
+ ## eth-native Cosmos signing (requires chain ≥ v3.1.83)
143
+
144
+ The unified account **signs on the Cosmos lane too**, with the `eth_secp256k1`
145
+ scheme (secp256k1 over `keccak256(signBytes)`, pubkey
146
+ `/cosmos.evm.crypto.v1.ethsecp256k1.PubKey`). `signClassicalEth` builds a
147
+ classical-only tx (for the one-time, bootstrap-exempt PQC key registration);
148
+ `signHybridEth` adds the ML-DSA-87 hybrid signature the ante requires for
149
+ everything else. Both return broadcast-ready `TxRaw` bytes.
150
+
151
+ ```js
152
+ import { walletFromMnemonic, signClassicalEth, signHybridEth } from "@qorechain/wallet-adapter";
153
+
154
+ const key = await walletFromMnemonic(mnemonic); // has { privateKey, pubkey, pqc }
155
+ const fee = { amount: [{ denom: "uqor", amount: "30000" }], gasLimit: 300000n };
156
+
157
+ // 1) one-time: register the account's ML-DSA-87 key (classical, PQC-exempt)
158
+ const regTx = await signClassicalEth({ key, chainId, accountNumber, sequence,
159
+ messages: [{ typeUrl: "/qorechain.pqc.v1.MsgRegisterPQCKeyV2", value: registerMsgBytes }],
160
+ fee: { amount: [{ denom: "uqor", amount: "600000" }], gasLimit: 6000000n } });
161
+
162
+ // 2) thereafter: hybrid eth_secp256k1 + ML-DSA-87 (e.g. a bank MsgSend)
163
+ const sendTx = await signHybridEth({ key, chainId, accountNumber, sequence,
164
+ messages: [{ typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: msgSendBytes }], fee });
165
+ ```
166
+
167
+ > **Requires QoreChain ≥ v3.1.83** — that release registers the `eth_secp256k1`
168
+ > pubkey on the node's interface registry so eth-native Cosmos txs decode. Both
169
+ > `qorechain-diana` (testnet) and `qorechain-vladi` (mainnet) run it.
170
+ > `@qorechain/chain-bridge` wraps this server-side (`keyType: 'eth_secp256k1'`,
171
+ > auto-registers the PQC key on first send). **Proven live** on QoreChain: register
172
+ > (code 0) + hybrid send (code 0) + an EVM transfer from the same key, one balance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qorechain/wallet-adapter",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
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
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.d.ts CHANGED
@@ -1,6 +1,48 @@
1
1
  export const HYBRID_SIG_TYPE_URL: string;
2
2
  export const ALGORITHM_ML_DSA_87: number;
3
3
 
4
+ // --- Unified wallet generation (one eth-native key → qor1 / 0x / svm) ---
5
+ export interface PqcKeypair { publicKey: Uint8Array; secretKey: Uint8Array; }
6
+ export interface QoreAddresses {
7
+ addressBytes: Uint8Array;
8
+ cosmos: string; // qor1… (bech32)
9
+ evm: string; // 0x… (EIP-55)
10
+ svm: string; // base58
11
+ }
12
+ export interface QoreWallet extends QoreAddresses {
13
+ mnemonic: string | null;
14
+ privateKey: string; // 0x-hex, 32 bytes
15
+ pubkey: string; // 0x-hex, 33-byte compressed secp256k1
16
+ pqc: PqcKeypair; // ML-DSA-87 (Dilithium-5)
17
+ }
18
+ export function generateQoreWallet(strength?: number): Promise<QoreWallet>;
19
+ export function walletFromMnemonic(mnemonic: string): Promise<QoreWallet>;
20
+ export function walletFromSeed(seed: Uint8Array | string): Promise<QoreWallet>;
21
+ export function addressesFrom20(addr20: Uint8Array): QoreAddresses;
22
+ export function qoreAddresses(opts: { cosmos?: string; evm?: string; hex?: string }): QoreAddresses;
23
+
24
+ // --- eth-native (eth_secp256k1) Cosmos signing (requires chain >= v3.1.83) ---
25
+ export const ETHSECP256K1_PUBKEY_TYPE: string;
26
+ export interface EthSignKey { privateKey: Uint8Array | string; pubkey: Uint8Array | string; pqc?: PqcKeypair; }
27
+ export interface EthSignArgs {
28
+ key: EthSignKey;
29
+ chainId: string;
30
+ accountNumber: number | bigint;
31
+ messages: Array<{ typeUrl: string; value: Uint8Array }>;
32
+ fee: any;
33
+ sequence: number | bigint;
34
+ memo?: string;
35
+ timeoutHeight?: bigint;
36
+ }
37
+ /** Classical-only eth_secp256k1 Cosmos tx (e.g. the bootstrap MsgRegisterPQCKeyV2). */
38
+ export function signClassicalEth(args: EthSignArgs): Promise<Uint8Array>;
39
+ /** Hybrid eth_secp256k1 + ML-DSA-87 Cosmos tx (key.pqc required). */
40
+ export function signHybridEth(args: EthSignArgs): Promise<Uint8Array>;
41
+
42
+ // --- EVM network descriptors (EIP-3085 / MetaMask) ---
43
+ export function qoreEvmChainParams(opts?: { evmChainId?: number; rpcUrl?: string; wsUrl?: string; explorerUrl?: string; testnet?: boolean }): any;
44
+ export function addQoreEvmToWallet(provider: any, opts?: any): Promise<any>;
45
+
4
46
  // --- Phantom / any-ed25519-wallet support ---
5
47
  export const SYSTEM_PROGRAM_ID: string;
6
48
  export function base58Encode(bytes: Uint8Array): string;
package/src/index.js CHANGED
@@ -31,8 +31,12 @@ export {
31
31
  } from './phantom.js';
32
32
  // Unified wallet generation: one eth-native key → cosmos/evm/svm addresses + PQC key.
33
33
  export {
34
- generateQoreWallet, walletFromMnemonic, addressesFrom20, qoreAddresses,
34
+ generateQoreWallet, walletFromMnemonic, walletFromSeed, addressesFrom20, qoreAddresses,
35
35
  } from './wallet.js';
36
+ // eth-native (eth_secp256k1) Cosmos signing — classical (register) + hybrid (PQC).
37
+ export {
38
+ signClassicalEth, signHybridEth, ETHSECP256K1_PUBKEY_TYPE,
39
+ } from './sign-eth.js';
36
40
  import { TxBody, AuthInfo, TxRaw, SignerInfo, ModeInfo, Fee } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
37
41
  import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing.js';
38
42
  import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
@@ -0,0 +1,91 @@
1
+ // @qorechain/wallet-adapter — eth-native (eth_secp256k1) Cosmos signing.
2
+ //
3
+ // A QoreChain account created eth-native (address = keccak(pubkey)[12:]) signs
4
+ // Cosmos SDK txs with the `eth_secp256k1` scheme: the classical signature is
5
+ // secp256k1 over the KECCAK-256 of the SignDoc (not sha256), and the account's
6
+ // pubkey is `/cosmos.evm.crypto.v1.ethsecp256k1.PubKey`. This is the same account
7
+ // that spends on the EVM lane, so its qor1/0x/svm forms are one identity.
8
+ //
9
+ // Mainnet requires the ML-DSA-87 hybrid extension in the tx body; signHybridEth
10
+ // adds it. signClassicalEth omits it (used for the one-time PQC key registration,
11
+ // which is bootstrap-exempt from the hybrid requirement).
12
+
13
+ import { Secp256k1, Keccak256 } from '@cosmjs/crypto';
14
+ import { fromHex } from '@cosmjs/encoding';
15
+ import { TxBody, AuthInfo, TxRaw, SignerInfo, ModeInfo, Fee, SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
16
+ import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing.js';
17
+ import { PubKey } from 'cosmjs-types/cosmos/crypto/secp256k1/keys.js';
18
+ import { mldsa } from '@qorechain/pqc';
19
+ import { frame, encodePqcHybridSignature, HYBRID_SIG_TYPE_URL, ALGORITHM_ML_DSA_87 } from './framing.js';
20
+
21
+ // cosmos/evm eth_secp256k1 pubkey type. Wire shape is identical to the cosmos
22
+ // secp256k1 PubKey ({1: bytes key}), only the typeUrl differs.
23
+ export const ETHSECP256K1_PUBKEY_TYPE = '/cosmos.evm.crypto.v1.ethsecp256k1.PubKey';
24
+
25
+ function toBytes(x) {
26
+ if (x instanceof Uint8Array) return x;
27
+ return fromHex(String(x).replace(/^0x/, ''));
28
+ }
29
+
30
+ function buildAuthInfo(compressedPubkey, sequence, fee) {
31
+ const pubAny = {
32
+ typeUrl: ETHSECP256K1_PUBKEY_TYPE,
33
+ value: PubKey.encode(PubKey.fromPartial({ key: toBytes(compressedPubkey) })).finish(),
34
+ };
35
+ const authInfo = AuthInfo.fromPartial({
36
+ signerInfos: [SignerInfo.fromPartial({
37
+ publicKey: pubAny,
38
+ modeInfo: ModeInfo.fromPartial({ single: { mode: SignMode.SIGN_MODE_DIRECT } }),
39
+ sequence: BigInt(sequence),
40
+ })],
41
+ fee: Fee.fromPartial(fee),
42
+ });
43
+ return AuthInfo.encode(authInfo).finish();
44
+ }
45
+
46
+ // eth_secp256k1 classical signature over a SignDoc: secp256k1 sign of keccak(signBytes),
47
+ // serialized as the 64-byte r‖s (low-s normalized by @cosmjs/crypto).
48
+ async function ethSign(signBytes, privateKey) {
49
+ const hash = new Keccak256(signBytes).digest();
50
+ const sig = await Secp256k1.createSignature(hash, toBytes(privateKey));
51
+ const out = new Uint8Array(64);
52
+ out.set(sig.r(32), 0);
53
+ out.set(sig.s(32), 32);
54
+ return out;
55
+ }
56
+
57
+ /**
58
+ * Classical-only eth_secp256k1 Cosmos tx (no PQC extension). Use for the one-time
59
+ * MsgRegisterPQCKeyV2 (bootstrap-exempt). `key` = { privateKey, pubkey } from
60
+ * generateQoreWallet/walletFromMnemonic.
61
+ */
62
+ export async function signClassicalEth({ key, chainId, accountNumber, messages, fee, sequence, memo = '', timeoutHeight = 0n }) {
63
+ const authInfoBytes = buildAuthInfo(key.pubkey, sequence, fee);
64
+ const bodyBytes = TxBody.encode(TxBody.fromPartial({ messages, memo, timeoutHeight })).finish();
65
+ const signBytes = SignDoc.encode(SignDoc.fromPartial({
66
+ bodyBytes, authInfoBytes, chainId, accountNumber: BigInt(accountNumber),
67
+ })).finish();
68
+ const classical = await ethSign(signBytes, key.privateKey);
69
+ return TxRaw.encode(TxRaw.fromPartial({ bodyBytes, authInfoBytes, signatures: [classical] })).finish();
70
+ }
71
+
72
+ /**
73
+ * Hybrid eth_secp256k1 + ML-DSA-87 Cosmos tx. `key` must include `pqc`
74
+ * ({publicKey, secretKey}) as produced by generateQoreWallet/walletFromMnemonic.
75
+ */
76
+ export async function signHybridEth({ key, chainId, accountNumber, messages, fee, sequence, memo = '', timeoutHeight = 0n }) {
77
+ const authInfoBytes = buildAuthInfo(key.pubkey, sequence, fee);
78
+ // B0 = body without the PQC extension.
79
+ const b0 = TxBody.encode(TxBody.fromPartial({ messages, memo, timeoutHeight })).finish();
80
+ // ML-DSA-87 over frame(B0, authInfo).
81
+ const pqcSig = mldsa.sign(key.pqc.secretKey, frame(b0, authInfoBytes));
82
+ const bodyWithExt = TxBody.encode(TxBody.fromPartial({
83
+ messages, memo, timeoutHeight,
84
+ extensionOptions: [{ typeUrl: HYBRID_SIG_TYPE_URL, value: encodePqcHybridSignature(ALGORITHM_ML_DSA_87, pqcSig) }],
85
+ })).finish();
86
+ const signBytes = SignDoc.encode(SignDoc.fromPartial({
87
+ bodyBytes: bodyWithExt, authInfoBytes, chainId, accountNumber: BigInt(accountNumber),
88
+ })).finish();
89
+ const classical = await ethSign(signBytes, key.privateKey);
90
+ return TxRaw.encode(TxRaw.fromPartial({ bodyBytes: bodyWithExt, authInfoBytes, signatures: [classical] })).finish();
91
+ }
package/src/wallet.js CHANGED
@@ -86,6 +86,40 @@ export async function walletFromMnemonic(mnemonic) {
86
86
  return fromMnemonicObj(mnemonic);
87
87
  }
88
88
 
89
+ /**
90
+ * Derive a unified QoreChain wallet directly from a 32-byte seed (no mnemonic).
91
+ *
92
+ * The seed is used as the secp256k1 private key, so a caller can derive one
93
+ * canonical eth-native account deterministically from any 32 bytes — e.g.
94
+ * `shake256(phantomSignature)` for the Phantom "connect → 3 addresses" flow, or
95
+ * an HKDF/KDF output from another wallet. Same 20-byte identity model as
96
+ * `walletFromMnemonic`: qor1 / 0x / svm all resolve to the same account and the
97
+ * same balance, and the key signs on every interface (incl. hybrid PQC).
98
+ *
99
+ * The ML-DSA-87 key is bound to `qorechain:pqc:v1|<qor1>|seed:<hex>` so it is
100
+ * recoverable from the same seed and never collides with a mnemonic wallet.
101
+ * `seed` accepts a 32-byte Uint8Array or a (0x-)hex string.
102
+ */
103
+ export async function walletFromSeed(seed) {
104
+ const privkey = typeof seed === 'string' ? fromHex(seed.replace(/^0x/, '')) : seed;
105
+ if (!(privkey instanceof Uint8Array) || privkey.length !== 32) {
106
+ throw new Error('seed must be 32 bytes (Uint8Array or hex)');
107
+ }
108
+ const kp = await Secp256k1.makeKeypair(privkey); // validates the scalar is in [1, n-1]
109
+ const uncompressed = kp.pubkey;
110
+ const compressed = Secp256k1.compressPubkey(uncompressed);
111
+ const addr20 = new Keccak256(uncompressed.slice(1)).digest().slice(12);
112
+ const enc = addressesFrom20(addr20);
113
+ const pqcSeed = shake256(new TextEncoder().encode(`qorechain:pqc:v1|${enc.cosmos}|seed:${toHex(privkey)}`), 32);
114
+ return {
115
+ mnemonic: null,
116
+ privateKey: '0x' + toHex(privkey),
117
+ pubkey: '0x' + toHex(compressed),
118
+ ...enc,
119
+ pqc: mldsa.keygen(pqcSeed),
120
+ };
121
+ }
122
+
89
123
  /** Convenience: the three address encodings of an existing account. */
90
124
  export function qoreAddresses({ cosmos, evm, hex }) {
91
125
  let addr20;