Skip to main content

Building on Orbinum

New here?

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:

ImportVersionMeaning
@orbinum/protocol0.6.0Public. Install it and run the snippet.
@orbinum/proof-generator8.0.0Public (GPL-3.0-or-later). The prover and its artifact loaders.
@orbinum/circuits0.15.0Public. The proving and verifying artifacts, pinned by the prover.
@orbinum/wallet-sdk0.5.0Private. The snippet shows the shape of a mechanism; it is not install-and-run.
These versions move together

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​

ModuleWhat it covers
client.substrateRaw Substrate WebSocket — custom RPC and low-level access
client.evmRaw EVM JSON-RPC (null without evmRpc)
client.evmExplorerEnriched block, transaction, address, and token-transfer queries
client.shieldedPoolShielded-pool extrinsics and Merkle queries
client.privacyprivacy_* RPC — Merkle proofs, nullifier status, pool stats
client.chainchain_* RPC — general chain state
client.zkVerifierzkVerifier_* RPC — circuit versions and VK hashes. Only circuits 1 and 2 are registered; every other id returns null
client.relayerStatusrelayer_* RPC — registry lookup and pending fees
client.precompilesshieldedPool, 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:

FieldContents
commitmentHex0x-prefixed 32-byte LE commitment of the recipient output
encryptedMemo0x-prefixed encrypted memo (180 bytes) of that output
leafIndexMerkle leaf index when known — informational, spends re-fetch the proof
txHashTransaction hash when known — informational, a reference the recipient can show
A slip grants no spend power

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 enforced

A 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:

MethodReturns
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 notes

Asking 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​