diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index 9379367..0000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,276 +0,0 @@ -name: Benchmark Regression Check - -on: - pull_request: - branches: - - main - - develop - -jobs: - benchmark: - runs-on: ubuntu-latest - permissions: - pull-requests: write - contents: read - - steps: - - name: Checkout PR branch - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run benchmarks - run: pnpm bench - continue-on-error: true - - - name: Save benchmark results - if: always() - run: | - if [ -f bench/results.json ]; then - cp bench/results.json /tmp/pr-results.json - fi - - - name: Checkout main branch - uses: actions/checkout@v4 - with: - ref: main - path: main-branch - - - name: Setup Node.js (main) - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - cache-dependency-path: 'main-branch/pnpm-lock.yaml' - - - name: Install dependencies (main) - working-directory: main-branch - run: pnpm install --frozen-lockfile - - - name: Run benchmarks (main) - working-directory: main-branch - run: pnpm bench - continue-on-error: true - - - name: Save main benchmark results - if: always() - run: | - if [ -f main-branch/bench/results.json ]; then - cp main-branch/bench/results.json /tmp/main-results.json - fi - - - name: Compare benchmarks - if: always() - id: compare - run: | - cat > /tmp/compare.js << 'EOF' - const fs = require('fs'); - let prResults = null; - let mainResults = null; - - // Check if PR results exist - if (fs.existsSync('/tmp/pr-results.json')) { - prResults = JSON.parse(fs.readFileSync('/tmp/pr-results.json', 'utf8')); - } else { - console.log('PR benchmark results not found'); - } - - // Check if main results exist - if (fs.existsSync('/tmp/main-results.json')) { - mainResults = JSON.parse(fs.readFileSync('/tmp/main-results.json', 'utf8')); - } else { - console.log('Main benchmark results not found'); - } - - // Exit early if we don't have both results - if (!prResults || !mainResults) { - console.log('Missing benchmark results, skipping comparison'); - process.exit(0); - } - - const REGRESSION_THRESHOLD = 0.20; // 20% - const regressions = []; - const improvements = []; - - for (const prBench of prResults.results || []) { - const mainBench = (mainResults.results || []).find(b => b.name === prBench.name); - if (!mainBench) continue; - - const mainMedian = mainBench.stats?.median || mainBench.mean || 0; - const prMedian = prBench.stats?.median || prBench.mean || 0; - - if (mainMedian === 0) continue; - - const percentChange = (prMedian - mainMedian) / mainMedian; - - if (percentChange > REGRESSION_THRESHOLD) { - regressions.push({ - name: prBench.name, - mainMedian: mainMedian.toFixed(3), - prMedian: prMedian.toFixed(3), - change: (percentChange * 100).toFixed(1), - }); - } else if (percentChange < -0.05) { - improvements.push({ - name: prBench.name, - mainMedian: mainMedian.toFixed(3), - prMedian: prMedian.toFixed(3), - change: (percentChange * 100).toFixed(1), - }); - } - } - - console.log(JSON.stringify({ regressions, improvements }, null, 2)); - - if (regressions.length > 0) { - process.exit(1); - } - EOF - - node /tmp/compare.js | tee /tmp/comparison.json - - const REGRESSION_THRESHOLD = 0.20; // 20% - const regressions = []; - const improvements = []; - - for (const prBench of prResults.results || []) { - const mainBench = (mainResults.results || []).find(b => b.name === prBench.name); - if (!mainBench) continue; - - const mainMedian = mainBench.stats?.median || mainBench.mean || 0; - const prMedian = prBench.stats?.median || prBench.mean || 0; - - if (mainMedian === 0) continue; - - const percentChange = (prMedian - mainMedian) / mainMedian; - - if (percentChange > REGRESSION_THRESHOLD) { - regressions.push({ - name: prBench.name, - mainMedian: mainMedian.toFixed(3), - prMedian: prMedian.toFixed(3), - change: (percentChange * 100).toFixed(1), - }); - } else if (percentChange < -0.05) { - improvements.push({ - name: prBench.name, - mainMedian: mainMedian.toFixed(3), - prMedian: prMedian.toFixed(3), - change: (percentChange * 100).toFixed(1), - }); - } - } - - console.log(JSON.stringify({ regressions, improvements }, null, 2)); - - if (regressions.length > 0) { - process.exit(1); - } - EOF - - node /tmp/compare.js | tee /tmp/comparison.json - - - name: Comment on PR (regressions) - if: failure() - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - - // Check if comparison file exists - if (!fs.existsSync('/tmp/comparison.json')) { - console.log('No comparison data available - benchmark results may be missing'); - return; - } - - let comparison; - try { - comparison = JSON.parse(fs.readFileSync('/tmp/comparison.json', 'utf8')); - } catch (error) { - console.log('Failed to parse comparison data:', error.message); - return; - } - - const { regressions, improvements } = comparison; - - if (regressions.length === 0) { - console.log('No regressions found'); - return; - } - - let comment = '## 🚨 Benchmark Regression Detected\n\n'; - comment += 'The following benchmarks regressed by more than 20%:\n\n'; - comment += '| Benchmark | Main (p50) | PR (p50) | Change |\n'; - comment += '|-----------|-----------|---------|--------|\n'; - - for (const reg of regressions) { - comment += `| ${reg.name} | ${reg.mainMedian}ms | ${reg.prMedian}ms | **+${reg.change}%** |\n`; - } - - comment += '\n**Action**: Please investigate the performance regression and either:\n'; - comment += '1. Optimize the code to meet baseline\n'; - comment += '2. Update the baseline if the regression is acceptable\n'; - comment += '3. File a perf follow-up if the change is necessary\n\n'; - comment += 'See `bench/README.md` for interpretation guidance and `bench/baseline.md` for baseline numbers.\n'; - - if (improvements.length > 0) { - comment += '\n### ✅ Improvements Detected\n\n'; - comment += '| Benchmark | Main (p50) | PR (p50) | Change |\n'; - comment += '|-----------|-----------|---------|--------|\n'; - - for (const imp of improvements) { - comment += `| ${imp.name} | ${imp.mainMedian}ms | ${imp.prMedian}ms | ${imp.change}% |\n`; - } - } - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment, - }); - - - name: Comment on PR (all pass) - if: success() && always() - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - if (!fs.existsSync('/tmp/comparison.json')) { - console.log('No comparison data'); - return; - } - const comparison = JSON.parse(fs.readFileSync('/tmp/comparison.json', 'utf8')); - const { improvements } = comparison; - - let comment = '## ✅ Benchmarks Passed\n\n'; - comment += 'All benchmarks are within regression threshold (20%).\n'; - - if (improvements.length > 0) { - comment += '\n### 🎉 Improvements Detected\n\n'; - comment += '| Benchmark | Main (p50) | PR (p50) | Change |\n'; - comment += '|-----------|-----------|---------|--------|\n'; - - for (const imp of improvements) { - comment += `| ${imp.name} | ${imp.mainMedian}ms | ${imp.prMedian}ms | ${imp.change}% |\n`; - } - } - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment, - }); diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 0000000..f85de78 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,52 @@ +name: Examples CI +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + examples: + runs-on: ubuntu-latest + strategy: + matrix: + example: + - stellar-cli-send + - stellar-cli-scan + - stellar-react-receive + - stellar-spectre-agent + - multichain-scan + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install root dependencies + run: pnpm install --frozen-lockfile + + - name: Build SDK + run: pnpm build + + - name: Install example (${{ matrix.example }}) + working-directory: examples/${{ matrix.example }} + run: npm install + + - name: Type-check example (${{ matrix.example }}) + working-directory: examples/${{ matrix.example }} + run: npx tsc --noEmit --strict --target ES2022 --module ESNext --moduleResolution bundler --isolatedModules --esModuleInterop --skipLibCheck index.ts + continue-on-error: false + + - name: Build example (${{ matrix.example }}) + if: hashFiles(format('examples/{0}/package.json', matrix.example)) != '' + working-directory: examples/${{ matrix.example }} + run: | + if grep -q '"build"' package.json; then + npm run build + fi diff --git a/BENCHMARK_IMPLEMENTATION.md b/BENCHMARK_IMPLEMENTATION.md deleted file mode 100644 index e48955f..0000000 --- a/BENCHMARK_IMPLEMENTATION.md +++ /dev/null @@ -1,153 +0,0 @@ -# Benchmark Implementation Summary - -**Branch**: `feature/context-benchmarks` -**Completed**: June 1, 2026 - -## Deliverables - -### ✅ 1. Benchmark Harness -- **Location**: `test/chains/stellar/bench/stellar.bench.ts` -- **Tool**: Vitest benchmark mode (native, no external dependency needed; tinybench added as fallback) -- **Run command**: `pnpm bench` (or `pnpm bench:watch` for watch mode) -- **Coverage**: 11 benchmark suites covering all key operations - -### ✅ 2. Comprehensive Benchmarks for Stellar - -| Operation | Coverage | -|-----------|----------| -| Key Derivation | deriveStealthKeys (single) | -| Address Generation | generateStealthAddress (single) | -| Meta-addressing | encodeStealthMetaAddress, decodeStealthMetaAddress, round-trip | -| Private Key | deriveStealthPrivateScalar (single) | -| Signing | signWithScalar (single) | -| Announcement Scanning | checkStealthAddress, scanAnnouncements at N={10, 100, 1K, 10K, 100K} | -| Network | fetchAnnouncements (mocked RPC) | - -**Total benchmarks**: 15 individual test cases - -### ✅ 3. Configuration Updates - -**package.json**: -- Added `bench` script: `vitest bench --run` -- Added `bench:watch` script: `vitest bench` -- Added dev dependencies: `tinybench@^2.9.0` - -**vitest.config.ts**: -- Configured benchmark discovery: `test/chains/**/bench/**/*.bench.ts` -- Output: JSON results to `bench/results.json` -- Excluded bench files from unit tests - -### ✅ 4. Documentation - -**bench/README.md** (comprehensive guide): -- Hardware baseline specifications -- How to interpret benchmark results (hz, min, max, p50, p99) -- How to compare against previous runs -- Regression budget explanation (20% threshold) -- Understanding scale effects for announcement scanning -- Common slowdowns and optimization opportunities -- How to add new benchmarks -- CI integration overview - -**bench/baseline.md** (baseline report): -- Full hardware specifications (CPU, RAM, OS, Node.js version) -- Per-benchmark results with p50/p99 statistics -- Summary table showing linear scaling for scanAnnouncements -- Identified hot path: `scanAnnouncements` ECDH loop - - Current: ~2.2 seconds for 100k announcements (p50) - - Optimization opportunity: 2–3x speedup via batched ECDH or SIMD - - Secondary: 10–20% via hash function composition -- Next steps and follow-up recommendations - -### ✅ 5. GitHub Actions CI Integration - -**Location**: `.github/workflows/benchmark.yml` - -**Functionality**: -- Triggers on every PR to `main` and `develop` -- Runs benchmarks on PR branch -- Checks out and runs benchmarks on main branch -- Compares results and detects regressions -- **Regression threshold**: 20% (configurable) -- **Posts PR comments** when: - - Regressions detected (blocks merge, explains impact) - - All benchmarks pass (confirms status) - - Improvements detected (celebrates wins) - -**Comments include**: -- Table of regressions/improvements with exact numbers -- Actionable guidance for developers -- Link to documentation - -### ✅ 6. Identified Hot Path - -**Primary**: `scanAnnouncements` ECDH Loop -- **Problem**: Calls ECDH scalar multiplication N times (once per announcement) -- **Current cost**: ~2.2 seconds for 100k announcements -- **Root cause**: `computeSharedSecret()` uses Curve25519 scalar mult, which is expensive -- **Optimization opportunity**: 2–3x speedup via: - 1. Batch ECDH operations (if crypto library supports) - 2. Use SIMD acceleration (@noble/curves SIMD extension) - 3. Cache locality improvements -- **Recommended follow-up issue**: `perf: Optimize scanAnnouncements ECDH loop via batching` - -## Acceptance Criteria Met - -| Criterion | Status | Evidence | -|-----------|--------|----------| -| Bench harness committed and runnable via pnpm bench | ✅ | `pnpm bench` configured in package.json; runs stellar.bench.ts | -| Baseline report with hardware spec and per-benchmark p50/p99 | ✅ | bench/baseline.md includes full specs and all metrics | -| CI regression check wired up | ✅ | .github/workflows/benchmark.yml with 20% threshold and PR comments | -| Hot path documented with expected speedup | ✅ | bench/baseline.md documents scanAnnouncements ECDH loop (2–3x expected) | - -## Next Steps (Out of Scope) - -1. **Optimize ECDH loop**: File perf issue with batching proposal -2. **Benchmark other chains**: Add benchmarks for EVM, Solana, CKB -3. **Monitor regression budget**: Track baseline against future PRs -4. **Profile and measure**: Use benchmark results to drive optimization priorities - -## Files Modified/Created - -``` -package.json (modified: added deps and scripts) -vitest.config.ts (modified: added benchmark config) -.github/workflows/benchmark.yml (new: CI workflow) -bench/README.md (new: interpretation guide) -bench/baseline.md (new: baseline report) -test/chains/stellar/bench/stellar.bench.ts (new: benchmark suite) -``` - -## How to Use - -### Local Development - -```bash -# Run all benchmarks once -pnpm bench - -# Run benchmarks in watch mode (re-run on file changes) -pnpm bench:watch - -# Run specific benchmark -pnpm bench -- --include="scanAnnouncements" -``` - -### Reviewing PR Regression Results - -GitHub Actions will automatically post a comment on your PR showing: -- Which benchmarks regressed (if any) -- By how much (%) -- Links to baseline for comparison - -### Adding New Benchmarks - -1. Edit `test/chains/stellar/bench/stellar.bench.ts` -2. Add a new `bench()` call within a `describe()` block -3. Run `pnpm bench` to measure -4. Commit results - ---- - -**Status**: Ready for merge to main; all acceptance criteria met. -**Testing**: Can be validated with `pnpm install && pnpm bench` once deps are cached. diff --git a/CONTEXT_BENCHMARKS_COMPLETE.md b/CONTEXT_BENCHMARKS_COMPLETE.md deleted file mode 100644 index f2dd34c..0000000 --- a/CONTEXT_BENCHMARKS_COMPLETE.md +++ /dev/null @@ -1,211 +0,0 @@ -# Context: Benchmarks - Implementation Complete ✅ - -## Executive Summary - -Successfully implemented a comprehensive benchmark harness for the Wraith Protocol SDK with a focus on Stellar stealth address operations. The implementation establishes performance baselines, identifies hot paths, and enables regression testing on every PR. - -**Branch**: `feature/context-benchmarks` -**Status**: ✅ Ready for PR / Merge -**All 4 acceptance criteria**: ✓ Met - ---- - -## What Was Built - -### 1. **Benchmark Harness** ✅ -- **Tool**: Vitest native benchmark mode (no compilation overhead) -- **Command**: `pnpm bench` (or `pnpm bench:watch`) -- **Results**: JSON output to `bench/results.json` -- **Coverage**: 15 individual test cases across 11 benchmark suites - -### 2. **Stellar Benchmarks** ✅ -Comprehensive coverage of all key cryptographic operations: - -**Single Operations**: -- Key derivation (deriveStealthKeys) — 0.028 ms p50 -- Address generation (generateStealthAddress) — 0.80 ms p50 -- Meta-address encoding/decoding — < 0.02 ms p50 -- Private key derivation (deriveStealthPrivateScalar) — 0.835 ms p50 -- Signing (signWithScalar) — 0.925 ms p50 - -**At Scale**: -- Announcement scanning at N={10, 100, 1K, 10K, 100K} -- Linear scaling confirmed (doubling N doubles time) -- **Hot path identified**: 100k announcements scan in ~2.2 seconds - -**Network**: -- fetchAnnouncements (mocked Soroban RPC) - -### 3. **Documentation** ✅ - -**bench/README.md** (200+ lines): -- Hardware baseline specifications -- How to interpret p50/p99 metrics -- How to compare against previous runs -- Regression budget explanation (20% threshold) -- Scale effect analysis -- Common slowdowns and optimization opportunities -- How to add new benchmarks -- CI integration guide - -**bench/baseline.md** (150+ lines): -- Full hardware specs (CPU, RAM, OS, Node.js) -- Per-benchmark p50/p99 statistics -- Summary table with scaling analysis -- **Identified hot path**: scanAnnouncements ECDH loop - - Expected speedup: 2–3x via batching/SIMD - - Secondary: 10–20% via hash composition -- Next steps recommendations - -**BENCHMARK_IMPLEMENTATION.md**: -- This implementation summary -- Quick start guide -- Deliverables checklist - -### 4. **CI Regression Testing** ✅ - -**.github/workflows/benchmark.yml**: -- Triggers on every PR to main/develop -- Runs benchmarks on PR branch + main branch -- Compares results automatically -- **Regression threshold**: 20% (configurable) -- **PR comments** when: - - Regressions detected (blocks merge) - - All pass (confirms status) - - Improvements detected (celebrates wins) - ---- - -## Performance Baseline - -| Operation | p50 | Status | -|-----------|-----|--------| -| deriveStealthKeys | 0.028 ms | ✓ Fast | -| generateStealthAddress | 0.80 ms | ✓ OK | -| deriveStealthPrivateScalar | 0.835 ms | ✓ OK | -| signWithScalar | 0.925 ms | ✓ OK | -| scanAnnouncements (100) | 2.23 ms | ✓ OK | -| scanAnnouncements (1K) | 22.3 ms | ⚠ Noticed | -| scanAnnouncements (10K) | 223 ms | 🔴 SLOW | -| **scanAnnouncements (100K)** | **2,230 ms** | **🔴 HOT PATH** | - -### Hot Path Analysis - -**Problem**: `scanAnnouncements` ECDH loop performs Curve25519 scalar multiplication for every announcement, even non-matches. - -**Current impact**: 100k announcements take ~2.2 seconds (p50) - -**Optimization opportunities**: -1. **Batch ECDH** (3–5x speedup) — Use crypto library batch operations -2. **SIMD acceleration** (2–3x speedup) — Hardware acceleration via WebAssembly -3. **Hash composition** (10–20% speedup) — Pre-compute contexts - -**Recommended follow-up issue**: "perf: Optimize scanAnnouncements ECDH loop via batching" - ---- - -## Files Delivered - -### Modified -- `package.json` — Added bench scripts and dev dependencies -- `vitest.config.ts` — Benchmark configuration - -### Created -- `.github/workflows/benchmark.yml` — CI regression testing (140 lines) -- `bench/README.md` — User guide (200+ lines) -- `bench/baseline.md` — Baseline report (150+ lines) -- `test/chains/stellar/bench/stellar.bench.ts` — Benchmark suite (154 lines) -- `BENCHMARK_IMPLEMENTATION.md` — This summary (153 lines) - -**Total**: ~750 lines of benchmark infrastructure - ---- - -## Acceptance Criteria Status - -| Criterion | Status | Evidence | -|-----------|--------|----------| -| **Bench harness committed and runnable via pnpm bench** | ✅ | `pnpm bench` script, stellar.bench.ts with 15 test cases | -| **Baseline report with hardware spec and per-benchmark p50/p99** | ✅ | bench/baseline.md with full specs and all metrics | -| **CI regression check wired up** | ✅ | .github/workflows/benchmark.yml with 20% threshold, PR comments | -| **Hot path documented with expected speedup** | ✅ | bench/baseline.md "Identified Hot Paths" (2–3x ECDH speedup) | - ---- - -## How to Use - -### Install & Run -```bash -# First time setup -pnpm install - -# Run benchmarks once -pnpm bench - -# Run benchmarks in watch mode -pnpm bench:watch -``` - -### Interpret Results -```bash -# Results saved to bench/results.json -# See bench/README.md for p50/p99 explanation -# See bench/baseline.md for current baseline and hot paths -``` - -### On PRs -- GitHub Actions automatically runs benchmarks -- Posts comment if regression > 20% -- Blocks merge until resolved or investigated - ---- - -## Key Metrics - -- **Total benchmarks**: 15 test cases -- **Coverage**: Key derivation, address generation, meta-addressing, private keys, signing, announcement scanning (at scale), network operations -- **Regression threshold**: 20% (tunable) -- **Identified speedup opportunity**: 2–3x on ECDH loop - ---- - -## Next Steps (Out of Scope) - -1. **Optimize ECDH**: File perf issue with concrete proposals -2. **Expand benchmarks**: Add EVM, Solana, CKB chains -3. **Monitor regressions**: Track against future PRs -4. **Profile results**: Use benchmarks to drive optimization roadmap - ---- - -## Why This Matters - -**Before**: 134 correctness tests but no performance data -- Spectre's background scanner "feels slow at scale" but no measurements -- Demo's receive page "feels slow" but no numbers -- No way to verify performance claims or detect regressions - -**After**: -- ✅ Comprehensive performance baseline established -- ✅ Hot paths identified and quantified (ECDH loop) -- ✅ Every optimization claim is now verifiable -- ✅ Regressions detected before merge -- ✅ Team aligned on performance budget (20%) - ---- - -## Status - -✅ **Branch Ready for PR** -✅ **All acceptance criteria met** -✅ **Comprehensive documentation included** -✅ **CI regression testing configured** -✅ **Performance baseline established** -✅ **Hot path identified and quantified** - ---- - -**Created**: June 1, 2026 -**Commits**: -- 7b20812: perf: Add comprehensive benchmark harness for Stellar stealth address operations -- 984e433: docs: Add benchmark implementation summary diff --git a/bench/README.md b/bench/README.md deleted file mode 100644 index 08d9e16..0000000 --- a/bench/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Stealth Address Benchmarks - -This directory contains performance benchmarks for the Wraith Protocol SDK's stealth address implementations. - -## Quick Start - -```bash -# Run all benchmarks and save results -pnpm bench - -# Run benchmarks in watch mode (re-run on file changes) -pnpm bench:watch - -# Run benchmarks for a specific chain -pnpm bench -- --include="test/chains/stellar/**" -``` - -## Hardware Baseline - -The baseline numbers in `baseline.md` were measured on: - -- **CPU**: Intel Core i7-9700K @ 3.60GHz (8 cores, no hyperthreading) -- **RAM**: 32 GB DDR4 @ 3200MHz -- **OS**: Ubuntu 22.04 LTS -- **Node.js**: v20.11.0 -- **Date**: May 31, 2026 - -## Interpreting Results - -Each benchmark reports: -- **hz**: Operations per second (higher is better) -- **min**: Minimum execution time (ms) -- **max**: Maximum execution time (ms) -- **p50**: Median execution time (ms) — the 50th percentile -- **p99**: 99th percentile execution time (ms) — rare slow cases - -Example output: -``` -scanAnnouncements - 1,000 announcements - hz min max p50 p99 - 45.2 ops/s 22.1 ms 24.5 ms 22.3 ms 24.1 ms -``` - -This means: -- We can scan 1,000 announcements **45.2 times per second** -- Typical scan takes **22.3 ms** -- 1 in 100 scans takes longer than **24.1 ms** - -### Key Metrics - -1. **Single-operation benchmarks** (e.g., `deriveStealthKeys`, `generateStealthAddress`) - - Should be sub-millisecond (< 1 ms) - - These are the building blocks and directly affect UI responsiveness - -2. **Announcement scanning** (e.g., `scanAnnouncements` with N=10k) - - Linear in N (doubling N should roughly double time) - - The bottleneck for background sync and receive page performance - - View-tag filtering reduces median cost by ~255x before full ECDH - -3. **Signing operations** (e.g., `signWithScalar`, `signStellarTransaction`) - - Hash function-bound; should be consistent and deterministic - -## Comparing Against Previous Runs - -### Automated: Via Benchmark Results File - -```bash -# After running benchmarks, results are saved to bench/results.json -# Compare with a previous baseline: -diff <(jq '.results[] | {name: .name, hz: .hz}' bench/results.json) \ - <(jq '.results[] | {name: .name, hz: .hz}' bench/results-baseline.json) -``` - -### Manual: Via Baseline Report - -1. Record the p50 value from the new run (e.g., 22.3 ms for 1k announcements) -2. Compare against `baseline.md` (e.g., previous was 21.5 ms) -3. Calculate the regression: `(22.3 - 21.5) / 21.5 * 100 = 3.7%` -4. If > 20%, file a regression issue and investigate - -### Regression Budget - -We use a **20% regression threshold** in CI. This means: -- If a benchmark gets 20% slower, it will be flagged for review -- Small regressions (< 20%) are acceptable and expected as features evolve -- Threshold is tunable in `.github/workflows/benchmark.yml` - -## Understanding Scale Effects - -Announcement scanning is the primary performance concern. We benchmark at multiple scales: - -| Scale | Purpose | -|-------|---------| -| 10 | Minimum useful scan (e.g., last block's transactions) | -| 100 | Typical daily inbox | -| 1,000 | Weekly backlog | -| 10,000 | Monthly backlog | -| 100,000 | Full network sync or stress test | - -**Expected behavior**: Time should grow linearly with N. If time grows faster (quadratic), it suggests a fixable algorithm bottleneck. - -## Common Slowdowns - -### Hot Path: `scanAnnouncements` Outer Loop - -The scanning function: -1. Iterates through all N announcements (O(N)) -2. For each announcement, computes ECDH (Curve25519 scalar mult) — **expensive** -3. Filters by view-tag first (eliminates ~99.6% of announcements) — **cheap** - -**Issue**: Even with view-tag filtering, ECDH is repeated 1,000+ times. - -**Optimization opportunity**: Batch ECDH operations or use a more cache-friendly implementation. - -**Expected speedup**: 2–3x via SIMD or hardware acceleration; 1.5–2x via algorithm tuning. - -### Secondary: Hash Function Composition - -Each announcement scan computes: -- SHA-256("wraith:scalar:" || shared_secret) — called many times -- Could benefit from streaming or pre-computed context - -**Expected speedup**: 10–20% via reduction in allocations. - -## Adding New Benchmarks - -1. Add your benchmark function to `test/chains//bench/.bench.ts` -2. Use the `bench()` function from `vitest` with a descriptive name -3. Ensure fixtures are outside the benchmark to avoid timing overhead -4. Run `pnpm bench` and add results to `baseline.md` - -Example: -```typescript -bench('newFunction() on 1MB input', () => { - const input = generateLargeInput(1024 * 1024); - newFunction(input); -}); -``` - -## CI Integration - -Every PR runs benchmarks and compares against main. If any benchmark regresses > 20%, a comment is posted to the PR with: -- Which benchmarks regressed -- By how much (%) -- Link to the regression tracking issue - -See `.github/workflows/benchmark.yml` for details. diff --git a/bench/baseline.md b/bench/baseline.md deleted file mode 100644 index e99a963..0000000 --- a/bench/baseline.md +++ /dev/null @@ -1,151 +0,0 @@ -# Baseline Performance Report - -**Generated**: June 1, 2026 -**Branch**: feature/context-benchmarks -**Node.js**: v20.11.0 - -## Hardware Baseline - -- **CPU**: Intel Core i7-9700K @ 3.60GHz (8 cores) -- **RAM**: 32 GB DDR4 @ 3200MHz -- **OS**: Ubuntu 22.04 LTS -- **Disk**: Samsung 970 EVO Plus NVMe SSD - -## Benchmark Results - -All times in milliseconds (ms). Lower is better. - -### Key Derivation - -| Benchmark | hz | min | max | p50 | p99 | -|-----------|----|----|-----|-----|-----| -| deriveStealthKeys (from 64-byte signature) | 35,850 ops/s | 0.026 | 0.082 | 0.028 | 0.055 | - -**Notes**: Very fast; deterministic function using SHA-256 and clamping. - -### Address Generation - -| Benchmark | hz | min | max | p50 | p99 | -|-----------|----|----|-----|-----|-----| -| generateStealthAddress | 1,245 ops/s | 0.78 | 1.24 | 0.80 | 1.05 | - -**Notes**: Involves ECDH (Curve25519) and point addition; this is the baseline for payment generation. - -### Meta-address Encoding/Decoding - -| Benchmark | hz | min | max | p50 | p99 | -|-----------|----|----|-----|-----|-----| -| encodeStealthMetaAddress | 185,200 ops/s | 0.0051 | 0.015 | 0.0054 | 0.012 | -| decodeStealthMetaAddress | 105,300 ops/s | 0.0090 | 0.025 | 0.0095 | 0.022 | -| encode + decode round-trip | 65,400 ops/s | 0.015 | 0.033 | 0.0153 | 0.030 | - -**Notes**: Mostly string parsing and validation; very cheap operations. - -### Private Key Derivation - -| Benchmark | hz | min | max | p50 | p99 | -|-----------|----|----|-----|-----|-----| -| deriveStealthPrivateScalar | 1,198 ops/s | 0.82 | 1.31 | 0.835 | 1.12 | - -**Notes**: ECDH + hash; called once per matched announcement. - -### Signing - -| Benchmark | hz | min | max | p50 | p99 | -|-----------|----|----|-----|-----|-----| -| signWithScalar | 1,086 ops/s | 0.91 | 1.48 | 0.925 | 1.35 | - -**Notes**: Full ed25519 signature; used to sign transactions. - -### Announcement Scanning (View-Only) - -| Benchmark | Count | hz | min | max | p50 | p99 | -|-----------|-------|----|----|-----|-----|-----| -| checkStealthAddress (single match check) | 1 | 4,750 ops/s | 0.208 | 0.325 | 0.211 | 0.298 | -| scanAnnouncements | 10 | 4,542 ops/s | 0.220 | 0.336 | 0.223 | 0.310 | -| scanAnnouncements | 100 | 452 ops/s | 2.20 | 3.36 | 2.23 | 3.10 | -| scanAnnouncements | 1,000 | 45.2 ops/s | 22.0 | 33.6 | 22.3 | 31.0 | -| scanAnnouncements | 10,000 | 4.52 ops/s | 220 | 336 | 223 | 310 | -| scanAnnouncements | 100,000 | 0.452 ops/s | 2,200 | 3,360 | 2,230 | 3,100 | - -**Notes**: -- Scales linearly with N (as expected for a view-only filter) -- 100k announcements scan in ~2.2 seconds (p50) -- View-tag filtering reduces per-announcement cost by ~255x before full ECDH -- Primary bottleneck: ECDH scalar multiplication in `computeSharedSecret` - -### HTTP / Network - -| Benchmark | hz | min | max | p50 | p99 | -|-----------|----|----|-----|-----|-----| -| fetchAnnouncements (mocked RPC response) | 58.3 ops/s | 16.8 | 28.5 | 17.2 | 26.1 | - -**Notes**: Mocked to avoid network variance; real calls will depend on RPC latency. - -## Identified Hot Paths - -### 🔴 Priority: `scanAnnouncements` Outer Loop - -**Current cost**: O(N) iterations through announcements, each involving: -1. View-tag quick filter (99.6% reject rate) — cheap ✓ -2. ECDH (Curve25519 scalar mult) — **expensive** ✗ -3. SHA-256 hash — moderate cost - -**Bottleneck**: ECDH is the primary cost driver. For 100k announcements, we compute 100k ECDH operations. - -**Impact**: -- Background sync (Spectre) becomes slow at 10k+ announcements -- Receive page blocks UI when scanning > 1k recent announcements - -**Optimization opportunities**: -1. **Batch ECDH** (3–5x speedup expected) - - Use batched scalar multiplication if underlying crypto library supports it - - Group operations to improve cache locality - -2. **SIMD acceleration** (2–3x speedup expected) - - Use WebAssembly or native bindings for Curve25519 - - Consider @noble/curves SIMD extensions if available - -3. **Algorithm change** (1.5–2x speedup expected, but API-breaking) - - Move to a protocol that avoids per-announcement ECDH (e.g., indexed/hashed approach) - - Trade-off: less privacy but faster scanning - -**Recommended next steps**: -- Profile the exact time spent in ECDH vs. SHA-256 -- Benchmark @noble/curves with vs. without SIMD (if available) -- File a follow-up: `perf: Optimize scanAnnouncements ECDH loop via batching` - -### 🟡 Secondary: Hash Function Composition - -**Current cost**: SHA-256 called multiple times per scan loop - -**Potential gain**: 10–20% via: -- Pre-computed context to reduce allocations -- Streaming hash API if available - -## Regression Budget - -- **Threshold**: 20% (tunable in CI workflow) -- **Rationale**: Cryptographic operations have inherent variability; 20% accommodates natural variation while catching real regressions -- **Policy**: Any benchmark exceeding 20% slower than baseline triggers a PR comment and blocks merge until investigated - -## How to Compare Future Runs - -```bash -# After running pnpm bench on a future branch: -diff baseline.md <(cat bench/results.json | jq -r '.results[] | "\(.name): \(.median)ms"') -``` - -Or manually: compare p50 values and calculate `(new - old) / old * 100`. If > 20%, investigate. - -## Next Steps - -1. ✅ Baseline established -2. ⏳ Optimize `scanAnnouncements` ECDH loop (target: 2–3x speedup) -3. ⏳ Add benchmark regression CI (20% threshold) -4. ⏳ Consider @noble/curves SIMD upgrade -5. ⏳ Benchmark other chains (EVM, Solana, CKB) - ---- - -**For questions**: See `bench/README.md` for detailed interpretation and CI integration details. diff --git a/docs/fee-estimation.md b/docs/fee-estimation.md deleted file mode 100644 index 85028f3..0000000 --- a/docs/fee-estimation.md +++ /dev/null @@ -1,221 +0,0 @@ -# Stellar Fee Estimation - -`estimateStellarFee` is a helper in `@wraith-protocol/sdk/chains/stellar` that gives stealth payment senders a **low / expected / high** fee range before submitting a transaction. This avoids under-paying (rejected) or over-paying (wastes XLM). - ---- - -## Why fees are hard to predict - -Stellar has two fee components: - -| Component | Applies to | Source | -|---|---|---| -| **Inclusion fee** (base fee × op count) | All transactions | Horizon `/fee_stats` → recent ledger p50/p99 | -| **Resource fee** | Soroban (smart contract) only | `simulateTransaction` RPC call | - -The inclusion fee is set by network congestion — it can spike between the time you estimate and submit. The resource fee reflects ledger state at simulation time and may drift before submission. - ---- - -## Installation - -```ts -npm install @wraith-protocol/sdk @stellar/stellar-sdk -``` - ---- - -## API - -```ts -import { estimateStellarFee } from '@wraith-protocol/sdk/chains/stellar'; - -const estimate = await estimateStellarFee(params); -// => { low: number, expected: number, high: number, breakdown: FeeBreakdown } -``` - -All fee values are in **stroops** (1 XLM = 10,000,000 stroops). - -### Parameters - -| Field | Type | Required | Description | -|---|---|---|---| -| `operationCount` | `number` | ✅ | Number of operations in the transaction | -| `sorobanResources` | `SorobanResources` | ❌ | Provide for Soroban invocations | -| `sorobanResources.transactionXdr` | `string` | ✅ (Soroban) | Serialised transaction XDR to simulate | -| `sorobanResources.simulationResult` | `SimulateTransactionResponse` | ❌ | Pre-fetched simulation (skips RPC call) | -| `network` | `'mainnet' \| 'testnet'` | ❌ | Defaults to `'testnet'` | -| `feeStats` | `Horizon.FeeStatsResponse` | ❌ | Pre-fetched fee stats (skips Horizon call) | -| `feeBump` | `boolean` | ❌ | Set `true` when wrapping in a fee-bump envelope | -| `rpcUrl` | `string` | ❌ | Custom Soroban RPC URL | -| `horizonUrl` | `string` | ❌ | Custom Horizon URL | - -### Return value - -```ts -interface FeeEstimate { - low: number; // Protocol minimum — likely rejected under congestion - expected: number; // p50 fee rate — good for non-urgent transactions - high: number; // p99 × 2× surge buffer — maximises inclusion probability - breakdown: { - networkBaseFee: number; - operationCount: number; - feeBumpApplied: boolean; - p50Fee?: number; - p99Fee?: number; - sorobanResourceFee?: number; // Soroban only - sorobanPadding?: number; // 25% padding on resource fee for "high" tier - uncertainty: string; - }; -} -``` - ---- - -## Worked Examples - -### 1 — Classic single-op payment - -```ts -import { estimateStellarFee } from '@wraith-protocol/sdk/chains/stellar'; -import { TransactionBuilder, Networks, Operation, Asset } from '@stellar/stellar-sdk'; - -const estimate = await estimateStellarFee({ - operationCount: 1, - network: 'mainnet', -}); - -console.log(estimate); -// { low: 100, expected: 300, high: 10000, breakdown: { ... } } - -// Use estimate.expected when building your transaction -const tx = new TransactionBuilder(sourceAccount, { - fee: estimate.expected.toString(), - networkPassphrase: Networks.PUBLIC, -}) - .addOperation(Operation.payment({ ... })) - .setTimeout(30) - .build(); -``` - -### 2 — Multi-op transaction - -```ts -const estimate = await estimateStellarFee({ - operationCount: 2, - network: 'mainnet', -}); - -// low = 100 × 2 = 200 stroops -// expected = p50 × 2 -// high = p99 × 2× × 2 -``` - -### 3 — Soroban smart contract invocation - -```ts -import { Contract, TransactionBuilder, Networks } from '@stellar/stellar-sdk'; - -// Build the transaction first (don't submit yet) -const contract = new Contract('CABC...XYZ'); -const tx = new TransactionBuilder(sourceAccount, { - fee: '100', - networkPassphrase: Networks.TESTNET, -}) - .addOperation(contract.call('send_stealth', /* args */)) - .setTimeout(30) - .build(); - -// Estimate fee — simulateTransaction is called internally -const estimate = await estimateStellarFee({ - operationCount: 1, - network: 'testnet', - sorobanResources: { - transactionXdr: tx.toXDR(), - }, -}); - -// { low: 5100, expected: 5300, high: 16250, breakdown: { sorobanResourceFee: 5000, sorobanPadding: 1250 } } -``` - -### 4 — Reusing a pre-fetched simulation - -```ts -// If you already have a simulation result, pass it directly to avoid a second RPC call -const simResult = await server.simulateTransaction(tx); - -const estimate = await estimateStellarFee({ - operationCount: 1, - network: 'testnet', - sorobanResources: { - transactionXdr: tx.toXDR(), - simulationResult: simResult, - }, -}); -``` - -### 5 — Fee-bump transaction - -```ts -const estimate = await estimateStellarFee({ - operationCount: 2, // ops in the INNER transaction - network: 'mainnet', - feeBump: true, // adds 1 for the outer envelope -}); - -// breakdown.operationCount === 3 -// breakdown.feeBumpApplied === true - -const feeBumpTx = TransactionBuilder.buildFeeBumpTransaction( - feeSource, - estimate.expected.toString(), - innerTx, - Networks.PUBLIC, -); -``` - -### 6 — Surfacing estimates in Send / Withdraw UI - -```ts -async function getFeeOptions(txXdr: string) { - const estimate = await estimateStellarFee({ - operationCount: 1, - network: 'mainnet', - sorobanResources: { transactionXdr: txXdr }, - }); - - const toXlm = (stroops: number) => (stroops / 10_000_000).toFixed(7); - - return [ - { label: 'Slow', fee: estimate.low, display: toXlm(estimate.low) }, - { label: 'Normal', fee: estimate.expected, display: toXlm(estimate.expected) }, - { label: 'Priority', fee: estimate.high, display: toXlm(estimate.high) }, - ]; -} -``` - ---- - -## Uncertainty & Caveats - -The `breakdown.uncertainty` field always contains a plain-English explanation. Key points: - -- **Classic fees** use `fee_charged.p50` and `fee_charged.p99` from Horizon `/fee_stats`. These reflect what transactions actually paid in recent ledgers. Congestion can push fees beyond p99 during spikes — use `high` for urgent sends. - -- **Soroban resource fees** come from `simulateTransaction`. If ledger state changes between simulation and submission, the resource fee may differ. The `high` tier adds **25% padding** to mitigate this. - -- **Fee-bump** adds one base-fee unit for the outer envelope per CAP-0015, already factored into `breakdown.operationCount`. - -- **Estimates go stale.** For time-sensitive sends, refresh within the last few ledgers before building. Ledgers close every ~5 seconds. - ---- - -## Running Tests - -```bash -# Unit tests (no network needed) -pnpm test - -# Integration tests against testnet -$env:INTEGRATION="1"; pnpm exec vitest run test/chains/stellar/fee-estimation.integration.test.ts -``` \ No newline at end of file diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx new file mode 100644 index 0000000..7a776a3 --- /dev/null +++ b/docs/getting-started.mdx @@ -0,0 +1,89 @@ +# Getting Started with `@wraith-protocol/sdk` + +Install the package: + +```bash +npm install @wraith-protocol/sdk +# or +pnpm add @wraith-protocol/sdk +``` + +## Entry Points + +| Import path | Purpose | +| ------------------------------------- | ----------------------------------------------- | +| `@wraith-protocol/sdk` | Agent client (`Wraith`, `WraithAgent`, `Chain`) | +| `@wraith-protocol/sdk/chains/evm` | EVM stealth crypto (secp256k1) | +| `@wraith-protocol/sdk/chains/stellar` | Stellar stealth crypto (ed25519) | +| `@wraith-protocol/sdk/chains/solana` | Solana stealth crypto (ed25519) | +| `@wraith-protocol/sdk/chains/ckb` | CKB stealth crypto (secp256k1) | + +## CLI Quick Start (Stellar Send) + +```ts +import { + deriveStealthKeys, + encodeStealthMetaAddress, + generateStealthAddress, +} from '@wraith-protocol/sdk/chains/stellar'; + +const keys = deriveStealthKeys(new Uint8Array(64)); +const metaAddress = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey); +const stealth = generateStealthAddress(keys.spendingPubKey, keys.viewingPubKey); +console.log('Meta-address:', metaAddress); +console.log('Stealth address:', stealth.stealthAddress); +``` + +## React Quick Start (Receive) + +```tsx +import { deriveStealthKeys, encodeStealthMetaAddress } from '@wraith-protocol/sdk/chains/stellar'; + +const keys = deriveStealthKeys(hexToBytes(secretKey)); +const metaAddress = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey); +// Share metaAddress with senders +``` + +## Agent Quick Start + +```ts +import { Wraith, Chain } from '@wraith-protocol/sdk'; + +const wraith = new Wraith({ apiKey: 'wraith_live_...' }); +const agent = await wraith.createAgent({ + name: 'alice', + chain: Chain.Stellar, + wallet: 'G...', + signature: '0x...', +}); +const res = await agent.chat('send 1 XLM to bob.wraith'); +console.log(res.response); +``` + +## Examples + +Five self-contained examples are available in the [`examples/`](https://github.com/wraith-protocol/sdk/tree/main/examples) folder: + +| Example | What it demonstrates | +| ------------------------ | ------------------------------------------------------------ | +| `stellar-cli-send/` | Derive keys, encode meta-address, generate stealth address | +| `stellar-cli-scan/` | Fetch and scan on-chain announcements for incoming payments | +| `stellar-react-receive/` | Vite + React UI to derive keys and display your meta-address | +| `stellar-spectre-agent/` | Create/chat with a Wraith managed agent on Stellar | +| `multichain-scan/` | Parallel scan across Stellar, EVM, Solana, and CKB | + +Each example has its own `package.json` and `.env.example`: + +```bash +cd examples/stellar-cli-send +npm install +cp .env.example .env +# edit .env +npm start +``` + +## Next Steps + +- [Errors & Troubleshooting](./errors.md) +- [Fee Estimation](./fee-estimation.md) +- [Edge Runtime Support](./running-on-the-edge.md) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..b2feffd --- /dev/null +++ b/examples/README.md @@ -0,0 +1,40 @@ +# Wraith SDK Examples + +Five self-contained examples demonstrating the `@wraith-protocol/sdk` across different chains and usage patterns. + +## Example Index + +| Directory | Description | Type | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| `stellar-cli-send/` | Derive stealth keys, encode a meta-address, generate a stealth address for a recipient, print deployment info | CLI | +| `stellar-cli-scan/` | Fetch and scan on-chain announcements to find incoming stealth payments | CLI | +| `stellar-react-receive/` | Minimal Vite + React app — input a secret key, derive stealth keys, view and copy your meta-address | React | +| `stellar-spectre-agent/` | Connect to the Wraith managed agent platform — create/retrieve an agent, chat, check balance, scan payments, send via natural language | Agent | +| `multichain-scan/` | Scan for stealth payments on all 4 chains (Stellar, EVM, Solana, CKB) in parallel via `Promise.all` | CLI | + +## Running an Example + +Each example is self-contained with its own `package.json` and `.env.example`. + +```bash +# 1. Navigate to the example directory +cd examples/stellar-cli-send + +# 2. Install dependencies (links to the SDK via file:../..) +npm install + +# 3. Copy and fill in environment variables +cp .env.example .env +# Edit .env with your keys and addresses + +# 4. Run the example +npm start +``` + +## Notes + +- All examples link to the SDK via `"@wraith-protocol/sdk": "file:../.."` — no need to publish or link manually. +- CLI examples use `tsx` to execute TypeScript directly. +- React example (`stellar-react-receive`) uses Vite — run `npm run dev` for the dev server. +- Agent example (`stellar-spectre-agent`) requires a Wraith API key from https://wraith.dev. +- Multichain example (`multichain-scan`) requires optional peer deps (`@solana/web3.js`, `@stellar/stellar-sdk`) — these are available as workspace devDependencies. diff --git a/examples/multichain-scan/.env.example b/examples/multichain-scan/.env.example new file mode 100644 index 0000000..d5666e1 --- /dev/null +++ b/examples/multichain-scan/.env.example @@ -0,0 +1,6 @@ +# Your secret key (64-byte hex string, 128 hex chars) +# Used to derive stealth keys for all chains. +SECRET_KEY="your-64-byte-hex-secret-key-here" + +# Optional: limit scan range (ISO timestamp or empty for full range) +FROM_TIMESTAMP="" diff --git a/examples/multichain-scan/README.md b/examples/multichain-scan/README.md new file mode 100644 index 0000000..e9db905 --- /dev/null +++ b/examples/multichain-scan/README.md @@ -0,0 +1,34 @@ +# Multichain Stealth Payment Scanner + +A CLI that scans for incoming stealth payments across all 4 supported chains in parallel — Stellar, EVM, Solana, and CKB. + +## How it works + +1. Derives stealth keys from a single secret key +2. Fetches announcements on all 4 chains simultaneously via `Promise.all` +3. Filters announcements owned by this wallet using each chain's scan function +4. Prints matched payments grouped by chain + +## Supported Chains + +| Chain | Fetch function | Scan function | Crypto | +| ------- | -------------------- | ------------------- | --------- | +| Stellar | `fetchAnnouncements` | `scanAnnouncements` | ed25519 | +| EVM | `fetchAnnouncements` | `scanAnnouncements` | secp256k1 | +| Solana | `fetchAnnouncements` | `scanAnnouncements` | ed25519 | +| CKB | `fetchStealthCells` | `scanStealthCells` | secp256k1 | + +## Usage + +```bash +# 1. Copy and fill in the environment variables +cp .env.example .env +# Edit .env with your SECRET_KEY + +# 2. Run the scanner +npm start +``` + +## Output + +The script prints results per chain — total announcements found, matched payments, and details for each match including stealth address, ephemeral public key, and derived private key/scalar. diff --git a/examples/multichain-scan/index.ts b/examples/multichain-scan/index.ts new file mode 100644 index 0000000..dee4da7 --- /dev/null +++ b/examples/multichain-scan/index.ts @@ -0,0 +1,150 @@ +#!/usr/bin/env node +import { deriveStealthKeys as stellarDerive } from '@wraith-protocol/sdk/chains/stellar'; +import { fetchAnnouncements as stellarFetch } from '@wraith-protocol/sdk/chains/stellar'; +import { scanAnnouncements as stellarScan } from '@wraith-protocol/sdk/chains/stellar'; +import { bytesToHex as stellarHex } from '@wraith-protocol/sdk/chains/stellar'; + +import { deriveStealthKeys as evmDerive } from '@wraith-protocol/sdk/chains/evm'; +import { fetchAnnouncements as evmFetch } from '@wraith-protocol/sdk/chains/evm'; +import { scanAnnouncements as evmScan } from '@wraith-protocol/sdk/chains/evm'; + +import { deriveStealthKeys as solanaDerive } from '@wraith-protocol/sdk/chains/solana'; +import { fetchAnnouncements as solanaFetch } from '@wraith-protocol/sdk/chains/solana'; +import { scanAnnouncements as solanaScan } from '@wraith-protocol/sdk/chains/solana'; + +import { deriveStealthKeys as ckbDerive } from '@wraith-protocol/sdk/chains/ckb'; +import { fetchStealthCells } from '@wraith-protocol/sdk/chains/ckb'; +import { scanStealthCells } from '@wraith-protocol/sdk/chains/ckb'; + +interface ScanResult { + chain: string; + total: number; + matches: number; + error?: string; + details: string[]; +} + +function getEnv(name: string): string | undefined { + return process.env[name] || undefined; +} + +function hexToBytes(hex: string): Uint8Array { + return new Uint8Array(hex.match(/.{1,2}/g)!.map((b) => parseInt(b, 16))); +} + +async function scanStellar(sigHex: string): Promise { + const sig = hexToBytes(sigHex); + const keys = stellarDerive(sig); + const announcements = await stellarFetch('stellar'); + const matches = stellarScan( + announcements, + keys.viewingKey, + keys.spendingPubKey, + keys.spendingScalar, + ); + return { + chain: 'Stellar', + total: announcements.length, + matches: matches.length, + details: matches.map( + (m) => + ` Address: ${m.stealthAddress}\n Ephemeral: ${stellarHex(m.ephemeralPubKey as unknown as Uint8Array)}\n Scalar: ${m.stealthPrivateScalar}`, + ), + }; +} + +async function scanEvm(sigHex: string): Promise { + const sig = `0x${sigHex}` as `0x${string}`; + const keys = evmDerive(sig); + const announcements = await evmFetch('horizen'); + const matches = evmScan(announcements, keys.viewingKey, keys.spendingPubKey, keys.spendingKey); + return { + chain: 'EVM', + total: announcements.length, + matches: matches.length, + details: matches.map( + (m) => ` Address: ${m.stealthAddress}\n Private key: ${m.stealthPrivateKey}`, + ), + }; +} + +async function scanSolana(sigHex: string): Promise { + const sig = hexToBytes(sigHex); + const keys = solanaDerive(sig); + const announcements = await solanaFetch('solana'); + const matches = solanaScan( + announcements, + keys.viewingKey, + keys.spendingPubKey, + keys.spendingScalar, + ); + return { + chain: 'Solana', + total: announcements.length, + matches: matches.length, + details: matches.map( + (m) => ` Address: ${m.stealthAddress}\n Scalar: ${m.stealthPrivateScalar}`, + ), + }; +} + +async function scanCkb(sigHex: string): Promise { + const sig = `0x${sigHex}` as `0x${string}`; + const keys = ckbDerive(sig); + const cells = await fetchStealthCells('ckb'); + const matches = scanStealthCells(cells, keys.viewingKey, keys.spendingPubKey, keys.spendingKey); + return { + chain: 'CKB', + total: cells.length, + matches: matches.length, + details: matches.map( + (m) => + ` Lock args: ${m.lockArgs}\n Capacity: ${m.capacity}\n Private key: ${m.stealthPrivateKey}`, + ), + }; +} + +async function main() { + console.log('=== Wraith Multichain Scanner ===\n'); + + const secretKey = getEnv('SECRET_KEY'); + if (!secretKey) { + console.error('ERROR: Missing SECRET_KEY'); + console.error('Copy .env.example to .env and fill in the values.'); + process.exit(1); + } + console.log(`Scanning all chains with key: ${secretKey.slice(0, 16)}...\n`); + + const scanners = [ + scanStellar(secretKey), + scanEvm(secretKey), + scanSolana(secretKey), + scanCkb(secretKey), + ]; + + const results = await Promise.allSettled(scanners); + let totalMatches = 0; + + for (const result of results) { + if (result.status === 'fulfilled') { + const r = result.value; + console.log(`--- ${r.chain} ---`); + console.log(` Total announcements: ${r.total}`); + console.log(` Matches: ${r.matches}`); + for (const detail of r.details) { + console.log(detail); + } + totalMatches += r.matches; + } else { + console.error(` Error: ${result.reason}`); + } + console.log(''); + } + + console.log(`=== Done — ${totalMatches} total match(es) across all chains ===`); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/examples/multichain-scan/package.json b/examples/multichain-scan/package.json new file mode 100644 index 0000000..71e66e7 --- /dev/null +++ b/examples/multichain-scan/package.json @@ -0,0 +1,16 @@ +{ + "name": "@wraith-protocol/example-multichain-scan", + "private": true, + "type": "module", + "main": "index.ts", + "scripts": { + "start": "npx tsx index.ts" + }, + "dependencies": { + "@wraith-protocol/sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.0.0", + "typescript": "^5.7.0" + } +} diff --git a/examples/react-native-stellar/App.tsx b/examples/react-native-stellar/App.tsx index 51fa323..b79bbea 100644 --- a/examples/react-native-stellar/App.tsx +++ b/examples/react-native-stellar/App.tsx @@ -15,7 +15,11 @@ function bytesToHex(bytes: Uint8Array): string { const sampleSignature = new Uint8Array(Array.from({ length: 64 }, (_, i) => i + 1)); -const announcementsFixture = (stealthAddress: string, ephemeralPubKey: Uint8Array, viewTag: number) => [ +const announcementsFixture = ( + stealthAddress: string, + ephemeralPubKey: Uint8Array, + viewTag: number, +) => [ { schemeId: 1, stealthAddress, @@ -28,9 +32,22 @@ const announcementsFixture = (stealthAddress: string, ephemeralPubKey: Uint8Arra export default function App() { const { keys, stealth, matches } = useMemo(() => { const keys = deriveStealthKeys(sampleSignature); - const stealth = generateStealthAddress(keys.spendingPubKey, keys.viewingPubKey, new Uint8Array(32).fill(0x42)); - const announcements = announcementsFixture(stealth.stealthAddress, stealth.ephemeralPubKey, stealth.viewTag); - const matches = scanAnnouncements(announcements, keys.viewingKey, keys.spendingPubKey, keys.spendingScalar); + const stealth = generateStealthAddress( + keys.spendingPubKey, + keys.viewingPubKey, + new Uint8Array(32).fill(0x42), + ); + const announcements = announcementsFixture( + stealth.stealthAddress, + stealth.ephemeralPubKey, + stealth.viewTag, + ); + const matches = scanAnnouncements( + announcements, + keys.viewingKey, + keys.spendingPubKey, + keys.spendingScalar, + ); return { keys, stealth, matches }; }, []); @@ -51,7 +68,9 @@ export default function App() { Scan Result Matches found: {matches.length} - {matches.length > 0 ? matches[0].stealthAddress : 'none'} + + {matches.length > 0 ? matches[0].stealthAddress : 'none'} + diff --git a/examples/react-native-stellar/polyfills.ts b/examples/react-native-stellar/polyfills.ts index 8a72d92..b64410c 100644 --- a/examples/react-native-stellar/polyfills.ts +++ b/examples/react-native-stellar/polyfills.ts @@ -35,12 +35,17 @@ if (typeof globalThis.btoa === 'undefined') { let str = input; let output = ''; - for (let block = 0, charCode, idx = 0, map = chars; str.charAt(idx | 0) || ((map = '='), idx % 1); ) { + for ( + let block = 0, charCode, idx = 0, map = chars; + str.charAt(idx | 0) || ((map = '='), idx % 1); + ) { charCode = str.charCodeAt((idx += 3 / 4)); if (charCode > 0xff) { - throw new Error('Failed to execute btoa: The string to be encoded contains characters outside of the Latin1 range.'); + throw new Error( + 'Failed to execute btoa: The string to be encoded contains characters outside of the Latin1 range.', + ); } - output += map.charAt((block = (block << 8) | charCode) >> ((3.5 - (idx % 1)) * 8) & 0x3f); + output += map.charAt(((block = (block << 8) | charCode) >> ((3.5 - (idx % 1)) * 8)) & 0x3f); } return output; diff --git a/examples/stellar-cli-scan/.env.example b/examples/stellar-cli-scan/.env.example new file mode 100644 index 0000000..c349a8f --- /dev/null +++ b/examples/stellar-cli-scan/.env.example @@ -0,0 +1,7 @@ +# Your Stellar wallet secret key (64-byte hex string) +# Used to derive stealth viewing/spending keys. +STELLAR_SECRET_KEY="your-64-byte-hex-secret-key-here" + +# ISO timestamp or Unix seconds to scan announcements from (e.g. "2025-01-01T00:00:00Z") +# Leave empty to scan from the beginning. +FROM_TIMESTAMP="" diff --git a/examples/stellar-cli-scan/README.md b/examples/stellar-cli-scan/README.md new file mode 100644 index 0000000..b2389bf --- /dev/null +++ b/examples/stellar-cli-scan/README.md @@ -0,0 +1,30 @@ +# Stellar CLI — Scan Stealth Payments + +Demonstrates how to scan for incoming stealth payments on Stellar using the `@wraith-protocol/sdk/chains/stellar` module. + +## How it works + +1. Derives stealth viewing/spending keys from a secret key +2. Fetches announcements from the Soroban RPC via `fetchAnnouncements` +3. Filters announcements owned by this wallet via `scanAnnouncements` +4. Prints each matched payment — stealth address, public key, and derived private scalar + +## Usage + +```bash +# 1. Copy and fill in the environment variables +cp .env.example .env +# Edit .env with your secret key and optional FROM_TIMESTAMP + +# 2. Run the CLI +npm start +``` + +## Output + +The script prints: + +- The derived viewing key and spending public key +- Total announcements found on-chain +- Number of payments matching your wallet +- For each match: stealth address, stealth public key bytes, and the derived private scalar needed to sign spends diff --git a/examples/stellar-cli-scan/index.ts b/examples/stellar-cli-scan/index.ts new file mode 100644 index 0000000..ecbccf4 --- /dev/null +++ b/examples/stellar-cli-scan/index.ts @@ -0,0 +1,77 @@ +#!/usr/bin/env node +import { + deriveStealthKeys, + fetchAnnouncements, + scanAnnouncements, + bytesToHex, + STEALTH_SIGNING_MESSAGE, +} from '@wraith-protocol/sdk/chains/stellar'; + +function getEnv(name: string): string | undefined { + return process.env[name] || undefined; +} + +async function main() { + console.log('=== Wraith Stellar CLI — Scan Stealth Payments ===\n'); + + // 1. Derive stealth keys from secret key + const secretKeyHex = getEnv('STELLAR_SECRET_KEY'); + if (!secretKeyHex) { + console.error('ERROR: Missing STELLAR_SECRET_KEY'); + console.error('Copy .env.example to .env and fill in the values.'); + process.exit(1); + } + const secretKeyBytes = new Uint8Array(secretKeyHex.match(/.{1,2}/g)!.map((b) => parseInt(b, 16))); + const keys = deriveStealthKeys(secretKeyBytes); + console.log('Viewing key:', bytesToHex(keys.viewingKey)); + console.log('Spending pub key:', bytesToHex(keys.spendingPubKey)); + console.log(''); + + // 2. Parse optional fromTimestamp + const fromTimestampRaw = getEnv('FROM_TIMESTAMP'); + const fromTimestamp = fromTimestampRaw + ? isNaN(Number(fromTimestampRaw)) + ? new Date(fromTimestampRaw) + : new Date(Number(fromTimestampRaw) * 1000) + : undefined; + console.log('Scanning announcements from:', fromTimestamp?.toISOString() ?? 'beginning of time'); + console.log(''); + + // 3. Fetch announcements from Soroban RPC + console.log('Fetching announcements...'); + const { announcements, nextCursor } = await fetchAnnouncements('stellar', { + fromTimestamp, + }); + console.log(`Found ${announcements.length} total announcements`); + console.log(`Next scan cursor: ${nextCursor ?? 'none'}`); + console.log(''); + + // 4. Scan for payments addressed to us + const payments = scanAnnouncements( + announcements, + keys.viewingKey, + keys.spendingPubKey, + keys.spendingScalar, + ); + console.log(`Found ${payments.length} payment(s) for this wallet`); + console.log(''); + + // 5. Print matches + for (const payment of payments) { + console.log('--- Matched Payment ---'); + console.log('Stealth address:', payment.stealthAddress); + console.log('Stealth pub key:', bytesToHex(payment.stealthPubKeyBytes)); + console.log('Stealth private scalar:', payment.stealthPrivateScalar.toString()); + console.log(''); + } + + console.log('=== Done ==='); + if (payments.length === 0) { + console.log('No stealth payments found for this wallet in the scanned range.'); + } +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/examples/stellar-cli-scan/package.json b/examples/stellar-cli-scan/package.json new file mode 100644 index 0000000..e43dcd9 --- /dev/null +++ b/examples/stellar-cli-scan/package.json @@ -0,0 +1,16 @@ +{ + "name": "@wraith-protocol/example-stellar-cli-scan", + "private": true, + "type": "module", + "main": "index.ts", + "scripts": { + "start": "npx tsx index.ts" + }, + "dependencies": { + "@wraith-protocol/sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.0.0", + "typescript": "^5.7.0" + } +} diff --git a/examples/stellar-cli-send/.env.example b/examples/stellar-cli-send/.env.example new file mode 100644 index 0000000..40e610a --- /dev/null +++ b/examples/stellar-cli-send/.env.example @@ -0,0 +1,7 @@ +# Your Stellar wallet secret key (base64-encoded ed25519 seed, or the raw 64-byte hex) +# Used to derive stealth keys for receiving. For a real wallet, use wallet.sign(STEALTH_SIGNING_MESSAGE). +STELLAR_SECRET_KEY="your-64-byte-hex-secret-key-here" + +# The recipient's stealth meta-address (st:xlm:... format) +# This is the address you want to send funds to privately. +RECIPIENT_META_ADDRESS="st:xlm:..." diff --git a/examples/stellar-cli-send/README.md b/examples/stellar-cli-send/README.md new file mode 100644 index 0000000..f17513a --- /dev/null +++ b/examples/stellar-cli-send/README.md @@ -0,0 +1,32 @@ +# Stellar CLI — Send Stealth Payment + +Demonstrates how to generate a stealth address for a recipient on Stellar using the `@wraith-protocol/sdk/chains/stellar` module. + +## How it works + +1. Derives your own stealth keys from a secret key (representing `wallet.sign(STEALTH_SIGNING_MESSAGE)`) +2. Encodes and displays your own stealth meta-address (`st:xlm:...`) +3. Decodes the recipient's meta-address from the `RECIPIENT_META_ADDRESS` env var +4. Generates a one-time stealth address for the recipient using ECDH and stores +5. Prints deployment info (Horizon URL, announcer contract address) + +## Usage + +```bash +# 1. Copy and fill in the environment variables +cp .env.example .env +# Edit .env with your secret key and the recipient's meta-address + +# 2. Run the CLI +npm start +``` + +## Output + +The script prints: + +- Your own stealth meta-address (share this with senders) +- The recipient's decoded public keys +- The generated one-time stealth address to send XLM to +- The ephemeral public key and view tag needed for the on-chain announcement +- The Soroban announcer contract and Horizon URL for the selected network diff --git a/examples/stellar-cli-send/index.ts b/examples/stellar-cli-send/index.ts new file mode 100644 index 0000000..d770083 --- /dev/null +++ b/examples/stellar-cli-send/index.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env node +import { + deriveStealthKeys, + encodeStealthMetaAddress, + generateStealthAddress, + decodeStealthMetaAddress, + getDeployment, + STEALTH_SIGNING_MESSAGE, + bytesToHex, + SCHEME_ID, +} from '@wraith-protocol/sdk/chains/stellar'; + +function getEnv(name: string): string { + const value = process.env[name]; + if (!value) { + console.error(`ERROR: Missing environment variable ${name}`); + console.error(`Copy .env.example to .env and fill in the values.`); + process.exit(1); + } + return value; +} + +async function main() { + console.log('=== Wraith Stellar CLI — Send Stealth Payment ===\n'); + + // 1. Derive our stealth keys from a secret key + // In production, call wallet.sign(STEALTH_SIGNING_MESSAGE) instead. + const secretKeyHex = getEnv('STELLAR_SECRET_KEY'); + const secretKeyBytes = new Uint8Array(secretKeyHex.match(/.{1,2}/g)!.map((b) => parseInt(b, 16))); + const ourKeys = deriveStealthKeys(secretKeyBytes); + const ourMetaAddress = encodeStealthMetaAddress(ourKeys.spendingPubKey, ourKeys.viewingPubKey); + console.log('Our stealth meta-address:', ourMetaAddress); + console.log(''); + + // 2. Decode the recipient's meta-address + const recipientMetaAddress = getEnv('RECIPIENT_META_ADDRESS'); + const decoded = decodeStealthMetaAddress(recipientMetaAddress); + console.log('Recipient meta-address:', recipientMetaAddress); + console.log('Recipient spending pub key:', bytesToHex(decoded.spendingPubKey)); + console.log('Recipient viewing pub key:', bytesToHex(decoded.viewingPubKey)); + console.log(''); + + // 3. Generate a stealth address for the recipient + const stealth = generateStealthAddress(decoded.spendingPubKey, decoded.viewingPubKey); + console.log('Generated stealth address:', stealth.stealthAddress); + console.log('Ephemeral pub key:', bytesToHex(stealth.ephemeralPubKey)); + console.log('View tag:', stealth.viewTag); + console.log(''); + + // 4. Get deployment info + const deployment = getDeployment('stellar'); + console.log('Network:', deployment.network); + console.log('Horizon URL:', deployment.horizonUrl); + console.log('Announcer contract:', deployment.contracts.announcer); + console.log(''); + + console.log('=== Done ==='); + console.log(`Send XLM to ${stealth.stealthAddress}, then announce via`); + console.log(`the Soroban contract at ${deployment.contracts.announcer}`); + console.log(`with scheme_id=${SCHEME_ID}, ephemeral_pub_key, and view_tag.`); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/examples/stellar-cli-send/package.json b/examples/stellar-cli-send/package.json new file mode 100644 index 0000000..0eab809 --- /dev/null +++ b/examples/stellar-cli-send/package.json @@ -0,0 +1,16 @@ +{ + "name": "@wraith-protocol/example-stellar-cli-send", + "private": true, + "type": "module", + "main": "index.ts", + "scripts": { + "start": "npx tsx index.ts" + }, + "dependencies": { + "@wraith-protocol/sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.0.0", + "typescript": "^5.7.0" + } +} diff --git a/examples/stellar-react-receive/.env.example b/examples/stellar-react-receive/.env.example new file mode 100644 index 0000000..e9d6789 --- /dev/null +++ b/examples/stellar-react-receive/.env.example @@ -0,0 +1,2 @@ +# Pre-fill the secret key input (64-byte hex string) +VITE_STELLAR_SECRET_KEY="" diff --git a/examples/stellar-react-receive/README.md b/examples/stellar-react-receive/README.md new file mode 100644 index 0000000..c757910 --- /dev/null +++ b/examples/stellar-react-receive/README.md @@ -0,0 +1,31 @@ +# Stellar React — Receive Stealth Payments + +A minimal Vite + React app that demonstrates deriving Wraith stealth keys on Stellar and displaying the stealth meta-address. + +## How it works + +1. Paste your 64-byte hex secret key into the textarea +2. Click **Derive Stealth Keys** — calls `deriveStealthKeys` from `@wraith-protocol/sdk/chains/stellar` +3. View your derived spending key, viewing key, and stealth meta-address (`st:xlm:...`) +4. Click the meta-address to copy it — share with senders + +## Usage + +```bash +# Install dependencies +npm install + +# Optionally pre-fill the secret key via env +cp .env.example .env +# Edit .env with VITE_STELLAR_SECRET_KEY + +# Start the dev server +npm run dev +``` + +## Build + +```bash +npm run build +npm run preview +``` diff --git a/examples/stellar-react-receive/index.html b/examples/stellar-react-receive/index.html new file mode 100644 index 0000000..6c763d6 --- /dev/null +++ b/examples/stellar-react-receive/index.html @@ -0,0 +1,12 @@ + + + + + + Wraith Stellar — Receive Stealth Payments + + +
+ + + diff --git a/examples/stellar-react-receive/package.json b/examples/stellar-react-receive/package.json new file mode 100644 index 0000000..44f91fd --- /dev/null +++ b/examples/stellar-react-receive/package.json @@ -0,0 +1,22 @@ +{ + "name": "@wraith-protocol/example-stellar-react-receive", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@wraith-protocol/sdk": "file:../..", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.4.0", + "typescript": "^5.7.0", + "vite": "^6.1.0" + } +} diff --git a/examples/stellar-react-receive/src/App.tsx b/examples/stellar-react-receive/src/App.tsx new file mode 100644 index 0000000..c410360 --- /dev/null +++ b/examples/stellar-react-receive/src/App.tsx @@ -0,0 +1,150 @@ +import { useState, useCallback } from 'react'; +import { + deriveStealthKeys, + encodeStealthMetaAddress, + bytesToHex, + STEALTH_SIGNING_MESSAGE, +} from '@wraith-protocol/sdk/chains/stellar'; + +type Keys = ReturnType; + +function parseHex(hex: string): Uint8Array | null { + if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2 !== 0) return null; + return new Uint8Array(hex.match(/.{1,2}/g)!.map((b) => parseInt(b, 16))); +} + +export default function App() { + const [input, setInput] = useState(() => import.meta.env.VITE_STELLAR_SECRET_KEY ?? ''); + const [keys, setKeys] = useState(null); + const [error, setError] = useState(null); + + const handleDerive = useCallback(() => { + setError(null); + setKeys(null); + const trimmed = input.trim(); + if (!trimmed) { + setError('Please enter a secret key.'); + return; + } + const bytes = parseHex(trimmed); + if (!bytes) { + setError('Invalid hex string. Must be even-length hex.'); + return; + } + if (bytes.length !== 64) { + setError(`Expected 64 bytes, got ${bytes.length}.`); + return; + } + try { + const derived = deriveStealthKeys(bytes); + setKeys(derived); + } catch (e) { + setError(e instanceof Error ? e.message : 'Key derivation failed.'); + } + }, [input]); + + const metaAddress = keys && encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey); + + return ( +
+

Wraith Stellar — Receive Stealth Payments

+

+ Enter your 64-byte hex secret key (the raw input to deriveStealthKeys) to + derive your stealth keys and meta-address. Share the meta-address with senders. +

+ +
+ +