Building on Orbinum
Get Started walks through install, network parameters, the faucet and your first call. This page is the reference for what the package exposes.
@orbinum/protocol is the public TypeScript SDK: chain access, address handling, encoding, note
types, payment slips, disclosure keys, and the precompile helpers.
It holds no keys. Nothing in it derives or handles a spending key. A dapp, a script, or an automation talks to the chain and asks an Orbinum wallet to pay; the wallet owns the custody.
It runs in the browser, in Node, in a React Native runtime, and in edge workers, and is published as ESM and CJS with bundled type definitions.
The custody boundary
This SDK is deliberately incomplete: it cannot build, open, or spend a note,
because each of those needs a spending key — and a package anyone can install is
the wrong place for one. That code lives in
@orbinum/wallet-sdk, which is not publicly
published. That page explains where the line falls and why.
Building a dapp? You do not need it. Everything a consumer of the chain does is here.
Code you can run, and code you can read
Snippets across these docs import from more than one package, and the import line tells you which kind you are looking at:
| Import | Version | Meaning |
|---|---|---|
@orbinum/protocol | 0.6.0 | Public. Install it and run the snippet. |
@orbinum/proof-generator | 8.0.0 | Public (GPL-3.0-or-later). The prover and its artifact loaders. |
@orbinum/circuits | 0.15.0 | Public. The proving and verifying artifacts, pinned by the prover. |
@orbinum/wallet-sdk | 0.5.0 | Private. The snippet shows the shape of a mechanism; it is not install-and-run. |
Runtime spec 16 changed the circuits, so the four are a matched set: wallet-sdk 0.5.0 expects
protocol ^0.6.0 and proof-generator ^8.0.0, and proof-generator 8.0.0 pins circuits 0.15.0
exactly. Mixing an older package with a spec-16 chain produces proofs the chain rejects after the
user has paid to generate them.
Snippets in the third category carry an explicit notice. They are kept because on a protocol page a code block is often the clearest way to name the inputs an operation consumes — not a promise that you can execute it.
Connecting
OrbinumClient.connect() opens the Substrate WebSocket connection and, if an EVM RPC URL is
supplied, the EVM client too.
import { OrbinumClient } from '@orbinum/protocol';
const client = await OrbinumClient.connect({
substrateWs: 'wss://rpc-1.testnet.orbinum.io',
evmRpc: 'https://rpc-1.testnet.orbinum.io',
});
const stats = await client.privacy.getPoolStats();
console.log('root:', stats.merkleRoot, 'leaves:', stats.commitmentCount);
client.destroy();
Client modules
| Module | What it covers |
|---|---|
client.substrate | Raw Substrate WebSocket — custom RPC and low-level access |
client.evm | Raw EVM JSON-RPC (null without evmRpc) |
client.evmExplorer | Enriched block, transaction, address, and token-transfer queries |
client.shieldedPool | Shielded-pool extrinsics and Merkle queries |
client.privacy | privacy_* RPC — Merkle proofs, nullifier status, pool stats |
client.chain | chain_* RPC — general chain state |
client.zkVerifier | zkVerifier_* RPC — circuit versions and VK hashes. Only circuits 1 and 2 are registered; every other id returns null |
client.relayerStatus | relayer_* RPC — registry lookup and pending fees |
client.precompiles | shieldedPool, crypto via an EVM wallet |
client.shieldedPool marshals an already-generated proof into an extrinsic; it never creates one.
Proving is custody-side.
Two of its calls need no proof at all, and are the relayer-facing half of the module:
commitRelay(commits) records the intent to relay an operation, and claimRelayFees(assetId, amount)
pays accrued fees out to a public balance. Both are also available as precompile calls through
client.precompiles.shieldedPool. See Gasless Fees.
Payment slips
A payment slip is the hand-off from a dapp to a wallet, and the interoperability seam between the two packages. Without one, a recipient must rescan the pool to discover a note that was just paid to them; a slip points straight at it.
A slip carries only data that is already public on-chain, sealed toward the recipient's viewing public key:
| Field | Contents |
|---|---|
commitmentHex | 0x-prefixed 32-byte LE commitment of the recipient output |
encryptedMemo | 0x-prefixed encrypted memo (180 bytes) of that output |
leafIndex | Merkle leaf index when known — informational, spends re-fetch the proof |
txHash | Transaction hash when known — informational, a reference the recipient can show |
It is a pointer, not a key. The recipient opens it with their own viewing key and still derives their own spending key to spend the note. A forged slip reconstructs nothing, because the recomputed commitment must match what is on-chain.
Sender
import { sealPaymentSlip, encodePaymentSlip } from '@orbinum/protocol';
const envelope = sealPaymentSlip(recipientIvkPacked, {
commitmentHex,
encryptedMemo,
});
const wire = encodePaymentSlip(envelope); // 'orbslip1:…' — hand this to the wallet
The recipient's packed viewing public key comes first: it is what the slip is sealed toward.
Recipient
import { decodePaymentSlip, openPaymentSlip } from '@orbinum/protocol';
const envelope = decodePaymentSlip(wire);
const fields = envelope && openPaymentSlip(recipientIvsk, envelope);
// → PaymentSlipFields | null
openPaymentSlip never throws — its input is a pasted string, and an exception would take down
the paste handler. A bad checksum, a wrong key, or a truncated slip all return null. The fields
are rebuilt one at a time rather than handed back wholesale, because a decrypted slip is still
untrusted input.
Turning those fields into a spendable note is custody, and needs a wallet.
Wire format
orbslip1: (PAYMENT_SLIP_SCHEME). The envelope is
ephPk(32) || nonce_suffix(8) || ciphertext || MAC(16). The version lives in the scheme name, so a
future reader can refuse a slip it does not understand rather than misparse it.
QR_SINGLE_CODE_MAX_CHARS is not enforcedA slip longer than 1800 characters still encodes, and nothing raises an error — it simply stops fitting in a single scannable QR code. If you render slips as QR codes, check the length yourself.
Privacy RPC methods
The node extends standard Substrate RPC with five methods for querying the shielded pool. They are Safe methods — a node serving them needs no unsafe RPC enabled:
| Method | Returns |
|---|---|
privacy_getMerkleRoot() | current Merkle root, as hex |
privacy_getMerkleProof(leaf_index) | sibling path for a leaf |
privacy_getMerkleProofByCommitment(commitment) | the same, found by commitment |
privacy_getNullifierStatus(nullifier) | whether a nullifier has been spent |
privacy_getPoolStats() | aggregate pool statistics, per asset |
client.privacy wraps them, so you rarely call them directly.
getNullifierStatus is not how a wallet checks its own notesAsking a server whether one specific nullifier is spent tells that server which note you hold. A wallet's scanner downloads the spent set and intersects it locally for exactly this reason — see Note Discovery.
Note disclosure
Prove a note's contents to an auditor without revealing spending keys or transaction history:
import { createNoteDisclosureKey, decodeNoteDisclosureKey } from '@orbinum/protocol';
Generating a key requires the decrypted note, which only its owner's wallet holds. Verifying one needs nothing but this package — an auditor, an exchange, or a quest verifier can check a disclosure with a public install and no special access. A forged key fails to decode rather than decoding into a lie, because the recomputed commitment must match.
See Note Disclosure for the key format and verification rules.
Precompiles from an EVM wallet
When the user holds an EVM wallet rather than a Substrate account, the precompile modules build and send the calls:
import {
ShieldedPoolPrecompile,
PRECOMPILE_ADDR,
decodePrecompileCalldata,
} from '@orbinum/protocol';
decodePrecompileCalldata is the inverse — it turns raw calldata back into a labelled call, which
is what the explorer uses to render precompile transactions.
Address helpers
Orbinum accounts have both an H160 and an AccountId32 form. This package converts between them:
import {
evmToSubstrate,
substrateToEvm,
isUnifiedAddress,
isImplicitEvmAccount,
accountIdHexToSs58,
} from '@orbinum/protocol';
See EVM ↔ Substrate accounts for the derivation rules.
Merkle forest geometry
The public, structural facts about the commitment forest — which tree a leaf belongs to, how many leaves a tree holds, whether an index is valid at all:
import { treeIdOf, LEAVES_PER_TREE, isValidLeafIndex } from '@orbinum/protocol';
Reasoning over which of your own notes to spend is coin selection, and lives in the wallet.
Learn More
📄️ @orbinum/protocol
The public TypeScript SDK — chain access, addresses, payment slips. It holds no keys.
📄️ @orbinum/wallet-sdk
The custody half of the protocol — what it holds, why it is closed, and how to get access.
📄️ EVM Compatibility
What works out of the box, and the one gap to know about.
📄️ Precompile Addresses
Every precompile Orbinum registers, at its real address.
📄️ ShieldedPool
Address: 0x0000000000000000000000000000000000000801
📄️ Balances
Address: 0x0000000000000000000000000000000000000802