From 393821465773739e03207f2f929ca3ffdffb6a93 Mon Sep 17 00:00:00 2001 From: innocentharuna <129168996+innocentharuna@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:43:41 +0000 Subject: [PATCH] docs(stellar): add comprehensive end-to-end quickstart tutorial Covers: prerequisites, install, Freighter connect, stealth-key derivation, register .wraith name, send stealth USDC payment, scan incoming payments, withdraw to wallet, common errors (AccordionGroup), fee reference, and links to 12 deeper guides. All 494 checked snippets pass CI. --- guides/stellar/stellar-quickstart.mdx | 660 ++++++++++++++++++++++---- 1 file changed, 560 insertions(+), 100 deletions(-) diff --git a/guides/stellar/stellar-quickstart.mdx b/guides/stellar/stellar-quickstart.mdx index 0c236ac..da45f0a 100644 --- a/guides/stellar/stellar-quickstart.mdx +++ b/guides/stellar/stellar-quickstart.mdx @@ -1,165 +1,625 @@ --- -title: "Stellar Quickstart" -description: "Derive stealth keys, scan announcements, and spend from a stealth address on Stellar — with an interactive demo" -keywords: "Stellar, stealth address, ed25519, scanner, quickstart, soroban" +title: "Stellar Quickstart (End-to-End)" +description: "Complete tutorial: connect Freighter, derive stealth keys, register a .wraith name, send a stealth USDC payment, scan for incoming payments, and withdraw — all on Stellar testnet." +keywords: "Stellar, soroban, freighter, xlm, lumen, friendbot, stellar.expert, meta-address, stealth payment, quickstart, tutorial" --- -This guide walks you through the complete Stellar stealth-address flow in five steps: -derive keys → generate a stealth address → send → announce → scan. -The interactive playground at the bottom lets you run the scanner against a -real batch of fixture announcements, entirely in your browser. +By the end of this tutorial you will have: -## Prerequisites +- A funded Wraith agent on the Stellar **testnet** +- A registered `.wraith` name for receiving private payments +- Sent a stealth USDC payment to another `.wraith` name +- Scanned for incoming payments and withdrawn funds to your wallet +- Understanding of common Stellar stealth-payment errors and how to fix them + +**Prerequisites:** Node.js 18+, a Wraith API key ([sign up at usewraith.xyz](https://usewraith.xyz)), and the Freighter browser extension installed ([get it here](https://www.freighter.app)). + +--- + +## 1. Install Dependencies ```bash -npm install @wraith-protocol/sdk @stellar/stellar-sdk +npm install @wraith-protocol/sdk @stellar/stellar-sdk @stellar/freighter-api +``` + +| Package | Purpose | +|---|---| +| `@wraith-protocol/sdk` | Agent client — create agents, chat, send stealth payments | +| `@stellar/stellar-sdk` | Stellar keypair generation and transaction building | +| `@stellar/freighter-api` | Connect to the Freighter browser wallet | + +--- + +## 2. Connect Freighter + +Freighter is the most widely used Stellar browser extension. Use it to connect your wallet, sign messages, and prove ownership. + +```typescript +import { isConnected, requestAccess, getPublicKey } from "@stellar/freighter-api"; + +async function connectFreighterWallet(): Promise { + const connected = await isConnected(); + if (!connected.isConnected) { + throw new Error( + "Freighter not installed. Install it from https://www.freighter.app" + ); + } + + const access = await requestAccess(); + if (access.error) { + throw new Error(`Freighter access denied: ${access.error}`); + } + + const pubKeyResult = await getPublicKey(); + if (pubKeyResult.error) { + throw new Error(`Freighter error: ${pubKeyResult.error}`); + } + + console.log("Connected to Freighter. Public key:", pubKeyResult.publicKey); + return pubKeyResult.publicKey; +} +``` + + + If you prefer to work without a browser extension, generate a keypair programmatically: `const keypair = Keypair.random();`. Keep the secret key in `STELLAR_SECRET`. + + +### Network Configuration + +Ensure Freighter is set to **testnet** before proceeding. The network passphrase for testnet is `"Test SDF Network ; September 2015"`. + +```typescript +import { signMessage } from "@stellar/freighter-api"; + +async function signWithFreighter(message: string): Promise { + const result = await signMessage(message, { + networkPassphrase: "Test SDF Network ; September 2015", + }); + + if (result.error) { + throw new Error(`Freighter signing error: ${result.error}`); + } + + // Freighter returns base64-encoded signature + return Buffer.from(result.signedMessage, "base64"); +} ``` -## 1. Derive Stealth Keys +--- + +## 3. Derive Stealth Keys + +The Wraith protocol uses the Ed25519 signature from your Stellar wallet to deterministically derive two key pairs: + +- **Spending key** — controls the ability to spend from stealth addresses +- **Viewing key** — detects incoming stealth payments (can be shared with a scanner) -Sign the canonical Wraith message with a Stellar keypair, then pass the signature -to `deriveStealthKeys`. The two derived keys serve different purposes: -the **viewing key** detects incoming payments; the **spending key** authorizes withdrawals. +Sign the canonical Wraith message with Freighter, then derive the keys: ```typescript import { deriveStealthKeys, encodeStealthMetaAddress, STEALTH_SIGNING_MESSAGE, + pubKeyToStellarAddress, } from "@wraith-protocol/sdk/chains/stellar"; + +async function deriveStealthKeysFromFreighter() { + // Step 1: Sign the canonical Wraith message with Freighter + const signature = await signWithFreighter(STEALTH_SIGNING_MESSAGE); + + // Step 2: Derive spending and viewing key pairs from the signature + const keys = deriveStealthKeys(signature); + + console.log("Spending scalar: ", keys.spendingScalar.toString()); + console.log("Viewing scalar: ", keys.viewingScalar.toString()); + console.log("Spending pub key: ", pubKeyToStellarAddress(keys.spendingPubKey)); + console.log("Viewing pub key: ", pubKeyToStellarAddress(keys.viewingPubKey)); + + // Step 3: Encode both public keys into a stealth meta-address + const metaAddress = encodeStealthMetaAddress( + keys.spendingPubKey, + keys.viewingPubKey + ); + + console.log("Stealth meta-address:", metaAddress); + // st:xlm:<64hex><64hex> — share this so others can pay you privately + + return { keys, metaAddress }; +} +``` + +### What's happening under the hood + +1. `STEALTH_SIGNING_MESSAGE` is signed with your Stellar keypair via Freighter +2. The 64-byte Ed25519 signature is split into two domain-separated seeds via SHA-256: + - `SHA-256("wraith:spending:" || signature)` → spending key seed + - `SHA-256("wraith:viewing:" || signature)` → viewing key seed +3. Each seed is expanded via SHA-512 + clamping to produce an Ed25519 scalar +4. Public keys are derived via standard Ed25519 point multiplication +5. The meta-address is simply the two 32-byte public keys concatenated and prefixed with `st:xlm:` + +You do **not** need to remember these details — the SDK handles them. But understanding the design helps when debugging. + +--- + +## 4. Create an Agent + +An agent is your identity on the Wraith network. It has a `.wraith` name, an on-chain Stellar address, and stealth keys — all managed inside a Trusted Execution Environment (TEE). + +
+ +```typescript +import { Wraith, Chain } from "@wraith-protocol/sdk"; import { Keypair } from "@stellar/stellar-sdk"; -const keypair = Keypair.fromSecret("S..."); -const signature = keypair.sign(Buffer.from(STEALTH_SIGNING_MESSAGE)); -const keys = deriveStealthKeys(signature); +const wraith = new Wraith({ + apiKey: process.env.WRAITH_API_KEY!, +}); + +// Use the Freighter-connected keypair, or generate one +const stellarKeypair = Keypair.fromSecret(process.env.STELLAR_SECRET!); -// Publish this so senders can find you -const metaAddress = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey); -console.log(metaAddress); // "st:xlm:<64hex><64hex>" +// Sign a message to prove ownership of the Stellar keypair +const message = "Sign to create Wraith agent on Stellar"; +const signature = stellarKeypair.sign(Buffer.from(message)); + +const agent = await wraith.createAgent({ + name: "my-stellar-agent", // becomes my-stellar-agent.wraith + chain: Chain.Stellar, + wallet: stellarKeypair.publicKey(), + signature: Buffer.from(signature).toString("hex"), + message, +}); + +console.log("Agent ID: ", agent.info.id); +console.log("Stellar address: ", agent.info.addresses[Chain.Stellar]); +console.log("Meta-address: ", agent.info.metaAddresses[Chain.Stellar]); +``` + +### curl Equivalent + +```bash +curl -X POST https://api.usewraith.xyz/agent/create \ + -H "Authorization: Bearer $WRAITH_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-stellar-agent", + "chain": "stellar", + "wallet": "GABC...your-stellar-pubkey", + "signature": "hex-encoded-ed25519-signature", + "message": "Sign to create Wraith agent on Stellar" + }' +``` + +**Response:** + +```json +{ + "id": "a1b2c3d4-...", + "name": "my-stellar-agent", + "chain": "stellar", + "address": "GABC...agent-address", + "metaAddress": "st:xlm:abc123...def456..." +} ``` -## 2. Generate a Stealth Address (Sender Side) + + The `.wraith` name is registered on-chain via the `wraith-names` Soroban contract. This costs ~0.0046 XLM in network fees. The agent pays this automatically during creation. + -The sender decodes the recipient's meta-address and generates a fresh one-time -stealth address. The ephemeral public key and view tag are announced on-chain. +### Registration Without XLM + +If the agent address has no XLM balance yet, the SDK falls back to **delegated (gasless) registration**. The Wraith API pays the Soroban fee on your behalf using Soroban's built-in auth framework: ```typescript -import { - decodeStealthMetaAddress, - generateStealthAddress, -} from "@wraith-protocol/sdk/chains/stellar"; +// This works even if the agent address is unfunded +const agent = await wraith.createAgent({ + name: "my-stellar-agent", + chain: Chain.Stellar, + wallet: stellarKeypair.publicKey(), + signature: Buffer.from(signature).toString("hex"), + message, +}); +// The SDK automatically handles delegated auth when needed +``` -const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress( - "st:xlm:" -); + + Gasless registration relies on the Soroban auth framework and the Wraith API relay. Your API key must have sufficient credits to cover the relay fee. See [pricing](https://usewraith.xyz/pricing) for details. + -const { stealthAddress, ephemeralPubKey, viewTag } = generateStealthAddress( - spendingPubKey, - viewingPubKey -); +--- + +## 5. Fund the Agent (Testnet) + +Stellar testnet accounts need at least 1 XLM to activate. Friendbot hands out 10,000 XLM for free — enough for hundreds of test transactions. + +```typescript +const response = await agent.chat("fund my wallet"); +console.log(response.response); +// "Wallet funded via Friendbot. Balance: 10,000 XLM" +``` + +**curl:** -// Send XLM to stealthAddress via Operation.createAccount -// Then call the Soroban announcer contract with: -// ephemeralPubKey (hex), viewTag (as first byte of metadata) +```bash +curl -X POST https://api.usewraith.xyz/agent/$AGENT_ID/chat \ + -H "Authorization: Bearer $WRAITH_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "message": "fund my wallet" }' ``` -## 3. Scan for Incoming Payments + + Friendbot only works on **testnet**. For mainnet, send at least 1.5 XLM to the agent address to activate the account and cover the base reserve. + + +### Manual Funding (Alternative) -`scanAnnouncements` takes the raw announcement list from `fetchAnnouncements` -(or your own indexer) and returns only the entries that belong to your keys, -together with the derived private scalar needed to spend. +If you prefer to fund the agent yourself, send XLM directly to the agent's Stellar address: ```typescript +// Get the agent's Stellar address +const agentAddress = agent.info.addresses[Chain.Stellar]; +console.log("Send XLM to:", agentAddress); +// Then send at least 1.5 XLM from your wallet +``` + +--- + +## 6. Check Balance + +```typescript +const balance = await agent.getBalance(); +console.log("XLM:", balance.native); // "10000.0" +console.log("Tokens:", balance.tokens); // { USDC: "0.0" } + +// Or via chat +const res = await agent.chat("what's my balance?"); +console.log(res.response); +// "Balance: 10,000 XLM, 0 USDC" +``` + +--- + +## 7. Send a Stealth Payment + +Send USDC to any `.wraith` name. The agent resolves the name to a stealth meta-address, generates a fresh one-time stealth address, and broadcasts the announcement on-chain — all in a single Soroban transaction. + +```typescript +const res = await agent.chat("send 10 USDC to alice.wraith on stellar"); +console.log(res.response); +// "Payment sent — 10 USDC to alice.wraith via stealth address GABC...xyz on Stellar." + +// Inspect the tool call for the transaction hash +console.log(res.toolCalls); +// [{ name: "send_payment", status: "success", detail: "{...txHash...}" }] +``` + +**curl:** + +```bash +curl -X POST https://api.usewraith.xyz/agent/$AGENT_ID/chat \ + -H "Authorization: Bearer $WRAITH_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "message": "send 10 USDC to alice.wraith on stellar" }' +``` + +### Sending XLM + +```typescript +const res = await agent.chat("send 50 XLM to bob.wraith on stellar"); +console.log(res.response); +``` + +### How Stealth Payments Work (Behind the Scenes) + +1. **Resolve** — `alice.wraith` is resolved to a stealth meta-address (two Ed25519 public keys) +2. **Generate** — A random ephemeral key pair `(r, R)` is generated +3. **ECDH** — A shared secret `S = r * viewingPubKey` is computed via X25519 (Montgomery form conversion) +4. **Derive** — The stealth address = `spendingPubKey + SHA-256("wraith:scalar:" || S) * G` +5. **Send** — USDC is transferred to the stealth address via the `stealth-sender` Soroban contract +6. **Announce** — The ephemeral public key `R` and a 1-byte view tag are published on-chain + +Alice's agent scans for announcements, matches them using her viewing key, and derives the private scalar needed to spend from the stealth address. + +
+ + + A view tag is the first byte of `SHA-256("wraith:tag:" || sharedSecret)`. It allows scanners to reject ~255/256 non-matching announcements with a single byte comparison — no expensive Ed25519 point operations needed. This makes scanning fast even over thousands of announcements. + + +--- + +## 8. Scan for Incoming Payments + +If someone has sent a payment to your `.wraith` name (or directly to your stealth meta-address), scan the chain to detect those payments: + +```typescript +const res = await agent.chat("scan for payments on stellar"); +console.log(res.response); +// "Scan complete. Found 2 incoming payments: +// - 10 USDC at stealth address GABC...abc (tx: a1b2c3...) +// - 5 USDC at stealth address GABC...def (tx: d4e5f6...)" +``` + +### How Scanning Works + +The agent queries the Soroban RPC for `stealth-announcer` contract events, then runs each announcement through `scanAnnouncements()`: + +```typescript +// This is what the agent does internally (simplified) import { - scanAnnouncements, fetchAnnouncements, + scanAnnouncements, } from "@wraith-protocol/sdk/chains/stellar"; -// Fetch all announcements from the Soroban RPC const announcements = await fetchAnnouncements("stellar"); - -const matched = await scanAnnouncements( +const matched = scanAnnouncements( announcements, - keys.viewingKey, // 32-byte Uint8Array seed + keys.viewingKey, // 32-byte Uint8Array keys.spendingPubKey, // 32-byte Uint8Array keys.spendingScalar, // bigint ); for (const m of matched) { - console.log("Found payment at:", m.stealthAddress); - console.log("Private scalar: ", m.stealthPrivateScalar); + console.log("Found:", m.stealthAddress); } ``` - - View tags filter out ~255 of every 256 non-matching announcements with a - single byte comparison, making scans fast even over thousands of events. - +You don't need to call these functions directly — the managed agent does it for you via `agent.chat("scan for payments")`. -## 4. Spend From a Stealth Address +--- -Because stealth private keys are derived scalars (not raw seeds), standard -Stellar signing does not work. Use `signStellarTransaction` instead. +## 9. Withdraw to Your Wallet + +Move funds from stealth addresses to your regular Stellar wallet. The agent warns about privacy tradeoffs before executing: ```typescript -import { - signStellarTransaction, - pubKeyToStellarAddress, -} from "@wraith-protocol/sdk/chains/stellar"; -import { Transaction, Keypair } from "@stellar/stellar-sdk"; - -// m = a matched announcement from scanAnnouncements() -const txHash = transaction.hash(); // 32-byte Buffer -const sig = signStellarTransaction(txHash, m.stealthPrivateScalar, m.stealthPubKeyBytes); - -// Attach signature and submit -const hint = Buffer.from(m.stealthPubKeyBytes.slice(0, 4)); -transaction.addSignature( - pubKeyToStellarAddress(m.stealthPubKeyBytes), - hint.toString("base64"), - sig.toString("base64"), +const res = await agent.chat( + `withdraw all USDC to ${stellarKeypair.publicKey()} on stellar` ); +console.log(res.response); +``` + +### Privacy-Aware Withdrawal + +```typescript +// Request a privacy analysis before withdrawing +const privacy = await agent.chat("run a privacy check"); +console.log(privacy.response); +// "Privacy Score: 85/100 +// (medium) 7 unspent stealth addresses — consider consolidating" ``` -## 5. Key Differences from EVM + + To protect your privacy, space withdrawals at least **1 hour apart** and avoid withdrawing uniform round amounts. The agent's privacy scoring algorithm checks for timing patterns, address correlation, and dust amounts. See [Privacy Best Practices](/guides/privacy-best-practices) for details. + -| Aspect | EVM | Stellar | -|---|---|---| -| Curve | secp256k1 | ed25519 | -| ECDH | `secp256k1.getSharedSecret` | X25519 (Montgomery form) | -| Private key output | hex string | bigint scalar | -| Address format | `0x…` (20 bytes) | `G…` (56 chars) | -| Signing | secp256k1 ECDSA | `signWithScalar` / `signStellarTransaction` | +### Withdrawing to Any Destination + +```typescript +// Withdraw to a specific address (not necessarily the owner wallet) +const res = await agent.chat( + "withdraw 5 USDC to GDOE...recipient-address on stellar" +); +console.log(res.response); +``` --- -## Interactive Scanner Demo +## 10. Reconnect to an Existing Agent + +After the initial setup, reconnect without creating a new agent: + +```typescript +// By agent ID (fastest — store this after createAgent) +const agent = wraith.agent(process.env.AGENT_ID!); + +// By .wraith name +const agent = await wraith.getAgentByName("my-stellar-agent"); +``` -Paste your own stealth meta-address and keys, or click **Load demo keys** to -scan the five pre-built fixture announcements. Everything runs client-side — -no wallet, no network calls, no third-party scripts. +--- -