Skip to main content

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 CryptoService to 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 noteSecretsMap variant 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-committed noteAddressHashes and never see the secrets. In that case you pass the hashes through the {publicViewingKey, noteAddressHashes, hint} variant with hint set to the request's tag, or you call session.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 RecipientDiscoveryData
Cryptography 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.