Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/snippets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.78.0"
components: rustfmt

# -----------------------------------------------------------------------
# Python — pinned minor version; patch resolved by setup-python.
Expand Down Expand Up @@ -134,6 +135,24 @@ jobs:
- name: Check extended snippets (Rust / Python / shell / JSON / TOML / YAML)
run: pnpm run check:snippets-extended

# -----------------------------------------------------------------------
# Compile the Rust quickstart crate — this is the ground-truth check
# that all documented crypto code actually compiles and its tests pass.
# The "network" feature (reqwest / tokio / soroban-client) is intentionally
# excluded here; it is only needed for live RPC calls, not for type-checking.
# -----------------------------------------------------------------------
- name: Compile Rust quickstart crate
working-directory: snippets/rust-quickstart
run: cargo build --features testnet 2>&1

- name: Check Rust quickstart formatting
working-directory: snippets/rust-quickstart
run: cargo fmt --check

- name: Run Rust quickstart unit tests
working-directory: snippets/rust-quickstart
run: cargo test --features testnet 2>&1

stellar-testnet-snippets:
name: Stellar snippet testnet validation
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ node_modules/
.npm/
test-results/
playwright-report/

# Rust build artifacts
snippets/rust-quickstart/target/
72 changes: 35 additions & 37 deletions guides/quickstarts/rust.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,30 @@ default = ["testnet"]
testnet = []
futurenet = []
mainnet = []
# "network" enables the scan + send modules (reqwest / tokio / soroban-client).
# Leave it off for a pure compile/type-check of the crypto modules.
network = ["dep:reqwest", "dep:serde_json", "dep:tokio"]

[dependencies]
ed25519-dalek = { version = "2", features = ["rand_core"] }
x25519-dalek = { version = "2", features = ["static_secrets"] }
ed25519-dalek = { version = "2", features = ["rand_core"] }
x25519-dalek = { version = "2", features = ["static_secrets"] }
curve25519-dalek = "4"
sha2 = "0.10"
rand = "0.8"
hex = "0.4"
stellar-strkey = "0.0.6"
stellar-xdr = { version = "20", features = ["std", "base64"] }
soroban-client = "0.10"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde_json = "1"
anyhow = "1"
sha2 = "0.10"
rand = "0.8"
hex = "0.4"
stellar-strkey = "0.0.8"
anyhow = "1"

# Optional — only needed when "network" is enabled.
reqwest = { version = "0.12", features = ["json"], optional = true }
serde_json = { version = "1", optional = true }
tokio = { version = "1", features = ["full"], optional = true }

[profile.release]
opt-level = 3
```

```rust no-check
```rust
// src/network.rs
// Compile-time network selection. Flip with --features futurenet / --features mainnet.

Expand Down Expand Up @@ -105,11 +108,10 @@ pub const STEALTH_SIGNING_MESSAGE: &str =

Decode the `S...` secret seed with `stellar-strkey`, then build an `ed25519-dalek` signing key. Keep the seed in memory only — never log it.

```rust no-check
```rust
// src/keypair.rs
use anyhow::Result;
use ed25519_dalek::{Signer, SigningKey};
use stellar_strkey::Decode;

pub struct Account {
pub seed: [u8; 32],
Expand All @@ -118,11 +120,13 @@ pub struct Account {
}

pub fn load_keypair(secret: &str) -> Result<Account> {
// `S...` -> 32-byte ed25519 seed
let seed = stellar_strkey::Decode::ed25519_secret_seed(secret)?;
// `S...` -> 32-byte ed25519 seed via stellar-strkey
let private = stellar_strkey::ed25519::PrivateKey::from_string(secret)
.map_err(|e| anyhow::anyhow!("invalid stellar secret: {e:?}"))?;
let seed: [u8; 32] = private.0;
let signing_key = SigningKey::from_bytes(&seed);
let address = stellar_strkey::Ed25519PublicKey(signing_key.verifying_key().to_bytes()).to_string();

let address =
stellar_strkey::ed25519::PublicKey(signing_key.verifying_key().to_bytes()).to_string();
Ok(Account { seed, signing_key, address })
}

Expand Down Expand Up @@ -150,7 +154,7 @@ cargo run --features testnet

This is the core of the Rust path: a self-contained reimplementation of the Wraith Stellar stealth spec (ed25519 + X25519, domain-separated SHA-256). It mirrors `deriveStealthKeys` / `generateStealthAddress` / `scanAnnouncements` from `@wraith-protocol/sdk/chains/stellar`.

```rust no-check
```rust
// src/stealth.rs
use anyhow::Result;
use curve25519_dalek::{
Expand Down Expand Up @@ -183,7 +187,7 @@ pub struct MatchedStealth {
}
```

```rust no-check
```rust
// src/stealth.rs (continued)
// --- domain-separated derivation (mirrors the TS spec) ---

Expand Down Expand Up @@ -226,7 +230,7 @@ pub fn derive_stealth_keys(sig: &[u8; 64]) -> StealthKeys {
}
```

```rust no-check
```rust
// src/stealth.rs (continued)
// --- X25519 ECDH between ed25519 keys (Montgomery form) ---

Expand Down Expand Up @@ -258,18 +262,18 @@ pub fn compute_view_tag(shared: &[u8; 32]) -> u8 {
}
```

```rust no-check
```rust
// src/stealth.rs (continued)
// --- generate a one-time stealth address for a recipient ---

pub fn generate_stealth_address(
spending_pub: &[u8; 32],
viewing_pub: &[u8; 32],
viewing_seed: &[u8; 32], // the recipient's viewing seed (not just the public key)
ephemeral_seed: &[u8; 32],
) -> GeneratedStealth {
let eph_pub = SigningKey::from_bytes(ephemeral_seed).verifying_key().to_bytes();
// shared = X25519(viewing_priv, ephemeral_pub)
let shared = x25519_shared(viewing_seed_for_pub(viewing_pub), &eph_pub);
let shared = x25519_shared(viewing_seed, &eph_pub);
let view_tag = compute_view_tag(&shared);
let h_scalar = hash_to_scalar("wraith:scalar:", &shared);

Expand All @@ -279,25 +283,19 @@ pub fn generate_stealth_address(
let stealth_point: EdwardsPoint =
spend_point + h_scalar * ED25519_BASEPOINT_POINT;
let stealth_pub = stealth_point.compress().to_bytes();
let stealth_address = stellar_strkey::Ed25519PublicKey(stealth_pub).to_string();
let stealth_address = stellar_strkey::ed25519::PublicKey(stealth_pub).to_string();

GeneratedStealth {
stealth_address,
ephemeral_pub: eph_pub,
view_tag,
}
}

// Helper: recover the viewing seed from its public key is impossible, so callers
// pass both the viewing seed (for ECDH) and the viewing pub (for the API shape).
fn viewing_seed_for_pub(_pub: &[u8; 32]) -> &'static [u8; 32] {
unreachable!("pass the real viewing seed; placeholder only")
}
```

For the recipient side you already hold the viewing seed, so replace `viewing_seed_for_pub` with the real seed and call `generate_stealth_address(spending_pub, viewing_pub, &ephemeral_seed)` directly from `derive_stealth_keys`.
After calling `derive_stealth_keys`, the `viewing_seed` is available directly on the returned `StealthKeys` struct. Pass it here: `generate_stealth_address(&keys.spending_pub, &keys.viewing_seed, &ephemeral_seed)`.

```rust no-check
```rust
// src/stealth.rs (continued)
// --- scan announcements and recover the stealth private scalar ---

Expand All @@ -321,7 +319,7 @@ pub fn scan_announcements(
let h_scalar = hash_to_scalar("wraith:scalar:", &shared);
let stealth_point = spend_point + h_scalar * ED25519_BASEPOINT_POINT;
let stealth_pub = stealth_point.compress().to_bytes();
if stellar_strkey::Ed25519PublicKey(stealth_pub).to_string() == *addr {
if stellar_strkey::ed25519::PublicKey(stealth_pub).to_string() == *addr {
matched.push(MatchedStealth {
stealth_address: addr.clone(),
stealth_scalar: spending_scalar + h_scalar,
Expand Down Expand Up @@ -441,15 +439,15 @@ pub async fn send_stealth_payment(
sender_address: &str,
token: &str, // Stellar Asset Contract id, e.g. XLM or USDC SAC
recipient_spending: &[u8; 32],
recipient_viewing: &[u8; 32],
recipient_viewing_seed: &[u8; 32], // recipient's viewing seed (from derive_stealth_keys)
amount: i128,
) -> Result<String> {
// 1. Fresh ephemeral key for this payment
let mut eph_seed = [0u8; 32];
OsRng.fill_bytes(&mut eph_seed);

// 2. Recipient's one-time address
let stealth = generate_stealth_address(recipient_spending, recipient_viewing, &eph_seed);
let stealth = generate_stealth_address(recipient_spending, recipient_viewing_seed, &eph_seed);

// 3. Build + submit a `stealth-sender.send` invocation.
// Shape is stable across soroban-client 0.9–0.11; adapt builder methods to your version.
Expand Down
Loading
Loading