YieldStreamdocsLaunch app

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

WhatValue
Distributor, testnetCC25WDHZ7SQC2UMZJHIPYM2U5ROYFXVDZMP3THMIL3IJY6HOCGGX2FPG
Distributor, pubnetnot deployed
WASM artifacttarget/wasm32v1-none/release/yieldstream_distributor.wasm
Test vectorstest-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:

  1. XDR, not raw bytes. u32 is 4 big-endian bytes. i128 is 16 big-endian bytes. Address is the ScVal account form with a 32-byte payload and its discriminating prefix — not the base-32 G…/C… string.
  2. Field order is fixed as written above. Any permutation gives a different root.
  3. 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.

Known traps in the current contract

  • sweep overwrites claimed_amount with total_amount — count claimed events for real figures (#8).
  • proof length is not bounded against tree depth (#6).
  • campaign stats are not validated against the root at creation (#7).
  • persistent TTLs are only bumped on touched keys (#14).