Developer guide
Prerequisites
- Rust 1.84 or newer (
rustup) - WASM target:
rustup target add wasm32v1-none - Stellar CLI v22 or newer
- Node 20+ for the SDK and app
Addresses and links
| What | Value |
|---|---|
| Distributor, testnet | CC25WDHZ7SQC2UMZJHIPYM2U5ROYFXVDZMP3THMIL3IJY6HOCGGX2FPG |
| Distributor, pubnet | not deployed |
| WASM artifact | target/wasm32v1-none/release/yieldstream_distributor.wasm |
| Test vectors | test-vectors/merkle_v1.json |
Build and test
cd yieldstream-contract cargo test # 21 tests stellar contract build # optimized WASM
Deploy to testnet
stellar keys generate --network testnet --fund stellar contract deploy \ --network testnet \ --source-account <IDENTITY> \ --wasm target/wasm32v1-none/release/yieldstream_distributor.wasm stellar contract invoke \ --network testnet --source-account <IDENTITY> \ --id <CONTRACT> \ -- initialize --admin <ADDRESS>
--source-account selects the signing identity. The short flag -i means --ignore-checks, not identity — mixing them up is a fast way to submit the wrong payload.
The leaf format, precisely
leaf = SHA256( 0x00
|| XDR(distribution_id : u32)
|| XDR(index : u32)
|| XDR(recipient : Address)
|| XDR(amount : i128) )
node = SHA256( 0x01 || min(a,b) || max(a,b) )Three details that break implementations when guessed:
- XDR, not raw bytes.
u32is 4 big-endian bytes.i128is 16 big-endian bytes.Addressis the ScVal account form with a 32-byte payload and its discriminating prefix — not the base-32G…/C…string. - Field order is fixed as written above. Any permutation gives a different root.
- Siblings are sorted before hashing, so a proof needs no direction flags.
Conformance: the vectors are the spec
merkle_v1.json pins two trees — a 4-leaf campaign (id 1, 120,000 total, root 627e65c5…153a) and a 2-leaf campaign (id 2, 1,250,000 total, root 8d5de42b…c8ff) — with every leaf hash and proof path. Any client that disagrees with these files is wrong, regardless of how reasonable its own output looks.
The Rust test suite regenerates and re-verifies them, the SDK's skipped suites are the acceptance criteria for whoever implements the hashing core.
Calling from TypeScript
The SDK does not do this yet. The shape of a claim call against the deployed contract, using @stellar/sdk and a Freighter signer:
import { Contract, SorobanRpc, xdr } from "@stellar/sdk";
const contract = new Contract(CONTRACT_ID);
const proof = proofHexList.map((h) =>
xdr.ScVal.scvBytes(Buffer.from(h, "hex"))
);
const tx = await contract.call(
"claim",
xdr.ScVal.scvU32(distributionId),
xdr.ScVal.scvU32(index),
Address.fromString(recipient).toScVal(),
xdr.ScVal.scvI128(amount),
xdr.ScVal.scvVec(proof),
);BytesN<32> arguments — the root and each proof element — arrive as scvBytes, and hex passed on the CLI must have no 0x prefix. Vec arguments are JSON arrays of hex strings when you go through the CLI.
Reading state
get_distribution(id) -> Distribution is_claimed(id, index) -> bool get_distribution_count() -> u32 get_admin() -> Address
Environment variables for this app
None are required today — the landing page and docs are static. The dashboard, when built, will need at minimum the contract id and network passphrase; those will be named in its issue rather than guessed here.