@qorechain/wallet-adapter 0.1.2 → 0.1.4

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
@@ -85,3 +85,30 @@ await fetch(`${rpc}`, { method:'POST', body: JSON.stringify({
85
85
  ## License
86
86
 
87
87
  Apache-2.0
88
+
89
+ ## Unified wallet generation (all 3 addresses)
90
+
91
+ Every account is one 20-byte identity rendered as three encodings that share a
92
+ single on-chain balance. Generate an eth-native wallet and get all three at once,
93
+ plus the ML-DSA-87 (Dilithium-5) key for the PQC-hybrid Cosmos ante:
94
+
95
+ ```js
96
+ import { generateQoreWallet, walletFromMnemonic } from "@qorechain/wallet-adapter";
97
+
98
+ const w = await generateQoreWallet(); // random 24-word mnemonic
99
+ // const w = await walletFromMnemonic(existing); // or recover
100
+ w.cosmos // qor1… (bech32)
101
+ w.evm // 0x… (EIP-55) (hex — EVM-native, spendable via eth_sendRawTransaction)
102
+ w.svm // <base58> (base58 of the 20 bytes + 12 zero-byte pad)
103
+ w.privateKey // 0x… (32B)
104
+ w.pqc // { publicKey, secretKey } ML-DSA-87
105
+ ```
106
+
107
+ The key is **eth-native** (address = `keccak256(pubkey)[12:]`), so it is spendable
108
+ on the EVM lane; `cosmos` and `svm` are just other encodings of the same 20 bytes,
109
+ under which the chain reads one `x/bank` balance. The account can sign EVM txs
110
+ (EIP-155) **and** PQC-hybrid Cosmos txs (the chain's Cosmos ante handles
111
+ `eth_secp256k1` and the hybrid decorator keys off the address).
112
+
113
+ `addressesFrom20(bytes20)` / `qoreAddresses({cosmos|evm|hex})` derive the three
114
+ encodings from a known account (for explorers / backends).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qorechain/wallet-adapter",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",
@@ -31,7 +31,9 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@qorechain/pqc": "^0.1.1",
34
- "cosmjs-types": "^0.9.0"
34
+ "cosmjs-types": "^0.9.0",
35
+ "@cosmjs/crypto": "^0.32.0",
36
+ "@cosmjs/encoding": "^0.32.0"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@noble/post-quantum": "^0.6.1"
package/src/index.js CHANGED
@@ -29,6 +29,14 @@ export {
29
29
  base58Encode, base58Decode, SYSTEM_PROGRAM_ID, systemTransferData,
30
30
  authSignBytes, buildPhantomSvmEnvelope, buildPhantomTransfer, registerAuthenticatorMsg,
31
31
  } from './phantom.js';
32
+ // Unified wallet generation: one eth-native key → cosmos/evm/svm addresses + PQC key.
33
+ export {
34
+ generateQoreWallet, walletFromMnemonic, addressesFrom20, qoreAddresses,
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';
32
40
  import { TxBody, AuthInfo, TxRaw, SignerInfo, ModeInfo, Fee } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
33
41
  import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing.js';
34
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 ADDED
@@ -0,0 +1,97 @@
1
+ // @qorechain/wallet-adapter — unified wallet generation.
2
+ //
3
+ // One eth-native secp256k1 keypair → the SAME 20-byte account rendered as all
4
+ // THREE QoreChain address encodings, so a wallet never "has funds on Cosmos but
5
+ // not EVM" again. The 20 bytes are the Ethereum derivation `keccak256(pubkey)[12:]`
6
+ // so the key is natively spendable on the EVM lane; the Cosmos (`qor1…`) and SVM
7
+ // (base58) forms are just other encodings of those same 20 bytes — the chain reads
8
+ // one x/bank balance for the account, visible under all three.
9
+ //
10
+ // {
11
+ // mnemonic, privateKey (0x hex, 32B),
12
+ // pubkey (33B compressed),
13
+ // addressBytes (20B),
14
+ // cosmos: "qor1…", // bech32
15
+ // evm: "0x…" (EIP-55), // hex
16
+ // svm: "<base58>", // base58(20B ‖ 12 zero bytes) = 32-byte SVM addr
17
+ // pqc: { publicKey, secretKey } // ML-DSA-87 (Dilithium-5), for the hybrid ante
18
+ // }
19
+ //
20
+ // Eth-native accounts are fully supported by the chain (the Cosmos ante's
21
+ // SigVerification handles eth_secp256k1 and the PQC hybrid decorator keys off the
22
+ // address), so the same wallet signs EVM txs (EIP-155) AND PQC-hybrid Cosmos txs.
23
+
24
+ import { Bip39, Random, Slip10, Slip10Curve, stringToPath, Secp256k1, Keccak256, EnglishMnemonic } from '@cosmjs/crypto';
25
+ import { toBech32, toHex, fromHex, fromBech32 } from '@cosmjs/encoding';
26
+ import { mldsa, shake256 } from '@qorechain/pqc';
27
+ import { base58Encode } from './phantom.js';
28
+
29
+ // Ethereum HD path (coin-type 60) — makes the 20-byte address the keccak
30
+ // derivation, so the key is EVM-native (spendable via eth_sendRawTransaction).
31
+ const ETH_HD_PATH = "m/44'/60'/0'/0/0";
32
+ const HRP = 'qor';
33
+
34
+ // EIP-55 mixed-case checksum for a 20-byte hex address (no 0x).
35
+ function toEip55(hex20) {
36
+ const lower = hex20.toLowerCase();
37
+ const hash = toHex(new Keccak256(new TextEncoder().encode(lower)).digest());
38
+ let out = '0x';
39
+ for (let i = 0; i < lower.length; i++) {
40
+ out += parseInt(hash[i], 16) >= 8 ? lower[i].toUpperCase() : lower[i];
41
+ }
42
+ return out;
43
+ }
44
+
45
+ // Derive the three encodings + the PQC key from a 20-byte account address.
46
+ // Exposed so SDKs / backends can render all three from a known account too.
47
+ export function addressesFrom20(addr20) {
48
+ if (addr20.length !== 20) throw new Error('address must be 20 bytes');
49
+ const hex = toHex(addr20);
50
+ const svmBytes = new Uint8Array(32);
51
+ svmBytes.set(addr20, 0); // right-pad with 12 zero bytes → the unified 32-byte SVM address
52
+ return { addressBytes: addr20, cosmos: toBech32(HRP, addr20), evm: toEip55(hex), svm: base58Encode(svmBytes) };
53
+ }
54
+
55
+ function derivePqc(cosmosAddr, mnemonic) {
56
+ const seed = shake256(new TextEncoder().encode(`qorechain:pqc:v1|${cosmosAddr}|${mnemonic}`), 32);
57
+ return mldsa.keygen(seed); // deterministic + recoverable from {address, mnemonic}
58
+ }
59
+
60
+ async function fromMnemonicObj(mnemonic) {
61
+ const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic));
62
+ const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, stringToPath(ETH_HD_PATH));
63
+ const kp = await Secp256k1.makeKeypair(privkey);
64
+ const uncompressed = kp.pubkey; // 65 bytes, 0x04 || X || Y
65
+ const compressed = Secp256k1.compressPubkey(uncompressed); // 33 bytes
66
+ // Ethereum address = last 20 bytes of keccak256 of the 64-byte pubkey (drop 0x04).
67
+ const addr20 = new Keccak256(uncompressed.slice(1)).digest().slice(12);
68
+ const enc = addressesFrom20(addr20);
69
+ return {
70
+ mnemonic,
71
+ privateKey: '0x' + toHex(privkey),
72
+ pubkey: '0x' + toHex(compressed),
73
+ ...enc,
74
+ pqc: derivePqc(enc.cosmos, mnemonic),
75
+ };
76
+ }
77
+
78
+ /** Generate a fresh unified QoreChain wallet (random 24-word mnemonic). */
79
+ export async function generateQoreWallet(strength = 256) {
80
+ const mnemonic = Bip39.encode(Random.getBytes(strength / 8)).toString();
81
+ return fromMnemonicObj(mnemonic);
82
+ }
83
+
84
+ /** Recover a unified QoreChain wallet from an existing BIP39 mnemonic. */
85
+ export async function walletFromMnemonic(mnemonic) {
86
+ return fromMnemonicObj(mnemonic);
87
+ }
88
+
89
+ /** Convenience: the three address encodings of an existing account. */
90
+ export function qoreAddresses({ cosmos, evm, hex }) {
91
+ let addr20;
92
+ if (evm) addr20 = fromHex(evm.replace(/^0x/, ''));
93
+ else if (hex) addr20 = fromHex(hex.replace(/^0x/, ''));
94
+ else if (cosmos) addr20 = fromBech32(cosmos).data;
95
+ else throw new Error('provide one of {cosmos, evm, hex}');
96
+ return addressesFrom20(addr20);
97
+ }