@p15/blind-mode
Amount privacy: the pluggable proof backend, transfer planning, the note
primitives, the note-vault and pSOL instruction builders, and private swap.
Depends on @p15/stealth.
npm install @p15/blind-modeThe backend seam
You never instantiate a concrete engine by hand. getBackend returns a
TransferProofBackend, and the active engine is noir-v1, which needs a prover.
import { getBackend, RemoteProver } from "@p15/blind-mode";
const prover = new RemoteProver("https://your-prover/api/prove");
const backend = getBackend("noir-v1", { prover });getBackend("noir-v1") throws without opts.prover. native-ct is reserved for
the Token-2022 Confidential Transfer path and takes no prover.
TransferProofBackend
Every method that builds transactions returns a BackendPlan. You do not assemble
instructions yourself for the common flows; you execute the plan.
| Method | Returns | Purpose |
|---|---|---|
estimateBudget(kind) | Promise<bigint> | Lamports to pre-fund the session key. kind is 'send' or 'withdraw'. |
planFund(input) | Promise<BackendPlan> | Make private: public tokens to a private self-note. |
planSend(input, inputNote) | Promise<BackendPlan> | Spend one note to a recipient note + change. |
planWithdraw(input, inputNote) | Promise<BackendPlan> | Private note back to a public token account. |
loadBalance(notes) | bigint | Sum the user's spendable notes. |
A BackendPlan is { transactions: PlanTx[], announceExtra?: Uint8Array }. Each
PlanTx is { label, signer: 'session' | 'wallet', instructions }. Send them in
order, each signed by the tagged payer. See the Recipes
executePlan helper. Each ~1.5 KB proof must be its own transaction, so a send is
two and a withdraw is three.
Inputs
FundInput: { owner, feePayer, mint, tokenProgram?, amount, scanSecret, spendScalar, rentFor }.
SendInput: { feePayer, senderScanSecret, senderSpendScalar, senderScanPublic, mint, amount, recipientMetaAddress, note?, rentFor }.
WithdrawInput: { feePayer, owner, scanSecret, spendScalar, mint, tokenProgram?, amount, destination, rentFor }.
rentFor is (space: bigint) => Promise<bigint>, normally
(s) => rpc.getMinimumBalanceForRentExemption(s).send().
SpendableNote (what the backend spends): { commitment: Uint8Array, amount: bigint, ephemeralPublic: Uint8Array }.
Prover
The backend delegates proof generation to a Prover.
| Export | Use |
|---|---|
RemoteProver(url) | POSTs { circuit, inputs } to a prover endpoint; returns { proof, publicWitness }. The dApp path. The witness never contains the spend scalar, so proving stays non-custodial. |
CliProver | Runs the local Noir toolchain (nargo + sunspot). The Node path. |
Prover, ProofResult, CircuitInputs | The seam types. toToml(inputs) serializes inputs for the CLI. |
See Self-host the prover for running one.
Note primitives
The math behind a note. The commitment matches the note-vault program exactly.
import {
noteCommitment, assetField, pubkeyToField, noteNullifier,
deriveNoteForSend, deriveNoteForReceive,
} from "@p15/blind-mode";
const asset = assetField(mintBytes); // Poseidon(mint) as a field
// Sender, from a recipient meta-address:
const out = deriveNoteForSend(amount, asset, recipientScanPublic, recipientSpendPublic);
// out.commitment, out.ownerPublic (= R), out.ephemeralPublic, out.secret, out.blinding
// Recipient, from a scanned announcement:
const note = deriveNoteForReceive(amount, asset, keys.scanSecret, keys.spendScalar, ephemeralPublic);
// note.ownerScalar (spendable), note.nullifier, note.commitmentA Note carries amount, asset, ownerHash (Poseidon(R)), blinding,
secret, commitment, and ownerPublic (the 32-byte R, used as the note-vault
PDA seed). noteNullifier(secret) = Poseidon(secret).
note-vault builders
Low-level instruction builders for the note-vault program.
The plan* backend methods use these; reach for them directly only when you need
custom transaction shapes.
findPoolAuthority, findPoolAta(mint, tokenProgram?), findNotePda(commitment),
commitmentBytes(commitment), buildCreatePoolAtaInstruction,
buildDepositInstruction, buildSpendInstruction, buildWithdrawInstruction.
Constants NOTE_VAULT_PROGRAM_ID, TRANSFER_VERIFIER_ID.
The tokenProgram? parameter defaults to Token-2022, but the active mints (SOL,
USDC) are on the legacy SPL Token program, so pass the mint's actual program
(TOKEN_PROGRAM_ADDRESS from @solana-program/token). The pool moves plain SPL
tokens with transfer_checked; there are no Confidential Transfers involved.
pSOL wrapper (legacy)
Builders for the psol-wrapper program, the earlier
program-minted Token-2022 wrapper. The note-model dApp wraps SOL natively
instead, so reach for these only if you specifically want pSOL:
buildInitializeInstruction, buildWrapInstruction, buildUnwrapInstruction,
findVaultPda, findMintAuthPda, findConfigPda, fetchWrapFeeLamports,
fetchFeeTreasury. Constants PSOL_PROGRAM_ID, PSOL_MINT, PSOL_DECIMALS = 9,
DEFAULT_WRAP_FEE_LAMPORTS.
Private swap
Turn a private note of one asset into a private note of another.
import { planPrivateSwap, getSwapRouter, JupiterRouter, DevnetStubRouter } from "@p15/blind-mode";
const router = getSwapRouter("devnet", {}); // or new JupiterRouter() on mainnet
const { amountOut } = await router.quote(mintIn, mintOut, inputNote.amount);
const plan = await planPrivateSwap(
{ feePayer, scanSecret, spendScalar, scanPublic, mintIn, tokenProgramIn,
mintOut, tokenProgramOut, minOut: (amountOut * 99n) / 100n, router, rentFor },
inputNote,
);SwapRouter is a seam (quote, plus instruction building). DevnetStubRouter
uses a fixed rate for devnet; JupiterRouter hits Jupiter on mainnet. See the
Private swap guide. v1 swaps a whole input note.
Verifier helpers
readTransferPublicInputs(witness) and verifierInstructionData(proof, witness)
build the CPI payload note-vault sends to the Groth16 verifier;
fieldToBytesBE / bytesBEToField convert field elements.