npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@remi-fzc/confidential

v0.1.5

Published

Confidential USDC on Stellar — client library with in-browser zero-knowledge proving

Readme

@remi-fzc/confidential

Confidential USDC on Stellar — a TypeScript client library that generates zero-knowledge proofs in the browser and settles against Soroban contracts.

Early release. The surface below is the real one — address-based, signer-pluggable, with the cryptographic internals deliberately unexported. Expect breaking changes before 1.0.

How far it has been exercised, precisely: the read path (registration and freeze status, the event feed, reconstructed balances) runs against Stellar testnet in a live dashboard. The write path was last exercised end to end on 2026-08-17 against 0.1.3, on testnet, by two headless harnesses: one drove operator-free onboarding, a self-paid register, then a fee-bumped deposit, merge and confidential transfer, asserting the sender's XLM was unchanged to the stroop; the other confirmed the same transfer reverts with Error(Contract, #3601) while the sender is frozen and succeeds again after unfreeze. withdraw is built and proven by the circuits but is not covered by either harness. Nothing here has touched mainnet.

Confidentiality, not anonymity. Addresses and the fact of a transfer are public on-chain. The amount is not.


Install

npm install @remi-fzc/confidential

@stellar/stellar-sdk (^14.6.1) is a required peer. npm installs it for you; pnpm with strict peers, and Yarn, do not, so add it explicitly there:

npm install @remi-fzc/confidential @stellar/stellar-sdk

Reading is dependency-light. The root entry point imports neither prover dependency, so a plain install pulls no WebAssembly at all and the nine root exports work immediately.

Proving needs two more packages, at these exact versions:

npm install @aztec/[email protected] @noir-lang/[email protected]

They are exact rather than cautious: proof compatibility is tied to the verification keys registered in the deployed verifier, so a proof built against a different backend version does not verify on-chain. They are declared optional peers so a read-only consumer does not download bb.js (11 MB on disk) — but register, transfer and withdraw do not work without them. Skip them and import "@remi-fzc/confidential/proving-bb" fails at import time, not at the first proof:

ERR_MODULE_NOT_FOUND: Cannot find package '@noir-lang/noir_js' imported from
  .../node_modules/@remi-fzc/confidential/dist/core/proving/prover.js

That message points inside this package, so it reads like a broken package. It is not: it is the optional peer, absent by design.

Requirements

Node 22+. dist/circuits/index.js loads the compiled circuits with JSON import attributes (import ... with { type: "json" }), which older Node, webpack and Jest do not accept. Node 22 still prints an ExperimentalWarning for it.

Browser proving needs a little more, below.

Content-Security-Policy (required for browser proving)

If you serve a CSP, it needs all of these or bbProver() fails:

  • script-src 'wasm-unsafe-eval' — bb.js instantiates WebAssembly.
  • worker-src 'self' blob: — a violation throws from the Worker constructor.
  • connect-src https://crs.aztec.network — bb.js fetches its structured reference string at proving time.
  • connect-src data: — bb.js loads its WASM from an inline data: URI.
  • connect-src entries for your Stellar RPC and Horizon endpoints.

These fail loudly, with a rejected promise, so they are easy to diagnose.

The failure that is NOT loud

The one thing that hangs instead of erroring is a bundler that rewrites bb.js's worker URL. bb.js does new Worker(new URL("./main.worker.js", import.meta.url)) and then awaits a "message" event with no error listener and no timeout — so if that URL 404s, the promise never settles and proving appears to stall forever.

Vite serves bb.js's browser build as native ESM from node_modules, so the worker resolves and this does not arise. Webpack and Next mangle the URL; pass bbProver({ backendLoader }) to load bb.js yourself from a path where its sibling worker and WASM files stay intact.

Cross-origin isolation is optional

You may have read that bb.js requires COOP/COEP and SharedArrayBuffer. It does not, and this package does not benefit from it today: UltraHonkBackend defaults to { threads: 1 } and bbProver() passes no thread count, so bb.js loads its single-threaded WASM either way. Without cross-origin isolation it simply reports one available thread and proceeds — no throw, no hang.

Setting the headers is harmless and would matter if this package later asks for more threads:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless

If you do set them, use credentialless rather than require-corprequire-corp blocks cross-origin responses that lack CORP headers, which includes Stellar RPC and Horizon.


The deployment you point it at

The library ships no addresses: they are configuration, so redeploying never needs a release. There is exactly one deployment today, on Stellar testnet:

const TESTNET = {
  rpcUrl: "https://soroban-testnet.stellar.org",
  token:   "CBU5YXEDZQE6ARDGEK4HQE6JVCV3O35B2NC6WZPZ47N4CMMOQESPOKEP",
  auditor: "CBZX3AZTETO64HNBQV2J5AINKLVK6NUZIOHVU6H6L3KU242LJXZOF7AJ",
  auditorId: 0,
  // The wrapped asset: classic USDC, 7 decimals.
  usdc: { code: "USDC", issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" },
};

Only token and auditor are passed to createClient. The proof verifier is never passed in: the token calls it cross-contract on every proof.

auditorId is the id your accounts register under, and it must exist in that registry. The library reads get_key(auditor_id) and does not translate a failure there, so a missing id surfaces as a raw simulation error rather than a typed ConfidentialError. To deploy your own wrapper, use github.com/Remi-FZC-LLC/stellar-confidential-token and its deployments/ output.

Unaudited, testnet only. Do not point anything holding real value at it.

Before the first call: what each side needs

Both sides of a confidential transfer run this library. There is no way to send to an address that has not onboarded, and no way to merge or withdraw on someone else's behalf — those operations are signed by the account itself.

| Needed by | What | Who provides it | | --- | --- | --- | | every write | the Stellar account exists and holds XLM, or you pass feeBump | you | | register | a prover (/proving-bb plus both peers) | you, once per account | | deposit | a USDC trustline and a classic USDC balance on that account | you, outside this library | | withdraw | a USDC trustline on the destination (which is the same account) | you, outside this library | | transfer | the recipient is already registered under an auditor id | the recipient | | spending received value | the recipient calls merge() with its own signer | the recipient |

None of this is added implicitly: the library will not create an account, add a trustline or fund anything. A missing trustline surfaces as a contract-level rejected, not as a readable precondition error, so check for it yourself (Horizon /accounts/{id} balances) before calling deposit or withdraw.

deposit(amount) and withdraw(amount) are self-only: both submit with from = to = the account. You cannot deposit on a user's behalf, or pay a withdrawal out to a third address.

Usage

import { createClient, keypairSigner, memoryStore } from "@remi-fzc/confidential";
import { bbProver } from "@remi-fzc/confidential/proving-bb";
import { deriveConfidentialKeys } from "@remi-fzc/confidential";
import { Keypair, Networks } from "@stellar/stellar-sdk";

const client = createClient({
  rpcUrl: "https://soroban-testnet.stellar.org",
  networkPassphrase: Networks.TESTNET,
  contracts: { token: TOKEN_ID, auditor: AUDITOR_ID },
  auditorId: 0,
  store: memoryStore(),
  prover: bbProver(), // omit for a read-only client — see below
});

const kp = Keypair.fromSecret(SECRET);
const account = client.account({
  signer: keypairSigner(SECRET, Networks.TESTNET),
  keys: await deriveConfidentialKeys(kp, TOKEN_ID, Networks.TESTNET),
  // Capture BEFORE the account's first write, and persist it beside the seed.
  fromLedger: await client.latestLedger(),
});

await account.register();
await account.deposit(25_000_000n);        // 2.5 USDC — public at this boundary
await account.merge();                     // received → spendable (on-chain)
await account.sync();                      // ← replay events into local state
console.log(await account.balance());      // { spendable: 25000000n, receiving: 0n }
await account.transfer(BOB, 10_000_000n);  // amount hidden on-chain

Reads are local; syncing is explicit. balance() never touches the network — it returns the last synced state, and zeros for an address that has never synced. sync() is what replays chain events into that state, and it is the expensive call. transfer and withdraw sync internally before building a witness; register, deposit and merge do not, so a UI that renders straight after a deposit must sync() first or it will show a stale, usually zero, balance.

Omitting prover gives a read-only client. Balances, events, deposit, merge and setFrozen all work without one, and the root entry point imports neither prover package, so a read-only consumer never installs them and no bundler pulls them. register, transfer and withdraw then throw prover-not-configured, naming the import to add. That is why the prover peers are declared optional. Note that a read-only client can only operate an account that some prover-enabled client already registered with the same keys.

Amounts

Always bigint base units. This deployment's USDC has 7 decimals, so 1 USDC === 10_000_000n. Never write the zeros by hand.


The operations

| Operation | ZK proof? | Effect | | --- | --- | --- | | register | yes | one-time account setup inside the wrapper | | deposit | no | public USDC → confidential | | merge | no | received balance → spendable | | transfer | yes | confidential → confidential, amount hidden | | withdraw | yes | confidential → public USDC |

deposit and merge need no proof — the contract verifies them arithmetically. That is why a deposit is fast and a transfer is not.


Writes are slow, and there are three outcomes, not two

Every write is build, simulate, sign, submit, then poll Soroban RPC for confirmation: up to 60 polls at 2 s, so a write can occupy the call for two minutes on top of proving time, and none of that is configurable. Do not put these calls in a request handler or any function with a short execution limit; run them on a worker you control and hand the caller the hash.

  • resolved — an OpResult { hash, maxFee }. Confirmed SUCCESS on-chain.
  • ConfidentialError — it did not happen. code is one of not-registered, insufficient-balance (carries balance), key-mismatch, prover-not-configured, rejected (carries contractCode; this deployment uses 3601 for a frozen account). state-diverged is also declared, for a divergence check that is not wired up — nothing throws it, so do not build a handler on it.
  • ConfirmationTimeout — the network accepted the submission and the poll window closed. This is not a failure. The transaction may still settle, so re-running the call can spend the same value twice, and treating it as failure can lose a settled payment. Look up err.hash, then sync().

That last case is handled deliberately: on a timeout the cached spendable opening is not advanced, because the throw happens before setSpendable. sync() will reconcile it against the chain.

import { ConfidentialError, ConfirmationTimeout } from "@remi-fzc/confidential";

try {
  const { hash } = await account.transfer(BOB, 10_000_000n);
} catch (err) {
  if (err instanceof ConfirmationTimeout) await resolveByHash(err.hash); // never retry
  else if (err instanceof ConfidentialError && err.code === "insufficient-balance")
    showShortfall(err.balance);
  else throw err;
}

State is fund-safety-critical

The store is not a cache. Soroban RPC serves a limited window of events, and an account's receiving balance is a running sum of them. Once a crediting event ages out of that window, the persisted blob is the only thing that can still open that value. Call account.sync() at least once per retention window, and treat the store as durable, encrypted-at-rest storage.

If you miss the window there is no error. The event source clamps a cold start up to the RPC's retention floor, so the first sync after the window closes succeeds, silently omits the aged-out credits and persists a cursor above them. That value becomes unspendable and nothing reports it. The engine has a divergence check, but it is not reachable from this surface, so compare account.balance() against your own record of settled operations and alarm on a gap.

memoryStore() keeps nothing across a reload or a process — tests and single-run scripts only. In a browser use localStorageStore(prefix); the prefix is required and has no default, because your reset flow is what has to enumerate it, and it throws rather than silently no-opping when storage is unavailable. In Node there is no shipped persistent store: implement StateStore yourself before moving any real value. It is three methods:

interface StateStore {
  load(key: string): Promise<string | null>;
  save(key: string, blob: string): Promise<void>;
  remove(key: string): Promise<void>;
}

The blob holds the balance openings (v, r): a full opening of the on-chain commitment, which is the confidentiality property. Treat it as a spending secret.


Three traps worth knowing before you ship

isRegistered cannot distinguish "not registered" from "cannot ask." It is a simulation whose errors are swallowed, so a wrong rpcUrl, a wrong token contract, a rate-limited RPC or an outage all return false. Its neighbour isFrozen throws in those cases. Verify your endpoint independently before trusting a false — and read a not-registered error from transfer ("G… must register before it can receive") as possibly an RPC failure rather than a statement about the recipient.

key-mismatch only proves the keys were derived for this token contract. The check compares the derived addrF against the token's; it does not compare the keys against the account's on-chain registration. Keys from the wrong seed, or derived under a different message, are accepted and then decrypt every event into a plausible-looking but meaningless balance. If you support key recovery or more than one derivation path, compare your derived Y and PVK against the registered ones yourself before spending.

incoming() is a history, not a queue, and it has a hard age limit. It returns every transfer received since the account's fromLedger — merged ones included — re-reading and decrypting the contract's event history on every call. It passes fromLedger to RPC unclamped, unlike sync(), so once that ledger falls out of the retention window the call fails with a raw RPC error, and keeps failing. Use balance().receiving, which comes from clamped cursor-based sync(), as the authoritative "waiting to be merged" figure, and keep your own record of what you have credited.


The surface

ClientcreateClient(options), then account(options), isRegistered, isFrozen, setFrozen (token owner only), events({ fromLedger }), latestLedger, close. Accountregister, deposit, merge, transfer, withdraw, balance (local), sync (network), incoming, forget, address, spendingKeyHex. KeysderiveConfidentialKeys, keyDerivationMessage. StoresmemoryStore, localStorageStore(prefix). ErrorsConfidentialError, ConfirmationTimeout. AuditorauditTransfer(k, transferEvent), where k is the auditor's Grumpkin secret scalar as a bigint. It opens the amount and the sender's post-transfer balance, for transfers only: withdraw-channel decryption and auditor-key derivation exist internally but are not exported, so a compliance integrator can audit transfers and not withdrawals. channelsAgree === false means k is not the auditor key for both parties, which happens when sender and recipient registered under different auditor ids.

Progress, and paying someone else's fees

Every operation takes { onProgress } and emits four stages: syncing, proving, proved, submitting. proved carries the measured ms and proofBytes; submitting carries sponsored. Proving takes seconds and is device-dependent, so drive a real indicator from these events rather than guessing a duration, and keep the tab alive.

await account.transfer(BOB, 10_000_000n, {
  onProgress: (e) => { if (e.stage === "proving") setBanner("Generating proof…"); },
});

createClient({ feeBump }) is consulted immediately before each write: return a FeeBump (a fee-source address plus signFeeBump) and that account pays the whole fee, so your users can hold zero XLM. Return undefined to have the account pay. Because it is called per operation, a runtime sponsorship toggle needs no client rebuild.

One client and one prover per process, reused

bbProver() retains a prover per circuit, each with its own WASM and CRS, so do not construct a client per request: a Node service that does will pay backend init every time and eventually exhaust memory. client.close() releases them.

Proving has one outbound dependency

On the first proof, bb.js downloads the structured reference string (g1.dat, g2.dat, grumpkin_g1.dat) from https://crs.aztec.network and caches it, in IndexedDB in a browser. Nothing about the transaction is sent — these are byte-range requests for public curve data — but if that host is unreachable, no proof can be generated. Allow it in your CSP and your egress rules, and mirror the CRS if you need independence from it.


Security model

Every secret stays on the client: key derivation, note decryption, balance reconstruction, witness construction, proof generation, and signing all run locally. Nothing server-side can reconstruct a key.

There is no hosted proving service, and one could not preserve confidentiality. The spending key sk is a private input to all three circuits, and generating a proof requires the witness. Any remote prover would therefore receive full, permanent spend authority over the account it proves for — including the viewing key, which derives from the same scalar. That is a property of the circuits, and no library interface can change it. Delegated proving that preserved confidentiality would need different circuits, MPC, or an attested TEE.

Key classes

| Key | Opens | Blast radius | | --- | --- | --- | | Spending key (sk) | authorises transfers; derives the viewing key | one account, permanently | | Viewing key (vk) | that account's own notes and balance | one account | | Auditor view key | every transfer in the wrapper | whole wrapper |

Because vk derives from sk, there is no spend-only disclosure.

Keys today: the app holds the seed

Transaction signing is pluggable: keypairSigner(secret, passphrase), or any object with { publicKey, sign(xdr) }, including a browser wallet's signTransaction. Key derivation is not. deriveConfidentialKeys(keypair, token, passphrase) needs the raw Stellar seed, because it derives sk from a deterministic ed25519 signature over a fixed message. So the only supported model today is app-managed: your application holds the seed.

If your product is non-custodial — wallet- or passkey-signed — this release cannot derive its keys for you. keyDerivationMessage() is exported, but the signature-to-key reduction and the Grumpkin point type it produces are internal, so you cannot complete that path yourself. Wallet-derived keys are not on this surface yet; do not design around them.

The message string is fixed by this package and is part of the derivation. Keys derived under any other string will not match a registration made with this one, so it cannot be changed per consumer.


Compliance states you must handle

The deployed wrapper is a compliant token. Its owner can freeze an account, and a frozen sender's transfer or withdraw reverts as ConfidentialError("rejected") with contractCode 3601. Surface that as a policy outcome, never as a retry. Call client.isFrozen(address) before a large operation if you want to fail early — noting it throws on a failed read.

client.setFrozen({ account, frozen, owner }) is on the surface for whoever holds the token owner's key. If you are an integrator rather than the issuer, this is not yours to call and the chain will reject it.


Roadmap

Shipped: the address-based public surface, the cryptographic core withdrawn from it (internal, not deleted), and a test suite.

Still open:

  • [ ] Mainnet exercise — testnet only so far
  • [ ] A Node file-backed StateStore; today Node consumers get memoryStore() or their own implementation
  • [ ] Recipient-side helpers beyond incoming()

Three subpaths, and only three: ., ./proving-bb, ./circuits. Earlier drafts planned /presets, /auditor, /compliance and /stellar-classic; each was cut for having no real caller. Deployment addresses in particular stay out, so redeploying a contract never requires an SDK release.


License

MIT, © 2026 Remi FZC LLC. See LICENSE.

This package redistributes third-party MIT code — the compiled circuits from OpenZeppelin/stellar-contracts and the cryptographic client from brozorec/stellar-confidential-token-demo. Required notices are in THIRD-PARTY-NOTICES.md; what was taken and what was changed is recorded in ATTRIBUTION.md.

This library's own source repository is private. What ships on npm is compiled JavaScript with type declarations, plus the compiled circuit artifacts. The contracts and the Noir circuits are public at github.com/Remi-FZC-LLC/stellar-confidential-token, and the artifacts in this package are byte-identical to the ones there — two shasum -a 256 runs close that link without trusting us.

Not affiliated with, endorsed by, or supported by OpenZeppelin, Aztec, Circle, or the Noir project.