Documentation
API reference
The complete @gloamtrade/sdk surface. Everything exports from one barrel; the crypto core is pure and runs anywhere, and proving and storage are injected so the same code serves a browser app and a node agent.
Import
One entry point. Tree-shake what you do not use. viem types (Address, Hex) flow through the surface; snarkjs is a lazy peer, imported only inside proveGroth16.
import {
buildShieldBoundIntent, buildUnshieldIntent, buildPrivateSendIntent,
makeBoundNotePoseidon, artifactProver,
syncTree, IncrementalMerkleTreePoseidon,
SEALED_VAULT, NATIVE_ASSET,
} from "@gloamtrade/sdk";Intent builders
The primary surface. Each builder returns an unsigned GloamIntent — a portable plan plus a resolved exec (the on-chain call). You sign and broadcast; the SDK never holds a key.
| Function | Signature | Status |
|---|---|---|
buildShieldBoundIntent | (p: ShieldBoundIntentParams) => Promise<ShieldIntent>Mints a note + shield proof → shieldBound(...) | Live |
buildShieldIntent | (p: ShieldIntentParams) => Promise<ShieldIntent>Plain shield(...), no proof | Legacy pools only |
buildUnshieldIntent | (p: UnshieldIntentParams) => Promise<GloamIntent<"unshield">>Witness + proof → unshield(...) (cash out) | Live |
buildPrivateSendIntent | (p: PrivateSendIntentParams) => Promise<PrivateSendIntent>Transfer witness → transfer(...); returns payment + change notes | Live |
buildPrivateTradeIntent | (p: PrivateTradeIntentParams) => Promise<PrivateTradeIntent>Sealed-swap witness → sealedSwap(...) | Disabled (H1) |
buildPrivateTradeIntent produces a valid intent, but the on-chain sealed-swap path is disabled pending the H1 solvency work — the builder is here so you can wire it ahead of the flip.
Parameters
interface ShieldIntentParams {
amountWei: bigint; // wei (native) or token base units
asset?: Address; // default NATIVE_ASSET (zero address)
chainId?: number; // default RH_TESTNET_CHAIN_ID
poolAddress?: Address; // default SEALED_VAULT
agentAddress?: Address | null;
}
interface ShieldBoundIntentParams extends ShieldIntentParams {
prover: Prover; // bound to the shield circuit artifacts
}
interface UnshieldIntentParams {
secretHex: `0x${string}`; // the note's spend key
amountWei: bigint;
to: Address; // public recipient
asset?: Address;
path: PoseidonMerklePath; // membership path (from the rebuilt tree)
prove: Prover;
poolAddress?: Address; chainId?: number; agentAddress?: Address | null;
}
interface PrivateSendIntentParams {
secretHex: `0x${string}`;
amountInWei: bigint; // the source note's amount
amountPayWei: bigint; // amount to send (remainder is your change note)
asset?: Address;
path: PoseidonMerklePath;
prove: Prover;
poolAddress?: Address; chainId?: number; agentAddress?: Address | null;
}Intent shape
Every builder returns this. The plan is safe to log or hand to an agent; the exec is the resolved call.
interface GloamIntent<K> {
intent: K; // one of the four IntentKind values
chainId: number;
agentAddress: Address | null;
plan: PlanFor<K>; // portable: symbol, amount — no secrets
privacy: string; // plain-language: what is and isn't hidden
execution: string; // plain-language: how to sign + broadcast
exec: IntentExec;
}
interface IntentExec {
poolAddress: Address;
fn: PoolFn; // e.g. "shieldBound", "unshield", "transfer"
valueWei: bigint; // ETH to attach (native shield only)
args: readonly unknown[]; // ABI-ordered call args
}ShieldIntent adds note: BoundNote; PrivateSendIntent and PrivateTradeIntent add paymentNote/outNote and changeNote (each a NoteExport).
Pinned public inputs
PUBLIC_INPUTS pins the exact signal ordering the deployed verifiers expect, so an integrator cannot drift out of proof compatibility.
PUBLIC_INPUTS.unshield // [root, nullifier, asset, amount, to]
PUBLIC_INPUTS.transfer // [root, nullifier, newCommitment0, newCommitment1]
PUBLIC_INPUTS.sealedSwap // [root, nullifier, newCOut, newCChange,
// assetIn, assetOut, amountOutMin, rateIn, rateOut]Notes
A private balance is a note: commitment = Poseidon(secret, amount, asset), nullifier = Poseidon(secret, commitment). The secret is the sole spend authority.
makeBoundNotePoseidon(amount: bigint, asset?: Address): Promise<BoundNote>
noteCommitmentPoseidon(secret: bigint, amount: bigint, asset?: Address): Promise<bigint>
noteNullifierPoseidon(secret: bigint, commitment: bigint): Promise<bigint>
openNotePoseidon(secretHex: Hex, amount: bigint, asset: Address)
: Promise<{ commitment: Hex; nullifier: Hex }>
randomSecretField(): Promise<bigint> // uniform, under the field prime
interface BoundNote {
secret: Hex; commitment: Hex; nullifier: Hex;
secretField: bigint; commitmentField: bigint; nullifierField: bigint;
}Prover
Proving is environment-bound, so it is injected. A Prover takes a circom input and returns packed proof bytes. artifactProver binds snarkjs to a fixed set of artifacts (file paths in node, URLs in the browser).
type Prover = (input: Record<string, string | string[]>) => Promise<ProveResult>
type ProveResult = { proofBytes: Hex; publicSignals?: string[] }
interface Groth16Artifacts { wasm: string; zkey: string }
artifactProver(a: Groth16Artifacts): Prover
proveGroth16(input, wasm: string, zkey: string)
: Promise<{ proofBytes: Hex; publicSignals: string[]; proof: Groth16Proof }>snarkjs is imported dynamically inside proveGroth16; install it as a peer where you prove. A browser app and a node signer differ only in whether wasm/zkey are URLs or paths.
Merkle tree
A depth-20 (MERKLE_DEPTH) incremental Poseidon tree. Rebuild it from the pool's Shielded events, then produce a membership path for a spend proof.
class IncrementalMerkleTreePoseidon {
leaves: bigint[];
insert(leaf: bigint): Promise<number> // returns the leaf index
path(leafIndex: number): Promise<PoseidonMerklePath>
}
type PoseidonMerklePath = {
leafIndex: number; leaf: bigint;
pathElements: bigint[]; pathIndices: number[]; root: bigint;
}
pathToCircomInput(path: PoseidonMerklePath) // -> circom-ready string fieldsTree sync
A spend needs a membership path, which needs the current tree. syncTree rebuilds it from chain — replaying Shielded, Transferred, and SealedSwappedleaves in on-chain order, so the root stays correct after any transfer or swap. viem's PublicClient is injected, so the same call works in the browser and in node.
syncTree(client: PublicClient, { pool, fromBlock?, chunkSize? }): Promise<SyncedTree>
// true if the rebuilt root equals the pool's on-chain currentRoot()
assertTreeMatchesChain(client, pool, synced): Promise<boolean>
interface SyncedTree {
tree: IncrementalMerkleTreePoseidon;
leaves: ChainLeaf[];
root: Hex; leafCount: number;
indexByCommitment: Map<string, number>;
pathForLeaf(i: number): Promise<PoseidonMerklePath | null>;
pathForCommitment(c: Hex): Promise<PoseidonMerklePath | null>; // the ergonomic path
}The usual flow: const synced = await syncTree(client, { pool}), then hand await synced.pathForCommitment(note.commitment) straight to buildUnshieldIntent or buildPrivateSendIntent.
Witness builders
Pure circom-input builders under the intent builders. Use these directly only if you drive proving yourself; otherwise the intent builders call them for you.
buildPoseidonUnshieldWitness({ secretHex, amount, asset?, to, path })
buildTransferWitness({ secretHex, amountIn, amountPay, asset?, path })
buildSealedSwapWitness({ secretHex, amountIn, amountSwap, assetIn?, assetOut,
amountOutMin, rateIn, rateOut, path })
sealedSwapArtifactsReady(): boolean
type NoteExport = { secret: Hex; commitment: Hex; nullifier: Hex;
amountWei: string; asset: Address }Each returns the circom input, the derived public inputs, the output notes, and a blocker string if the witness is not provable (for example a commitment that is not in the tree).
Sealed-rate math
Pure integer math for sealed swaps, proven against the circuit equality amountOut · rateOut === amountSwap · rateIn. Rates are USD cents (SEALED_RATE_SCALE = 100) to keep the constraint exact.
marksToSealedRates(ethUsd, outUsd, source?): SealedRateQuote | null
estimateSealedOut(amountSwap: bigint, rateIn: bigint, rateOut: bigint): bigint
exactSealedAmounts(amountSwapWanted: bigint, rateIn: bigint, rateOut: bigint)
: { amountSwap: bigint; amountOut: bigint } | null // exact, no rounding leak
formatSealedAmount(wei: bigint, maxFrac?: number): string
fallbackOneToOneRates(): SealedRateQuote
type SealedRateQuote = { rateIn: bigint; rateOut: bigint; /* … */ }Size-privacy policy
The public floor an integrator publishes for a spend. The default mode keeps the on-chain minimum at SIZE_PRIVACY_OUT_MIN = 1 so the settled size leaks nothing; a slippage mode trades some privacy for a tighter guarantee.
type SizePrivacyMode = "max" | "slippage"
publicAmountOutMin(expectedOut: bigint, mode: SizePrivacyMode, bps?: number): bigint
coarsenMarkUsd(usd: number): number // bucket a mark so quotes don't fingerprint
SEALED_PRIVACY_FACTS // plain-language privacy notesField math & proof packing
Low-level helpers, shared by the note and witness layers.
// poseidon.ts
poseidon2(a: bigint, b: bigint): Promise<bigint>
poseidon3(a: bigint, b: bigint, c: bigint): Promise<bigint>
toField(x: bigint | string | number): bigint // reduce mod the field prime
fieldToHex(f: bigint): `0x${string}`
hexToField(hex: string): bigint
// proof.ts
packGroth16Proof(proof: Groth16Proof): Hex // -> the bytes the verifier wants
fieldToBytes32(field: bigint | string): HexConstants
| Constant | Value / meaning |
|---|---|
SEALED_VAULT | The hardened C1/C2/C3 pool — the default deposit target |
SHIELD_VERIFIER | The deployed shield verifier the hardened pool checks against |
GLOAM_PAY_MEMO | The encrypted-memo contract for private-pay discovery |
NATIVE_ASSET | Zero address — native ETH |
RH_TESTNET_CHAIN_ID / RH_MAINNET_CHAIN_ID | 46630 / 4663 |
FIELD_PRIME | The BN254 scalar field modulus |
MERKLE_DEPTH / SEALED_RATE_SCALE | 20 / 100 |
The default SEALED_VAULT is the hardened pool — never the retired 0x4F38 deployment. Pass poolAddress to any builder to override it.
Guardrails
Robinhood Chain testnet 46630 with dev-ceremony keys. Real proofs only, never a mock. Note secrets are the sole spend authority — persist and protect them. See the quickstart to put this together, or the production gate for what mainnet still needs.