Documentation Index
Fetch the complete documentation index at: https://docs.usewraith.xyz/llms.txt
Use this file to discover all available pages before exploring further.
Low-level stealth address functions for EVM-compatible chains (Horizen, Ethereum, Polygon, Base, etc.) using secp256k1. Import from @wraith-protocol/sdk/chains/evm.
Most developers should use the Agent Client instead. These primitives are for power users building custom stealth address integrations.
Import
import {
// Stealth kit (crypto)
deriveStealthKeys,
generateStealthAddress,
checkStealthAddress,
scanAnnouncements,
deriveStealthPrivateKey,
encodeStealthMetaAddress,
decodeStealthMetaAddress,
signNameRegistration,
signNameRegistrationOnBehalf,
signNameUpdate,
signNameRelease,
metaAddressToBytes,
// Builders (transactions)
buildSendStealth,
buildSendERC20,
buildRegisterName,
buildUpdateName,
buildReleaseName,
buildRegisterMetaAddress,
buildAnnounce,
buildResolveName,
// Chain data
fetchAnnouncements,
getDeployment,
DEPLOYMENTS,
// ABIs (if you need raw encoding)
SENDER_ABI,
NAMES_ABI,
REGISTRY_ABI,
ANNOUNCER_ABI,
// Constants
STEALTH_SIGNING_MESSAGE,
SCHEME_ID,
META_ADDRESS_PREFIX,
} from "@wraith-protocol/sdk/chains/evm";
Types
type HexString = `0x${string}`;
interface StealthKeys {
spendingKey: HexString; // 32-byte private key
viewingKey: HexString; // 32-byte 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
viewTag: number; // 0-255
}
interface Announcement {
schemeId: bigint;
stealthAddress: HexString;
caller: HexString;
ephemeralPubKey: HexString;
metadata: HexString; // first byte is view tag
}
interface MatchedAnnouncement extends Announcement {
stealthPrivateKey: HexString;
}
Constants
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 for on-chain compatibility
const META_ADDRESS_PREFIX = "st:eth:0x";
Functions
deriveStealthKeys(signature)
Derive spending and viewing key pairs from a wallet signature.
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)
Algorithm:
- Split the 65-byte signature:
r = sig[0:32], s = sig[32:64]
spendingKey = keccak256(r)
viewingKey = keccak256(s)
- Derive compressed public keys from each private key
Same signature always produces the same keys. The spending key is never equal to the viewing key.
generateStealthAddress(spendingPubKey, viewingPubKey, ephemeralKey?)
Generate a one-time stealth address for a recipient.
const result = generateStealthAddress(
keys.spendingPubKey,
keys.viewingPubKey
);
console.log(result.stealthAddress); // "0x..." (fresh one-time address)
console.log(result.ephemeralPubKey); // "0x..." (publish with announcement)
console.log(result.viewTag); // 0-255 (publish with announcement)
Algorithm:
- Generate random ephemeral key pair
(r, R = r * G)
- Compute ECDH shared secret
S = r * viewingPubKey
hashedSecret = keccak256(S)
viewTag = hashedSecret[0]
stealthPoint = spendingPubKey + hash(S) * G
stealthAddress = keccak256(stealthPoint)[12:32]
Each call produces a different address (new ephemeral key). Pass an explicit ephemeralKey for deterministic testing.
checkStealthAddress(ephemeralPubKey, viewingKey, spendingPubKey, viewTag)
Check if a stealth address announcement belongs to you.
const result = checkStealthAddress(
announcement.ephemeralPubKey,
keys.viewingKey,
keys.spendingPubKey,
viewTag
);
if (result.isMatch) {
console.log(result.stealthAddress); // the address that matched
}
Uses view tag for fast rejection — eliminates ~255/256 non-matching announcements without computing the full stealth address.
scanAnnouncements(announcements, viewingKey, spendingPubKey, spendingKey)
Scan an array of on-chain announcements and return the ones that belong to you.
const announcements: Announcement[] = [
// from subgraph query or chain events
];
const matched = scanAnnouncements(
announcements,
keys.viewingKey,
keys.spendingPubKey,
keys.spendingKey
);
for (const m of matched) {
console.log(m.stealthAddress); // address you control
console.log(m.stealthPrivateKey); // private key for this address
}
For each announcement:
- Skip if
schemeId doesn’t match
- Extract view tag from metadata
- Check if it matches using
checkStealthAddress
- If matched, derive the stealth private key
deriveStealthPrivateKey(spendingKey, ephemeralPubKey, viewingKey)
Compute the private key that controls a specific stealth address.
const privateKey = deriveStealthPrivateKey(
keys.spendingKey,
stealth.ephemeralPubKey,
keys.viewingKey
);
// Use this key to sign transactions from the stealth address
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(privateKey);
console.log(account.address); // matches the stealth address
Algorithm:
S = viewingKey * ephemeralPubKey (shared secret)
hashScalar = keccak256(S) mod n
stealthPrivateKey = (spendingKey + hashScalar) mod n
Encode two public keys into a stealth meta-address string.
const metaAddress = encodeStealthMetaAddress(
keys.spendingPubKey,
keys.viewingPubKey
);
// "st:eth:0x02abc...03def..."
Both keys must be 33-byte compressed secp256k1 points.
Decode a meta-address back into its component public keys.
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(
"st:eth:0x02abc...03def..."
);
Validates the prefix, length, and that both keys are valid curve points.
Strip the st:eth: prefix from a meta-address, returning the raw hex bytes.
const bytes = metaAddressToBytes("st:eth:0x02abc...03def...");
// "0x02abc...03def..."
Sign a message for on-chain .wraith name registration.
const metaBytes = metaAddressToBytes(metaAddress);
const sig = signNameRegistration("alice", metaBytes, keys.spendingKey);
// 65-byte hex signature for the WraithNames contract
Sign with a nonce for delegated registration.
const sig = signNameRegistrationOnBehalf("alice", metaBytes, keys.spendingKey, 0n);
Sign a name update to point to a new meta-address.
const sig = signNameUpdate("alice", newMetaBytes, keys.spendingKey);
signNameRelease(name, spendingKey)
Sign a name release to give up ownership.
const sig = signNameRelease("alice", keys.spendingKey);
End-to-End Flow
import {
deriveStealthKeys,
generateStealthAddress,
scanAnnouncements,
deriveStealthPrivateKey,
encodeStealthMetaAddress,
decodeStealthMetaAddress,
STEALTH_SIGNING_MESSAGE,
SCHEME_ID,
} from "@wraith-protocol/sdk/chains/evm";
import type { HexString, Announcement } from "@wraith-protocol/sdk/chains/evm";
// 1. Recipient: derive keys from wallet signature
const sig = await wallet.signMessage(STEALTH_SIGNING_MESSAGE);
const keys = deriveStealthKeys(sig as HexString);
// 2. Recipient: publish stealth meta-address
const metaAddress = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey);
// Share "st:eth:0x..." or register as a .wraith name
// 3. Sender: generate stealth address from meta-address
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(metaAddress);
const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);
// 4. Sender: send ETH to stealth.stealthAddress
// Sender: call announcer contract with (stealth.ephemeralPubKey, stealth.viewTag)
// 5. Recipient: fetch and scan announcements
const announcements = await fetchAnnouncements("horizen");
const matched = scanAnnouncements(
announcements,
keys.viewingKey,
keys.spendingPubKey,
keys.spendingKey
);
// 6. Recipient: spend from stealth address
for (const m of matched) {
const account = privateKeyToAccount(m.stealthPrivateKey);
// Sign and submit transactions from account.address
}
Transaction Builders
Builders return { to, data, value? } objects — submit with any library (viem, ethers, wagmi, raw RPC).
buildSendStealth(params)
Send ETH privately via stealth address. Uses the WraithSender contract for atomic send + announce.
import { buildSendStealth } from "@wraith-protocol/sdk/chains/evm";
const { transaction, stealthAddress, ephemeralPubKey, viewTag } = buildSendStealth({
recipientMetaAddress: "st:eth:0x...",
amount: "0.1",
chain: "horizen",
});
// Submit with viem
await walletClient.sendTransaction(transaction);
// Or ethers
await signer.sendTransaction(transaction);
// Or wagmi
await sendTransaction(transaction);
Returns: { transaction: { to, data, value }, stealthAddress, ephemeralPubKey, viewTag }
buildSendERC20(params)
Send an ERC-20 token privately. The sender must have approved the WraithSender contract first.
import { buildSendERC20 } from "@wraith-protocol/sdk/chains/evm";
const { transaction } = buildSendERC20({
recipientMetaAddress: "st:eth:0x...",
token: "0x4b36cb6E...", // token contract address
amount: 1000000n, // raw token amount (with decimals)
chain: "horizen",
gasTip: "0.001", // optional ETH tip for recipient's gas
});
await walletClient.sendTransaction(transaction);
buildRegisterName(params)
Register a .wraith name on-chain. The name is bound to the spending key.
import { buildRegisterName, deriveStealthKeys } from "@wraith-protocol/sdk/chains/evm";
const keys = deriveStealthKeys(signature);
const tx = buildRegisterName({
name: "alice",
stealthKeys: keys,
chain: "horizen",
});
await walletClient.sendTransaction(tx);
buildUpdateName(params)
Update a .wraith name’s meta-address. Must be signed by the current owner.
import { buildUpdateName } from "@wraith-protocol/sdk/chains/evm";
const tx = buildUpdateName({
name: "alice",
newStealthKeys: newKeys,
currentSpendingKey: oldKeys.spendingKey,
chain: "horizen",
});
await walletClient.sendTransaction(tx);
buildReleaseName(params)
Release a .wraith name. After release, anyone can register it.
import { buildReleaseName } from "@wraith-protocol/sdk/chains/evm";
const tx = buildReleaseName({
name: "alice",
spendingKey: keys.spendingKey,
chain: "horizen",
});
await walletClient.sendTransaction(tx);
Register a stealth meta-address in the ERC-6538 registry. Makes it discoverable by wallet address.
import { buildRegisterMetaAddress } from "@wraith-protocol/sdk/chains/evm";
const tx = buildRegisterMetaAddress({
metaAddress: "st:eth:0x...",
chain: "horizen",
});
await walletClient.sendTransaction(tx);
buildAnnounce(params)
Publish a stealth address announcement on-chain. Use this if you’re sending assets directly (not via WraithSender) and need to announce separately.
import { buildAnnounce } from "@wraith-protocol/sdk/chains/evm";
const tx = buildAnnounce({
stealthAddress: "0x...",
ephemeralPubKey: "0x...",
viewTag: 42,
chain: "horizen",
});
await walletClient.sendTransaction(tx);
buildResolveName(params)
Build calldata to resolve a .wraith name. Returns data for a static call (read-only, no transaction).
import { buildResolveName } from "@wraith-protocol/sdk/chains/evm";
const call = buildResolveName({ name: "alice", chain: "horizen" });
// Use with a static call
const result = await publicClient.call({ to: call.to, data: call.data });
Contract ABIs
The raw ABIs are exported if you need to encode calls yourself:
import { SENDER_ABI, NAMES_ABI, REGISTRY_ABI, ANNOUNCER_ABI } from "@wraith-protocol/sdk/chains/evm";
Chain Deployments
The SDK ships with deployed contract addresses and subgraph URLs for supported chains. No need to deploy contracts yourself.
getDeployment(chain)
Returns the full deployment config for a chain:
const deployment = getDeployment("horizen");
// {
// chainId: 2651420,
// name: "Horizen Testnet",
// rpcUrl: "https://horizen-testnet.rpc.caldera.xyz/http",
// explorerUrl: "https://horizen-testnet.explorer.caldera.xyz",
// subgraphUrl: "https://api.goldsky.com/api/public/...",
// contracts: {
// announcer: "0x8AE65c05E7eb48B9bA652781Bc0a3DBA09A484F3",
// registry: "0x953E6cEdcdfAe321796e7637d33653F6Ce05c527",
// sender: "0x226C5eb4e139D9fa01cc09eA318638b090b12095",
// names: "0x3d46f709a99A3910f52bD292211Eb5D557F882D6",
// },
// }
DEPLOYMENTS
Access all deployments directly:
import { DEPLOYMENTS } from "@wraith-protocol/sdk/chains/evm";
console.log(Object.keys(DEPLOYMENTS)); // ["horizen"]
Supported EVM Chains
| Chain | Chain ID | Status |
|---|
| Horizen Testnet | 2651420 | Live |
Fetching Announcements
fetchAnnouncements(chain, subgraphUrl?)
Fetches all stealth address announcements from the Goldsky subgraph for the specified chain. Handles pagination automatically.
const announcements = await fetchAnnouncements("horizen");
// Returns Announcement[] — ready to pass to scanAnnouncements()
Override the subgraph URL if needed:
const announcements = await fetchAnnouncements("horizen", "https://my-custom-subgraph.com/graphql");