@qorechain/chain-bridge
v0.2.1
Published
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.
Maintainers
Readme
@qorechain/chain-bridge
Server-side, autonomous bridge between a Web2 backend (dashboard, exchange, payment worker) and QoreChain's on-chain modules. It turns an off-chain event — "USDT payment confirmed", "user requested a contract" — into a real on-chain effect, with no CLI and no human in the loop.
It handles the two things a plain backend can't: the FIPS-204 ML-DSA-87 hybrid signature the chain's PQC ante chain requires on every Cosmos tx, and the exact wire encoding of QoreChain's custom module messages.
USDT paid ──► backend verifies ──► chain-bridge.grantLicenseSet() ──► x/license MsgGrantLicense (PQC-signed)
└─► license is REAL on-chainInstall
npm install @qorechain/chain-bridgePeer stack (installed automatically): @cosmjs/proto-signing,
@cosmjs/stargate, cosmjs-types, ethers, and the published
@qorechain/wallet-adapter + @qorechain/pqc. Pure JS — runs in an AWS Lambda
(no native addons).
Quick start
import { ChainBridge } from '@qorechain/chain-bridge';
const bridge = new ChainBridge({
cosmosRpc: process.env.QORE_RPC, // http://node:26657
chainId: process.env.QORE_CHAIN_ID, // qorechain-diana
cosmosRest: process.env.QORE_LCD, // http://node:1317 — picks the PQC sign-bytes form
evmRpc: process.env.QORE_EVM_RPC, // http://node:8545
evmChainId: 9800,
authorityMnemonic: process.env.LICENSE_AUTHORITY_MNEMONIC, // genesis license.authority
signerMnemonic: process.env.DEPLOYER_MNEMONIC, // funded operator key
});
// After a USDT payment is verified, grant the license on-chain to the user's
// REAL qor1 address (NOT a custodial pseudo-address):
const { grants, dropped } = await bridge.grantLicenseSet({
grantee: 'qor1user…',
type: 'validator',
chains: ['Solana', 'Ethereum'], // → validator_operator, validator_solana, validator_ethereum
metadata: 'licenseId:abc123',
});
// grants = [{ featureId, txHash }, …]API
| Method | Module / path | Signer | PQC |
|---|---|---|---|
| grantLicenseSet({grantee,type,chains}) | x/license MsgGrantLicense ×N | authority | hybrid |
| grantLicense / revokeLicense / suspendLicense / resumeLicense | x/license | authority | hybrid |
| registerLightNode({operatorMnemonic,…}) | x/lightnode MsgRegisterLightNode | operator | hybrid |
| deployEvm({bytecode,abi?,args?}) | EVM eth_sendRawTransaction | deployer | none (EVM lane) |
| deploySvm({bytecode}) | x/svm MsgDeployProgram | signer | hybrid |
| sendTokens({to,amountUqor}) | bank MsgSend | signer | hybrid |
| registerPqc({mnemonic}) | x/pqc MsgRegisterPQCKeyV2 | self | classical (exempt) |
featureIdsFor({type, chains}) maps a dashboard license to on-chain feature IDs
(validator_*, bridge_*, qcb_bridge, lightnode_operator); unknown chains
are dropped, never minted into an invalid ID.
Keys & PQC bootstrap
License/lifecycle messages must be signed by the chain's configured
license.authority (genesis app_state.license.authority). Pass that key's
24-word mnemonic as authorityMnemonic — keep it in a secrets manager
(SSM / Secrets Manager), never on disk.
Every hybrid signer needs its ML-DSA-87 public key registered on-chain once.
chain-bridge derives that key deterministically and auto-registers it on
first use if the chain reports it missing (the registration tx is PQC-exempt,
so it goes through classically). You can also pre-register at deploy time:
qorechaind tx pqc register-key <ml-dsa-pubkey-hex> hybrid --from authorityCanonical PQC derivation (v0.1.1+)
The ecosystem-standard ML-DSA-87 seed is address-bound and identical across
@qorechain/wallet-adapter, the SDK, and both signer paths here:
seed = SHAKE-256( "qorechain:pqc:v1|" + <account-bech32-address> + "|" + <mnemonic> )
key = ML-DSA-87.keygen(seed) # deterministic (FIPS-204)So the same mnemonic yields the same PQC key in every tool — recover it with
qorechaind tx pqc recover-key <name> <address> --derivation adapter.
Legacy note. Before v0.1.1 the coin-118 path derived
SHAKE-256(mnemonic)(no address binding). The chain cannot rotate a key within one algorithm, so accounts registered that way keep the legacy key;chain-bridgederives it too and falls back to it automatically when a canonical signature is rejected. Recover a legacy key withrecover-key … --derivation bridge.
Hybrid sign-bytes form (v1 / v2, chain v3.2.0 / testnet v3.1.98)
The ML-DSA-87 signature covers one of two byte forms, and a network accepts exactly one at any height:
v1 (legacy): BE32(len B0) ‖ B0 ‖ BE32(len A) ‖ A
v2: "qorechain-pqc-hybrid-v2" ‖ BE64(len chainId) ‖ chainId ‖ BE32(len B0) ‖ B0 ‖ BE32(len A) ‖ Aqorechain-vladi (mainnet) and qorechain-diana (testnet) verify v1 until the
upgrade that carries the switch is applied on them and v2 afterwards; they upgrade
at different heights (testnet is on v2; mainnet stays on v1 until its own
upgrade). Any other chain verifies v2 from its first block.
Two plan names carry the same switch: the release is v3.2.0, which is the
name mainnet applies, while the testnet already applied it as v3.1.98 and keeps
that record. Either one applied means the network verifies v2, so the resolver
asks for both.
signBytesVersion: 'auto'(default) askscosmosRest(/cosmos/upgrade/v1beta1/applied_plan/v3.2.0, then/applied_plan/v3.1.98; v2 iff the numeric height of either is > 0, stopping at the first positive one) and caches the answer for 60 s. On those two networkscosmosRestis required — without it, or if a query fails, hybrid calls throw instead of guessing.signBytesVersion: 'v1' | 'v2'pins the form (no query, no retry).- With
'auto', a refusal withpqccode 21 ("hybrid PQC signature verification failed") re-resolves once and, if the network's answer changed, re-signs and broadcasts once more. Only then does the legacy-key fallback (see the legacy note above) apply.
Hybrid results carry signBytesVersion, the form used.
Decimals
- Cosmos lane (
uqor): 6 decimals.sendTokensamounts are inuqor. - EVM lane (
aqor): 18 decimals.deployEvmvalueis in wei (aqor).
Tests
npm test # wire-encoder + feature-map tests (offline, no chain needed)Author: Liviu Epure · License: Apache-2.0
