Stealth module
The stealth module ships on its own subpath, @privacy-pools-v2/sdk/stealth, separate from the main barrel; nothing in it is reachable from @privacy-pools-v2/sdk. It implements ERC-5564 stealth addresses for two schemes and the glue that binds an announcement to a pool withdrawal. Usage walkthroughs live on Stealth withdraw; this page is the export reference.
import { deriveStealthAddress, scanAnnouncement } from "@privacy-pools-v2/sdk/stealth";
Schemes and constants
SCHEME_CANONICAL // 0x0001, EIP-5564 secp256k1 scheme 1
SCHEME_FLUIDKEY // 0xfffe, FluidKey recipe (placeholder id until registered)
CANONICAL_ANNOUNCER_ADDRESS // 0x55649e01b5df198d18d95b5cc5051630cfd45564
assertCanonicalAnnouncer(announcer: EvmAddress): void
VIEW_TAG_BYTES // 1
ERC5564_ANNOUNCE_ABI // the announce(uint256,address,bytes,bytes) ABI fragment
assertCanonicalAnnouncer throws StealthAddressAnnouncementError for any address other than the canonical CREATE2 singleton; every builder that emits an announce call runs it.
Sender side
resolveRecipient(identifier: RecipientIdentifier): MetaAddress
generateEphemeralKeyPair(scheme: SchemeId): EphemeralKeyPair
deriveStealthAddress(metaAddress: MetaAddress, ephemeral: EphemeralKeyPair, schemeId: SchemeId): StealthDerivation
deriveStealthSharedSecret(...) // the ECDH shared-secret hash the scheme uses
computeViewTag(...) // first byte of the shared hash
encodeViewTagAsMetadata(viewTag): Hex // the 1-byte announcement metadata
buildAnnounceCall(announcement: Announcement, announcer: EvmAddress): EvmCall
composeWithdrawalBundle(withdrawCall: DeclaredWithdrawCall, announcement: Announcement, announcer: EvmAddress): StealthWithdrawalBundle
prepareStealthWithdrawal(request: StealthWithdrawalRequest): Promise<StealthWithdrawalBundle>
toStealthAnnouncementFields(derivation: StealthDerivation): StealthAnnouncementFields
resolveRecipientmaps an identifier to a typed meta-address.{ kind: "raw", spendingPubKey }(65-byte uncompressed) yields a FluidKey meta-address at0xfffe;{ kind: "canonical", spendingPubKey, viewingPubKey }(two 33-byte compressed keys, both curve-validated) yields a canonical meta-address at0x0001.generateEphemeralKeyPairdraws from WebCrypto only (rejection-sampled, no fallback) and produces the ephemeral public key in the scheme's wire format: compressed for canonical, uncompressed for FluidKey.deriveStealthAddressdispatches onmetaAddress.scheme; the explicitschemeIdargument is defensive and must match (StealthAddressSchemeMismatchErrorotherwise). It returns{ stealthAddress, announcement }and deliberately drops the meta-address from the result.composeWithdrawalBundlerequires the withdraw call'sdeclaredDestinationto equal the announcement'sstealthAddress, the client-side form of the bindingPrivacyPoolRelay.relayAndAnnounceenforces on-chain.prepareStealthWithdrawalis the orchestrator: resolve, derive, call yourgenerateWithdrawCall(stealthAddress)callback, and compose the bundle againstrequest.announcerAddress.toStealthAnnouncementFieldsconverts a derivation into theannouncementobjectRelayerInteractor.relayWithAnnounceexpects.
Recipient side
scanAnnouncement(announcement: Announcement, credentials: ScanCredentials): EvmAddress | null
deriveStealthPrivateKey(announcement: Announcement, credentials: DerivePrivCredentials): Scalar32
recoverStealthSharedSecret(...)
verifyAnnouncementBindsToWithdrawal(announcement, withdrawalLog: WithdrawalLogEvent): boolean
scanDepositNoteData(data: Hex, credentials: ScanCredentials): DepositStealthMatch | null
encodeStealthDepositPayload(payload: StealthDepositPayload): Hex
decodeStealthDepositPayload(data: Hex): StealthDepositPayload
scanAnnouncementrequiresannouncement.schemeId === credentials.scheme, recomputes the shared hash from the recipient's key material (viewing private key for canonical, spending private key for FluidKey), compares its first byte to the view tag, and only on a hit reconstructs the candidate and compares it to the announced address.deriveStealthPrivateKeyproduces the spending key for a matched address and self-validates it against the announced address (StealthAddressScanIntegrityErroron mismatch).verifyAnnouncementBindsToWithdrawalis the recipient-side mirror of the on-chain binding: it checks an announcement against a same-transaction withdrawal event.- The deposit-payload trio carries an ephemeral key, view tag, and ECDH-encrypted note payload inside a deposit's opaque
noteData.datainstead of a public announcement. UnlikescanAnnouncementthere is no separately claimed on-chain address to verify against, so the derived candidate is the result. Used by the cross-chain reshield dust rescue.
Types
type SchemeId = typeof SCHEME_CANONICAL | typeof SCHEME_FLUIDKEY;
type MetaAddress = CanonicalMetaAddress | FluidKeyMetaAddress; // discriminated on `scheme`
type Announcement = { schemeId; stealthAddress; ephemeralPubKey; viewTag; ... };
type StealthDerivation = { stealthAddress: EvmAddress; announcement: Announcement };
type RecipientIdentifier =
| { kind: "raw"; spendingPubKey: Hex }
| { kind: "canonical"; spendingPubKey: Hex; viewingPubKey: Hex };
type StealthWithdrawalRequest = {
recipient: RecipientIdentifier;
generateWithdrawCall: (stealthAddress: EvmAddress) => Promise<EvmCall> | EvmCall;
announcerAddress: EvmAddress;
schemeId?: SchemeId;
};
type StealthWithdrawalBundle = { withdraw: EvmCall; announce: EvmCall };
type DeclaredWithdrawCall = { to: EvmAddress; data: Hex; declaredDestination: EvmAddress };
type EvmCall = { to: EvmAddress; data: Hex };
type ScanCredentials / DerivePrivCredentials // per-scheme unions of the recipient's key material
type StealthDepositPayload / DepositStealthMatch / WithdrawalLogEvent
Branded hex types (CompressedPubKey, UncompressedPubKey, Scalar32, EvmAddress) validate width, prefix, and curve prefix at the codec boundary. assertNever is exported for exhaustive switches over the scheme union.
Errors
All extend StealthAddressError: StealthAddressDeriveError, StealthAddressUnsupportedSchemeError, StealthAddressSchemeMismatchError, StealthAddressAnnouncementError, StealthAddressScanIntegrityError. They are runtime values on this subpath, so instanceof works here, unlike most errors on the main barrel.
Relation to the main SDK
RelayerInteractor.relayWithAnnounce(relayer, params) on the main barrel is the transport for the atomic relayed path; its params are WithdrawalRelayParams & { announcement: StealthAnnouncementFields }. PoolSessionReshieldCrosschain accepts a dustRescue: { recipientIdentifier } opt-in that reuses this module's derivation for the destination-chain dust deposit. Stealth does not compose with batch withdrawal or with a yield unwrap.
Source: v2-monorepo/packages/sdk/src/stealth/