This page tracks the current public exports of @cloak.dev/sdk.
UTXO API
The UTXO free functions are the primary entrypoint. There is no client class to construct.
transact is the single engine entrypoint and the sign of externalAmount classifies the
flow: positive shields (deposit), negative withdraws, zero is a shielded transfer.
transfer, partialWithdraw and fullWithdraw are shaped wrappers over it.
Every one of these takes a TransactOptions object. connection, programId and relayUrl
are required on all flows; supply either depositorKeypair (programmatic) or the wallet
callbacks signTransaction / signMessage plus walletPublicKey (browser). relayUrl has
no default and is not read from the environment, so pass it explicitly on every call. Every flow
except a deposit needs an authenticated sender and throws before proof generation without one:
see Request authentication.
UTXO primitives:
selectUtxos needs two checks at every call site, not one. It returns null instead of throwing
when the available notes do not cover the target, so a truthy check is the whole insufficient-balance
path. And it knows nothing about the circuit’s two-input cap: it will happily return three notes for
a large target, and transact then throws Maximum 2 input UTXOs allowed before proving. Check the
length as well as the null. partialWithdraw and fullWithdraw consolidate down to two inputs for
you; transfer does not, so cap the selection yourself.
That consolidation is not free, and it is worth budgeting for. partialWithdraw merges the two
smallest notes at a time, so n inputs cost n - 2 shielded transactions before the withdrawal
itself, each one separately proved, signed and submitted. fullWithdraw delegates to it and behaves
the same. In a browser that is n - 2 extra wallet approvals, and options.onProgress is the only
thing that reports them: it fires Consolidating notes (n remaining)... on each round. Each merge
also creates a fresh intermediate note that the SDK never hands back, and its blinding is fresh
randomness held only in that process, so a run that dies between a merge and the final withdrawal
strands that note on-chain permanently, exactly as the change-note warning below describes. Prefer
two inputs where you can, and treat a many-input withdrawal as a long resumable operation rather
than a single call.
serializeUtxo / deserializeUtxo are a fixed 128-byte encoding of one note: amount, private key,
blinding, mint and leaf index. That is the full spending secret in a byte array, so treat the output
as key material and encrypt it at rest. deserializeUtxo is async because it re-derives the public
key and recomputes commitment. One edge to know: a note at leaf index 0 comes back with index
undefined, because the encoding cannot tell index 0 from unset.
Note storage and key material. The SDK persists nothing on its own; these are the pieces you build a
store out of, and the full pattern is
Where your notes live between sessions.
UtxoWallet is the live note set:
Utxo objects per mint, spent flags, balance, stats and input selection. It does not persist
itself. serialize() and the static UtxoWallet.deserialize(json) give you a JSON string that
handles the bigint fields; where that string goes is your choice. It drops commitment, so
recompute with computeUtxoCommitment after restoring, or verifyUtxos reports every restored
note as skipped rather than spent or unspent.
LocalStorageAdapter implements
StorageAdapter over browser localStorage, holding CloakNote records and wallet keys under the
keys cloak_notes and cloak_wallet_keys by default.
MemoryStorageAdapter is the same
interface held in process. It persists nothing and loses everything on reload: use it for tests, or
when storage lives entirely on your side.
importWalletKeys / exportWalletKeys move a CloakKeyPair in and out of a JSON string.
expandSpendKey derives (ask, nsk, ovk) from a spend key; getNkFromUtxoPrivateKey returns the
32-byte nk for a note’s private key. nk is the incoming viewing base: it is what decrypts chain
notes and what the recovery helpers below scan with.
The two halves do not connect. UtxoWallet does not take a StorageAdapter, and a StorageAdapter
does not store Utxo objects. The note set is the half that decides solvency.
Do not build sends with UtxoWallet.prepareSend or with SimpleWallet, even though both are
exported. prepareSend derives the recipient output from a freshly generated random keypair rather
than from the recipient’s key, so the note it produces is unspendable by the recipient and by you.
SimpleWallet.sync() throws Sync not yet implemented. Use transfer, which builds the recipient
output from the Cloak pubkey you pass it. The rest of UtxoWallet is sound: addUtxo, markSpent,
getBalance, getUnspentUtxos, getStats, selectInputs, serialize and deserialize.
Note helpers, all free functions:
Full flow guide: UTXO Transactions
Result types
Every UTXO flow resolves to a TransactResult:
swapUtxo and swapWithChange both resolve to a UtxoSwapResult, which extends it:
Persist refund. It is the recovery secret for reclaiming the input if the swap times out
before its second transaction lands.
Recovery helpers
Two exports let a wallet rebuild notes from key material alone, with no local note store.
createRecoverableDepositUtxo builds a deposit output whose keypair and blinding are derived from
(nk, noteSalt) instead of raw randomness, which is what makes a later cold scan able to rebuild it.
It resolves to { utxo, noteSalt }, and the same salt must reach transact as
options.chainNoteSalt alongside options.chainNoteViewingKeyNk. A salt that never reaches the
chain note leaves the deposit exactly as undiscoverable as before.
discoverSwapRefunds walks program history for swap timeout-refund leaves that belong to the holder
of viewingKeyNk and returns each in spendable form. It is forward-looking only: it finds refunds
whose keys were derived from nk, so a swap built without an nk has no derivation to replay and no
key-only scan can recover it. An empty result is not proof that a wallet has no stranded refund.
Change notes are outside both helpers. createUtxo draws each change note’s blinding from fresh
randomness and publishes it nowhere, so a change note whose secrets you lost is unspendable by
anyone, permanently. Persist result.outputUtxos before you report success:
where your notes live between sessions.
Proof and Merkle utilities
Notes:
- UTXO transact/swap flows submit 256-byte proofs and 264-byte public inputs.
Fee constants
Fees are enforced on-chain from each pool’s PoolConfig; transact reads the live config before
building a fee-bearing proof. These constants mirror the deployed SOL-pool defaults for display
math. SPL pools (USDC/USDT) charge 450_000 base units fixed (0.45) + 0.3%; see
Fee Model.
These values cover the native SOL pool. SPL pools (USDC and USDT) charge a fixed 0.45 token,
450,000 raw units at 6 decimals, plus the same 0.3 percent variable fee. The program reads the
effective values from the pool_config PDA; the SDK does not compute SPL fees.
Advanced TransactOptions
Five options a standard integration never sets, listed because nothing else documents them.
expectedMint — every flow takes the pool from the notes it is handed, so the SDK cannot
otherwise tell a deliberate USDC withdrawal from a SOL withdrawal that was handed a USDC note by
mistake. Both are well-formed and both settle. Set expectedMint and the SDK refuses the
transaction when the input notes disagree. Optional for backward compatibility — pass it on every
flow you can.
relaySupplementalAlt — when true and relayUrl is set, an SPL deposit that needs a
supplemental address lookup table asks for a shared table to be extended instead of creating a
depositor-signed throwaway one, so the user signs exactly once instead of twice. Any failure
falls back to the depositor-signed ephemeral-ALT path automatically. Off by default, which preserves
the old behaviour exactly.
transactionVersion — 0 (default) builds a v0 message with ComputeBudget instructions. 1
builds a Transaction V1 message (SIMD-0385): 4,096-byte packet, every account static with no lookup
tables, compute budget in the message header. It needs the cluster’s v1 gate active and a signer
that signs v1 messages — a local key pair does; a browser wallet may not yet.
externalFeePayer — lets a deposit come from a wallet holding no SOL: the adapter’s payer covers
the network fee and on-chain rent and is reimbursed in an SPL token inside the same transaction.
relayAuthBatch — the handle transactBatch passes to each item so the row is authenticated by
the batch’s single signature. It takes precedence over depositorKeypair and relayAuthSigner.
Set it yourself only if you drive the coordinator directly.
Request authentication and endpoint pinning
Private sends, withdrawals and swaps carry a signed sender. Standard integrations only pass
signMessage + walletPublicKey (browser) or depositorKeypair (server) in TransactOptions and
never touch these; they exist for callers driving submission themselves.
maxWalletApprovals on TransactOptions caps wallet dialogs for one swap (default 5). Sends and
withdrawals sign exactly once. Full contract: Request authentication.
Error utilities
Detailed usage: Error Handling
Versioned exports
VERSION tracked the package version incorrectly before 0.2.5 and is now in step with it. It is
still informational — to confirm which version you are on, check the published package with
npm view @cloak.dev/sdk version or npm ls @cloak.dev/sdk.
Batch signing
transactBatch runs N private spends behind one wallet approval. Each item is proved
concurrently so every proof exists before the wallet is asked, then all items are submitted under a
single batch signature.
results has one entry per item in input order, and a rejected item leaves the others unaffected.
approvals counts the wallet dialogs actually raised: 1 for the batch, plus 1 per wave of
stale-root re-proves.
The call requires relayUrl and an authenticated sender — depositorKeypair, or walletPublicKey
with signMessage, or signer. Both are checked before any proof is computed, so a batch that
cannot be authenticated fails without doing work.
transactBatch does not plan the rows. Two items that spend the same note collide on their
nullifier, and an item that spends another item’s change cannot be proved until that change has
landed. Give each item its own already-landed input notes; selecting them is the caller’s job.
Above RELAY_BATCH_AUTH_MAX_ITEMS the call throws — chunk the payout. The practical bound is
lower than 64, because the chain keeps a ring of 100 recent roots and each item consumes room in
it.
Solana Kit runtime
Since 0.2.5 the SDK is built on @solana/kit. These are the helpers a normal integration touches.
connection takes a CloakRpc, programId and other addresses take Kit Address strings, and
depositorKeypair takes a KeyPairSigner. Wiring a browser wallet is covered in Solana Kit
integration.
Protocol and service docs