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

@solana/zk-sdk

v0.5.3

Published

Universal WASM package for the rust solana-zk-sdk

Readme

@solana/zk-sdk

WebAssembly bindings for the Rust solana-zk-sdk. Use it from Node, the browser, or a bundler to generate the zero-knowledge proofs used by the Token-2022 confidential-balances extension, and to derive the ElGamal and AES keys those balances are encrypted under.

Install

npm install @solana/zk-sdk

The package ships three builds, one per wasm-pack target. Import the package root and the right one is selected for you; the subpaths are there when you need to pin a build explicitly:

| Import | Target | Init | |---|---|---| | @solana/zk-sdk | Node and bundlers — node gets the Node build, browser bundlers get the bundler build | none, ready on import | | @solana/zk-sdk/node | Node.js | none, ready on import | | @solana/zk-sdk/web | browser, no bundler | call the default init() once before use | | @solana/zk-sdk/bundler | Vite / webpack / etc. | none, the bundler loads the wasm |

// Node or a bundler
import { ConfidentialKeys } from "@solana/zk-sdk";

// Browser without a bundler
import init, { ConfidentialKeys } from "@solana/zk-sdk/web";
await init();

The web build is the one case the root export cannot pick for you: it needs an explicit init(), so import it by subpath. @solana/zk-sdk/bundler also resolves to the Node build under the node condition — the ESM bundler build cannot load its .wasm outside a bundler, so a Node runtime (including the server side of a bundled app) is always served the Node build.

Confidential-balances key derivation

A confidential token account is encrypted under two keys: an ElGamal keypair (the balance ciphertext) and an AES key (the decryptable_available_balance fast-path). ConfidentialKeys derives both deterministically from a single source of key material, through a shared HKDF-SHA512 chain identified by the protocol string solana-conf-bal/v1. Re-deriving from the same input always yields the same keys, on any platform that implements the same chain.

There is one entry point per source of key material. Pick the one that matches how your wallet holds its secret:

| Source | Method | Notes | |---|---|---| | WebAuthn passkey | prfInput + fromPrf | the only viable path for passkeys | | Ed25519 wallet signature | signerMessage + fromSignature | today's universal wallet path | | Raw input key material | fromIkm | Secure Enclave / KMS HMAC, BIP39 seed, etc. |

All three converge on the same spine, so fromSignature(sig), fromIkm(bytes), and fromPrf(out) over the same input bytes produce identical keys.

const keys = ConfidentialKeys.fromPrf(prfOutput);
const elgamal = keys.elgamal(); // ElGamalKeypair
const ae = keys.ae();           // AeKey

The standard message

signerMessage() and prfInput() take no arguments and return the constant standard message, the bytes solana-conf-bal/v1. Seed-scoped derivation lives in signerMessageWithSeed and prfInputWithSeed. The derived keys are bound to the wallet alone, one key pair for all of the wallet's confidential accounts.

Keys match the other SDKs (the Token-2022 clients, Rust, Go) on the deterministic Ed25519 path, since they all sign the same constant message with the same wallet key. The PRF and raw-IKM paths feed different input material into the KDF, so they derive different keys for the same wallet and reproduce them only from the same credential or key material. An account keeps whichever adapter provisioned it.

Wallets SHOULD refuse to sign any message starting with solana-conf-bal/v1 through generic signMessage and expose the derivation signature only via a dedicated key-derivation capability, since the signature is the account's decryption key material.

Non-standard seed scoping

signerMessageWithSeed(seed) and prfInputWithSeed(seed) build solana-conf-bal/v1 || seed for schemes that genuinely need keys scoped more finely than the wallet. Keys derived from a non-empty seed will not match the standard keys other clients derive for the same wallet. For single-signer PDA wallets specifically, use pdaWalletPublicSeed to bind the derived keys to the wallet program, wallet PDA, mint, and concrete token account:

const publicSeed = ConfidentialKeys.pdaWalletPublicSeed(
  programId.toBytes(),
  walletPda.toBytes(),
  mint.toBytes(),
  tokenAccount.toBytes(),
);
const message = ConfidentialKeys.signerMessageWithSeed(publicSeed);

Passkeys (WebAuthn PRF)

Passkey ECDSA signing is randomized by spec, so signature-based derivation is impossible on passkey authenticators. The PRF (hmac-secret) extension is deterministic by construction and is the only path. PRF must be enabled when the credential is registered; legacy credentials that predate it cannot be used.

import { ConfidentialKeys } from "@solana/zk-sdk";

// 1. One-time, at credential registration (pure WebAuthn, not this SDK).
//    Enabling PRF here is mandatory.
const cred = await navigator.credentials.create({
  publicKey: { /* rp, user, challenge, pubKeyCredParams */, extensions: { prf: {} } },
});
const credentialId = new Uint8Array(cred.rawId);

// 2. Per session: evaluate the PRF over the standard input, then derive.
const salt = ConfidentialKeys.prfInput();

const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: crypto.getRandomValues(new Uint8Array(32)),
    allowCredentials: [{ id: credentialId, type: "public-key" }],
    extensions: { prf: { eval: { first: salt } } },
  },
});

const prf = assertion.getClientExtensionResults().prf?.results?.first;
if (!prf) throw new Error("authenticator returned no PRF result");

const keys = ConfidentialKeys.fromPrf(new Uint8Array(prf));

fromPrf accepts a 32-byte output (a single prf.results.first) or a 64-byte output (first || second concatenated), and rejects an all-zero result.

prfInput returns the standard message solana-conf-bal/v1, byte-identical to signerMessage (prfInputWithSeed mirrors signerMessageWithSeed for non-standard scoping). It is passed to prf.eval.first as-is: browsers apply the mandatory SHA-256("WebAuthn PRF" || 0x00 || input) prefixing before the authenticator, so the input must not be pre-hashed. A non-browser or direct-CTAP hmac-secret consumer must reproduce that prefixing over this message to derive matching keys.

Ed25519 wallet signature

For a normal Solana wallet, derive from a single deterministic Ed25519 signature over the standard message.

import { ConfidentialKeys } from "@solana/zk-sdk";

const message = ConfidentialKeys.signerMessage();      // constant: "solana-conf-bal/v1"
const signature = await wallet.signMessage(message);   // 64-byte Ed25519 signature
const keys = ConfidentialKeys.fromSignature(signature);

wallet.signMessage stands in for whatever signing capability the integration has; a wallet that implements the refusal above provides this signature through a dedicated derivation API instead, and a filesystem or server signer just signs the bytes. The signer must be deterministic (RFC 8032): a randomized one returns a different signature, and different keys, on every call.

The all-zero (default) signature is rejected: some signers return it instead of raising an error, and the resulting keys would be predictable.

Raw input key material

When the wallet exposes an HMAC/HKDF primitive directly (iOS Secure Enclave, AWS KMS GenerateMac, a BIP39 seed, HKDF over an Ed25519 seed), pass that output straight in.

const keys = ConfidentialKeys.fromIkm(ikm); // 32 to 65535 bytes

Things to get right

  • Identity bindings are forever. WebAuthn PRF output is bound to the Relying Party ID; the signature path is bound to the signing key; raw IKM is bound to its source. Change the binding and the old balance can no longer be decrypted. Commit to a stable RP ID at provisioning and never change it.
  • Cache the derived keys for the session. Do not re-prompt for a signature or device unlock per transfer. Derive once, hold the keys in memory, wipe on background.
  • Back up device-bound keys. Deterministic derivation does not survive device or credential loss. Passkey and Secure Enclave wallets need an explicit recovery path (synced credentials or wrapped-key escrow).

Background

The derivation scheme and the per-wallet adapter typology are described in the Solana confidential-balances single-signer derivation framework. The Rust spine and its test vectors live in zk-sdk/src/encryption/derivation.rs.