Skip to main content
The Stellar chain module provides the cryptographic primitives for stealth addresses on Stellar. The scheme is conceptually equivalent to the EVM module — derive keys, generate stealth addresses, scan announcements, spend — but the underlying cryptography is entirely different: ed25519 curves, X25519 ECDH, SHA-256 domain-separated hashing, and Stellar G... address encoding. Import from @wraith-protocol/sdk/chains/stellar.
Most developers should use the agent client instead. These primitives are for power users building custom Stellar stealth address integrations without the managed platform.

Installation

The Stellar primitives depend on @stellar/stellar-sdk as an optional peer dependency. It is not installed automatically — add it explicitly alongside the main SDK.
npm install @wraith-protocol/sdk @stellar/stellar-sdk
If you import @wraith-protocol/sdk/chains/stellar without @stellar/stellar-sdk installed, you will get a module resolution error at runtime. The peer dependency is required for Stellar G... address encoding via StrKey.

Key differences from EVM

The Stellar module uses a fundamentally different cryptographic stack. If you are familiar with the EVM primitives, the table below summarizes what changes.
AspectEVMStellar
Curvesecp256k1ed25519
Key formatHexString (0x-prefixed)Uint8Array (raw bytes)
ECDH methodsecp256k1 getSharedSecretX25519 (Montgomery form conversion)
Address format0x... (20 bytes)G... (56-char Stellar address)
Public key size33 bytes (compressed)32 bytes
Meta-address prefixst:eth:0xst:xlm:
Hash functionkeccak256SHA-256 (domain-separated)
Private key outputHexStringbigint scalar
Signingsecp256k1 ECDSAed25519 with raw scalar
The most important difference is that Stellar stealth private keys are bigint scalars, not hex strings. Standard Stellar Keypair.fromRawEd25519Seed() does not accept a derived scalar directly — you must use signWithScalar() or signStellarTransaction() from this module.

Import

import {
  deriveStealthKeys,
  generateStealthAddress,
  scanAnnouncements,
  deriveStealthPrivateScalar,
  signStellarTransaction,
  signWithScalar,
  encodeStealthMetaAddress,
  decodeStealthMetaAddress,
  pubKeyToStellarAddress,
  computeSharedSecret,
  computeViewTag,
  checkStealthAddress,
  seedToScalar,
  hashToScalar,
  deriveStealthPubKey,
  bytesToHex,
  hexToBytes,
  STEALTH_SIGNING_MESSAGE,
  SCHEME_ID,
  META_ADDRESS_PREFIX,
  L,
} from "@wraith-protocol/sdk/chains/stellar";
import type {
  StealthKeys,
  GeneratedStealthAddress,
  Announcement,
  MatchedAnnouncement,
} from "@wraith-protocol/sdk/chains/stellar";

Constants

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

const SCHEME_ID = 1;                // number (not bigint like EVM)
const META_ADDRESS_PREFIX = "st:xlm:";
const L = 2n**252n + 27742317777372353535851937790883648493n; // ed25519 group order

Core types

interface StealthKeys {
  spendingKey:    Uint8Array;  // 32-byte seed
  spendingScalar: bigint;      // clamped scalar derived from SHA-512(seed)
  viewingKey:     Uint8Array;  // 32-byte seed
  viewingScalar:  bigint;      // clamped scalar
  spendingPubKey: Uint8Array;  // 32-byte ed25519 public key
  viewingPubKey:  Uint8Array;  // 32-byte ed25519 public key
}

interface GeneratedStealthAddress {
  stealthAddress:  string;      // Stellar G... address
  ephemeralPubKey: Uint8Array;  // 32-byte ed25519 public key
  viewTag:         number;      // 0-255
}

interface Announcement {
  schemeId:        number;
  stealthAddress:  string;  // G... address
  caller:          string;  // G... address
  ephemeralPubKey: string;  // hex-encoded 32 bytes
  metadata:        string;  // hex-encoded; first byte = view tag
}

interface MatchedAnnouncement extends Announcement {
  stealthPrivateScalar: bigint;      // scalar for signing from this address
  stealthPubKeyBytes:   Uint8Array;  // 32-byte ed25519 public key
}

Functions

deriveStealthKeys(signature)

Derive spending and viewing key pairs from a 64-byte ed25519 signature. The same signature always produces the same keys.
const sig = stellarKeypair.sign(Buffer.from(STEALTH_SIGNING_MESSAGE));
const keys = deriveStealthKeys(sig);

console.log(keys.spendingKey);    // Uint8Array (32-byte seed)
console.log(keys.spendingScalar); // bigint (clamped scalar)
console.log(keys.viewingKey);     // Uint8Array (32-byte seed)
console.log(keys.viewingScalar);  // bigint (clamped scalar)
console.log(keys.spendingPubKey); // Uint8Array (32-byte public key)
console.log(keys.viewingPubKey);  // Uint8Array (32-byte public key)
signature
Uint8Array
required
A 64-byte ed25519 signature from stellarKeypair.sign(Buffer.from(STEALTH_SIGNING_MESSAGE)).
Returns: StealthKeys How it works:
  1. spendingKey = SHA-256("wraith:spending:" || signature) — 32-byte seed
  2. viewingKey = SHA-256("wraith:viewing:" || signature) — 32-byte seed
  3. Each seed is expanded to a clamped scalar via seedToScalar() (SHA-512 + bit clamping)
  4. Public keys are derived via ed25519.getPublicKey(seed)
Domain-separated hashing replaces the EVM approach of splitting r/s components, because ed25519 signature components do not have the same independence guarantees.

generateStealthAddress(spendingPubKey, viewingPubKey, ephemeralSeed?)

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

console.log(result.stealthAddress);  // "G..." — send XLM here
console.log(result.ephemeralPubKey); // Uint8Array — publish in announcement
console.log(result.viewTag);         // 0-255 — publish in announcement
spendingPubKey
Uint8Array
required
The recipient’s 32-byte ed25519 spending public key.
viewingPubKey
Uint8Array
required
The recipient’s 32-byte ed25519 viewing public key.
ephemeralSeed
Uint8Array
Override the randomly generated ephemeral seed. Use only for deterministic testing.
Returns: GeneratedStealthAddress How it works:
  1. Generate a random ephemeral ed25519 seed
  2. Compute ECDH shared secret via X25519 (keys converted to Montgomery form)
  3. viewTag = computeViewTag(sharedSecret) = SHA-256("wraith:tag:" || sharedSecret)[0]
  4. hScalar = hashToScalar(sharedSecret) = SHA-256 output reduced mod L
  5. stealthPoint = spendingPubKey + hScalar × G (ed25519 point addition)
  6. Encode as Stellar G... address via StrKey.encodeEd25519PublicKey

scanAnnouncements(announcements, viewingKey, spendingPubKey, spendingScalar)

Scan Soroban contract event announcements and return only those that belong to you. Call this on the recipient’s side.
// Fetch from Soroban events via sorobanServer.getEvents()
const announcements: Announcement[] = [/* ... */];

const matched = scanAnnouncements(
  announcements,
  keys.viewingKey,
  keys.spendingPubKey,
  keys.spendingScalar,  // bigint, not spendingKey!
);

for (const m of matched) {
  console.log(m.stealthAddress);        // "G..."
  console.log(m.stealthPrivateScalar);  // bigint — use for signing
  console.log(m.stealthPubKeyBytes);    // Uint8Array — use for signing
}
announcements
Announcement[]
required
Array of announcements from Soroban contract events.
viewingKey
Uint8Array
required
Your 32-byte ed25519 viewing private key seed (from deriveStealthKeys).
spendingPubKey
Uint8Array
required
Your 32-byte ed25519 spending public key (from deriveStealthKeys).
spendingScalar
bigint
required
Your clamped spending scalar (from keys.spendingScalar). Note: this is spendingScalar, not spendingKey. This differs from the EVM module, which takes the raw private key bytes.
Returns: MatchedAnnouncement[]
The fourth argument is spendingScalar (a bigint), not the raw spendingKey bytes. Passing keys.spendingKey will cause incorrect results without a type error — make sure you pass keys.spendingScalar.

deriveStealthPrivateScalar(spendingScalar, viewingKey, ephemeralPubKey)

Derive the private scalar for a specific stealth address. Use this when you need the scalar directly without running a full scan.
const scalar = deriveStealthPrivateScalar(
  keys.spendingScalar,
  keys.viewingKey,
  ephemeralPubKey,  // Uint8Array — from the announcement
);
// scalar is (spendingScalar + hashScalar) mod L
spendingScalar
bigint
required
Your clamped spending scalar (keys.spendingScalar).
viewingKey
Uint8Array
required
Your 32-byte viewing private key seed (keys.viewingKey).
ephemeralPubKey
Uint8Array
required
The 32-byte ephemeral public key from the announcement.
Returns: bigint(spendingScalar + hashScalar) mod L

Signing from a stealth address

Stealth private keys on Stellar are derived scalars. They are not valid ed25519 seeds, so you cannot use Keypair.fromRawEd25519Seed() with them. Use the signing functions from this module instead.

signStellarTransaction(txHash, stealthScalar, stealthPubKey)

Sign a Stellar transaction hash with a stealth private scalar.
const txHash = transaction.hash(); // 32-byte Buffer from @stellar/stellar-sdk
const sig = signStellarTransaction(txHash, m.stealthPrivateScalar, m.stealthPubKeyBytes);

// Add the signature to the transaction envelope
transaction.addSignature(
  pubKeyToStellarAddress(m.stealthPubKeyBytes),
  sig.toString("base64"),
);
// Then submit to Horizon
txHash
Uint8Array | Buffer
required
The 32-byte transaction hash from the Stellar SDK.
stealthScalar
bigint
required
The stealth private scalar from deriveStealthPrivateScalar or MatchedAnnouncement.stealthPrivateScalar.
stealthPubKey
Uint8Array
required
The 32-byte stealth public key from MatchedAnnouncement.stealthPubKeyBytes.
Returns: Uint8Array — 64-byte ed25519 signature

signWithScalar(message, scalar, publicKey)

Sign arbitrary message bytes with a raw scalar. Use when you need to sign something other than a transaction hash.
const signature = signWithScalar(messageBytes, stealthScalar, stealthPubKeyBytes);
// Uint8Array (64-byte ed25519 signature)

Meta-address utilities

encodeStealthMetaAddress(spendingPubKey, viewingPubKey)

Encode two 32-byte ed25519 public keys into a Stellar stealth meta-address string.
const metaAddress = encodeStealthMetaAddress(
  keys.spendingPubKey,
  keys.viewingPubKey,
);
// "st:xlm:abc123...def456..."

decodeStealthMetaAddress(metaAddress)

Decode a Stellar meta-address back into its component public keys. Use this on the sender’s side before calling generateStealthAddress.
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(
  "st:xlm:abc123...def456...",
);
// Both are Uint8Array (32 bytes)

pubKeyToStellarAddress(publicKey)

Convert a 32-byte ed25519 public key to a Stellar G... address.
const address = pubKeyToStellarAddress(m.stealthPubKeyBytes);
// "GABC..."

Helper utilities

bytesToHex(bytes) / hexToBytes(hex)

Convert between Uint8Array and hex strings. Useful when interacting with systems that expect hex-encoded keys.
const hex = bytesToHex(new Uint8Array([0xab, 0xcd]));
// "abcd"

const bytes = hexToBytes("abcd");
// Uint8Array [0xab, 0xcd]

seedToScalar(seed)

Convert a 32-byte ed25519 seed to its clamped scalar. This mirrors standard ed25519 private key expansion.
const scalar = seedToScalar(keys.spendingKey);
// bigint — clamped scalar used for ed25519 point multiplication
How it works: SHA-512(seed) → take the lower 32 bytes → apply bit clamping (a[0] &= 248; a[31] &= 127; a[31] |= 64) → interpret as little-endian bigint.

hashToScalar(sharedSecret)

Hash a shared secret to a scalar for stealth address derivation.
const scalar = hashToScalar(sharedSecret);
// bigint — SHA-256("wraith:scalar:" || sharedSecret) reduced mod L

computeSharedSecret(privateKey, publicKey)

Compute an X25519 ECDH shared secret from ed25519 keys. Ed25519 keys are converted to Montgomery form before the Diffie-Hellman exchange.
const shared = computeSharedSecret(keys.viewingKey, ephemeralPubKey);
// Uint8Array (32 bytes)

End-to-end example

import {
  deriveStealthKeys,
  generateStealthAddress,
  scanAnnouncements,
  signStellarTransaction,
  encodeStealthMetaAddress,
  decodeStealthMetaAddress,
  pubKeyToStellarAddress,
  STEALTH_SIGNING_MESSAGE,
} from "@wraith-protocol/sdk/chains/stellar";
import type { Announcement } from "@wraith-protocol/sdk/chains/stellar";

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

// 1. Derive keys from Stellar wallet signature
const sig = stellarKeypair.sign(Buffer.from(STEALTH_SIGNING_MESSAGE));
const keys = deriveStealthKeys(sig);

// 2. Publish the stealth meta-address so senders can find you
const metaAddress = encodeStealthMetaAddress(
  keys.spendingPubKey,
  keys.viewingPubKey,
);
// Share "st:xlm:..." 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 XLM via Operation.createAccount (new accounts require 1 XLM minimum)
// stealth.ephemeralPubKey + stealth.viewTag — publish via Soroban announcer contract

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

// 5. Fetch announcements from Soroban events
const announcements: Announcement[] = [
  /* await sorobanServer.getEvents({ ... }) */
];

const matched = scanAnnouncements(
  announcements,
  keys.viewingKey,
  keys.spendingPubKey,
  keys.spendingScalar,  // pass spendingScalar, not spendingKey
);

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

// 6. Sign and submit transactions from matched stealth addresses
for (const m of matched) {
  const transaction = buildYourTransaction(/* ... */);
  const txHash = transaction.hash();

  const sig = signStellarTransaction(
    txHash,
    m.stealthPrivateScalar,
    m.stealthPubKeyBytes,
  );

  transaction.addSignature(
    pubKeyToStellarAddress(m.stealthPubKeyBytes),
    Buffer.from(sig).toString("base64"),
  );

  await server.submitTransaction(transaction);
}

Stellar-specific considerations

Account creation: Stellar requires accounts to exist with a minimum balance of 1 XLM. When sending to a new stealth address for the first time, use Operation.createAccount rather than Operation.payment. Announcement source: Announcements come from Soroban contract events via sorobanServer.getEvents(), not a subgraph. The event structure matches the Announcement interface above. Signing stealth addresses: You must use signStellarTransaction() or signWithScalar() from this module. Stealth scalars are derived values that are not valid seeds for Keypair.fromRawEd25519Seed() — the standard Stellar SDK signing path will not work.