Welcome to Trellis. This guide is the single source of truth for setting up the repository, running the baseline checks, claiming issues, and opening a reviewable pull request.
Install and verify these tools before changing code:
- Rust stable toolchain, installed through
rustup. - The
wasm32-unknown-unknowntarget for Soroban contract builds. stellarCLI 26.x or newer. Install docs: https://developers.stellar.org/docs/tools/cli/install-cli.- Git.
Verify Rust and Git:
rustc --version
cargo --version
git --versionInstall the Soroban WASM target:
rustup target add wasm32-unknown-unknownVerify the Stellar CLI after installing it:
stellar --versionFork Trellis-Ecosystem/trellis on GitHub, then clone your fork locally:
git clone https://github.com/<your-github-user>/trellis.git
cd trellisAdd the upstream remote so you can sync with the canonical repository:
git remote add upstream https://github.com/Trellis-Ecosystem/trellis.git
git remote -vBefore starting any task, update your local branch:
git fetch upstream
git checkout master
git merge upstream/masterTrellis is a Rust workspace with a Soroban contract, a CLI, and a frontend scaffold.
trellis/
├── contracts/trellis_core/ # Soroban smart contract crate
├── cli/trellis_cli/ # Rust CLI binary for contract workflows
├── frontend/ # React/Vite frontend scaffold
├── indexer/ # Optional event indexer + SSE relay (Node, SQLite)
├── tests/cli_integration/ # CLI + contract tests against a local Stellar node
├── Cargo.toml # Workspace manifest
├── DEPLOYMENT.md # Testnet deployment and verification guide
└── README.md # Product overview and quickstart
This crate contains the escrow contract compiled to WASM for Soroban.
Files in contracts/trellis_core/src/:
types.rs— shared contract data types, includingAgreement,Milestone, andEscrowStatus.storage.rs— ledger storage helpers andDataKeyaccess patterns.errors.rs—TrellisErrorvariants returned by contract entrypoints.events.rs— event emission helpers used by off-chain consumers.lib.rs— the#[contractimpl]entrypoints and state-transition logic.test.rs— Soroban sandbox integration tests for the core contract lifecycle.
This crate builds the trellis command-line binary. It wraps the Stellar CLI/Soroban RPC flow and exposes user-facing commands such as init, lock-funds, submit-work, approve-release, raise-dispute, resolve-dispute, cancel-milestone, and status.
Important files:
src/main.rs—clapcommand parser and top-level dispatch.src/config.rs— environment-driven RPC, network, contract, and source-account configuration.src/rpc.rs— shell-out invocation layer andInvokeOutputhandling.src/commands/mod.rs— command implementations.src/utils.rs— small CLI utility helpers.
Trellis ships pre-commit hooks in .githooks/ that catch lint errors, formatting issues, and accidentally committed secrets before they reach CI. The hooks are not installed automatically — run the setup command once after cloning:
./scripts/install-hooks.shThis is equivalent to:
git config core.hooksPath .githooks| Check | Scope | Triggered when |
|---|---|---|
cargo fmt --check |
Rust workspace | Any .rs file is staged |
cargo clippy -D warnings |
Rust workspace | Any .rs file is staged |
npm run lint (oxlint) |
frontend/ |
Any .ts/.tsx/.js/.jsx file is staged |
npm run typecheck (tsc) |
frontend/ |
Any .ts/.tsx/.js/.jsx file is staged |
| Secret scan | All staged files | Every commit |
The secret scan uses gitleaks if it is installed, otherwise falls back to a basic regex scan. Install gitleaks for full coverage:
# macOS
brew install gitleaks
# Linux (replace VERSION with latest from https://github.com/gitleaks/gitleaks/releases)
VERSION=8.27.2
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaksUse bypass flags sparingly — CI will still catch the same issues:
# Skip all checks (emergency only)
SKIP_HOOKS=1 git commit -m "..."
# Skip individual checks
SKIP_RUST_FMT=1 git commit ... # skip cargo fmt
SKIP_RUST_LINT=1 git commit ... # skip cargo clippy
SKIP_JS_LINT=1 git commit ... # skip oxlint
SKIP_TYPECHECK=1 git commit ... # skip tsc
SKIP_SECRETS=1 git commit ... # skip secret scanIf gitleaks flags a false positive, add a # gitleaks:allow comment on the triggering line, or add a per-rule allowance in .gitleaks.toml at the repository root with a justification comment.
Before committing contract code, run Semgrep to detect common Soroban vulnerability patterns:
semgrep --config .semgrep/ contracts/Install Semgrep if not already available:
brew install semgrep # macOS
pip install semgrep # Linux/Windows with PythonFix any warnings before opening a PR. The CI will automatically scan all pull requests.
Run the contract test suite before changing contract code:
cd contracts/trellis_core
cargo testThe suite currently runs 89 tests, split across four modules:
| Module | Tests | Coverage |
|---|---|---|
src/test.rs |
63 | Example-based lifecycle, error paths, role checks, TTL extension, resolver rotation, and the init-atomicity / funding-order / not-found / out-of-range index suites |
src/test_properties.rs |
14 | proptest invariants — balance conservation, invalid amounts, and milestone isolation |
src/test_panic_boundaries.rs |
9 | Panic-boundary and fuzz coverage for every entrypoint |
src/test_storage.rs |
3 | Per-milestone ledger layout and TTL behaviour |
| Total | 89 |
Representative example-based tests in src/test.rs include test_happy_path, test_double_init_fails, test_dispute_and_refund_to_payer, test_cancel_unfunded_milestone, test_cancel_funded_milestone_fails_with_invalid_state_transition, test_get_agreement, test_get_milestone_unknown_agreement_returns_error, test_batch_lock_funds_partial_failure, test_failed_zero_amount_init_leaves_no_state_corruption, test_multi_milestone_funding_order_independent, test_batch_lock_funds_out_of_order_ids, test_agreement_not_found_on_mutating_entrypoints, test_invalid_milestone_on_mutating_entrypoints, and the six *_wrong_role_fails authorization tests.
The badge and the counts above are refreshed by the test-count-badge workflow on every push to master, so the documented total always tracks cargo test.
If any baseline test fails, open an issue before continuing. Do not start feature, bug-fix, or documentation work on a broken baseline unless your assigned issue is specifically about that failure.
After running tests from inside contracts/trellis_core, return to the workspace root when you are done:
cd ../..The Rust unit tests run the contract in the Soroban sandbox and never call
the real stellar CLI. The cli-integration job in
contract-ci.yml covers that gap. It
runs on every push and PR that touches contracts/ or cli/, after the
verify job passes, and it:
- starts a local Stellar node (
stellar container start local, Standalone network, RPC athttp://localhost:8000/soroban/rpc); - builds the PR's
trellis_coreWASM andtrellisCLI from source, then deploys that WASM and a native-XLM test token to the local node; - runs real
trelliscommands against it (init,lock-funds,submit-work,approve-release,status), plus the dispute and cancellation paths and a set of error cases, checking each command's--jsonoutput; - fails if any command errors unexpectedly or any output doesn't match.
No testnet access or secrets are involved, so a failure here points to your change, not to shared infrastructure.
Running it locally. You need Docker, the stellar CLI 26.x+, and the
wasm32-unknown-unknown target. From the repo root:
bash tests/cli_integration/run.shThe script is idempotent: it reuses a running local node, and each run deploys a fresh contract with fresh agreement IDs. To debug a failure:
- Each failed assertion prints
FAIL: <description> (expected …, got …), and the summary lists them all. Copy the matchingtrellis …command fromtests/cli_integration/run.shand rerun it by hand with the sameSTELLAR_RPC_URL/TRELLIS_CONTRACT_IDto see the full output. - Inspect the node directly, e.g.
curl -s -X POST http://localhost:8000/soroban/rpc -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}'. - Run
stellar container logs localfor node-side errors, andstellar container stop localfor a clean slate before rerunning.
See tests/cli_integration/README.md for
the full list of covered flows.
Build the CLI from its crate directory:
cd cli/trellis_cli
cargo build --releaseThe compiled binary lands in the workspace root target directory, not inside the CLI crate:
- macOS/Linux:
target/release/trellis - Windows:
target/release/trellis.exe
It does not land at cli/trellis_cli/target/release/.
Return to the workspace root after the build:
cd ../..Read this before touching contract code. Trellis models each milestone as a state machine:
Pending ──lock_funds──► Funded ──submit_work──► WorkSubmitted ──approve_and_release──► Completed
│ │ │
│ raise_dispute raise_dispute
│ └──────────────────────────┘
│ │
cancel_unfunded ▼
│ Disputed
│ ┌───────┴────────┐
│ │ │
▼ ▼ ▼
Cancelled resolve_dispute resolve_dispute
(refund payer) (release payee)
│ │
▼ ▼
Refunded Completed
Transitions and entrypoints:
Pending -> Fundedis triggered bylock_funds. The payer deposits the milestone amount into the contract.Pending -> Cancelledis triggered bycancel_unfunded_milestone. The payer cancels a milestone that has never been funded.Funded -> WorkSubmittedis triggered bysubmit_work. The payee attaches proof of completed work.Funded -> Disputedis triggered byraise_dispute. Either payer or payee can request resolver review before work is submitted.WorkSubmitted -> Completedis triggered byapprove_and_release. The payer accepts the work and funds are released to the payee.Funded -> Completed/WorkSubmitted -> Completedis triggered byrelease_partialonce the cumulative partial releases equal the milestone amount. A partial release that leaves funds in escrow keeps the milestone in its current status;approve_and_releaseandresolve_disputethen only move the remaining escrowed amount.WorkSubmitted -> Disputedis triggered byraise_dispute. Either side can escalate submitted work for resolver review.Disputed -> Refundedis triggered byresolve_disputewhen the resolver rules for the payer.Disputed -> Completedis triggered byresolve_disputewhen the resolver rules for the payee.- When a transition leaves every milestone in
CompletedorRefunded, the contract also emitsagreement_completed(trls_cmpl) once for the whole agreement. get_agreementis read-only. It does not transition state; it returns the current agreement snapshot.initcreates the agreement and starts each milestone inPending.
Never add a new transition or bypass an existing state without first discussing it in the linked issue.
- Browse open issues and look for
good first issueorhelp wantedlabels. - Comment exactly:
I'd like to work on this. - Wait for maintainer confirmation before opening a PR.
- Do not open a PR for an issue nobody has confirmed you can work on.
- If an issue has been claimed but shows no activity after its stated timeframe, comment asking whether it is still being worked on.
This avoids duplicated effort and keeps maintainers from reviewing competing solutions for the same small task.
Use short, scoped branch names:
feat/short-descriptionfor features.fix/short-descriptionfor bug fixes.docs/short-descriptionfor documentation.test/short-descriptionfor testnet verification tasks.
Examples:
git checkout -b feat/agreement-status-page
git checkout -b fix/dispute-resolution-edge-case
git checkout -b docs/contributing-guide
git checkout -b test/live-status-commandThe repository uses a CODEOWNERS file so that pull requests are automatically routed to the maintainers responsible for the code you touched. Review requests are issued when the PR is opened and re-evaluated on every push.
| Area | Pattern | Owners |
|---|---|---|
| Everything else (default fallback) | * |
@ALLEN-AYODEJI |
| Smart contracts | /contracts/, root Cargo.toml, Cargo.lock, rust-toolchain.toml |
@ALLEN-AYODEJI |
| CLI | /cli/ |
@ALLEN-AYODEJI |
| Frontend | /frontend/ |
@ALLEN-AYODEJI, @Folex1275 |
| Scripts and tooling | /scripts/, /Makefile |
@ALLEN-AYODEJI |
| CI and GitHub configuration | /.github/ |
@ALLEN-AYODEJI, @Oryke, @Felamsy |
| Root documentation | /*.md |
@ALLEN-AYODEJI |
How matching works:
- A PR that touches files in several areas requests a review from the owners of every matching area. A PR changing both
contracts/trellis_core/src/lib.rsandfrontend/src/App.tsxis assigned to the contract owner and the frontend owner. - When several patterns match the same file, the last matching rule in the file wins. Specific rules are therefore listed after broader ones.
- CODEOWNERS only requests reviews. Whether an owner's approval is mandatory is controlled by branch protection ("Require review from Code Owners") on the default branch.
Rules for changing ownership:
- Owners must have write access to this repository. A username or team without access is silently ignored by GitHub, and no review request is sent.
- To become an owner for an area, or to change an assignment, open an issue describing your experience with that area. Do not edit
.github/CODEOWNERSdirectly without agreement.
All of the following must be true before requesting review:
cargo testpasses 89/89 incontracts/trellis_core.cargo buildpasses with zero warnings in both Rust crates you touched.- The PR description explains what changed and why.
- The PR references the issue number using
Closes #X. - No files outside the linked issue's scope are changed.
- No changes are made to
contracts/trellis_coreunless the issue explicitly requires contract changes. - No new dependencies are added without prior discussion in the issue thread.
- If you changed contract code: regenerate test snapshots and commit them (see Test Snapshots below).
Suggested final checks from the workspace root:
cargo test
cargo build --workspaceFor contract-specific work, also run:
cd contracts/trellis_core
cargo test
cd ../..For CLI-specific work, also run:
cd cli/trellis_cli
cargo build --release
cd ../..The Soroban test framework records ledger state at each test step into JSON files under contracts/trellis_core/test_snapshots/. These files are committed to the repository so reviewers can see exactly what state the contract produces for every test case.
Generated suites do not write snapshots. test_properties.rs and test_panic_boundaries.rs run hundreds of generated cases per test, so their shared fixture test_utils::setup_mocked builds its Env with EnvTestConfig { capture_snapshot_at_drop: false }. Only the hand-written suites (test.rs, test_storage.rs) produce snapshot files, so a PR that adds one test changes only the files it touches.
Why this matters: If you change contract logic or add/remove entrypoints, the snapshot files will diverge from what the tests actually produce. A PR with stale snapshots will fail the CI snapshot-validation step, even if cargo test itself passes.
Whenever you change contract code, regenerate the snapshots before opening a PR:
make test-snapshots-updateThis is equivalent to:
SOROBAN_TEST_SNAPSHOT_FILE=overwrite cargo test --manifest-path contracts/trellis_core/Cargo.tomlAfter regenerating, review the diff:
git diff contracts/trellis_core/test_snapshots/If the diff looks correct (ledger entries reflecting your intentional change), stage and commit the updated snapshot files as part of the same commit or PR that contains the contract change.
After the regular cargo test step, CI runs:
SOROBAN_TEST_SNAPSHOT_FILE=overwrite cargo test --manifest-path contracts/trellis_core/Cargo.toml
git diff --exit-code contracts/trellis_core/test_snapshots/If any snapshot file differs from what is committed, the build fails with an error message directing you to run make test-snapshots-update.
- Never manually edit snapshot JSON files. They are generated automatically.
- Do not add
test_snapshots/to.gitignore. The files must be tracked. - If you add a new test, its snapshot file will be created by
make test-snapshots-updateand must be committed. - If you delete a test, delete its snapshot file from the repository in the same PR.
- Do not re-enable snapshot capture in
setup_mockedor add a capturingEnvto the property or panic-boundary suites; it would reintroduce thousands of generated files.
Follow the existing patterns in the file you edit. Do not introduce a new style in the same PR.
Rules:
- No commented-out code in PRs.
- No leftover
println!debug statements in committed code. - Contract code never uses
panic!; return aTrellisErrorvariant instead. - CLI code follows the existing shell-out plus
InvokeOutputpattern inrpc.rsunless the issue specifically asks you to change it. - Keep Soroban contract code compatible with
#![no_std]expectations. - Prefer small, reviewable PRs over broad refactors.
Before committing Rust changes, format them:
cargo fmtThe frontend (frontend/package.json) follows Semantic Versioning:
- MAJOR — breaking changes to the contract interface the frontend depends on, or a rewrite of core user flows.
- MINOR — new user-facing features that stay backward compatible (e.g. a new page, a new wallet capability).
- PATCH — bug fixes, accessibility fixes, styling, and other non-feature changes.
To cut a new version:
cd frontend
npm version patch # or minor / majorThis updates package.json and creates a matching git tag. The version is injected into the build via vite.config.ts (__APP_VERSION__, sourced from npm_package_version) and rendered in the app footer, so every deployed build is traceable to a version.
Run the frontend checks before opening a PR that touches frontend/:
cd frontend
npm ci
npm run lint
npm run typecheck
npm run test
npm run buildFor changes that affect contract calls or wallet flows, also verify manually against Testnet:
- Follow the deployment/setup steps in DEPLOYMENT.md to point the frontend at a Testnet contract instance.
- Run
npm run devand exercise the affected flow end to end (e.g. init, fund, submit, approve/dispute) using a Freighter Testnet account. - Confirm the transaction succeeds in Freighter and the resulting state is reflected in the UI.
frontend/src/lib/config.ts is the single
source of truth for the variables the frontend reads, and
frontend/.env.example is what new contributors copy
to get started. The two must list the same VITE_* names. If they drift
apart, cp .env.example .env gives a newcomer a setup that is silently
missing a variable.
| Variable | Required | Read by |
|---|---|---|
VITE_CONTRACT_ID |
Yes (dev fallback to the testnet contract) | getContractId() |
VITE_RPC_URL |
Yes (dev fallback to public testnet RPC) | getRpcUrl() |
VITE_NETWORK_PASSPHRASE |
Yes (dev fallback to the testnet passphrase) | getNetworkPassphrase() |
VITE_INDEXER_URL |
No; empty means direct RPC polling | getIndexerUrl() |
When you add, rename, or remove a variable:
- Update
config.ts, and give required variables aDEV_FALLBACKSentry. - Add or remove the key in
.env.examplein the same commit. Leave optional variables empty and add a one-line comment above them. - Update the table above.
- If tests need a value, add it to the
test.envblock infrontend/vite.config.ts.
Before opening the PR, check that the two lists match. This command prints nothing when they agree:
diff <(grep -o "VITE_[A-Z_]*" frontend/src/lib/config.ts | sort -u) \
<(grep -o "^VITE_[A-Z_]*" frontend/.env.example | sort -u)Reviewers: please confirm this for any PR that touches either file.
Use the right channel for the question:
- Open a GitHub Discussion for general questions about Trellis, Soroban, or contributor workflow.
- Comment directly on the issue you are working on for issue-specific questions.
- Tag the maintainer if you are blocked for more than 24 hours.
When asking for help, include:
- Your operating system.
- The command you ran.
- The full error output.
- The branch and issue number.
- What you already tried.