Encrypt a note to a published viewing key
What this lets you do
Send a note directly to a known viewing pubkey, bypassing the on-chain Keystore lookup. Two cases need this:
- Payment requests. The payer encrypts to the request's per-request public key, which the recipient derives deterministically and includes in the request artifact rather than publishing in the keystore.
- P2P off-chain key exchange. Two parties exchanged viewing pubkeys directly (e.g., via a secure channel) and want to skip keystore registration.
Constraints & limits
- Caller supplies the noteSecretsMap. Unlike the evmAddress variant, where the SDK generates noteSecrets, this variant requires you to provide them. Use
CryptoServiceto generate cryptographically random 32-byte secrets. - No on-chain attestation that the pubkey belongs to anyone. The keystore registration is what proves "this address claims this viewing key." Encrypting directly bypasses that proof, so only use this path when you trust where the pubkey came from.
- This is the direct public-key path, not payment-request fulfillment. The
noteSecretsMapvariant shown here is for sends outside a payment request, where you generate the secrets yourself and deliver them out-of-band. Fulfilling a payment request works differently, because you hold only the request's pre-committednoteAddressHashesand never see the secrets. In that case you pass the hashes through the{publicViewingKey, noteAddressHashes, hint}variant withhintset to the request'stag, or you callsession.prepareFulfillPaymentRequest(...), which wires that up for you.
What it unlocks next
- Payment requests, since this is the encryption mechanism the request flow uses under the hood.
- Trustless P2P flows where both sides exchange pubkeys out-of-band and skip keystore gas costs.
- Privacy-preserving group payouts: one sender pays many recipients, each with their own pubkey already shared off-chain.
How to use it
import { CryptoService, PoseidonHashService } from "@privacy-pools-v2/sdk";
const crypto = new CryptoService();
const hashService = await PoseidonHashService.create();
const noteSecret = crypto.generateSecret();
const noteSecretsMap = new Map();
const recipientNoteAddressHash = hashService.hash([recipientAddress, noteSecret]);
noteSecretsMap.set(recipientNoteAddressHash, noteSecret);
const prepared = await session.prepareTransfer({
inputCommitments: [noteToSpend.commitment],
amount,
tokenId,
recipientDiscoveryData: {
publicViewingKey: recipientPubkey,
noteSecretsMap,
hint: routingHint,
},
});
const result = await session.relayTransfer(prepared.relayOptions[0]);
noteAddressHash binds the note to the recipient's EVM address, so compute it with the SDK's Poseidon hash service. Payment-request fulfillment uses the noteAddressHashes variant or session.prepareFulfillPaymentRequest(...) instead.
Behind the scenes
SDK call
session.prepareTransfer({recipientDiscoveryData: {publicViewingKey, noteSecretsMap, hint?}}), variant 2 of RecipientDiscoveryDataCryptography The SDK generates an ephemeral X25519 keypair per output note, runs ECDH against the supplied publicViewingKey, derives an encryption key, and encrypts the note payload. The hint is not encrypted. It's emitted separately as the event's public indexed topic.
On-chain effect A
Note(hint, encryptedData) event, where the hint is a public, indexed routing topic (a chosen tag for this direct send) and encryptedData is the sealed payload. The event has the same shape as registered-recipient transfers, just with a chosen hint instead of the recipient's auto-derived one.