@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.

Shell
npm install @p15/blind-mode

The 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.

TypeScript
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.

MethodReturnsPurpose
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)bigintSum 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.

ExportUse
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.
CliProverRuns the local Noir toolchain (nargo + sunspot). The Node path.
Prover, ProofResult, CircuitInputsThe 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.

TypeScript
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.commitment

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

TypeScript
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.