Skip to main content
The Wraith Protocol Stellar contracts are Rust Soroban programs that mirror the EVM contract set, adapted for Stellar’s account model and ed25519 cryptography. Instead of Solidity’s msg.sender and msg.value, Soroban contracts use caller auth and Soroban token contract calls. Instead of subgraph indexing, announcement events are fetched via Soroban RPC.

Contract set

ContractPurpose
stealth-announcerEmits stealth address announcement events on Stellar
stealth-registryMaps Stellar addresses to 64-byte stealth meta-addresses
stealth-senderAtomic token transfer + announcement in one transaction
wraith-names.wraith name to meta-address mapping on Stellar

stealth-announcer

The announcer emits a contract event with topic "announce" for every stealth payment. Recipients scan these events via Soroban RPC to discover payments. The contract stores no state.

Interface

pub fn announce(
    env: Env,
    caller: Address,
    scheme_id: u32,
    stealth_address: Address,
    ephemeral_pub_key: BytesN<32>,
    metadata: Bytes,
);
The emitted event contains all announcement data:
// Event: ("announce", caller, scheme_id, stealth_address, ephemeral_pub_key, metadata)

Usage

import { SCHEME_ID, bytesToHex } from "@wraith-protocol/sdk/chains/stellar";
import { TransactionBuilder, xdr } from "@stellar/stellar-sdk";

// After generating a stealth address, announce it
const tx = new TransactionBuilder(account, { fee: "100" })
  .addOperation(announcerContract.call(
    "announce",
    xdr.ScVal.scvAddress(callerAddress),
    xdr.ScVal.scvU32(SCHEME_ID),
    xdr.ScVal.scvAddress(stealthAddress),
    xdr.ScVal.scvBytes(ephemeralPubKey),
    xdr.ScVal.scvBytes(metadata),
  ))
  .build();
Use stealth-sender instead of calling the announcer directly. It atomically handles both the token transfer and the announcement.

stealth-registry

The registry maps Stellar addresses to 64-byte stealth meta-addresses (32 bytes spend key + 32 bytes view key). It requires auth from the registrant and enforces the 64-byte length constraint.

Interface

pub fn register_keys(
    env: Env,
    registrant: Address,
    scheme_id: u32,
    stealth_meta_address: Bytes,
);

pub fn stealth_meta_address_of(
    env: Env,
    registrant: Address,
    scheme_id: u32,
) -> Bytes;

Validation

  • Meta-address must be exactly 64 bytes (32 bytes spend key + 32 bytes view key)
  • Requires auth from registrant

Usage

import { encodeStealthMetaAddress } from "@wraith-protocol/sdk/chains/stellar";
import { TransactionBuilder, xdr } from "@stellar/stellar-sdk";

// Encode the 64-byte meta-address (raw key material, no "st:xlm:" prefix)
const metaAddress = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey);

const tx = new TransactionBuilder(account, { fee: "100" })
  .addOperation(registryContract.call(
    "register_keys",
    xdr.ScVal.scvAddress(registrantAddress),
    xdr.ScVal.scvU32(SCHEME_ID),
    xdr.ScVal.scvBytes(metaAddress),
  ))
  .build();

stealth-sender

stealth-sender atomically transfers a Soroban token and emits an announcement event. Initialize it once after deployment with the announcer contract address.

Interface

pub fn init(env: Env, admin: Address, announcer: Address);

pub fn send(
    env: Env,
    caller: Address,
    token: Address,
    stealth_address: Address,
    amount: i128,
    scheme_id: u32,
    ephemeral_pub_key: BytesN<32>,
    metadata: Bytes,
);

pub fn batch_send(
    env: Env,
    caller: Address,
    token: Address,
    stealth_addresses: Vec<Address>,
    amounts: Vec<i128>,
    scheme_id: u32,
    ephemeral_pub_keys: Vec<BytesN<32>>,
    metadatas: Vec<Bytes>,
);
The send function:
  1. Transfers amount of token from caller to stealth_address
  2. Calls the announcer contract to emit the announcement event

Usage

import { generateStealthAddress, SCHEME_ID } from "@wraith-protocol/sdk/chains/stellar";
import { TransactionBuilder, xdr } from "@stellar/stellar-sdk";

const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);

// Transfer XLM and announce atomically
const tx = new TransactionBuilder(account, { fee: "100" })
  .addOperation(senderContract.call(
    "send",
    callerAddress,
    xlmTokenAddress,
    stealth.stealthAddress,
    amount,
    SCHEME_ID,
    stealth.ephemeralPubKey,
    viewTagMetadata,
  ))
  .build();

wraith-names

wraith-names maps human-readable names to stealth meta-addresses on Stellar. Names are hashed via SHA-256 for storage keys. Unlike the EVM contract, ownership verification uses Soroban’s built-in caller auth instead of on-chain ECDSA recovery.

Interface

pub fn register(env: Env, caller: Address, name: String, meta_address: Bytes);
pub fn update(env: Env, caller: Address, name: String, new_meta_address: Bytes);
pub fn release(env: Env, caller: Address, name: String);
pub fn resolve(env: Env, name: String) -> Bytes;
pub fn name_of(env: Env, meta_address: Bytes) -> String;

Validation

  • Name: 3–32 characters, lowercase alphanumeric only
  • Meta-address: exactly 64 bytes
  • register, update, and release require caller auth
Stellar name validation is stricter than EVM — hyphens are not allowed in Stellar names.

Usage

import { encodeStealthMetaAddress } from "@wraith-protocol/sdk/chains/stellar";
import { TransactionBuilder } from "@stellar/stellar-sdk";

const metaAddress = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey);

// Register
const registerTx = new TransactionBuilder(account, { fee: "100" })
  .addOperation(namesContract.call("register", callerAddress, "alice", metaBytes))
  .build();

// Resolve
const resolved = await namesContract.call("resolve", "alice");

Deployment

Build

Build each contract from its directory:
cd contracts/stealth-announcer && soroban contract build
cd contracts/stealth-registry && soroban contract build
cd contracts/stealth-sender && soroban contract build
cd contracts/wraith-names && soroban contract build

Deploy

Deploy each WASM binary to the Stellar testnet:
soroban contract deploy \
  --wasm target/wasm32-unknown-unknown/release/stealth_announcer.wasm \
  --network testnet \
  --source <deployer-key>

Initialize stealth-sender

After deploying both the announcer and sender, link them:
soroban contract invoke \
  --id <sender-contract-id> \
  --network testnet \
  --source <admin-key> \
  -- init \
  --admin <admin-address> \
  --announcer <announcer-contract-id>

Fetching announcement events

Stellar announcements are fetched via Soroban RPC — there is no subgraph:
const events = await sorobanServer.getEvents({
  startLedger: lastProcessedLedger,
  filters: [{
    type: "contract",
    contractIds: [announcerContractId],
    topics: [["announce"]],
  }],
});

Differences from EVM contracts

AspectEVM (Solidity)Stellar (Soroban/Rust)
LanguageSolidityRust
Curvesecp256k1ed25519
Name ownership verificationOn-chain ECDSA recoverySoroban caller auth
Event indexingSubgraph / The GraphSoroban RPC getEvents
Account modelAddress always existsMust createAccount first
Token transfersmsg.value / safeTransferFromSoroban token contract calls
Gas sponsorshipEIP-7702 (WraithWithdrawer)Not applicable