Skip to main content
Soroban smart contracts (Rust) for stealth address operations on the Stellar network.

Contract Set

ContractPurpose
stealth-announcerEmits stealth address announcements
stealth-registryMaps addresses to stealth meta-addresses
stealth-senderAtomic send + announce
wraith-names.wraith name to meta-address mapping

stealth-announcer

Emits announcement events. No persistent storage.

Interface

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

Event

Emits a contract event with topic "announce":
// Event: ("announce", caller, scheme_id, stealth_address, ephemeral_pub_key, metadata)

Usage

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

// After generating a stealth address
const tx = new TransactionBuilder(account, { fee: "100" })
  .addOperation(contract.call(
    "announce",
    xdr.ScVal.scvAddress(callerAddress),
    xdr.ScVal.scvU32(SCHEME_ID),
    xdr.ScVal.scvAddress(stealthAddress),
    xdr.ScVal.scvBytes(ephemeralPubKey),
    xdr.ScVal.scvBytes(metadata),
  ))
  .build();

stealth-registry

Maps addresses to 64-byte stealth meta-addresses.

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

  • Enforces 64-byte meta-address length (32 bytes spend + 32 bytes view)
  • Requires auth from registrant

Usage

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

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

stealth-sender

Atomic send + announce. Initializes 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 an event

Usage

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

const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);

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

wraith-names

Name to meta-address mapping. Names are hashed via SHA-256 for storage keys.

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: must be exactly 64 bytes
  • Caller auth required for register, update, release

Usage

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

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

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

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

Deployment

Build

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

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:
soroban contract invoke \
  --id <sender-contract-id> \
  --network testnet \
  --source <admin-key> \
  -- init \
  --admin <admin-address> \
  --announcer <announcer-contract-id>

Event Fetching

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

Differences from EVM Contracts

AspectEVM (Solidity)Stellar (Soroban)
LanguageSolidityRust
Name sig verificationOn-chain ECDSA recoveryCaller auth (Soroban built-in)
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