Skip to main content
Solana programs (smart contracts) for stealth address operations, written in Rust with the Anchor framework.

Program Set

ProgramPurpose
wraith-announcerEmits stealth address announcement events
wraith-senderAtomic SOL/SPL transfer + announcement
wraith-names.wraith name to meta-address mapping

wraith-announcer

Emits announcement events via Anchor’s emit!() macro. Stateless — no on-chain storage.

Instruction

pub fn announce(
    ctx: Context<Announce>,
    scheme_id: u32,
    stealth_address: Pubkey,
    ephemeral_pub_key: [u8; 32],
    metadata: Vec<u8>,
) -> Result<()>

Accounts

#[derive(Accounts)]
pub struct Announce<'info> {
    #[account(mut)]
    pub caller: Signer<'info>,
}

Event

#[event]
pub struct AnnouncementEvent {
    pub scheme_id: u32,
    pub stealth_address: Pubkey,
    pub caller: Pubkey,
    pub ephemeral_pub_key: [u8; 32],
    pub metadata: Vec<u8>,
}

Usage

import { SCHEME_ID } from "@wraith-protocol/sdk/chains/solana";
import { Program } from "@coral-xyz/anchor";

const program = new Program(idl, programId, provider);

await program.methods
  .announce(
    SCHEME_ID,
    stealthAddressPubkey,
    Array.from(ephemeralPubKey),
    Array.from(metadata),
  )
  .accounts({ caller: wallet.publicKey })
  .rpc();

wraith-sender

Atomic SOL or SPL token transfer + announcement in one instruction. Sends funds to the stealth address and emits an announcement event.

Instructions

send_sol

Transfer SOL to a stealth address and emit an announcement.
pub fn send_sol(
    ctx: Context<SendSol>,
    amount: u64,
    scheme_id: u32,
    stealth_address: Pubkey,
    ephemeral_pub_key: [u8; 32],
    metadata: Vec<u8>,
) -> Result<()>
Accounts:
#[derive(Accounts)]
pub struct SendSol<'info> {
    #[account(mut)]
    pub sender: Signer<'info>,
    /// CHECK: stealth address, receives SOL
    #[account(mut)]
    pub stealth_account: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

send_spl

Transfer SPL tokens to a stealth address’s associated token account and emit an announcement.
pub fn send_spl(
    ctx: Context<SendSpl>,
    amount: u64,
    scheme_id: u32,
    stealth_address: Pubkey,
    ephemeral_pub_key: [u8; 32],
    metadata: Vec<u8>,
) -> Result<()>
Accounts:
#[derive(Accounts)]
pub struct SendSpl<'info> {
    #[account(mut)]
    pub sender: Signer<'info>,
    #[account(mut)]
    pub sender_token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub stealth_token_account: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
}

Usage

import { generateStealthAddress, SCHEME_ID } from "@wraith-protocol/sdk/chains/solana";
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";

const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);
const metadata = [stealth.viewTag]; // first byte is view tag

// Send SOL + announce atomically
await program.methods
  .sendSol(
    new BN(0.1 * LAMPORTS_PER_SOL),
    SCHEME_ID,
    new PublicKey(stealth.stealthAddress),
    Array.from(stealth.ephemeralPubKey),
    metadata,
  )
  .accounts({
    sender: wallet.publicKey,
    stealthAccount: new PublicKey(stealth.stealthAddress),
    systemProgram: SystemProgram.programId,
  })
  .rpc();

wraith-names

PDA-based name to meta-address mapping. Names are stored in Program Derived Addresses (PDAs) seeded by ["name", nameBytes].

Instructions

register

Register a new .wraith name.
pub fn register(
    ctx: Context<Register>,
    name: String,
    meta_address: [u8; 64],
) -> Result<()>

update

Update the meta-address for a name you own.
pub fn update(
    ctx: Context<Update>,
    new_meta_address: [u8; 64],
) -> Result<()>

release

Release a name. Closes the PDA account and returns rent to the owner.
pub fn release(ctx: Context<Release>) -> Result<()>

resolve

Look up a name’s meta-address. Read-only.
pub fn resolve(ctx: Context<Resolve>) -> Result<[u8; 64]>

Account Structure

#[account]
pub struct NameRecord {
    pub name: String,               // max 32 bytes
    pub meta_address: [u8; 64],     // spending pub + viewing pub
    pub owner: Pubkey,
    pub created_at: i64,
}

PDA Derivation

Name records are stored at PDAs derived from the name:
seeds = [b"name", name.as_bytes()]
This means names are globally unique and can be resolved without knowing the owner.

Validation

  • Name: 3-32 characters, lowercase alphanumeric and hyphens only
  • Meta-address: must be exactly 64 bytes (two 32-byte ed25519 public keys)
  • Only the owner can update or release a name

Error Codes

#[error_code]
pub enum WraithError {
    #[msg("Name must be 3-32 characters")]
    InvalidNameLength,
    #[msg("Name must be lowercase alphanumeric or hyphens")]
    InvalidNameCharacter,
    #[msg("Only the owner can modify this name")]
    NotOwner,
}

Usage

import { encodeStealthMetaAddress } from "@wraith-protocol/sdk/chains/solana";
import { PublicKey } from "@solana/web3.js";

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

// Derive PDA for the name
const [nameRecordPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("name"), Buffer.from("alice")],
  programId,
);

// Register
await program.methods
  .register("alice", Array.from(metaAddressBytes))
  .accounts({
    nameRecord: nameRecordPda,
    owner: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .rpc();

// Resolve
const record = await program.account.nameRecord.fetch(nameRecordPda);
console.log(record.metaAddress); // [u8; 64]

Project Structure

contracts/
  solana/
    Anchor.toml
    Cargo.toml
    programs/
      wraith-announcer/
        Cargo.toml
        src/lib.rs
      wraith-sender/
        Cargo.toml
        src/lib.rs
      wraith-names/
        Cargo.toml
        src/lib.rs
    tests/
      wraith-announcer.ts
      wraith-sender.ts
      wraith-names.ts

Deployment

Build

anchor build

Deploy to Devnet

anchor deploy --provider.cluster devnet

Test

anchor test
Tests cover:
  • wraith-announcer — event emission, multiple callers, metadata preservation
  • wraith-sendersend_sol transfers + emits, send_spl token transfer + emits, insufficient funds rejection
  • wraith-names — register/resolve, name validation (too short, invalid chars), update by owner, update by non-owner (rejected), release and re-register

Differences from EVM and Stellar Contracts

AspectEVM (Solidity)Stellar (Soroban)Solana (Anchor)
LanguageSolidityRustRust (Anchor)
Contract modelBytecode deploymentWASM deploymentBPF program deployment
Name storageContract storageContract storagePDAs (Program Derived Addresses)
Name sig verificationOn-chain ECDSA recoveryCaller authSigner constraint
Event indexingSubgraph / The GraphSoroban RPC getEventsProgram transaction logs
Account modelAddress always existsMust createAccount firstAddress always valid, no deployment
Token transfersmsg.value / safeTransferFromSoroban token contractSystemProgram / SPL Token
Gas sponsorshipEIP-7702 (WraithWithdrawer)Not applicableFee payer pattern
Min balanceNone1 XLM~0.00089 SOL (rent exemption)