@qorechain/sdk
v0.8.2
Published
Official TypeScript SDK for building decentralized applications on the QoreChain network.
Readme
@qorechain/sdk
Official TypeScript SDK for building decentralized applications on the QoreChain network — a quantum-safe, triple-VM Layer 1 with native, EVM, and SVM accounts.
Features
- Full message coverage — typed composers for every chain message (bank, staking, distribution, gov, authz, feegrant, IBC, and the QoreChain custom modules), resolved through a message registry.
- Browser wallets — Keplr/Leap (Native); MetaMask/EIP-1193 and
Phantom/Wallet-Standard via the
@qorechain/evmand@qorechain/svmadapters. - Auto-gas — simulation-based fee estimation, with EVM EIP-1559 and SVM compute-budget helpers in the adapters.
- Subscriptions — new-block and tx event streams over the consensus RPC.
- Error decoding — structured, human-readable transaction errors.
- NFT helpers — ERC-721 / ERC-1155 wrappers in
@qorechain/evm. - CosmWasm lifecycle — query, upload, instantiate/instantiate2, execute, migrate, and admin management.
- PQC — ML-DSA-87 (Dilithium-5) signing and hybrid-signature transactions,
plus quantum-safe DX helpers (
ensurePqcRegistered/migrateToHybrid). - Sidechains, paychains & rollups — high-level
createMultilayerClientandcreateRollupClienthelpers (v0.4.0). - Unified cross-VM calls —
createCrossVMClientwith atomic triple-VM transactions overMsgCrossVMCall(v0.5.0). - AI pre-flight — on-chain risk/anomaly scoring (
simulateWithRiskScore) over the EVM precompiles before you sign (v0.5.0). - React kit — hooks + connect kit in
@qorechain/react.
Install
npm install @qorechain/sdkQuickstart
Connect
createClient() targets the public testnet (chain id qorechain-diana) by
default. The defaults point at localhost, so pass endpoints to talk to a real
node.
import { createClient } from "@qorechain/sdk";
const client = createClient(); // testnet, localhost defaults
const remote = createClient({
endpoints: {
rest: "https://api-testnet.qore.host", // Native REST (LCD)
rpc: "https://rpc-testnet.qore.host", // consensus RPC (for signing)
evmRpc: "https://evm-testnet.qore.host", // EVM + qor_ JSON-RPC
},
});Mainnet (chain id qorechain-vladi) is live; target it with
createClient({ network: "mainnet", endpoints }), overriding the localhost
defaults with your node URLs.
Accounts
One mnemonic derives native (qor1…), EVM (0x…), and SVM (base58) accounts.
import {
generateMnemonic,
deriveNativeAccount,
deriveEvmAccount,
deriveSvmAccount,
} from "@qorechain/sdk";
const mnemonic = generateMnemonic();
const native = await deriveNativeAccount(mnemonic); // native.address → "qor1..."
const evm = await deriveEvmAccount(mnemonic); // evm.address → "0x..."
const svm = await deriveSvmAccount(mnemonic); // svm.address → base58Read on-chain state
const balances = await client.rest.getAllBalances(native.address);
const tokenomics = await client.qor.getTokenomicsOverview();Send a transfer
import { directSignerFromPrivateKey, toBase } from "@qorechain/sdk";
const account = await deriveNativeAccount(mnemonic);
const signer = await directSignerFromPrivateKey(account.privateKey, "qor");
const tx = await client.connectTx(signer);
const fee = await client.fees.estimate(); // or "fast" | "normal" | "slow"
const result = await tx.bankSend(
"qor1recipientaddress...",
[{ denom: "uqor", amount: toBase("1.5") }],
{ fee },
);
console.log(result.transactionHash);Quantum-safe signing
QoreChain supports post-quantum cryptography via ML-DSA-87 (Dilithium-5) and a
hybrid posture. The key/sign/verify primitives are available today through
generatePqcKeypair, pqcSign, pqcVerify, and the pluggable PqcSigner /
HybridSigner, and hybrid transactions end-to-end through buildHybridTx /
signAndBroadcastHybrid.
import { generatePqcKeypair, pqcSign, pqcVerify } from "@qorechain/sdk";
const keypair = generatePqcKeypair();
const message = new TextEncoder().encode("hello");
const signature = pqcSign(keypair.secretKey, message);
const ok = pqcVerify(keypair.publicKey, message, signature);Hybrid sign-bytes: v1 / v2 per network (v0.8.0)
The ML-DSA-87 half of a hybrid transaction signs the body without the PQC
extension (B0) and the AuthInfo bytes (A), in one of two forms:
v1: BE32(len B0) ‖ B0 ‖ BE32(len A) ‖ A
v2: "qorechain-pqc-hybrid-v2" ‖ BE64(len chainId) ‖ chainId ‖ BE32(len B0) ‖ B0 ‖ BE32(len A) ‖ AA network verifies exactly one form at any height. qorechain-diana
(testnet) switched to v2 at its v3.1.98 upgrade; qorechain-vladi (mainnet)
stays on v1 until its own, named v3.2.0; chains born later are v2 from genesis. So pass
the network's REST endpoint and let the SDK ask (signBytesVersion: "auto", the
default):
import { signAndBroadcastHybrid, isHybridSignBytesRejection } from "@qorechain/sdk";
const res = await signAndBroadcastHybrid({
registry, signer, pqcKeypair, messages, fee,
chainId: "qorechain-diana",
rest: "https://api-testnet.qore.host", // picks v1 or v2 for this network
accountNumber, sequence,
transport, // e.g. a connected StargateClient
});"auto"readsGET {rest}/cosmos/upgrade/v1beta1/applied_plan/<name>for each name inSIGN_BYTES_V2_UPGRADES("v3.2.0", then"v3.1.98"), stopping at the first applied height above 0, and caches the answer for about a minute. Both names matter: the testnet switched underv3.1.98and keeps that record, mainnet switches underv3.2.0. A client that asks for only one answers v1 on the other network, and the chain then refuses every hybrid tx withpqccode 21.- On
qorechain-vladi/qorechain-diana,"auto"withoutrest— or with a node that cannot be asked — throws rather than guess. Force a form withsignBytesVersion: "v1"or"v2". signAndBroadcastHybridandEthNativeSigner.signAndBroadcastre-resolve and retry once on apqccode 21 refusal. If you broadcast yourself, catch the refusal withisHybridSignBytesRejection(err)and rebuild withbuildHybridTx({ ..., forceRefreshSignBytesVersion: true }).built.signBytesVersionreports the form a built transaction used.- The same rule covers the PQC key-migration payload (
migrationSignBytes) and the bridge attestation payload (bridgeAttestationSignBytes). restis optional in the TypeScript types, so a caller that forgets it compiles cleanly and only fails at runtime onqorechain-vladi/qorechain-diana. Cover your wiring with a runtime test, not just a type check.- In unit tests, pass
signBytesVersion: "v1"or"v2"explicitly (or injectfetch):"auto"asks the network, so a test that omits it silently depends on a live node.
EVM authorisation window (v0.8.2, chain v3.2.0)
From chain v3.2.0 an EVM transaction is admitted only from an account that has a registered PQC key and an open authorisation window. The window is opened by a Cosmos-lane message, so the classical secp256k1 key can never authorise spending on its own. The testnet enforces it since its v3.2.0 upgrade; mainnet from its own.
import {
openEvmWindowMsg,
fetchEvmWindow,
classifyEvmWindowRejection,
} from "@qorechain/sdk";
// 1. The refusal tells you which state you are in.
try {
await walletClient.sendTransaction({ to, value });
} catch (err) {
switch (classifyEvmWindowRejection(err)) {
case "no-pqc-key": /* register a PQC key first */ break;
case "no-window": /* ask the user to authorise, below */ break;
case "exhausted": /* the window ran out — open a new one */ break;
case "invalid": /* the bounds were refused */ break;
default: throw err; // not a window problem
}
}
// 2. Open one on the Cosmos lane (hybrid-signed, like any other message).
await signer.signAndBroadcast(transport, {
chainId, accountNumber, sequence, fee,
messages: [openEvmWindowMsg({
sender: account.cosmos,
blocks: 300, // 1…17280
maxTxs: 5, // 1…1000
maxValue: "2000000" // uqor: value PLUS the maximum fee
})],
});
// 3. THEN read the nonce, because opening advanced it, and send.
const nonce = await publicClient.getTransactionCount({ address: account.evm });
await walletClient.sendTransaction({ to, value, nonce });
// Whatever remains of the window, at any time:
const w = await fetchEvmWindow({ rest, address: account.cosmos });
// w.live, w.remainingTxs, w.remainingValue, w.remainingBlocks — all bigint- Opening advances the EVM nonce. The identity is unified, so the Cosmos sequence is the EVM nonce. Signing the EVM transaction first gives "nonce too low". Order: open, read the nonce, sign.
maxValuecovers value plus the maximum fee (gas limit × gas fee cap): a 1,000 uqor transfer with 21,000 gas at 112.5 gwei consumes 3,363 uqor. Wei to uqor rounds up.blocksis a block count. 17,280 is roughly 5 hours on the testnet (~1.03 s blocks) and 15 on mainnet (~3.1 s) — not the "24 hours" the chain constant's comment suggests. Show blocks, or compute from recent block time.- There is no "open automatically before sending" helper, on purpose: a window opened on every send is a click nobody reads. Show the limits and let the user authorise, then reuse the live window until it is exhausted.
CosmWasm contracts
Interact with CosmWasm contracts via thin wrappers over
@cosmjs/cosmwasm-stargate. client.cosmwasm() opens a read-only client at the
rpc endpoint; for writes, connect a SigningCosmWasmClient with an offline
signer.
import {
connectCosmWasmSigner,
queryContractSmart,
getContractInfo,
instantiate,
execute,
} from "@qorechain/sdk";
// Reads
const cw = await client.cosmwasm();
const info = await getContractInfo(cw, "qor1contract...");
const state = await queryContractSmart(cw, "qor1contract...", { get_count: {} });
// Writes
const signing = await connectCosmWasmSigner("https://rpc-testnet.qore.host", signer);
const inst = await instantiate(signing, sender, codeId, { count: 0 }, "my-contract", {
fee: "auto",
});
await execute(signing, sender, inst.contractAddress, { increment: {} }, "auto");Sidechains, paychains & rollups (v0.4.0)
QoreChain's multilayer module lets a dApp register and operate its own
sidechains/paychains, and the rollup development kit (rdk) provides an
optimistic rollup lifecycle. Two high-level, strongly-typed helpers wrap the
typed message composers and query clients so you never hand-build protobuf Any
payloads. Both bind to a connected TxClient; pass a typed query client (from
connectQueryClients) for the reads.
import {
createClient,
createMultilayerClient,
createRollupClient,
connectQueryClients,
} from "@qorechain/sdk";
const client = createClient({ endpoints });
const tx = await client.connectTx(signer);
const query = await connectQueryClients("https://grpc.example");
// --- Multilayer: register → anchor → route ---
const ml = createMultilayerClient(tx, { query });
await ml.registerSidechain({ layerId: "game-l2", description: "game sidechain" });
await ml.registerPaychain({ layerId: "pay-l2", description: "payments paychain" });
await ml.anchorState({ layerId: "game-l2", layerHeight: 100n, stateRoot, validatorSetHash });
await ml.routeTransaction({ transactionPayload, preferredLayer: "game-l2" });
// Reads
const layer = await ml.getLayer("game-l2");
const layers = await ml.listLayers();
const stats = await ml.getRoutingStats();
// --- Rollups: create → submitBatch → executeWithdrawal ---
const rollup = createRollupClient(tx, { query, qor: client.qor });
await rollup.createRollup({ rollupId: "r1", profile: "default", vmType: "evm" });
await rollup.submitBatch({ rollupId: "r1", batchIndex: 0n, stateRoot, txCount: 12n });
await rollup.executeWithdrawal({ rollupId: "r1", batchIndex: 0n, withdrawalIndex: 0n, recipient, denom: "uqor", amount: 100n, proof });
// Reads (typed gRPC + qor_ conveniences)
const r = await rollup.getRollup("r1");
const batch = await rollup.getLatestBatch("r1");
const rollupStatus = await rollup.getRollupStatus("r1");Every write also has a *Msg(...) variant (registerSidechainMsg,
submitBatchMsg, …) that returns an EncodeObject for batching with other
messages. The challenge game (challengeBatch / resolveChallenge) and lifecycle
controls are exposed on the rollup client too.
You can also reach the typed query clients directly — connectQueryClients
returns multilayer, rdk, bridge, and crossvm clients (among others):
const layers = await query.multilayer.layers({});
const rollups = await query.rdk.rollups({});
const bridgeChains = await query.bridge.chainConfigs({});New in chain v3.1.83: amm, license, and abstractaccount typed query clients;
the multilayer anchor / anchors state-anchor queries; and abstractaccount
registerAuthenticator / revokeAuthenticator message composers.
See the multilayer and rollups guides.
AI pre-flight risk scoring (v0.5.0)
QoreChain exposes an on-chain AI risk/anomaly model to any dApp through plain
eth_calls, so you can get an advisory verdict on a transaction before it is
signed or broadcast. The helpers live in @qorechain/evm (which owns viem) and
are re-exported from @qorechain/sdk for discovery — install @qorechain/evm
and viem (an optional peer) to use them.
simulateWithRiskScore bundles a gas estimate, a risk score from the
aiRiskScore precompile (0x…0B01), and an anomaly check from the
aiAnomalyCheck precompile (0x…0B02) into one PreflightResult:
import { simulateWithRiskScore, aiRiskScore, aiAnomalyCheck } from "@qorechain/sdk";
import { createPublicClient, http } from "viem";
const evm = createPublicClient({ transport: http("https://evm.example") });
const verdict = await simulateWithRiskScore(evm, {
from: "0xSender",
to: "0xContract",
data: "0x…", // calldata
value: 0n,
});
if (!verdict.safe) throw new Error("AI pre-flight flagged this transaction");
// Or call the precompiles individually:
const risk = await aiRiskScore(evm, "0xCalldata"); // { score, level }
const anomaly = await aiAnomalyCheck(evm, "0xSender", 1_000_000n); // { anomalyScore, flagged }See the AI pre-flight guide.
Unified cross-VM calls (v0.5.0)
QoreChain routes calls across its EVM, CosmWasm, and SVM execution environments
over a single MsgCrossVMCall. createCrossVMClient builds, signs, and
broadcasts these for you — including callAtomic, which packs several calls into
one transaction so a triple-VM workflow settles atomically.
This SDK encodes the payload per VM: an { evm: { abi, functionName, args } }
payload is ABI-encoded with viem's encodeFunctionData, a { cosmwasm: {...} }
payload is JSON.stringify'd to UTF-8, and a raw { payload } is sent as-is.
import { createCrossVMClient, connectQueryClients } from "@qorechain/sdk";
const query = await connectQueryClients("https://grpc.example");
const xvm = createCrossVMClient(tx, { query });
// Single call into a CosmWasm contract (payload JSON-encoded).
const res = await xvm.call({
targetVm: "cosmwasm",
targetContract: "qor1contract…",
payload: { cosmwasm: { increment: {} } },
});
// Atomic triple-VM batch in ONE tx.
const atomic = await xvm.callAtomic([
{ targetVm: "evm", targetContract: "0xC…", payload: { evm: { abi, functionName: "swap", args: [a, b] } } },
{ targetVm: "svm", targetContract: "Prog…", payload: { payload: rawBytes } },
{ targetVm: "cosmwasm", targetContract: "qor1…", payload: { cosmwasm: { stake: {} } } },
]);
// build-only (returns an EncodeObject) and read message status:
const msg = xvm.buildCall({ targetVm: "evm", targetContract: "0xC…", payload: { payload: rawBytes } });
const status = await xvm.getMessage("42");targetVm is one of "evm" | "cosmwasm" | "svm" (see VM_TYPES). See the
cross-VM guide.
Quantum-safe DX (v0.5.0)
QoreChain enforces hybrid post-quantum signatures (ML-DSA-87 + secp256k1) by default. These helpers make a dApp PQC-protected in one idempotent call: check whether the signer's Dilithium key is registered, register it if not, and route subsequent transactions through the hybrid signing path.
import {
isPqcRegistered,
getPqcStatus,
ensurePqcRegistered,
migrateToHybrid,
migratePqcKey,
} from "@qorechain/sdk";
// Read-only status (over the qor_ namespace or the pqcKeyStatus precompile).
const registered = await isPqcRegistered(client.qor, native.address);
const status = await getPqcStatus(client.qor, native.address);
// Idempotent: registers the signer's PQC key only if it isn't already.
await ensurePqcRegistered({ tx, signer: pqcSigner /* … */ });
// Migrate an existing classical account to hybrid signing, then sign hybrid.
const path = await migrateToHybrid({ tx, signer: pqcSigner /* … */ });
// Rotate an account's on-chain PQC key (MsgMigratePQCKey).
await migratePqcKey({ tx, /* new key material … */ });See the quantum-safe guide.
Unified eth-native wallet (v0.6.0)
One eth_secp256k1 key = ONE 20-byte identity rendered three ways — qor1…
(bech32), 0x… (EIP-55), and SVM base58 (the 20 bytes right-padded with 12 zero
bytes to 32). A deposit to any of the three lands in the same balance, and the
key spends on all lanes. deriveUnifiedAccount uses the Ethereum HD path
(m/44'/60'/0'/0/{index}); unifiedAccountFromSeed builds one directly from a
32-byte secret. addressesFrom20 / qoreAddresses convert between the three
encodings. The legacy coin-type-118 deriveNativeAccount still works (additive).
Native-lane signing over the eth key: signClassicalEth is a classical secp256k1
signature over keccak256(SignDoc) with pubkey type
/cosmos.evm.crypto.v1.ethsecp256k1.PubKey; signHybridEth adds the ML-DSA-87
post-quantum signature. The account parser (parseEthPubkeyAny) reads
account number / sequence from an eth_secp256k1 on-chain pubkey.
import { deriveUnifiedAccount, resolveSignBytesVersion, signHybridEth } from "@qorechain/sdk";
const account = await deriveUnifiedAccount(mnemonic);
account.cosmos; // "qor1…" — QoreChain Native lane
account.evm; // "0x…" — EIP-55 checksummed
account.svm; // "<base58>" — 20 bytes + 12 zero pad
// Sign a QoreChain Native tx from the unified eth key (hybrid PQC path).
// signHybridEth is synchronous: resolve the sign-bytes form for the network
// first (v1 on mainnet until its v3.1.98 upgrade, v2 after).
const signBytesVersion = await resolveSignBytesVersion({
chainId: "qorechain-vladi",
rest: "https://api.qore.host",
});
const signed = signHybridEth({
account, // privateKey, publicKey and the pqc keypair
messages: [msg.cosmos.send(/* … */)],
chainId: "qorechain-vladi",
accountNumber,
sequence,
fee,
signBytesVersion,
encodeMessage: (m) => registry.encodeAsAny(m),
});
// Or let EthNativeSigner resolve and retry for you:
// new EthNativeSigner(account, { rest: "https://api.qore.host" }).signAndBroadcast(transport, params)unifiedAccountFromSeed uses the 32 bytes it is given as the spend key, so
pass real secret entropy — a CSPRNG seed or a mnemonic-derived key — never a
wallet signature or any other value a third party can request. (The signature-
derived helpers unifiedAccountFromPhantomSignature / connectPhantomUnified
were removed in v0.8.0 for exactly that reason and now throw.) To let an
external wallet key act for an account, use the authenticator lanes below.
See the unified-wallet guide.
Cross-VM message reads
You can also read cross-VM message state without the client above:
const pending = await client.crossvm.pending();
const message = await client.crossvm.message("42");
const params = await client.crossvm.params();
// Or track a message by id over the EVM JSON-RPC namespace:
const status = await client.qor.getCrossVmMessage("42");Authenticator lanes (v0.7.0 / chain v3.1.85)
A linked external key — a Phantom ed25519 key, or a MetaMask secp256k1 key bound by address — can spend from the ONE canonical PQC account through a relayer that submits the tx and pays the fee (its own hybrid-PQC signature satisfies the ante). The external key never produces an ML-DSA co-signature; its signature over the domain-separated, replay-bound sign-bytes is the authorization. Every lane runs under least-privilege, spending-limit and revocable terms enforced on-chain.
Three messages carry the lanes:
| Message | Type URL | Composer |
|---|---|---|
| MsgExecuteEVM | /qorechain.abstractaccount.v1.MsgExecuteEVM | msg.abstractaccount.executeEvm |
| MsgExecuteCosmos | /qorechain.abstractaccount.v1.MsgExecuteCosmos | msg.abstractaccount.executeCosmos |
| MsgRotatePQCKey | /qorechain.pqc.v1.MsgRotatePQCKey | msg.pqc.rotatePqcKey |
Sign-bytes helpers rebuild the exact digest the chain re-derives, so you can
verify byte-for-byte before signing: evmAuthSignBytes / cosmosAuthSignBytes
(32-byte SHA-256 digests) and rotationSignBytes (the domain-separated string
both keys sign).
NONCE semantics:
MsgExecuteEVM.nonce= the account's current EVM nonce (the relayer is a different account than the owner, so its envelope does not bump the account's nonce — pass the value as-is, do not+1).MsgExecuteCosmos.nonce= the per-authenticator sequence for(account, pubkey), a store counter distinct from the account's own sequence.
Wallet builders sign the digest and return a ready-to-broadcast message:
buildPhantomExecuteEvm / buildPhantomExecuteCosmos (ed25519),
buildMetaMaskExecuteEvm / buildMetaMaskExecuteCosmos (secp256k1-by-address),
and registerEthAuthenticatorMsg to link an EVM key first.
Permission taxonomy & errors. Query the on-chain permission schema with
client.query.getPermissionSchema() (REST) / client.grpc.permissionSchema()
(gRPC) and compare a candidate action against it before submitting. Failed lanes
surface structured codes via decodeTxError — codespace abstractaccount: 5
SpendingLimitExceeded, 6 SessionKeyExpired, 10 PermissionDenied, 11
AuthenticatorReplay; codespace pqc: 21 HybridVerifyFailed.
Key rotation. Migrate a legacy shake256(mnemonic) key to the canonical,
address-bound key with rotatePqcKeyMsgFromMnemonic (dual-signs over
rotationSignBytes); derivePqcLegacy re-derives the old key for the old-key
half of the signature.
import {
buildPhantomExecuteEvm,
rotatePqcKeyMsgFromMnemonic,
} from "@qorechain/sdk";
// Phantom ed25519 authenticator spends from the canonical account; the relayer
// broadcasts and pays. `nonce` is the account's CURRENT EVM nonce (no +1).
const execMsg = await buildPhantomExecuteEvm({
wallet, // { publicKey, signMessage }
relayer: "qor1relayer…",
chainId: "qorechain-diana",
account: "qor1canonical…",
to: "0xRecipient…",
value: "1000000000000000000", // 1 QOR in aqor (wei)
nonce: await client.qor.getTransactionCount("0xAccount…"),
});
// One-shot: migrate a legacy shake256(mnemonic) key to the address-bound key.
const { msg: rotateMsg } = rotatePqcKeyMsgFromMnemonic({
account: "qor1canonical…",
mnemonic,
chainId: "qorechain-diana",
});See the Authenticators guide for the full lifecycle (register → spend → rotate → revoke).
Network reference
- Mainnet chain id:
qorechain-vladi(live). - Testnet chain id:
qorechain-diana(live). - Token:
QOR/uqor(10^6 base units per QOR). - Default endpoints point at localhost — override them to reach a real node.
License
Apache-2.0
