Skip to main content
The EVM chain module gives you direct access to the cryptographic primitives that power stealth addresses on Horizen, Ethereum, Polygon, Base, and any other EVM-compatible chain. All operations use secp256k1 and follow the ERC-5564 stealth address scheme. Import from @wraith-protocol/sdk/chains/evm.
Most developers should use the agent client instead. These primitives are for power users who need to build custom stealth address integrations without the managed platform — for example, integrating stealth addresses into an existing wallet or dApp.

When to use primitives vs the agent client

Use the agent client (@wraith-protocol/sdk) when you want managed infrastructure: the TEE handles key storage, payment scanning, AI interaction, and on-chain transactions. Use the EVM primitives (@wraith-protocol/sdk/chains/evm) when you need full control: you manage your own keys, query announcements yourself, and sign transactions with your own tooling.

Import

import {
  deriveStealthKeys,
  generateStealthAddress,
  scanAnnouncements,
  deriveStealthPrivateKey,
  encodeStealthMetaAddress,
  decodeStealthMetaAddress,
  checkStealthAddress,
  signNameRegistration,
  metaAddressToBytes,
  STEALTH_SIGNING_MESSAGE,
  SCHEME_ID,
  META_ADDRESS_PREFIX,
} from "@wraith-protocol/sdk/chains/evm";
import type {
  HexString,
  StealthKeys,
  StealthMetaAddress,
  GeneratedStealthAddress,
  Announcement,
  MatchedAnnouncement,
} from "@wraith-protocol/sdk/chains/evm";

Constants

// Prompt the user to sign exactly this message for key derivation
const STEALTH_SIGNING_MESSAGE =
  "Sign this message to generate your Wraith stealth keys.\n\nChain: Horizen\nNote: This signature is used for key derivation only and does not authorize any transaction.";

const SCHEME_ID = 1n;                     // bigint — ERC-5564 scheme ID
const META_ADDRESS_PREFIX = "st:eth:0x";  // prefix for stealth meta-addresses

Core types

type HexString = `0x${string}`;

interface StealthKeys {
  spendingKey:    HexString;  // 32-byte secp256k1 private key
  viewingKey:     HexString;  // 32-byte secp256k1 private key
  spendingPubKey: HexString;  // 33-byte compressed public key
  viewingPubKey:  HexString;  // 33-byte compressed public key
}

interface GeneratedStealthAddress {
  stealthAddress:  HexString;  // 20-byte EVM address
  ephemeralPubKey: HexString;  // 33-byte compressed public key — publish with announcement
  viewTag:         number;     // 0-255 — publish with announcement
}

interface Announcement {
  schemeId:        bigint;
  stealthAddress:  HexString;
  caller:          HexString;
  ephemeralPubKey: HexString;
  metadata:        HexString;  // first byte is the view tag
}

interface MatchedAnnouncement extends Announcement {
  stealthPrivateKey: HexString;  // spending key for this stealth address
}

Functions

deriveStealthKeys(signature)

Derive a spending and viewing key pair from a wallet signature. The same signature always produces the same keys — this is deterministic key derivation.
const signature = await wallet.signMessage(STEALTH_SIGNING_MESSAGE);
const keys = deriveStealthKeys(signature as HexString);

console.log(keys.spendingKey);    // "0x..." (32-byte private key)
console.log(keys.viewingKey);     // "0x..." (32-byte private key)
console.log(keys.spendingPubKey); // "0x02..." (33-byte compressed)
console.log(keys.viewingPubKey);  // "0x03..." (33-byte compressed)
signature
HexString
required
A 65-byte EIP-191 wallet signature (0x-prefixed hex). Sign STEALTH_SIGNING_MESSAGE with the user’s wallet.
Returns: StealthKeys How it works: The 65-byte signature is split into two 32-byte halves. spendingKey = keccak256(r) and viewingKey = keccak256(s), where r = sig[0:32] and s = sig[32:64]. Compressed public keys are derived from each private key. The two keys are always distinct.
Never store the signature itself — derive the keys at runtime and keep the private keys in memory only as long as needed.

generateStealthAddress(spendingPubKey, viewingPubKey, ephemeralKey?)

Generate a one-time stealth address for a recipient. Call this on the sender’s side using the recipient’s public keys from their stealth meta-address.
const result = generateStealthAddress(
  recipientKeys.spendingPubKey,
  recipientKeys.viewingPubKey,
);

console.log(result.stealthAddress);  // "0x..." — send funds here
console.log(result.ephemeralPubKey); // "0x..." — publish in the announcement
console.log(result.viewTag);         // 0-255 — publish in the announcement
spendingPubKey
HexString
required
The recipient’s 33-byte compressed secp256k1 spending public key.
viewingPubKey
HexString
required
The recipient’s 33-byte compressed secp256k1 viewing public key.
ephemeralKey
HexString
Override the randomly generated ephemeral private key. Use only for deterministic testing — in production this should always be random.
Returns: GeneratedStealthAddress How it works:
  1. Generate a random ephemeral key pair (r, R = r × G)
  2. Compute ECDH shared secret: S = r × viewingPubKey
  3. hashedSecret = keccak256(S)
  4. viewTag = hashedSecret[0]
  5. stealthPoint = spendingPubKey + keccak256(S) × G
  6. stealthAddress = keccak256(stealthPoint)[12:32]
Each call produces a different stealth address (new random ephemeral key), making payments unlinkable on-chain.

scanAnnouncements(announcements, viewingKey, spendingPubKey, spendingKey)

Scan an array of on-chain ERC-5564 announcements and return only those that belong to you. Call this on the recipient’s side periodically or after fetching new events from the subgraph or chain.
import type { Announcement } from "@wraith-protocol/sdk/chains/evm";

// Fetch announcements from your subgraph or on-chain events
const announcements: Announcement[] = [/* ... */];

const matched = scanAnnouncements(
  announcements,
  keys.viewingKey,
  keys.spendingPubKey,
  keys.spendingKey,
);

for (const m of matched) {
  console.log(m.stealthAddress);    // "0x..." — address you control
  console.log(m.stealthPrivateKey); // "0x..." — private key for this address
}
announcements
Announcement[]
required
Array of on-chain announcements fetched from the ERC-5564 announcer contract events or a subgraph.
viewingKey
HexString
required
Your 32-byte secp256k1 viewing private key (from deriveStealthKeys).
spendingPubKey
HexString
required
Your 33-byte compressed secp256k1 spending public key (from deriveStealthKeys).
spendingKey
HexString
required
Your 32-byte secp256k1 spending private key (from deriveStealthKeys). Used to derive the stealth private key for matched announcements.
Returns: MatchedAnnouncement[] How it works: For each announcement, the function first checks the schemeId, then extracts the view tag from metadata[0] and performs a cheap tag comparison. Only ~1/256 non-matching announcements reach the full ECDH check. Matched announcements include the computed stealthPrivateKey.

deriveStealthPrivateKey(spendingKey, ephemeralPubKey, viewingKey)

Compute the private key that controls a specific stealth address. Call this when you want to spend from a stealth address you have already identified as yours.
import { privateKeyToAccount } from "viem/accounts";

const privateKey = deriveStealthPrivateKey(
  keys.spendingKey,
  stealth.ephemeralPubKey,
  keys.viewingKey,
);

// Use with viem to sign and submit transactions
const account = privateKeyToAccount(privateKey);
console.log(account.address); // matches the stealth address
spendingKey
HexString
required
Your 32-byte secp256k1 spending private key.
ephemeralPubKey
HexString
required
The 33-byte ephemeral public key from the announcement.
viewingKey
HexString
required
Your 32-byte secp256k1 viewing private key.
Returns: HexString — the 32-byte private key for the stealth address How it works:
  1. S = viewingKey × ephemeralPubKey (ECDH shared secret)
  2. hashScalar = keccak256(S) mod n
  3. stealthPrivateKey = (spendingKey + hashScalar) mod n
scanAnnouncements calls this internally and includes stealthPrivateKey on each MatchedAnnouncement. Call deriveStealthPrivateKey directly only if you have an announcement and want the key without scanning a batch.

Meta-address utilities

encodeStealthMetaAddress(spendingPubKey, viewingPubKey)

Encode two public keys into a stealth meta-address string. Share this string so senders can generate stealth addresses for you.
const metaAddress = encodeStealthMetaAddress(
  keys.spendingPubKey,
  keys.viewingPubKey,
);
// "st:eth:0x02abc...03def..."
Both keys must be 33-byte compressed secp256k1 points (0x02... or 0x03...).

decodeStealthMetaAddress(metaAddress)

Decode a meta-address string back into its component public keys. Use this on the sender’s side before calling generateStealthAddress.
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(
  "st:eth:0x02abc...03def...",
);
Validates the st:eth:0x prefix, total length, and that both keys are valid secp256k1 points. Throws if validation fails.

metaAddressToBytes(metaAddress)

Strip the st:eth: prefix from a meta-address, returning only the raw hex bytes. Required before passing a meta-address to the name registration functions.
const bytes = metaAddressToBytes("st:eth:0x02abc...03def...");
// "0x02abc...03def..."

End-to-end example

This example shows the full sender → recipient flow using the EVM primitives directly.
import {
  deriveStealthKeys,
  generateStealthAddress,
  scanAnnouncements,
  deriveStealthPrivateKey,
  encodeStealthMetaAddress,
  decodeStealthMetaAddress,
  STEALTH_SIGNING_MESSAGE,
} from "@wraith-protocol/sdk/chains/evm";
import type { HexString, Announcement } from "@wraith-protocol/sdk/chains/evm";
import { privateKeyToAccount } from "viem/accounts";

// ── Recipient setup ──────────────────────────────────────────────────

// 1. Derive keys from wallet signature (done once; same sig → same keys)
const sig = await wallet.signMessage(STEALTH_SIGNING_MESSAGE);
const keys = deriveStealthKeys(sig as HexString);

// 2. Publish the stealth meta-address so senders can find you
const metaAddress = encodeStealthMetaAddress(
  keys.spendingPubKey,
  keys.viewingPubKey,
);
// Share "st:eth:0x..." publicly or register a .wraith name

// ── Sender ───────────────────────────────────────────────────────────

// 3. Decode the recipient's meta-address
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(metaAddress);

// 4. Generate a fresh one-time stealth address
const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);
// stealth.stealthAddress — send ETH/tokens here
// stealth.ephemeralPubKey + stealth.viewTag — publish via ERC-5564 announcer

// ── Recipient scanning ───────────────────────────────────────────────

// 5. Fetch announcements from the subgraph (Goldsky) or chain events
const announcements: Announcement[] = [/* fetched from chain */];

const matched = scanAnnouncements(
  announcements,
  keys.viewingKey,
  keys.spendingPubKey,
  keys.spendingKey,
);

// ── Recipient spending ───────────────────────────────────────────────

// 6. Sign and submit transactions from matched stealth addresses
for (const m of matched) {
  const account = privateKeyToAccount(m.stealthPrivateKey);
  // account.address === m.stealthAddress
  // Use account with viem walletClient to sign transactions
}