@qorechain/chain-bridge 0.1.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.
- package/README.md +115 -0
- package/package.json +46 -0
- package/src/cosmos.js +212 -0
- package/src/evm.js +60 -0
- package/src/features.js +82 -0
- package/src/index.d.ts +67 -0
- package/src/index.js +344 -0
- package/src/proto.js +131 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @qorechain/chain-bridge
|
|
2
|
+
|
|
3
|
+
Server-side, autonomous bridge between a Web2 backend (dashboard, exchange,
|
|
4
|
+
payment worker) and QoreChain's on-chain modules. It turns an off-chain event —
|
|
5
|
+
"USDT payment confirmed", "user requested a contract" — into a **real on-chain
|
|
6
|
+
effect**, with no CLI and no human in the loop.
|
|
7
|
+
|
|
8
|
+
It handles the two things a plain backend can't: the FIPS-204 **ML-DSA-87 hybrid
|
|
9
|
+
signature** the chain's PQC ante chain requires on every Cosmos tx, and the
|
|
10
|
+
exact wire encoding of QoreChain's custom module messages.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
USDT paid ──► backend verifies ──► chain-bridge.grantLicenseSet() ──► x/license MsgGrantLicense (PQC-signed)
|
|
14
|
+
└─► license is REAL on-chain
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @qorechain/chain-bridge
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Peer stack (installed automatically): `@cosmjs/proto-signing`,
|
|
24
|
+
`@cosmjs/stargate`, `cosmjs-types`, `ethers`, and the published
|
|
25
|
+
`@qorechain/wallet-adapter` + `@qorechain/pqc`. Pure JS — runs in an AWS Lambda
|
|
26
|
+
(no native addons).
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { ChainBridge } from '@qorechain/chain-bridge';
|
|
32
|
+
|
|
33
|
+
const bridge = new ChainBridge({
|
|
34
|
+
cosmosRpc: process.env.QORE_RPC, // http://node:26657
|
|
35
|
+
chainId: process.env.QORE_CHAIN_ID, // qorechain-diana
|
|
36
|
+
evmRpc: process.env.QORE_EVM_RPC, // http://node:8545
|
|
37
|
+
evmChainId: 9800,
|
|
38
|
+
authorityMnemonic: process.env.LICENSE_AUTHORITY_MNEMONIC, // genesis license.authority
|
|
39
|
+
signerMnemonic: process.env.DEPLOYER_MNEMONIC, // funded operator key
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// After a USDT payment is verified, grant the license on-chain to the user's
|
|
43
|
+
// REAL qor1 address (NOT a custodial pseudo-address):
|
|
44
|
+
const { grants, dropped } = await bridge.grantLicenseSet({
|
|
45
|
+
grantee: 'qor1user…',
|
|
46
|
+
type: 'validator',
|
|
47
|
+
chains: ['Solana', 'Ethereum'], // → validator_operator, validator_solana, validator_ethereum
|
|
48
|
+
metadata: 'licenseId:abc123',
|
|
49
|
+
});
|
|
50
|
+
// grants = [{ featureId, txHash }, …]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
| Method | Module / path | Signer | PQC |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `grantLicenseSet({grantee,type,chains})` | x/license `MsgGrantLicense` ×N | authority | hybrid |
|
|
58
|
+
| `grantLicense / revokeLicense / suspendLicense / resumeLicense` | x/license | authority | hybrid |
|
|
59
|
+
| `registerLightNode({operatorMnemonic,…})` | x/lightnode `MsgRegisterLightNode` | operator | hybrid |
|
|
60
|
+
| `deployEvm({bytecode,abi?,args?})` | EVM `eth_sendRawTransaction` | deployer | none (EVM lane) |
|
|
61
|
+
| `deploySvm({bytecode})` | x/svm `MsgDeployProgram` | signer | hybrid |
|
|
62
|
+
| `sendTokens({to,amountUqor})` | bank `MsgSend` | signer | hybrid |
|
|
63
|
+
| `registerPqc({mnemonic})` | x/pqc `MsgRegisterPQCKeyV2` | self | classical (exempt) |
|
|
64
|
+
|
|
65
|
+
`featureIdsFor({type, chains})` maps a dashboard license to on-chain feature IDs
|
|
66
|
+
(`validator_*`, `bridge_*`, `qcb_bridge`, `lightnode_operator`); unknown chains
|
|
67
|
+
are **dropped**, never minted into an invalid ID.
|
|
68
|
+
|
|
69
|
+
## Keys & PQC bootstrap
|
|
70
|
+
|
|
71
|
+
License/lifecycle messages must be signed by the chain's configured
|
|
72
|
+
`license.authority` (genesis `app_state.license.authority`). Pass that key's
|
|
73
|
+
24-word mnemonic as `authorityMnemonic` — keep it in a secrets manager
|
|
74
|
+
(SSM / Secrets Manager), never on disk.
|
|
75
|
+
|
|
76
|
+
Every hybrid signer needs its ML-DSA-87 public key registered on-chain once.
|
|
77
|
+
`chain-bridge` derives that key deterministically and **auto-registers it on
|
|
78
|
+
first use** if the chain reports it missing (the registration tx is PQC-exempt,
|
|
79
|
+
so it goes through classically). You can also pre-register at deploy time:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
qorechaind tx pqc register-key <ml-dsa-pubkey-hex> hybrid --from authority
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Canonical PQC derivation (v0.1.1+)
|
|
86
|
+
|
|
87
|
+
The **ecosystem-standard** ML-DSA-87 seed is address-bound and identical across
|
|
88
|
+
`@qorechain/wallet-adapter`, the SDK, and both signer paths here:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
seed = SHAKE-256( "qorechain:pqc:v1|" + <account-bech32-address> + "|" + <mnemonic> )
|
|
92
|
+
key = ML-DSA-87.keygen(seed) # deterministic (FIPS-204)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
So the same mnemonic yields the **same** PQC key in every tool — recover it with
|
|
96
|
+
`qorechaind tx pqc recover-key <name> <address> --derivation adapter`.
|
|
97
|
+
|
|
98
|
+
> **Legacy note.** Before v0.1.1 the coin-118 path derived `SHAKE-256(mnemonic)`
|
|
99
|
+
> (no address binding). The chain cannot rotate a key within one algorithm, so
|
|
100
|
+
> accounts registered that way keep the legacy key; `chain-bridge` derives it too
|
|
101
|
+
> and falls back to it automatically when a canonical signature is rejected.
|
|
102
|
+
> Recover a legacy key with `recover-key … --derivation bridge`.
|
|
103
|
+
|
|
104
|
+
## Decimals
|
|
105
|
+
|
|
106
|
+
- Cosmos lane (`uqor`): 6 decimals. `sendTokens` amounts are in `uqor`.
|
|
107
|
+
- EVM lane (`aqor`): 18 decimals. `deployEvm` `value` is in wei (aqor).
|
|
108
|
+
|
|
109
|
+
## Tests
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm test # wire-encoder + feature-map tests (offline, no chain needed)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Author: Liviu Epure · License: Apache-2.0
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qorechain/chain-bridge",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Server-side, autonomous bridge between Web2 backends (dashboards, exchanges) and the QoreChain on-chain modules. Grants licenses on-chain (PQC-signed), deploys contracts on the 3 VMs, registers light nodes, and moves QOR — all from a backend, no CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./proto": "./src/proto.js",
|
|
11
|
+
"./features": "./src/features.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@cosmjs/proto-signing": "^0.32.0",
|
|
25
|
+
"@cosmjs/stargate": "^0.32.0",
|
|
26
|
+
"@qorechain/pqc": "^0.1.1",
|
|
27
|
+
"@qorechain/wallet-adapter": "^0.1.4",
|
|
28
|
+
"cosmjs-types": "^0.9.0",
|
|
29
|
+
"ethers": "^6.13.0"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"qorechain",
|
|
33
|
+
"cosmos",
|
|
34
|
+
"pqc",
|
|
35
|
+
"ml-dsa",
|
|
36
|
+
"license",
|
|
37
|
+
"bridge",
|
|
38
|
+
"evm",
|
|
39
|
+
"svm"
|
|
40
|
+
],
|
|
41
|
+
"author": "Liviu Epure",
|
|
42
|
+
"license": "Apache-2.0",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/cosmos.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// @qorechain/chain-bridge — cosmos tx signing + broadcast.
|
|
2
|
+
//
|
|
3
|
+
// Two broadcast paths:
|
|
4
|
+
// submitHybrid() — the normal path. Layers the FIPS-204 ML-DSA-87 hybrid
|
|
5
|
+
// signature the chain's PQC ante chain requires (via
|
|
6
|
+
// @qorechain/wallet-adapter's QoreChainSigner), then the
|
|
7
|
+
// classical secp256k1 signature. Used for MsgGrantLicense,
|
|
8
|
+
// MsgRegisterLightNode, MsgDeployProgram, MsgSend, …
|
|
9
|
+
// submitClassical() — secp256k1-only, for the PQC-exempt registration msgs
|
|
10
|
+
// (MsgRegisterPQCKeyV2 et al.) used to bootstrap a key.
|
|
11
|
+
//
|
|
12
|
+
// Both derive the secp256k1 wallet from a mnemonic and the ML-DSA-87 keypair
|
|
13
|
+
// deterministically as SHAKE-256(mnemonic) — identical to the chain's
|
|
14
|
+
// examples/server-signer, so a key registered there works here and vice versa.
|
|
15
|
+
|
|
16
|
+
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
|
|
17
|
+
import { StargateClient, accountFromAny } from '@cosmjs/stargate';
|
|
18
|
+
import { BaseAccount } from 'cosmjs-types/cosmos/auth/v1beta1/auth.js';
|
|
19
|
+
import { TxBody, AuthInfo, TxRaw, SignerInfo, ModeInfo, Fee } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
|
|
20
|
+
import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing.js';
|
|
21
|
+
import { PubKey } from 'cosmjs-types/cosmos/crypto/secp256k1/keys.js';
|
|
22
|
+
import {
|
|
23
|
+
frame, encodePqcHybridSignature, HYBRID_SIG_TYPE_URL, ALGORITHM_ML_DSA_87,
|
|
24
|
+
walletFromMnemonic, signClassicalEth, signHybridEth,
|
|
25
|
+
} from '@qorechain/wallet-adapter';
|
|
26
|
+
import { mldsa, shake256 } from '@qorechain/pqc';
|
|
27
|
+
|
|
28
|
+
// Build the full signer context from a mnemonic.
|
|
29
|
+
//
|
|
30
|
+
// keyType:
|
|
31
|
+
// 'secp256k1' (default) — coinType-118 cosmos-native account
|
|
32
|
+
// (address = ripemd160(sha256(pubkey))). Signs Cosmos txs
|
|
33
|
+
// with sha256-secp256k1. PQC seed = SHAKE-256(mnemonic).
|
|
34
|
+
// This is the historical path — the faucet and every already
|
|
35
|
+
// -registered coinType-118 account MUST keep using it.
|
|
36
|
+
// 'eth_secp256k1' — eth-native unified account (address = keccak(pubkey)[12:]),
|
|
37
|
+
// so its qor1 / 0x / svm forms are ONE identity spendable on
|
|
38
|
+
// both the Cosmos and EVM lanes. Signs Cosmos txs with
|
|
39
|
+
// keccak-secp256k1 and the /cosmos.evm.crypto.v1.ethsecp256k1
|
|
40
|
+
// pubkey (requires qorechain-core >= v3.1.83). PQC key +
|
|
41
|
+
// signing are delegated to @qorechain/wallet-adapter's
|
|
42
|
+
// walletFromMnemonic / signHybridEth / signClassicalEth so the
|
|
43
|
+
// same key registered by the wallet SDK verifies here.
|
|
44
|
+
export async function signerContext(mnemonic, prefix = 'qor', { keyType = 'secp256k1' } = {}) {
|
|
45
|
+
if (keyType === 'eth_secp256k1') {
|
|
46
|
+
const w = await walletFromMnemonic(mnemonic); // { privateKey, pubkey, cosmos, evm, svm, pqc }
|
|
47
|
+
return {
|
|
48
|
+
ethNative: true,
|
|
49
|
+
address: w.cosmos,
|
|
50
|
+
pubkeySecp256k1: w.pubkey, // compressed eth_secp256k1 pubkey (used as ecdsa_pubkey in register)
|
|
51
|
+
pqc: w.pqc,
|
|
52
|
+
ethKey: { privateKey: w.privateKey, pubkey: w.pubkey, pqc: w.pqc },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix });
|
|
56
|
+
const [acct] = await wallet.getAccounts();
|
|
57
|
+
// CANONICAL ecosystem PQC derivation: bind the ML-DSA-87 seed to the account
|
|
58
|
+
// address, IDENTICAL to @qorechain/wallet-adapter / SDK and to the eth_secp256k1
|
|
59
|
+
// branch above (walletFromMnemonic). Unifies the derivation across every tool so
|
|
60
|
+
// the same mnemonic yields the same key everywhere.
|
|
61
|
+
const pqc = mldsa.keygen(shake256(new TextEncoder().encode(`qorechain:pqc:v1|${acct.address}|${mnemonic}`), 32));
|
|
62
|
+
// LEGACY fallback: pre-fix chain-bridge derived coin-118 PQC keys as
|
|
63
|
+
// shake256(mnemonic) (no address binding). Accounts registered that way still
|
|
64
|
+
// hold that key on-chain, and the chain cannot rotate a key within the same
|
|
65
|
+
// algorithm — so keep the legacy key available; _submitHybridAuto falls back to
|
|
66
|
+
// it when the canonical signature is rejected.
|
|
67
|
+
const pqcLegacy = mldsa.keygen(shake256(new TextEncoder().encode(mnemonic), 32));
|
|
68
|
+
return { wallet, address: acct.address, pubkeySecp256k1: acct.pubkey, pqc, pqcLegacy };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function feeFor(gasLimit, gasPrice) {
|
|
72
|
+
// gasPrice in uqor/gas (the chain's min-gas-price is gas-proportional).
|
|
73
|
+
const amount = Math.ceil(Number(gasLimit) * Number(gasPrice));
|
|
74
|
+
return { amount: [{ denom: 'uqor', amount: String(amount) }], gasLimit: BigInt(gasLimit) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// cosmjs's default account parser calls decodePubkey, which throws
|
|
78
|
+
// "Pubkey type URL '/cosmos.evm.crypto.v1.ethsecp256k1.PubKey' not recognized"
|
|
79
|
+
// once an eth-native account has sent (its on-chain pubkey is ethsecp256k1). We
|
|
80
|
+
// never need the account's stored pubkey (authInfo is built from ctx), so decode
|
|
81
|
+
// only address/number/sequence from the BaseAccount and ignore the pubkey.
|
|
82
|
+
function qoreAccountParser(any) {
|
|
83
|
+
if (any.typeUrl === '/cosmos.auth.v1beta1.BaseAccount') {
|
|
84
|
+
const ba = BaseAccount.decode(any.value);
|
|
85
|
+
return { address: ba.address, pubkey: null, accountNumber: Number(ba.accountNumber), sequence: Number(ba.sequence) };
|
|
86
|
+
}
|
|
87
|
+
return accountFromAny(any);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function withClient(rpc, fn) {
|
|
91
|
+
const client = await StargateClient.connect(rpc, { accountParser: qoreAccountParser });
|
|
92
|
+
try { return await fn(client); } finally { client.disconnect(); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertOk(res, what) {
|
|
96
|
+
if (res.code !== 0) {
|
|
97
|
+
const err = new Error(`${what} failed: code ${res.code}: ${res.rawLog || ''}`);
|
|
98
|
+
err.code = res.code;
|
|
99
|
+
err.rawLog = res.rawLog;
|
|
100
|
+
err.txHash = res.transactionHash;
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
txHash: res.transactionHash, code: res.code, height: res.height,
|
|
105
|
+
gasUsed: res.gasUsed, events: res.events || [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Find the first attribute value for (eventType, attrKey) in a DeliverTx result.
|
|
110
|
+
export function findEventAttr(events, eventType, attrKey) {
|
|
111
|
+
for (const ev of events || []) {
|
|
112
|
+
if (ev.type !== eventType) continue;
|
|
113
|
+
for (const a of ev.attributes || []) {
|
|
114
|
+
const k = typeof a.key === 'string' ? a.key : Buffer.from(a.key).toString();
|
|
115
|
+
if (k === attrKey) return typeof a.value === 'string' ? a.value : Buffer.from(a.value).toString();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Hybrid (PQC + classical) broadcast. messages: [{ typeUrl, value:Uint8Array }].
|
|
122
|
+
// Mirrors the chain's `tx pqc cosign`:
|
|
123
|
+
// B0 = TxBody{messages, memo, timeoutHeight} (no extension)
|
|
124
|
+
// sigP = ML-DSA-87(deterministic).sign( frame(B0, authInfo) )
|
|
125
|
+
// body = TxBody{...B0, extensionOptions:[PQCHybridSignature{1, sigP}]}
|
|
126
|
+
// sigC = wallet.signDirect( SignDoc{body, authInfo, chainId, accountNumber} )
|
|
127
|
+
// tx = TxRaw{ body, authInfo, [sigC] }
|
|
128
|
+
export async function submitHybrid({ ctx, rpc, chainId, messages, gasLimit, gasPrice, memo = '', what = 'tx' }) {
|
|
129
|
+
return withClient(rpc, async (client) => {
|
|
130
|
+
const acct = await client.getAccount(ctx.address);
|
|
131
|
+
if (!acct) throw new Error(`account ${ctx.address} not found on-chain — fund it first`);
|
|
132
|
+
|
|
133
|
+
// eth-native path: delegate the whole build+sign to the wallet SDK (keccak
|
|
134
|
+
// secp256k1 + ethsecp256k1 pubkey + ML-DSA-87 hybrid), then broadcast.
|
|
135
|
+
if (ctx.ethNative) {
|
|
136
|
+
const txBytes = await signHybridEth({
|
|
137
|
+
key: ctx.ethKey, chainId, accountNumber: acct.accountNumber,
|
|
138
|
+
messages, fee: feeFor(gasLimit, gasPrice), sequence: acct.sequence, memo,
|
|
139
|
+
});
|
|
140
|
+
return assertOk(await client.broadcastTx(txBytes), what);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const pubAny = {
|
|
144
|
+
typeUrl: '/cosmos.crypto.secp256k1.PubKey',
|
|
145
|
+
value: PubKey.encode(PubKey.fromPartial({ key: ctx.pubkeySecp256k1 })).finish(),
|
|
146
|
+
};
|
|
147
|
+
const authInfo = AuthInfo.fromPartial({
|
|
148
|
+
signerInfos: [SignerInfo.fromPartial({
|
|
149
|
+
publicKey: pubAny,
|
|
150
|
+
modeInfo: ModeInfo.fromPartial({ single: { mode: SignMode.SIGN_MODE_DIRECT } }),
|
|
151
|
+
sequence: BigInt(acct.sequence),
|
|
152
|
+
})],
|
|
153
|
+
fee: Fee.fromPartial(feeFor(gasLimit, gasPrice)),
|
|
154
|
+
});
|
|
155
|
+
const authInfoBytes = AuthInfo.encode(authInfo).finish();
|
|
156
|
+
|
|
157
|
+
const b0 = TxBody.encode(TxBody.fromPartial({ messages, memo, timeoutHeight: 0n })).finish();
|
|
158
|
+
// @qorechain/pqc >=0.1.1 signs deterministically (FIPS-204 §3.4) by default,
|
|
159
|
+
// as QoreChain's PQC ante verifier requires.
|
|
160
|
+
const pqcSig = mldsa.sign(ctx.pqc.secretKey, frame(b0, authInfoBytes));
|
|
161
|
+
const bodyBytes = TxBody.encode(TxBody.fromPartial({
|
|
162
|
+
messages, memo, timeoutHeight: 0n,
|
|
163
|
+
extensionOptions: [{ typeUrl: HYBRID_SIG_TYPE_URL, value: encodePqcHybridSignature(ALGORITHM_ML_DSA_87, pqcSig) }],
|
|
164
|
+
})).finish();
|
|
165
|
+
|
|
166
|
+
const { signature } = await ctx.wallet.signDirect(ctx.address, {
|
|
167
|
+
bodyBytes, authInfoBytes, chainId, accountNumber: BigInt(acct.accountNumber),
|
|
168
|
+
});
|
|
169
|
+
const classicalSig = typeof signature.signature === 'string'
|
|
170
|
+
? Uint8Array.from(Buffer.from(signature.signature, 'base64')) : signature.signature;
|
|
171
|
+
const txBytes = TxRaw.encode(TxRaw.fromPartial({ bodyBytes, authInfoBytes, signatures: [classicalSig] })).finish();
|
|
172
|
+
return assertOk(await client.broadcastTx(txBytes), what);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Classical secp256k1-only broadcast (for PQC-exempt registration msgs).
|
|
177
|
+
export async function submitClassical({ ctx, rpc, chainId, messages, gasLimit, gasPrice, memo = '', what = 'tx' }) {
|
|
178
|
+
return withClient(rpc, async (client) => {
|
|
179
|
+
const acct = await client.getAccount(ctx.address);
|
|
180
|
+
if (!acct) throw new Error(`account ${ctx.address} not found on-chain — fund it first`);
|
|
181
|
+
if (ctx.ethNative) {
|
|
182
|
+
const txBytes = await signClassicalEth({
|
|
183
|
+
key: ctx.ethKey, chainId, accountNumber: acct.accountNumber,
|
|
184
|
+
messages, fee: feeFor(gasLimit, gasPrice), sequence: acct.sequence, memo,
|
|
185
|
+
});
|
|
186
|
+
return assertOk(await client.broadcastTx(txBytes), what);
|
|
187
|
+
}
|
|
188
|
+
const pubAny = {
|
|
189
|
+
typeUrl: '/cosmos.crypto.secp256k1.PubKey',
|
|
190
|
+
value: PubKey.encode(PubKey.fromPartial({ key: ctx.pubkeySecp256k1 })).finish(),
|
|
191
|
+
};
|
|
192
|
+
const authInfo = AuthInfo.fromPartial({
|
|
193
|
+
signerInfos: [SignerInfo.fromPartial({
|
|
194
|
+
publicKey: pubAny,
|
|
195
|
+
modeInfo: ModeInfo.fromPartial({ single: { mode: SignMode.SIGN_MODE_DIRECT } }),
|
|
196
|
+
sequence: BigInt(acct.sequence),
|
|
197
|
+
})],
|
|
198
|
+
fee: Fee.fromPartial(feeFor(gasLimit, gasPrice)),
|
|
199
|
+
});
|
|
200
|
+
const authInfoBytes = AuthInfo.encode(authInfo).finish();
|
|
201
|
+
const bodyBytes = TxBody.encode(TxBody.fromPartial({ messages, memo, timeoutHeight: 0n })).finish();
|
|
202
|
+
const signDoc = { bodyBytes, authInfoBytes, chainId, accountNumber: BigInt(acct.accountNumber) };
|
|
203
|
+
const { signature } = await ctx.wallet.signDirect(ctx.address, signDoc);
|
|
204
|
+
const sig = typeof signature.signature === 'string'
|
|
205
|
+
? Uint8Array.from(Buffer.from(signature.signature, 'base64'))
|
|
206
|
+
: signature.signature;
|
|
207
|
+
const txRaw = TxRaw.encode(TxRaw.fromPartial({ bodyBytes, authInfoBytes, signatures: [sig] })).finish();
|
|
208
|
+
return assertOk(await client.broadcastTx(txRaw), what);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { feeFor };
|
package/src/evm.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @qorechain/chain-bridge — EVM contract deployment.
|
|
2
|
+
//
|
|
3
|
+
// The EVM lane is the only deploy path that is PQC-exempt: it uses a standard
|
|
4
|
+
// secp256k1 Ethereum tx (eth_sendRawTransaction) over JSON-RPC, exactly like
|
|
5
|
+
// any EVM chain. The native currency is the 18-decimal `aqor` view of QOR and
|
|
6
|
+
// the testnet chain-id is 9800 (mainnet 9801). ethers handles gas/nonce.
|
|
7
|
+
//
|
|
8
|
+
// Two modes:
|
|
9
|
+
// - { abi, args } → ContractFactory deploy (constructor args supported)
|
|
10
|
+
// - { bytecode } → raw create tx (precompiled creation bytecode)
|
|
11
|
+
|
|
12
|
+
import { ethers } from 'ethers';
|
|
13
|
+
|
|
14
|
+
export async function deployEvm({
|
|
15
|
+
evmRpc,
|
|
16
|
+
evmChainId,
|
|
17
|
+
privateKey,
|
|
18
|
+
mnemonic,
|
|
19
|
+
bytecode,
|
|
20
|
+
abi,
|
|
21
|
+
args = [],
|
|
22
|
+
value, // optional wei to send with constructor (aqor, 18-dec)
|
|
23
|
+
}) {
|
|
24
|
+
if (!evmRpc) throw new Error('deployEvm: evmRpc is required');
|
|
25
|
+
if (!bytecode) throw new Error('deployEvm: bytecode is required');
|
|
26
|
+
|
|
27
|
+
const provider = new ethers.JsonRpcProvider(
|
|
28
|
+
evmRpc,
|
|
29
|
+
evmChainId ? { chainId: Number(evmChainId), name: 'qorechain-evm' } : undefined,
|
|
30
|
+
);
|
|
31
|
+
const wallet = privateKey
|
|
32
|
+
? new ethers.Wallet(privateKey, provider)
|
|
33
|
+
: ethers.Wallet.fromPhrase(mnemonic).connect(provider);
|
|
34
|
+
|
|
35
|
+
const overrides = value != null ? { value } : {};
|
|
36
|
+
|
|
37
|
+
if (abi) {
|
|
38
|
+
const factory = new ethers.ContractFactory(abi, bytecode, wallet);
|
|
39
|
+
const contract = await factory.deploy(...args, overrides);
|
|
40
|
+
const receipt = await contract.deploymentTransaction().wait();
|
|
41
|
+
return {
|
|
42
|
+
vm: 'evm',
|
|
43
|
+
address: await contract.getAddress(),
|
|
44
|
+
txHash: receipt.hash,
|
|
45
|
+
blockNumber: receipt.blockNumber,
|
|
46
|
+
deployer: wallet.address,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = bytecode.startsWith('0x') ? bytecode : '0x' + bytecode;
|
|
51
|
+
const sent = await wallet.sendTransaction({ data, ...overrides });
|
|
52
|
+
const receipt = await sent.wait();
|
|
53
|
+
return {
|
|
54
|
+
vm: 'evm',
|
|
55
|
+
address: receipt.contractAddress,
|
|
56
|
+
txHash: receipt.hash,
|
|
57
|
+
blockNumber: receipt.blockNumber,
|
|
58
|
+
deployer: wallet.address,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/features.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @qorechain/chain-bridge — dashboard license → on-chain feature_id mapping.
|
|
2
|
+
//
|
|
3
|
+
// The dashboard stores a license as { type, tier, chains[] }; the chain gates
|
|
4
|
+
// on flat feature_id strings (x/license/types/feature_ids.go). This module maps
|
|
5
|
+
// one to the other, normalizing free-text chain names to the canonical slugs.
|
|
6
|
+
//
|
|
7
|
+
// type → feature_ids:
|
|
8
|
+
// light_node → ["lightnode_operator"]
|
|
9
|
+
// validator → ["validator_operator", "validator_<chain>"…]
|
|
10
|
+
// cross_network → ["qcb_bridge", "bridge_<chain>"…]
|
|
11
|
+
// Unknown chains are dropped (logged by the caller) rather than minting an
|
|
12
|
+
// invalid feature_id the chain would reject.
|
|
13
|
+
|
|
14
|
+
// Canonical per-chain slugs (mirror feature_ids.go exactly).
|
|
15
|
+
const VALIDATOR_CHAINS = new Set([
|
|
16
|
+
'akash', 'algorand', 'arbitrum', 'avalanche', 'babylon', 'base', 'berachain',
|
|
17
|
+
'blast', 'bsc', 'celestia', 'cosmoshub', 'cronos', 'ethereum', 'filecoin',
|
|
18
|
+
'hedera', 'hyperliquid', 'injective', 'kaia', 'linea', 'mantle', 'monad',
|
|
19
|
+
'noble', 'optimism', 'osmosis', 'plasma', 'polygon', 'scroll', 'sei',
|
|
20
|
+
'solana', 'sonic', 'starknet', 'stellar', 'stride', 'sui', 'ton', 'xrpl',
|
|
21
|
+
'zksync_era',
|
|
22
|
+
]);
|
|
23
|
+
const BRIDGE_CHAINS = new Set([
|
|
24
|
+
'algorand', 'arbitrum', 'avalanche', 'base', 'berachain', 'bitcoin', 'blast',
|
|
25
|
+
'bsc', 'cardano', 'cronos', 'ethereum', 'filecoin', 'hedera', 'hyperliquid',
|
|
26
|
+
'injective', 'kaia', 'linea', 'mantle', 'monad', 'near', 'optimism', 'plasma',
|
|
27
|
+
'polkadot', 'polygon', 'scroll', 'sei', 'solana', 'sonic', 'starknet',
|
|
28
|
+
'stellar', 'sui', 'tezos', 'ton', 'tron', 'xrpl', 'zksync_era',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// Free-text → canonical slug aliases (lowercase keys).
|
|
32
|
+
const ALIASES = {
|
|
33
|
+
eth: 'ethereum', ether: 'ethereum',
|
|
34
|
+
bnb: 'bsc', binance: 'bsc', 'bnb smart chain': 'bsc', 'bnb chain': 'bsc', 'binance smart chain': 'bsc',
|
|
35
|
+
'zksync': 'zksync_era', 'zksync era': 'zksync_era', 'zk sync': 'zksync_era',
|
|
36
|
+
'cosmos': 'cosmoshub', 'cosmos hub': 'cosmoshub', atom: 'cosmoshub',
|
|
37
|
+
'xrp': 'xrpl', ripple: 'xrpl', 'xrp ledger': 'xrpl',
|
|
38
|
+
matic: 'polygon', 'polygon pos': 'polygon',
|
|
39
|
+
avax: 'avalanche', 'avalanche c-chain': 'avalanche',
|
|
40
|
+
btc: 'bitcoin', sol: 'solana', dot: 'polkadot', xtz: 'tezos', trx: 'tron',
|
|
41
|
+
'arbitrum one': 'arbitrum', op: 'optimism', 'op mainnet': 'optimism',
|
|
42
|
+
fil: 'filecoin', xlm: 'stellar', algo: 'algorand',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function normalizeChain(name) {
|
|
46
|
+
const k = String(name || '').trim().toLowerCase();
|
|
47
|
+
if (!k) return '';
|
|
48
|
+
if (ALIASES[k]) return ALIASES[k];
|
|
49
|
+
return k.replace(/[\s\-./]+/g, '_');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Returns { featureIds: string[], dropped: string[] }.
|
|
53
|
+
export function featureIdsFor({ type, chains = [] } = {}) {
|
|
54
|
+
const t = String(type || '').trim().toLowerCase();
|
|
55
|
+
const ids = [];
|
|
56
|
+
const dropped = [];
|
|
57
|
+
const list = Array.isArray(chains) ? chains : (chains ? [chains] : []);
|
|
58
|
+
|
|
59
|
+
if (t === 'light_node' || t === 'lightnode' || t === 'light-node') {
|
|
60
|
+
ids.push('lightnode_operator');
|
|
61
|
+
} else if (t === 'validator') {
|
|
62
|
+
ids.push('validator_operator');
|
|
63
|
+
for (const c of list) {
|
|
64
|
+
const s = normalizeChain(c);
|
|
65
|
+
if (VALIDATOR_CHAINS.has(s)) ids.push(`validator_${s}`);
|
|
66
|
+
else if (s) dropped.push(c);
|
|
67
|
+
}
|
|
68
|
+
} else if (t === 'cross_network' || t === 'bridge' || t === 'crossnetwork') {
|
|
69
|
+
ids.push('qcb_bridge');
|
|
70
|
+
for (const c of list) {
|
|
71
|
+
const s = normalizeChain(c);
|
|
72
|
+
if (BRIDGE_CHAINS.has(s)) ids.push(`bridge_${s}`);
|
|
73
|
+
else if (s) dropped.push(c);
|
|
74
|
+
}
|
|
75
|
+
} else if (t) {
|
|
76
|
+
dropped.push(`type:${type}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { featureIds: [...new Set(ids)], dropped };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const __test__ = { VALIDATOR_CHAINS, BRIDGE_CHAINS };
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Type definitions for @qorechain/chain-bridge
|
|
2
|
+
|
|
3
|
+
export interface ChainBridgeConfig {
|
|
4
|
+
cosmosRpc: string; // e.g. http://node:26657
|
|
5
|
+
chainId: string; // e.g. qorechain-diana
|
|
6
|
+
cosmosRest?: string; // optional REST/LCD (:1317)
|
|
7
|
+
evmRpc?: string; // EVM JSON-RPC (:8545)
|
|
8
|
+
evmChainId?: number; // 9800 testnet / 9801 mainnet
|
|
9
|
+
prefix?: string; // bech32 prefix, default 'qor'
|
|
10
|
+
denom?: string; // base denom, default 'uqor'
|
|
11
|
+
gasPrice?: number; // uqor/gas, default 0.1
|
|
12
|
+
authorityMnemonic?: string; // x/license authority key (for grant/revoke/…)
|
|
13
|
+
signerMnemonic?: string; // funded operator/treasury key (deploy/send)
|
|
14
|
+
evmDeployerKey?: string; // EVM deployer private key (hex)
|
|
15
|
+
gas?: Partial<Record<'grant' | 'licenseAction' | 'registerPqc' | 'registerLightNode' | 'deploySvm' | 'send', number>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TxResult {
|
|
19
|
+
txHash: string;
|
|
20
|
+
code: number;
|
|
21
|
+
height?: number;
|
|
22
|
+
gasUsed?: bigint;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GrantSetResult {
|
|
26
|
+
grants: Array<{ featureId: string; txHash: string; height?: number }>;
|
|
27
|
+
dropped: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EvmDeployResult {
|
|
31
|
+
vm: 'evm';
|
|
32
|
+
address: string;
|
|
33
|
+
txHash: string;
|
|
34
|
+
blockNumber: number;
|
|
35
|
+
deployer: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LicenseSpec { type: string; chains?: string[] }
|
|
39
|
+
export interface FeatureMapResult { featureIds: string[]; dropped: string[] }
|
|
40
|
+
|
|
41
|
+
export class ChainBridge {
|
|
42
|
+
constructor(cfg: ChainBridgeConfig);
|
|
43
|
+
featureIdsFor(license: LicenseSpec): FeatureMapResult;
|
|
44
|
+
normalizeChain(name: string): string;
|
|
45
|
+
|
|
46
|
+
registerPqc(opts: { mnemonic: string }): Promise<TxResult>;
|
|
47
|
+
|
|
48
|
+
grantLicense(opts: { grantee: string; featureId: string; expiresAt?: number; metadata?: string; authorityMnemonic?: string }): Promise<TxResult>;
|
|
49
|
+
grantLicenseSet(opts: { grantee: string; type: string; chains?: string[]; expiresAt?: number; metadata?: string; authorityMnemonic?: string }): Promise<GrantSetResult>;
|
|
50
|
+
revokeLicense(opts: { grantee: string; featureId: string; authorityMnemonic?: string }): Promise<TxResult>;
|
|
51
|
+
suspendLicense(opts: { grantee: string; featureId: string; authorityMnemonic?: string }): Promise<TxResult>;
|
|
52
|
+
resumeLicense(opts: { grantee: string; featureId: string; authorityMnemonic?: string }): Promise<TxResult>;
|
|
53
|
+
|
|
54
|
+
registerLightNode(opts: { operatorMnemonic: string; nodeType?: string; version?: string; capabilities?: string[] }): Promise<TxResult>;
|
|
55
|
+
|
|
56
|
+
deployEvm(opts: { bytecode: string; abi?: any[]; args?: any[]; value?: bigint; evmRpc?: string; evmChainId?: number; privateKey?: string; mnemonic?: string }): Promise<EvmDeployResult>;
|
|
57
|
+
deploySvm(opts: { bytecode: string | Uint8Array; signerMnemonic?: string; gasLimit?: number }): Promise<TxResult>;
|
|
58
|
+
|
|
59
|
+
sendTokens(opts: { to: string; amountUqor: string | number; signerMnemonic?: string }): Promise<TxResult>;
|
|
60
|
+
|
|
61
|
+
createVestingAccount(opts: { toAddress: string; amountUqor: string | number; endTime: number; delayed?: boolean; signerMnemonic?: string }): Promise<TxResult>;
|
|
62
|
+
createPeriodicVestingAccount(opts: { toAddress: string; startTime: number; periods: Array<{ length: number; amountUqor: string | number }>; signerMnemonic?: string }): Promise<TxResult>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function featureIdsFor(license: LicenseSpec): FeatureMapResult;
|
|
66
|
+
export function normalizeChain(name: string): string;
|
|
67
|
+
export default ChainBridge;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// @qorechain/chain-bridge
|
|
2
|
+
//
|
|
3
|
+
// Autonomous, server-side bridge between a Web2 backend and QoreChain's on-chain
|
|
4
|
+
// modules. Lets a dashboard / exchange / payment worker turn an off-chain event
|
|
5
|
+
// (USDT payment confirmed, contract requested) into a REAL on-chain effect with
|
|
6
|
+
// no CLI and no human in the loop:
|
|
7
|
+
//
|
|
8
|
+
// grantLicense / grantLicenseSet → x/license MsgGrantLicense (PQC-signed)
|
|
9
|
+
// revoke / suspend / resume → x/license lifecycle msgs (PQC-signed)
|
|
10
|
+
// registerLightNode → x/lightnode MsgRegisterLightNode (PQC)
|
|
11
|
+
// deployEvm → EVM eth_sendRawTransaction (secp256k1)
|
|
12
|
+
// deploySvm → x/svm MsgDeployProgram (PQC-signed)
|
|
13
|
+
// sendTokens → bank MsgSend (PQC-signed)
|
|
14
|
+
// registerPqc → x/pqc MsgRegisterPQCKeyV2 (bootstrap)
|
|
15
|
+
//
|
|
16
|
+
// License/lifecycle messages must be signed by the chain's configured
|
|
17
|
+
// `license.authority` (genesis: app_state.license.authority). Pass that key's
|
|
18
|
+
// mnemonic as `authorityMnemonic`. Deploys / sends use `signerMnemonic`
|
|
19
|
+
// (a funded operator/treasury key) unless a per-call mnemonic is given.
|
|
20
|
+
|
|
21
|
+
import { MsgSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx.js';
|
|
22
|
+
import { MsgCreateVestingAccount, MsgCreatePeriodicVestingAccount } from 'cosmjs-types/cosmos/vesting/v1beta1/tx.js';
|
|
23
|
+
import * as proto from './proto.js';
|
|
24
|
+
import { signerContext, submitHybrid, submitClassical, findEventAttr } from './cosmos.js';
|
|
25
|
+
import { deployEvm } from './evm.js';
|
|
26
|
+
import { featureIdsFor, normalizeChain } from './features.js';
|
|
27
|
+
|
|
28
|
+
const DEFAULTS = {
|
|
29
|
+
prefix: 'qor',
|
|
30
|
+
denom: 'uqor',
|
|
31
|
+
gasPrice: 0.1, // uqor/gas — matches the chain's gas-proportional min fee
|
|
32
|
+
gas: {
|
|
33
|
+
grant: 600000,
|
|
34
|
+
licenseAction: 400000,
|
|
35
|
+
registerPqc: 6000000, // large: 2592-byte ML-DSA pubkey + classical
|
|
36
|
+
registerLightNode: 500000,
|
|
37
|
+
deploySvm: 3000000,
|
|
38
|
+
storeWasm: 6000000,
|
|
39
|
+
instantiateWasm: 1000000,
|
|
40
|
+
send: 300000,
|
|
41
|
+
vesting: 400000,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// rawLog fragments that mean "the signer has no registered PQC key yet".
|
|
46
|
+
const PQC_MISSING = /pqc|hybrid signature|public key not found|no pqc key|register.*key/i;
|
|
47
|
+
|
|
48
|
+
export class ChainBridge {
|
|
49
|
+
constructor(cfg = {}) {
|
|
50
|
+
if (!cfg.cosmosRpc) throw new Error('ChainBridge: cosmosRpc is required');
|
|
51
|
+
if (!cfg.chainId) throw new Error('ChainBridge: chainId is required');
|
|
52
|
+
this.cfg = {
|
|
53
|
+
...DEFAULTS,
|
|
54
|
+
...cfg,
|
|
55
|
+
gas: { ...DEFAULTS.gas, ...(cfg.gas || {}) },
|
|
56
|
+
};
|
|
57
|
+
this._ctxCache = new Map(); // mnemonic → signerContext (lazy)
|
|
58
|
+
this._pqcEnsured = new Set(); // addresses we've already bootstrapped this run
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _ctx(mnemonic) {
|
|
62
|
+
if (!mnemonic) throw new Error('ChainBridge: no mnemonic available for this operation');
|
|
63
|
+
// keyType: 'secp256k1' (coinType-118, default) | 'eth_secp256k1' (unified
|
|
64
|
+
// eth-native, qor1/0x/svm one identity). Cache per (mnemonic, keyType).
|
|
65
|
+
const keyType = this.cfg.keyType || 'secp256k1';
|
|
66
|
+
const cacheKey = `${keyType}:${mnemonic}`;
|
|
67
|
+
if (!this._ctxCache.has(cacheKey)) {
|
|
68
|
+
this._ctxCache.set(cacheKey, await signerContext(mnemonic, this.cfg.prefix, { keyType }));
|
|
69
|
+
}
|
|
70
|
+
return this._ctxCache.get(cacheKey);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
featureIdsFor(license) { return featureIdsFor(license); }
|
|
74
|
+
normalizeChain(name) { return normalizeChain(name); }
|
|
75
|
+
|
|
76
|
+
// ── PQC bootstrap ──────────────────────────────────────────────────────────
|
|
77
|
+
// Register the signer's ML-DSA-87 public key on-chain so its hybrid txs verify.
|
|
78
|
+
// Uses the classical (PQC-exempt) path. Idempotent within a process run.
|
|
79
|
+
async registerPqc({ mnemonic }) {
|
|
80
|
+
const ctx = await this._ctx(mnemonic);
|
|
81
|
+
const value = proto.encodeMsgRegisterPQCKeyV2({
|
|
82
|
+
sender: ctx.address,
|
|
83
|
+
publicKey: ctx.pqc.publicKey,
|
|
84
|
+
algorithmId: 1, // ML-DSA-87 / Dilithium-5
|
|
85
|
+
ecdsaPubkey: ctx.pubkeySecp256k1,
|
|
86
|
+
keyType: 'hybrid',
|
|
87
|
+
});
|
|
88
|
+
const res = await submitClassical({
|
|
89
|
+
ctx,
|
|
90
|
+
rpc: this.cfg.cosmosRpc,
|
|
91
|
+
chainId: this.cfg.chainId,
|
|
92
|
+
messages: [{ typeUrl: proto.TYPE_URLS.registerPqcV2, value }],
|
|
93
|
+
gasLimit: this.cfg.gas.registerPqc,
|
|
94
|
+
gasPrice: this.cfg.gasPrice,
|
|
95
|
+
memo: 'chain-bridge: register pqc key',
|
|
96
|
+
what: 'register-pqc-key',
|
|
97
|
+
});
|
|
98
|
+
this._pqcEnsured.add(ctx.address);
|
|
99
|
+
return res;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Submit a hybrid tx, auto-bootstrapping the signer's PQC key once if the
|
|
103
|
+
// chain rejects it for a missing PQC registration.
|
|
104
|
+
async _submitHybridAuto({ mnemonic, messages, gasLimit, memo, what }) {
|
|
105
|
+
const ctx = await this._ctx(mnemonic);
|
|
106
|
+
const args = {
|
|
107
|
+
ctx, rpc: this.cfg.cosmosRpc, chainId: this.cfg.chainId,
|
|
108
|
+
messages, gasLimit, gasPrice: this.cfg.gasPrice, memo, what,
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
return await submitHybrid(args);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const log = String(err.rawLog || err.message || '');
|
|
114
|
+
// (a) Legacy-derivation account: its on-chain PQC key was registered with the
|
|
115
|
+
// old shake256(mnemonic) derivation, so the canonical signature is rejected
|
|
116
|
+
// (code 21). Retry with the legacy key. Checked FIRST — PQC_MISSING is broad
|
|
117
|
+
// and would otherwise also match this "…verification failed" message.
|
|
118
|
+
if (ctx.pqcLegacy && !ctx.usedLegacy && /verification failed/i.test(log)) {
|
|
119
|
+
return submitHybrid({ ...args, ctx: { ...ctx, pqc: ctx.pqcLegacy, usedLegacy: true } });
|
|
120
|
+
}
|
|
121
|
+
// (b) No PQC key registered yet → register the CANONICAL key + retry.
|
|
122
|
+
if (!this._pqcEnsured.has(ctx.address) && PQC_MISSING.test(log)) {
|
|
123
|
+
await this.registerPqc({ mnemonic });
|
|
124
|
+
return submitHybrid(args);
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── x/license ────────────────────────────────────────────────────────────--
|
|
131
|
+
async grantLicense({ grantee, featureId, expiresAt = 0, metadata = '', authorityMnemonic }) {
|
|
132
|
+
const mnemonic = authorityMnemonic || this.cfg.authorityMnemonic;
|
|
133
|
+
const ctx = await this._ctx(mnemonic);
|
|
134
|
+
const value = proto.encodeMsgGrantLicense({
|
|
135
|
+
authority: ctx.address, grantee, featureId, expiresAt, metadata,
|
|
136
|
+
});
|
|
137
|
+
return this._submitHybridAuto({
|
|
138
|
+
mnemonic,
|
|
139
|
+
messages: [{ typeUrl: proto.TYPE_URLS.grantLicense, value }],
|
|
140
|
+
gasLimit: this.cfg.gas.grant,
|
|
141
|
+
memo: `chain-bridge: grant ${featureId}`,
|
|
142
|
+
what: `grant-license:${featureId}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Grant every feature_id implied by a dashboard license {type, chains}.
|
|
147
|
+
// Returns { grants: [{featureId, txHash}], dropped: [...] }. Each grant is an
|
|
148
|
+
// independent tx so a single failure doesn't roll back the others.
|
|
149
|
+
async grantLicenseSet({ grantee, type, chains = [], expiresAt = 0, metadata = '', authorityMnemonic }) {
|
|
150
|
+
const { featureIds, dropped } = featureIdsFor({ type, chains });
|
|
151
|
+
const grants = [];
|
|
152
|
+
for (const featureId of featureIds) {
|
|
153
|
+
const res = await this.grantLicense({ grantee, featureId, expiresAt, metadata, authorityMnemonic });
|
|
154
|
+
grants.push({ featureId, txHash: res.txHash, height: res.height });
|
|
155
|
+
}
|
|
156
|
+
return { grants, dropped };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async _licenseAction(kind, { grantee, featureId, authorityMnemonic }) {
|
|
160
|
+
const mnemonic = authorityMnemonic || this.cfg.authorityMnemonic;
|
|
161
|
+
const ctx = await this._ctx(mnemonic);
|
|
162
|
+
const value = proto.encodeMsgLicenseAction({ authority: ctx.address, grantee, featureId });
|
|
163
|
+
const typeUrl = proto.TYPE_URLS[`${kind}License`];
|
|
164
|
+
return this._submitHybridAuto({
|
|
165
|
+
mnemonic,
|
|
166
|
+
messages: [{ typeUrl, value }],
|
|
167
|
+
gasLimit: this.cfg.gas.licenseAction,
|
|
168
|
+
memo: `chain-bridge: ${kind} ${featureId}`,
|
|
169
|
+
what: `${kind}-license:${featureId}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
revokeLicense(a) { return this._licenseAction('revoke', a); }
|
|
173
|
+
suspendLicense(a) { return this._licenseAction('suspend', a); }
|
|
174
|
+
resumeLicense(a) { return this._licenseAction('resume', a); }
|
|
175
|
+
|
|
176
|
+
// ── x/lightnode ──────────────────────────────────────────────────────────--
|
|
177
|
+
async registerLightNode({ operatorMnemonic, nodeType = 'SX', version = '1.0.0', capabilities = [] }) {
|
|
178
|
+
const ctx = await this._ctx(operatorMnemonic);
|
|
179
|
+
const value = proto.encodeMsgRegisterLightNode({
|
|
180
|
+
operator: ctx.address, nodeType, version, capabilities,
|
|
181
|
+
});
|
|
182
|
+
return this._submitHybridAuto({
|
|
183
|
+
mnemonic: operatorMnemonic,
|
|
184
|
+
messages: [{ typeUrl: proto.TYPE_URLS.registerLightNode, value }],
|
|
185
|
+
gasLimit: this.cfg.gas.registerLightNode,
|
|
186
|
+
memo: 'chain-bridge: register light node',
|
|
187
|
+
what: 'register-light-node',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── x/svm ────────────────────────────────────────────────────────────────--
|
|
192
|
+
async deploySvm({ bytecode, signerMnemonic, gasLimit }) {
|
|
193
|
+
const mnemonic = signerMnemonic || this.cfg.signerMnemonic;
|
|
194
|
+
const ctx = await this._ctx(mnemonic);
|
|
195
|
+
const bytes = typeof bytecode === 'string'
|
|
196
|
+
? Uint8Array.from(Buffer.from(bytecode.replace(/^0x/, ''), 'hex'))
|
|
197
|
+
: bytecode;
|
|
198
|
+
const value = proto.encodeMsgDeployProgram({ sender: ctx.address, bytecode: bytes });
|
|
199
|
+
return this._submitHybridAuto({
|
|
200
|
+
mnemonic,
|
|
201
|
+
messages: [{ typeUrl: proto.TYPE_URLS.deployProgram, value }],
|
|
202
|
+
gasLimit: gasLimit || this.cfg.gas.deploySvm,
|
|
203
|
+
memo: 'chain-bridge: deploy svm program',
|
|
204
|
+
what: 'deploy-svm-program',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── CosmWasm (WASM VM) ─────────────────────────────────────────────────────
|
|
209
|
+
// Store wasm bytecode → returns { txHash, codeId }.
|
|
210
|
+
async storeWasm({ wasmBytes, signerMnemonic, gasLimit }) {
|
|
211
|
+
const mnemonic = signerMnemonic || this.cfg.signerMnemonic;
|
|
212
|
+
const ctx = await this._ctx(mnemonic);
|
|
213
|
+
const bytes = typeof wasmBytes === 'string'
|
|
214
|
+
? Uint8Array.from(Buffer.from(wasmBytes.replace(/^0x/, ''), 'hex'))
|
|
215
|
+
: wasmBytes;
|
|
216
|
+
const value = proto.encodeMsgStoreCode({ sender: ctx.address, wasmByteCode: bytes });
|
|
217
|
+
const res = await this._submitHybridAuto({
|
|
218
|
+
mnemonic,
|
|
219
|
+
messages: [{ typeUrl: proto.TYPE_URLS.storeCode, value }],
|
|
220
|
+
gasLimit: gasLimit || this.cfg.gas.storeWasm,
|
|
221
|
+
memo: 'chain-bridge: store wasm',
|
|
222
|
+
what: 'store-wasm',
|
|
223
|
+
});
|
|
224
|
+
const codeId = findEventAttr(res.events, 'store_code', 'code_id');
|
|
225
|
+
return { ...res, codeId: codeId ? Number(codeId) : undefined };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Instantiate a stored code id → returns { txHash, contractAddress }.
|
|
229
|
+
async instantiateWasm({ codeId, initMsg = {}, label = 'qorechain-contract', admin = '', signerMnemonic, funds = [] }) {
|
|
230
|
+
const mnemonic = signerMnemonic || this.cfg.signerMnemonic;
|
|
231
|
+
const ctx = await this._ctx(mnemonic);
|
|
232
|
+
const value = proto.encodeMsgInstantiateContract({
|
|
233
|
+
sender: ctx.address, admin, codeId,
|
|
234
|
+
label, msg: JSON.stringify(initMsg), funds,
|
|
235
|
+
});
|
|
236
|
+
const res = await this._submitHybridAuto({
|
|
237
|
+
mnemonic,
|
|
238
|
+
messages: [{ typeUrl: proto.TYPE_URLS.instantiate, value }],
|
|
239
|
+
gasLimit: this.cfg.gas.instantiateWasm,
|
|
240
|
+
memo: 'chain-bridge: instantiate wasm',
|
|
241
|
+
what: 'instantiate-wasm',
|
|
242
|
+
});
|
|
243
|
+
const contractAddress = findEventAttr(res.events, 'instantiate', '_contract_address');
|
|
244
|
+
return { ...res, contractAddress };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Store + instantiate in one call → { codeId, contractAddress, txHashes }.
|
|
248
|
+
async deployWasm({ wasmBytes, initMsg = {}, label = 'qorechain-contract', admin = '', signerMnemonic }) {
|
|
249
|
+
const stored = await this.storeWasm({ wasmBytes, signerMnemonic });
|
|
250
|
+
if (!stored.codeId) throw new Error('deployWasm: could not read code_id from store tx events');
|
|
251
|
+
const inst = await this.instantiateWasm({ codeId: stored.codeId, initMsg, label, admin, signerMnemonic });
|
|
252
|
+
return {
|
|
253
|
+
vm: 'wasm',
|
|
254
|
+
codeId: stored.codeId,
|
|
255
|
+
contractAddress: inst.contractAddress,
|
|
256
|
+
txHashes: [stored.txHash, inst.txHash],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── EVM ──────────────────────────────────────────────────────────────────--
|
|
261
|
+
async deployEvm(opts) {
|
|
262
|
+
return deployEvm({
|
|
263
|
+
evmRpc: opts.evmRpc || this.cfg.evmRpc,
|
|
264
|
+
evmChainId: opts.evmChainId || this.cfg.evmChainId,
|
|
265
|
+
privateKey: opts.privateKey || this.cfg.evmDeployerKey,
|
|
266
|
+
mnemonic: opts.mnemonic || this.cfg.signerMnemonic,
|
|
267
|
+
bytecode: opts.bytecode,
|
|
268
|
+
abi: opts.abi,
|
|
269
|
+
args: opts.args || [],
|
|
270
|
+
value: opts.value,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── vesting (x/auth/vesting) ────────────────────────────────────────────────
|
|
275
|
+
// Create a continuous (or delayed) vesting account for `toAddress`, funded by
|
|
276
|
+
// the signer. Single end_time. delayed=true → all locked until end_time (cliff
|
|
277
|
+
// only); delayed=false → linear from start (account creation) to end_time.
|
|
278
|
+
async createVestingAccount({ toAddress, amountUqor, endTime, delayed = false, signerMnemonic }) {
|
|
279
|
+
const mnemonic = signerMnemonic || this.cfg.signerMnemonic;
|
|
280
|
+
const ctx = await this._ctx(mnemonic);
|
|
281
|
+
const value = MsgCreateVestingAccount.encode(MsgCreateVestingAccount.fromPartial({
|
|
282
|
+
fromAddress: ctx.address,
|
|
283
|
+
toAddress,
|
|
284
|
+
amount: [{ denom: this.cfg.denom, amount: String(amountUqor) }],
|
|
285
|
+
endTime: BigInt(Math.trunc(endTime)),
|
|
286
|
+
delayed,
|
|
287
|
+
})).finish();
|
|
288
|
+
return this._submitHybridAuto({
|
|
289
|
+
mnemonic,
|
|
290
|
+
messages: [{ typeUrl: '/cosmos.vesting.v1beta1.MsgCreateVestingAccount', value }],
|
|
291
|
+
gasLimit: this.cfg.gas.vesting,
|
|
292
|
+
memo: 'chain-bridge: create vesting account',
|
|
293
|
+
what: 'create-vesting-account',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Create a periodic vesting account — the right tool for "TGE unlock + cliff +
|
|
298
|
+
// linear" schedules. `periods` = [{ length: seconds, amountUqor }] (length is
|
|
299
|
+
// RELATIVE to the previous period; the amount unlocks at the END of the period).
|
|
300
|
+
// Sum of amounts MUST equal the total funded amount.
|
|
301
|
+
async createPeriodicVestingAccount({ toAddress, startTime, periods, signerMnemonic }) {
|
|
302
|
+
const mnemonic = signerMnemonic || this.cfg.signerMnemonic;
|
|
303
|
+
const ctx = await this._ctx(mnemonic);
|
|
304
|
+
const vestingPeriods = periods.map((p) => ({
|
|
305
|
+
length: BigInt(Math.trunc(p.length)),
|
|
306
|
+
amount: [{ denom: this.cfg.denom, amount: String(p.amountUqor) }],
|
|
307
|
+
}));
|
|
308
|
+
const value = MsgCreatePeriodicVestingAccount.encode(MsgCreatePeriodicVestingAccount.fromPartial({
|
|
309
|
+
fromAddress: ctx.address,
|
|
310
|
+
toAddress,
|
|
311
|
+
startTime: BigInt(Math.trunc(startTime)),
|
|
312
|
+
vestingPeriods,
|
|
313
|
+
})).finish();
|
|
314
|
+
return this._submitHybridAuto({
|
|
315
|
+
mnemonic,
|
|
316
|
+
messages: [{ typeUrl: '/cosmos.vesting.v1beta1.MsgCreatePeriodicVestingAccount', value }],
|
|
317
|
+
gasLimit: Math.max(this.cfg.gas.vesting, 200000 + periods.length * 8000),
|
|
318
|
+
memo: 'chain-bridge: create periodic vesting account',
|
|
319
|
+
what: 'create-periodic-vesting-account',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── bank ─────────────────────────────────────────────────────────────────--
|
|
324
|
+
async sendTokens({ to, amountUqor, signerMnemonic }) {
|
|
325
|
+
const mnemonic = signerMnemonic || this.cfg.signerMnemonic;
|
|
326
|
+
const ctx = await this._ctx(mnemonic);
|
|
327
|
+
const value = MsgSend.encode(MsgSend.fromPartial({
|
|
328
|
+
fromAddress: ctx.address,
|
|
329
|
+
toAddress: to,
|
|
330
|
+
amount: [{ denom: this.cfg.denom, amount: String(amountUqor) }],
|
|
331
|
+
})).finish();
|
|
332
|
+
return this._submitHybridAuto({
|
|
333
|
+
mnemonic,
|
|
334
|
+
messages: [{ typeUrl: '/cosmos.bank.v1beta1.MsgSend', value }],
|
|
335
|
+
gasLimit: this.cfg.gas.send,
|
|
336
|
+
memo: 'chain-bridge: send',
|
|
337
|
+
what: 'send-tokens',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export { featureIdsFor, normalizeChain } from './features.js';
|
|
343
|
+
export * as proto from './proto.js';
|
|
344
|
+
export default ChainBridge;
|
package/src/proto.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// @qorechain/chain-bridge — minimal protobuf wire encoders.
|
|
2
|
+
//
|
|
3
|
+
// QoreChain's custom modules (license, pqc, lightnode, svm) have no published
|
|
4
|
+
// cosmjs-types codecs, so we hand-encode the handful of Msg types we need.
|
|
5
|
+
// Their shapes are trivial (a few strings / bytes / varints), and proto3 wire
|
|
6
|
+
// format is stable, so a ~60-line encoder is more robust than dragging in a
|
|
7
|
+
// proto runtime. Field numbers/types mirror the .proto exactly:
|
|
8
|
+
// x/license/v1/tx.proto, x/pqc/v1/tx.proto, x/lightnode/v1/tx.proto,
|
|
9
|
+
// x/svm/v1/tx.proto.
|
|
10
|
+
//
|
|
11
|
+
// Each encoder returns a Uint8Array suitable as the `value` of a cosmos Any
|
|
12
|
+
// ({ typeUrl, value }) — which is exactly what QoreChainSigner.signHybrid and
|
|
13
|
+
// our classical signer consume.
|
|
14
|
+
|
|
15
|
+
const EMPTY = new Uint8Array(0);
|
|
16
|
+
const utf8 = (s) => new TextEncoder().encode(s);
|
|
17
|
+
|
|
18
|
+
// LEB128 varint of a non-negative integer (Number or BigInt).
|
|
19
|
+
function varint(n) {
|
|
20
|
+
let v = typeof n === 'bigint' ? n : BigInt(Math.trunc(n));
|
|
21
|
+
if (v < 0n) throw new RangeError('varint: negative value');
|
|
22
|
+
const out = [];
|
|
23
|
+
do {
|
|
24
|
+
let b = Number(v & 0x7fn);
|
|
25
|
+
v >>= 7n;
|
|
26
|
+
if (v > 0n) b |= 0x80;
|
|
27
|
+
out.push(b);
|
|
28
|
+
} while (v > 0n);
|
|
29
|
+
return Uint8Array.from(out);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function concat(chunks) {
|
|
33
|
+
let total = 0;
|
|
34
|
+
for (const c of chunks) total += c.length;
|
|
35
|
+
const out = new Uint8Array(total);
|
|
36
|
+
let o = 0;
|
|
37
|
+
for (const c of chunks) { out.set(c, o); o += c.length; }
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// wire types: 0 = varint, 2 = length-delimited
|
|
42
|
+
const tag = (field, wire) => varint((field << 3) | wire);
|
|
43
|
+
|
|
44
|
+
const lenDelim = (field, bytes) => concat([tag(field, 2), varint(bytes.length), bytes]);
|
|
45
|
+
|
|
46
|
+
// proto3: scalar default values are omitted on the wire.
|
|
47
|
+
const strField = (field, s) => (s ? lenDelim(field, utf8(s)) : EMPTY);
|
|
48
|
+
const bytesField = (field, b) => (b && b.length ? lenDelim(field, b instanceof Uint8Array ? b : Uint8Array.from(b)) : EMPTY);
|
|
49
|
+
const varintField = (field, n) => (n ? concat([tag(field, 0), varint(n)]) : EMPTY);
|
|
50
|
+
|
|
51
|
+
// ── typeUrls (cosmos Any uses a leading slash) ──────────────────────────────
|
|
52
|
+
export const TYPE_URLS = {
|
|
53
|
+
grantLicense: '/qorechain.license.v1.MsgGrantLicense',
|
|
54
|
+
revokeLicense: '/qorechain.license.v1.MsgRevokeLicense',
|
|
55
|
+
suspendLicense: '/qorechain.license.v1.MsgSuspendLicense',
|
|
56
|
+
resumeLicense: '/qorechain.license.v1.MsgResumeLicense',
|
|
57
|
+
registerPqcV2: '/qorechain.pqc.v1.MsgRegisterPQCKeyV2',
|
|
58
|
+
registerLightNode: '/qorechain.lightnode.v1.MsgRegisterLightNode',
|
|
59
|
+
deployProgram: '/qorechain.svm.v1.MsgDeployProgram',
|
|
60
|
+
storeCode: '/cosmwasm.wasm.v1.MsgStoreCode',
|
|
61
|
+
instantiate: '/cosmwasm.wasm.v1.MsgInstantiateContract',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ── x/license ───────────────────────────────────────────────────────────────
|
|
65
|
+
// MsgGrantLicense{ authority=1, grantee=2, feature_id=3, expires_at=4, metadata=5 }
|
|
66
|
+
export function encodeMsgGrantLicense({ authority, grantee, featureId, expiresAt = 0, metadata = '' }) {
|
|
67
|
+
return concat([
|
|
68
|
+
strField(1, authority),
|
|
69
|
+
strField(2, grantee),
|
|
70
|
+
strField(3, featureId),
|
|
71
|
+
varintField(4, expiresAt),
|
|
72
|
+
strField(5, metadata),
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
// Revoke/Suspend/Resume share { authority=1, grantee=2, feature_id=3 }
|
|
76
|
+
export function encodeMsgLicenseAction({ authority, grantee, featureId }) {
|
|
77
|
+
return concat([strField(1, authority), strField(2, grantee), strField(3, featureId)]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── x/pqc ─────────────────────────────────────────────────────────────────--
|
|
81
|
+
// MsgRegisterPQCKeyV2{ sender=1, public_key=2, algorithm_id=3, ecdsa_pubkey=4, key_type=5 }
|
|
82
|
+
export function encodeMsgRegisterPQCKeyV2({ sender, publicKey, algorithmId = 1, ecdsaPubkey, keyType = 'hybrid' }) {
|
|
83
|
+
return concat([
|
|
84
|
+
strField(1, sender),
|
|
85
|
+
bytesField(2, publicKey),
|
|
86
|
+
varintField(3, algorithmId),
|
|
87
|
+
bytesField(4, ecdsaPubkey),
|
|
88
|
+
strField(5, keyType),
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── x/lightnode ───────────────────────────────────────────────────────────--
|
|
93
|
+
// MsgRegisterLightNode{ operator=1, node_type=2, version=3, capabilities=4 (repeated) }
|
|
94
|
+
export function encodeMsgRegisterLightNode({ operator, nodeType, version, capabilities = [] }) {
|
|
95
|
+
return concat([
|
|
96
|
+
strField(1, operator),
|
|
97
|
+
strField(2, nodeType),
|
|
98
|
+
strField(3, version),
|
|
99
|
+
...(capabilities || []).map((c) => strField(4, c)),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── x/svm ─────────────────────────────────────────────────────────────────--
|
|
104
|
+
// MsgDeployProgram{ sender=1, bytecode=2 }
|
|
105
|
+
export function encodeMsgDeployProgram({ sender, bytecode }) {
|
|
106
|
+
return concat([strField(1, sender), bytesField(2, bytecode)]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── CosmWasm (wasmd) ─────────────────────────────────────────────────────────
|
|
110
|
+
// MsgStoreCode{ sender=1, wasm_byte_code=2 } (instantiate_permission=3 omitted → default)
|
|
111
|
+
export function encodeMsgStoreCode({ sender, wasmByteCode }) {
|
|
112
|
+
return concat([strField(1, sender), bytesField(2, wasmByteCode)]);
|
|
113
|
+
}
|
|
114
|
+
// cosmos.base.v1beta1.Coin{ denom=1, amount=2 }
|
|
115
|
+
function encodeCoin({ denom, amount }) {
|
|
116
|
+
return concat([strField(1, denom), strField(2, String(amount))]);
|
|
117
|
+
}
|
|
118
|
+
// MsgInstantiateContract{ sender=1, admin=2, code_id=3 (uint64), label=4, msg=5 (bytes), funds=6 (repeated Coin) }
|
|
119
|
+
export function encodeMsgInstantiateContract({ sender, admin = '', codeId, label, msg, funds = [] }) {
|
|
120
|
+
const msgBytes = typeof msg === 'string' ? utf8(msg) : (msg || utf8('{}'));
|
|
121
|
+
return concat([
|
|
122
|
+
strField(1, sender),
|
|
123
|
+
strField(2, admin),
|
|
124
|
+
varintField(3, codeId),
|
|
125
|
+
strField(4, label),
|
|
126
|
+
bytesField(5, msgBytes),
|
|
127
|
+
...funds.map((c) => lenDelim(6, encodeCoin(c))),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const __test__ = { varint, concat, tag, lenDelim, strField, bytesField, varintField };
|