Skip to content

Commit b5b2f02

Browse files
Merge branch 'develop' into docs/stellar-multisig-withdrawal
2 parents cdc7bec + 877737c commit b5b2f02

24 files changed

Lines changed: 4575 additions & 110 deletions

.github/workflows/snippets.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Snippet checks
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- develop
8+
- main
9+
10+
jobs:
11+
check-snippets:
12+
name: Compile docs snippets
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Node
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: 22
22+
cache: npm
23+
24+
- name: Install dependencies
25+
run: npm ci
26+
27+
- name: Check snippets
28+
run: npm run check:snippets
29+
30+
stellar-testnet-snippets:
31+
name: Stellar snippet testnet validation
32+
runs-on: ubuntu-latest
33+
continue-on-error: true
34+
steps:
35+
- name: Checkout
36+
uses: actions/checkout@v4
37+
38+
- name: Setup Node
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version: 22
42+
cache: npm
43+
44+
- name: Install dependencies
45+
run: npm ci
46+
47+
- name: Validate Stellar testnet snippets
48+
run: npm run check:stellar-testnet

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
reference/
2+
node_modules/
3+
.npm/

api-reference/endpoints.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Create a new agent with a `.wraith` name.
3434

3535
**Request:**
3636

37-
```typescript
37+
```typescript no-check
3838
{
3939
name: string; // "alice" (becomes alice.wraith)
4040
chain: string; // "horizen" | "stellar" | "ethereum" | ...
@@ -154,7 +154,7 @@ Send a natural language message to the AI agent.
154154

155155
**Request:**
156156

157-
```typescript
157+
```typescript no-check
158158
{
159159
message: string;
160160
conversationId?: string; // continue existing conversation
@@ -163,7 +163,7 @@ Send a natural language message to the AI agent.
163163

164164
**Response:**
165165

166-
```typescript
166+
```typescript no-check
167167
{
168168
response: string; // agent's text reply
169169
toolCalls?: ToolCall[]; // tools the agent executed
@@ -276,7 +276,7 @@ GET /agent/:id/notifications
276276

277277
**Response:**
278278

279-
```typescript
279+
```typescript no-check
280280
{
281281
notifications: Notification[];
282282
unreadCount: number;
@@ -363,7 +363,7 @@ All errors return JSON with a `message` field:
363363

364364
### Example
365365

366-
```typescript
366+
```typescript no-check
367367
// 400 Bad Request
368368
{
369369
"message": "Name must be 3-32 characters, lowercase alphanumeric and hyphens only",

api-reference/types.mdx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ enum Chain {
3737
Base = "base",
3838
Stellar = "stellar",
3939
Solana = "solana",
40+
CKB = "ckb",
4041
All = "all",
4142
}
4243
```
@@ -281,6 +282,10 @@ import type {
281282
GeneratedStealthAddress,
282283
Announcement,
283284
MatchedAnnouncement,
285+
FederationRecord,
286+
FederationCache,
287+
FederationError,
288+
FederationErrorCode,
284289
} from "@wraith-protocol/sdk/chains/stellar";
285290
```
286291

@@ -330,6 +335,72 @@ interface MatchedAnnouncement extends Announcement {
330335

331336
---
332337

338+
## Stellar Federation Types
339+
340+
Exported from `@wraith-protocol/sdk/chains/stellar`:
341+
342+
```typescript
343+
import type {
344+
FederationRecord,
345+
FederationCache,
346+
FederationError,
347+
FederationErrorCode,
348+
} from "@wraith-protocol/sdk/chains/stellar";
349+
```
350+
351+
### `FederationRecord`
352+
353+
The resolved result of a `name*domain.com` lookup.
354+
355+
```typescript
356+
interface FederationRecord {
357+
federationAddress: string; // "alice*example.com" — the address that was queried
358+
accountId: string; // "GABC..." or "st:xlm:..." — resolved destination
359+
memoType?: "text" | "id" | "hash";
360+
memoValue?: string; // required for exchange deposit addresses
361+
}
362+
```
363+
364+
When `accountId` starts with `st:xlm:` it is a Wraith stealth meta-address and should be decoded with `decodeStealthMetaAddress()` before sending. Otherwise it is a plain `G...` public key.
365+
366+
### `FederationCache`
367+
368+
Pluggable cache interface accepted by `resolveStellarFederation()`. Implement this with any backend (in-memory, Redis, etc.).
369+
370+
```typescript
371+
interface FederationCache {
372+
get(key: string): Promise<FederationRecord | undefined>;
373+
set(key: string, record: FederationRecord, ttlMs: number): Promise<void>;
374+
}
375+
```
376+
377+
### `FederationErrorCode`
378+
379+
```typescript
380+
type FederationErrorCode =
381+
| "NOT_FOUND" // federation server returned 404 / unknown address
382+
| "DNS_FAILURE" // could not fetch stellar.toml (network or DNS error)
383+
| "NO_FEDERATION_SERVER" // stellar.toml exists but has no FEDERATION_SERVER field
384+
| "INVALID_TOML" // stellar.toml content is malformed
385+
| "MALFORMED_RESPONSE" // federation server response is missing required fields
386+
| "TIMEOUT" // request exceeded options.timeoutMs
387+
| "NETWORK_ERROR"; // fetch failed for any other reason
388+
```
389+
390+
### `FederationError`
391+
392+
Thrown by `resolveStellarFederation()` on any failure. Always check `err.code` rather than `err.message` for programmatic handling.
393+
394+
```typescript
395+
interface FederationError extends Error {
396+
code: FederationErrorCode;
397+
message: string;
398+
cause?: unknown; // the underlying network error or parse error, if any
399+
}
400+
```
401+
402+
---
403+
333404
## Chain Connector Types
334405

335406
Internal types used by the TEE server. Documented here for developers building custom chain connectors.

architecture/chain-connectors.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ class ChainRegistry {
347347

348348
### Usage in Agent Service
349349

350-
```typescript
350+
```typescript no-check
351351
async sendPayment(agentId: string, recipient: string, amount: string) {
352352
const agent = await this.db.agents.findOneBy({ id: agentId });
353353
const connector = this.chainRegistry.get(agent.chain);

architecture/overview.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ The AI can call multiple tools in one response. A cap (10 iterations) prevents i
7272

7373
Each tool call routes to the appropriate chain connector:
7474

75-
```typescript
75+
```typescript no-check
7676
import { Chain } from "@wraith-protocol/sdk";
7777

7878
// The agent service resolves the connector automatically
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
title: "Stellar Cryptography"
3+
description: "Design rationale, view tag derivation, and RFC-compatible cryptography for Stellar stealth payments."
4+
---
5+
6+
Wraith Protocol implements a non-interactive stealth payment scheme on Stellar. This page documents the cryptography decisions behind the implementation and exactly where each concept is realized in the SDK.
7+
8+
## Why ed25519?
9+
10+
Unlike EVM environments, which rely on `secp256k1`, the Stellar network uses the **ed25519** curve for all account addressing and signatures.
11+
To ensure that stealth accounts are valid Stellar accounts that can sign transactions, the protocol's stealth derivations must perform point addition on the ed25519 curve.
12+
13+
- **Curve definition**: `scalar.ts:1` (via [@noble/curves/ed25519](https://github.com/paulmillr/noble-curves))
14+
15+
## X25519 ECDH and Edwards-to-Montgomery Conversion
16+
17+
Standard ed25519 points (in Edwards form) are optimized for signing, not for Diffie-Hellman key exchange. To securely establish a shared secret between sender and receiver without interaction, we must use **X25519** ECDH.
18+
This requires converting the public and private ed25519 keys from Edwards coordinates to Montgomery coordinates, as specified in [RFC 7748](https://datatracker.ietf.org/doc/html/rfc7748).
19+
20+
- **Edwards-to-Montgomery conversion**: `stealth.ts:91-92`
21+
- **X25519 shared secret**: `stealth.ts:20` and `stealth.ts:93`
22+
23+
## Domain Separation Prefixes
24+
25+
We use domain-separation prefixes in SHA-256 hashes to prevent cryptographic collisions between different key derivation phases.
26+
27+
- `wraith:spending:`: Separates the derivation of the spending seed (`keys.ts:25`).
28+
- `wraith:viewing:`: Separates the derivation of the viewing seed (`keys.ts:26`).
29+
- `wraith:scalar:`: Prevents the hash scalar from colliding with the base shared secret before it's reduced modulo L (`scalar.ts:202`, `scalar.ts:220`).
30+
- `wraith:stellar:view-tag:v2:`: Domains the derivation for the 1-byte view tag (`stealth.ts:8`).
31+
- `wraith:tag:`: The legacy v1 view tag prefix (`stealth.ts:9`).
32+
33+
## View Tag Derivation
34+
35+
To avoid performing an expensive X25519 ECDH operation for every incoming transaction, the sender derives a 1-byte **view tag** and publishes it alongside their ephemeral public key.
36+
37+
**Derivation:**
38+
```
39+
view_tag = SHA-256("wraith:stellar:view-tag:v2:" || R_ephemeral || V_recipient)[0]
40+
```
41+
42+
- **Implementation**: `stealth.ts:99`
43+
- **Performance impact**: This creates a cheap public prefilter before the X25519 shared secret computation (`scan.ts:12`).
44+
- **False-positive rate**: A 1-byte tag produces a false-positive rate of `1/256` (~0.39%). For non-matching announcements, the protocol skips the expensive elliptic curve operations 99.61% of the time.
45+
46+
```mermaid
47+
sequenceDiagram
48+
participant Network
49+
participant Scanner
50+
Network->>Scanner: Fetch Announcements (R, view_tag)
51+
Note over Scanner: Compare cheap view_tag first
52+
alt Match view_tag
53+
Scanner->>Scanner: X25519(v, R) -> shared_secret
54+
Scanner->>Scanner: Derive expected stealth address
55+
alt Match Address
56+
Scanner->>Network: Recovered match!
57+
end
58+
else Mismatch view_tag
59+
Note over Scanner: Skip (99.61% of non-matches)
60+
end
61+
```
62+
63+
## Private Scalar vs. Seeds and RFC 8032
64+
65+
Standard ed25519 signing libraries expect a 32-byte seed as the private key, which they hash (via SHA-512) to produce both the private scalar and a deterministic nonce.
66+
67+
In our non-interactive stealth scheme, the stealth private key is a *derived scalar*, not a raw seed:
68+
```
69+
stealth_scalar = (spending_scalar + hash_scalar) mod L
70+
```
71+
72+
Because we only hold the resulting scalar, we cannot use off-the-shelf seed-based signing APIs. Instead, the SDK exposes a custom `signWithScalar` function to deterministically sign transactions using a raw scalar directly, while maintaining strict [RFC 8032](https://datatracker.ietf.org/doc/html/rfc8032) compatibility for ed25519 signatures.
73+
74+
- **`signWithScalar` implementation**: `scalar.ts:251`
75+
76+
## Meta-Address Encoding
77+
78+
To accept stealth payments, users publish a single "meta-address" that encapsulates both their spending and viewing public keys.
79+
80+
- **Prefix**: `st:xlm:` (`constants.ts:43`).
81+
- **Encoding**: Consists of the prefix concatenated with the hex-encoded 32-byte spending public key and the 32-byte viewing public key (`meta-address.ts:10`).
82+
- **Stellar StrKey compatibility**: To turn the final derived public stealth key into a standard Stellar address format (`G...`), we utilize Stellar's `StrKey` encoding logic (`scalar.ts:171`).
83+
84+
## Key Derivation Overview
85+
86+
```mermaid
87+
flowchart TD
88+
S(Sender) -->|Generates| r(Ephemeral Private Key 'r')
89+
r --> R(Ephemeral Public Key 'R')
90+
S --> |Recipient's| V(Viewing Public Key 'V')
91+
S --> |Recipient's| K(Spending Public Key 'K')
92+
r & V --> X25519(X25519 ECDH)
93+
X25519 --> SS(Shared Secret)
94+
R & V --> VT(View Tag)
95+
SS --> |Hash mod L| HS(Hash Scalar)
96+
HS & K --> |Point Addition| SP(Stealth Public Key)
97+
SP --> |StrKey Encoding| SA(Stellar Address 'G...')
98+
```

architecture/tee.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ The chain connector determines how the raw seed becomes usable keys:
5858

5959
Every time an operation needs agent keys (sending a payment, scanning, withdrawing), the keys are re-derived from the TEE root secret. No private key material touches disk.
6060

61-
```typescript
61+
```typescript no-check
6262
// Every chat message re-derives keys
6363
async chat(agentId: string, message: string) {
6464
const agent = await this.db.agents.findOneBy({ id: agentId });

0 commit comments

Comments
 (0)