Contributing
Setup
- Rust toolchain (stable) with the
wasm32v1-nonetarget:rustup target add wasm32v1-none - Stellar CLI, for building and deploying the contract
Clone the repo, then build Tholos’s wasm once before anything else:
cargo build -p tholos --target wasm32v1-none --release
cargo test
The first command is required, not optional: demo-consumer imports Tholos’s
compiled wasm at compile time (contractimport!), so cargo test,
cargo clippy --workspace, and any IDE build of the workspace will fail on a fresh
checkout until that file exists. Only re-run it after changing contracts/tholos;
demo-consumer alone doesn’t need a rebuild between runs.
Project layout
contracts/
tholos/ The assertion and dispute contract
src/
lib.rs Contract logic
test.rs Unit tests (soroban-sdk testutils, mocked ledger and auth)
demo-consumer/ Minimal example contract that calls into Tholos
src/
lib.rs Cross-contract call pattern from docs/src/INTEGRATION.md
test.rs Validates that pattern against Tholos's real compiled wasm
scripts/
testnet-smoke.sh End-to-end check against real Stellar testnet infrastructure
.github/workflows/
ci.yml Runs fmt, clippy, tests, and the wasm build on every push/PR
demo-consumer exists to keep INTEGRATION.md honest: it’s not a product, it’s a
compiled check that the documented integration pattern actually works. If you
change Tholos’s public interface, update demo-consumer too if it uses the
changed function, and re-run its test.
If a second real contract is added later (e.g. a market factory), it should live as
its own crate under contracts/, added to the [workspace] members list in the
root Cargo.toml, following the same layout as contracts/tholos.
Testing philosophy
There are two layers, and they catch different things:
- Unit tests (
cargo test) run against a mocked ledger and mocked auth. Fast, deterministic, and where most new behavior should be covered, including every newErrorvariant you introduce: if you add a new failure path, add a test that triggers it. - The testnet smoke script (
scripts/testnet-smoke.sh) deploys to a real network and exercises real auth, real storage TTLs, and a real SAC token. This is the only thing that can catch a class of bug unit tests structurally can’t (for example, an auth check that’s satisfied bymock_all_auths()in tests but fails against a real signature). Run it before opening a PR that changes contract behavior in a way that affects the deployed flow, not for every change.
Code standards
- Naming:
snake_casefor functions and variables,PascalCasefor types (Assertion,Status,Error),UPPER_SNAKE_CASEfor constants (INSTANCE_BUMP_AMOUNT). - Error handling: contract entry points return
Result<T, Error>; add a newErrorvariant rather than panicking for anything a caller could plausibly trigger (bad input, wrong state, missing auth). Reserve.unwrap()for values that are only unreachable because of a prior check in the same function (seeSelf::get, which unwraps instance storage thatinitializeis responsible for guaranteeing exists), and prefer propagatingError::NotInitializedwhere that precondition can’t be locally guaranteed instead, asupdate_resolversdoes. - Doc comments: every public contract function gets a
///summary covering what it does, who must sign it, and whichErrors it can return. - Security: validate all inputs and assume callers are adversarial. Never read a storage key without either handling the “missing” case explicitly or having a preceding check in the same function that guarantees it exists.
Docs site
docs/ is an mdBook that publishes this
repo’s docs as a site, deployed automatically from main by
.github/workflows/docs.yml. Where a given doc’s real content lives depends on
whether GitHub treats it specially:
README.md,CONTRIBUTING.md(this file), andSECURITY.mdstay at the repo root, because GitHub does something with them there (README renders on the repo homepage, CONTRIBUTING is linked when opening an issue/PR, SECURITY.md powers the Security tab). Theirdocs/src/copies are one-line{{#include ../../X.md}}stubs; edit the root file, not the stub.ARCHITECTURE.md,CHANGELOG.md,CONTRACT.md,DEPLOYMENT.md,GLOSSARY.md, andINTEGRATION.mdget no special treatment from GitHub at root, so their real content lives directly underdocs/src/, with no root duplicate. Edit them there; they’re still normal markdown files GitHub renders fine if you click intodocs/src/CONTRACT.mddirectly, they just aren’t at the repo’s top level.
Preview locally with mdbook serve docs (requires cargo install mdbook).
Before opening a PR
Run the same checks CI runs, in this order (see the note above on why the wasm build has to come first):
cargo fmt --check
shellcheck scripts/*.sh
cargo build -p tholos --target wasm32v1-none --release
cargo clippy --workspace --all-targets -- -D warnings
cargo test
If you changed the contract’s public interface (functions, types, errors), update
CONTRACT.md to match; it’s meant to stay in sync with
lib.rs, not drift into a separate design doc.
Commit messages
One-line, imperative, conventional-commit style: feat:, fix:, docs:, test:,
ci:, etc., followed by a concise summary. No comma-separated lists of unrelated
changes in a single message; split them into separate commits instead.
Opening a PR
CI (fmt, clippy, tests, wasm build) must pass before merge. The PR template
(.github/pull_request_template.md) is pre-filled when you open a PR; fill it out
rather than deleting it. If the change affects bond amounts, resolver behavior, or
anything with an economic consequence, say so explicitly in the summary so it’s easy
to reason about from the PR alone.