@qorechain/connect 0.1.0 → 0.2.1

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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/package.json +25 -5
  3. package/src/index.js +29 -7
package/README.md CHANGED
@@ -30,5 +30,28 @@ PQC tx committed **code 0** (ML-DSA accepted); SVM read OK.
30
30
  *before* the wallet `signDirect`s it, so the wallet needs zero changes.
31
31
  - **SVM** → reads native; `tx svm execute` is a cosmos tx → same PQC layer.
32
32
 
33
+ ## Hybrid sign-bytes form (v1 / v2, chain v3.2.0 / testnet v3.1.98)
34
+
35
+ The ML-DSA-87 signature covers one of two byte forms and a network accepts exactly
36
+ one at any height: v1 `BE32(len B0) ‖ B0 ‖ BE32(len A) ‖ A`, or v2, which prefixes
37
+ `"qorechain-pqc-hybrid-v2" ‖ BE64(len chainId) ‖ chainId`. `qorechain-vladi`
38
+ (mainnet) and `qorechain-diana` (testnet) verify v1 until the upgrade that carries
39
+ the switch is applied on them and v2 afterwards; testnet is on v2, **mainnet stays
40
+ on v1 until its own upgrade**. Other chains verify v2 from genesis. The switch
41
+ ships under two plan names — mainnet applies `v3.2.0`, the testnet already applied
42
+ `v3.1.98` — and the resolver asks for both, so either one applied means v2.
43
+
44
+ ```js
45
+ await QoreConnect.sendCosmos(keplr, { address, pubkey, mlsk, acct, seq, to, amount,
46
+ chainId: 'qorechain-vladi', rpc, rest: lcdUrl /* or signBytesVersion: 'v1' | 'v2' */ });
47
+ // -> { vm:'cosmos', code, hash, pqc:true, signBytesVersion: 'v1' | 'v2' }
48
+ ```
49
+
50
+ `signBytesVersion: 'auto'` (default) asks `rest` (or `QORE_REST`) through
51
+ `@qorechain/wallet-adapter`'s resolver and caches the answer for 60 s; on vladi /
52
+ diana without `rest` it throws instead of guessing. With `'auto'`, a refusal with
53
+ `pqc` code 21 re-resolves once and, if the answer changed, re-signs and broadcasts
54
+ once more. Explicit `'v1'` / `'v2'` never retries.
55
+
33
56
  ## License
34
57
  Apache-2.0
package/package.json CHANGED
@@ -1,12 +1,32 @@
1
1
  {
2
2
  "name": "@qorechain/connect",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "One universal wallet connector for QoreChain across all 3 VMs (EVM, Cosmos, SVM). Detects any wallet by its standard interface (EIP-1193, OfflineDirectSigner, Solana Wallet Standard) and auto-layers the FIPS-204 PQC signature only where the chain requires it.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "license": "Apache-2.0",
8
- "files": ["src", "README.md"],
9
- "publishConfig": { "access": "public" },
10
- "repository": { "type": "git", "url": "https://github.com/qorechain/qorechain-connect.git" },
11
- "peerDependencies": { "@qorechain/wallet-adapter": "^0.1.0", "ethers": "^6", "@cosmjs/proto-signing": "^0.32" }
8
+ "files": [
9
+ "src",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "test": "node --test"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/qorechain/qorechain-connect.git"
21
+ },
22
+ "peerDependencies": {
23
+ "@qorechain/wallet-adapter": "^0.2.1",
24
+ "ethers": "^6",
25
+ "@cosmjs/proto-signing": "^0.32"
26
+ },
27
+ "devDependencies": {
28
+ "@qorechain/wallet-adapter": "^0.2.1",
29
+ "@noble/post-quantum": "^0.6.1",
30
+ "cosmjs-types": "^0.9.0"
31
+ }
12
32
  }
package/src/index.js CHANGED
@@ -4,8 +4,14 @@ import { TxBody, AuthInfo, TxRaw, SignerInfo, ModeInfo, Fee, SignDoc } from 'cos
4
4
  import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing.js';
5
5
  import { PubKey } from 'cosmjs-types/cosmos/crypto/secp256k1/keys.js';
6
6
  import { MsgSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx.js';
7
- const CHAIN=process.env.QORE_CHAIN_ID||'qorechain-diana', RPC=process.env.QORE_RPC||'http://127.0.0.1:26657', SVMRPC=process.env.QORE_SVM_RPC||'http://127.0.0.1:8899';
8
- const frame=(b0,a)=>{const o=new Uint8Array(8+b0.length+a.length);const d=new DataView(o.buffer);d.setUint32(0,b0.length,false);o.set(b0,4);d.setUint32(4+b0.length,a.length,false);o.set(a,8+b0.length);return o;};
7
+ import { hybridSignBytes, resolveSignBytesVersion, isHybridSignBytesRejection } from '@qorechain/wallet-adapter';
8
+ const CHAIN=process.env.QORE_CHAIN_ID||'qorechain-diana', RPC=process.env.QORE_RPC||'http://127.0.0.1:26657', SVMRPC=process.env.QORE_SVM_RPC||'http://127.0.0.1:8899', REST=process.env.QORE_REST||undefined;
9
+ // Hybrid PQC sign-bytes come in two forms and a network accepts exactly one at a time:
10
+ // v1 (legacy): BE32(len b0) ‖ b0 ‖ BE32(len a) ‖ a
11
+ // v2: "qorechain-pqc-hybrid-v2" ‖ BE64(len chainId) ‖ chainId ‖ v1-layout
12
+ // qorechain-vladi / qorechain-diana verify v1 until the upgrade carrying the switch is applied
13
+ // (plan 'v3.2.0' on mainnet, 'v3.1.98' on the testnet — either one counts), v2 after; any other
14
+ // chain verifies v2. 'auto' asks the LCD (`rest`) for both names, see @qorechain/wallet-adapter.
9
15
  const encExt=(id,sig)=>{const vi=n=>{const b=[];while(n>0x7f){b.push((n&0x7f)|0x80);n>>>=7;}b.push(n);return b;};return Uint8Array.from([0x08,...vi(id),0x12,...vi(sig.length),...sig]);};
10
16
 
11
17
  export class QoreConnect {
@@ -23,19 +29,35 @@ export class QoreConnect {
23
29
  return {vm:'evm',hash,pqc:false};
24
30
  }
25
31
  // Cosmos (PQC-required): wallet signDirect's the final body; connector layers ML-DSA-87.
26
- static async sendCosmos(w,{address,pubkey,mlsk,acct,seq,to,amount}){
32
+ // signBytesVersion: 'auto' (default; needs `rest` = LCD URL on vladi/diana) | 'v1' | 'v2'.
33
+ // With 'auto', a refusal with pqc code 21 re-resolves once and, if the answer changed,
34
+ // re-signs and broadcasts once more; any other outcome is returned as-is.
35
+ static async sendCosmos(w,{address,pubkey,mlsk,acct,seq,to,amount,chainId=CHAIN,rpc=RPC,rest=REST,signBytesVersion='auto',fetch:fx}){
36
+ const mode=signBytesVersion??'auto';
37
+ const ro={chainId,rest,signBytesVersion:mode,...(fx?{fetch:fx}:{})};
38
+ const post=fx||globalThis.fetch;
39
+ let version=await resolveSignBytesVersion(ro);
40
+ let r=await this._cosmosOnce(w,{address,pubkey,mlsk,acct,seq,to,amount,chainId,rpc,version,post});
41
+ if(mode==='auto'&&isHybridSignBytesRejection(r)){
42
+ const fresh=await resolveSignBytesVersion({...ro,forceRefresh:true});
43
+ if(fresh!==version){version=fresh;r=await this._cosmosOnce(w,{address,pubkey,mlsk,acct,seq,to,amount,chainId,rpc,version,post});}
44
+ }
45
+ return {vm:'cosmos',code:r.code,hash:r.hash,pqc:true,signBytesVersion:version,...(r.code?{codespace:r.codespace,log:r.log}:{})};
46
+ }
47
+ static async _cosmosOnce(w,{address,pubkey,mlsk,acct,seq,to,amount,chainId,rpc,version,post}){
27
48
  const pubAny={typeUrl:'/cosmos.crypto.secp256k1.PubKey',value:PubKey.encode(PubKey.fromPartial({key:pubkey})).finish()};
28
49
  const msg={typeUrl:'/cosmos.bank.v1beta1.MsgSend',value:MsgSend.encode(MsgSend.fromPartial({fromAddress:address,toAddress:to,amount:[{denom:'uqor',amount}]})).finish()};
29
50
  const authInfo=AuthInfo.fromPartial({signerInfos:[SignerInfo.fromPartial({publicKey:pubAny,modeInfo:ModeInfo.fromPartial({single:{mode:SignMode.SIGN_MODE_DIRECT}}),sequence:BigInt(seq)})],fee:Fee.fromPartial({amount:[{denom:'uqor',amount:'25000'}],gasLimit:200000n})});
30
51
  const aib=AuthInfo.encode(authInfo).finish();
31
52
  const b0=TxBody.encode(TxBody.fromPartial({messages:[msg]})).finish();
32
- const pqcSig=ml_dsa87.sign(frame(b0,aib),mlsk); // <-- the PQC layer
53
+ const pqcSig=ml_dsa87.sign(hybridSignBytes(version,chainId,b0,aib),mlsk,{extraEntropy:false}); // <-- the PQC layer (deterministic, as the chain requires)
33
54
  const bodyExt=TxBody.encode(TxBody.fromPartial({messages:[msg],extensionOptions:[{typeUrl:'/qorechain.pqc.v1.PQCHybridSignature',value:encExt(1,pqcSig)}]})).finish();
34
- const signDoc=SignDoc.fromPartial({bodyBytes:bodyExt,authInfoBytes:aib,chainId:CHAIN,accountNumber:BigInt(acct)});
55
+ const signDoc=SignDoc.fromPartial({bodyBytes:bodyExt,authInfoBytes:aib,chainId,accountNumber:BigInt(acct)});
35
56
  const {signature}=await w.signDirect(address,signDoc); // <-- wallet's standard signature
36
57
  const txRaw=TxRaw.encode(TxRaw.fromPartial({bodyBytes:bodyExt,authInfoBytes:aib,signatures:[Uint8Array.from(Buffer.from(signature.signature,'base64'))]})).finish();
37
- const r=await(await fetch(RPC,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',id:1,method:'broadcast_tx_sync',params:{tx:Buffer.from(txRaw).toString('base64')}})})).json();
38
- return {vm:'cosmos',code:r.result.code,hash:r.result.hash,pqc:true};
58
+ const r=await(await post(rpc,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',id:1,method:'broadcast_tx_sync',params:{tx:Buffer.from(txRaw).toString('base64')}})})).json();
59
+ if(!r.result) throw new Error(`broadcast_tx_sync failed: ${JSON.stringify(r.error||r)}`);
60
+ return r.result;
39
61
  }
40
62
  // SVM: Solana wallets read natively; execute goes through the cosmos-PQC path.
41
63
  static async readSvm(w){