Skip to content
Trellis-EcosystemPublic

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Latest commit

 

History

504 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Trellis Trustless Milestone Escrow on Stellar Soroban LIVE ON TESTNET | Contract: CAUAO7C...

Trustless, milestone-based escrow for freelance and remote work — built on Stellar's Soroban smart contract platform.

Contract CI Testnet E2E Frontend CI npm audit CodeQL Rust Soroban Deployed License Status Contributor Covenant


The Problem

Remote work and freelance contracting run on trust that often doesn't exist between strangers across borders. Clients hesitate to pay upfront. Workers hesitate to deliver without payment guarantees. The usual fix — a centralized escrow middleman — adds fees, delays, and a single point of failure.

Trellis removes the middleman. Funds are locked on-chain, released milestone by milestone as work is verified, with a built-in dispute process if either party disagrees. No platform holds your money. The contract does.

This matters everywhere, but especially for contributors in emerging markets — where access to reliable, low-fee, borderless payment infrastructure can be the difference between taking on international work or not.

💡 Why Soroban?
Contracts are written in real Rust, compiled to WASM, and run on the Stellar network — which has fast finality, low fees (~fractions of a cent), and an established USDC presence via Circle's Stellar Asset Contract. This makes it well-suited for cross-border payment use cases.

Live on Testnet

LIVE ON STELLAR TESTNET Network: Test SDF Network ; September 2015 Contract: CAUAO7CYKULE2K4EJMQ6LLRUHP7Y7JYOH6G2VBXKYG7PTETE3UZ3DU7Q

A live test agreement exists on-chain and is queryable right now:

trellis status --agreement-id 0101010101010101010101010101010101010101010101010101010101010101
Attribute Value
Contract ID CAUAO7CYKULE2K4EJMQ6LLRUHP7Y7JYOH6G2VBXKYG7PTETE3UZ3DU7Q
Network Stellar Testnet
Explorer View on Stellar Lab

Full deployment details, every verified command, and step-by-step deployment instructions are in DEPLOYMENT.md.


How It Works

Trellis models a freelance engagement as an agreement made up of one or more milestones, each with its own funding, work submission, and release lifecycle.

PENDING FUNDED WORK SUBMITTED COMPLETED DISPUTED REFUNDED CANCELLED lock_funds submit_work approve & release cancel_unfunded raise_dispute raise_dispute resolve (refund) resolve (release) Payer action Payee action Either party Resolver action

The Roles

Role Responsibility
Payer 🧑‍💼 Funds milestones and approves completed work
Payee 👨‍💻 Submits proof of completed work and receives payment on approval
Dispute Resolver ⚖️ A neutral third party who can rule on disputes, releasing funds to either side

The Guarantees

  • ✅ Funds are held by the contract — not by either party or a platform
  • ✅ Either party can raise a dispute — neither can unilaterally freeze funds by going silent
  • ✅ Unfunded milestones can be cancelled — walk away cleanly with no cost
  • ✅ Every state transition emits an on-chain event — off-chain clients track progress in real time without polling

Architecture

Trellis is a monorepo with three layers:

LAYER 1 · ON-CHAIN contracts/trellis_core/ Soroban Smart Contract (Rust → WASM) lib.rs types.rs storage.rs events.rs LAYER 2 · OFF-CHAIN cli/trellis_cli/ Command-Line Interface (Rust + clap + reqwest) main.rs config.rs rpc.rs LAYER 3 · WEB frontend/ Web Dashboard (React + Vite + TypeScript + Tailwind) App.tsx components/ lib/ Stellar Network

Contract Entrypoints

The contract exposes 18 public entrypoints. CI (scripts/check-readme-entrypoints.sh) fails if a pub fn in contracts/trellis_core/src/lib.rs is missing from this table.

Function Caller Effect
init Payer Creates a new agreement with one or more milestones (each amount must be strictly positive)
lock_funds Payer Deposits funds for a milestone into the contract
submit_work Payee Submits proof of completed work for a funded milestone
approve_and_release Payer Approves submitted work, releases the remaining escrowed funds to payee
release_partial Payer Releases part of a Funded/WorkSubmitted milestone's escrowed funds as a progress payment; the milestone completes once fully released
raise_dispute Payer or Payee Flags a milestone for resolver review
resolve_dispute Dispute Resolver Rules on a dispute — refunds payer or pays payee
resolve_dispute_split Dispute Resolver Settles a dispute with an explicit split: payer_amount and payee_amount must be non-negative and sum exactly to the milestone's still-escrowed remainder (after any release_partial)
rotate_resolver Payer and Payee Replaces the dispute resolver when the incumbent is lost, compromised or unresponsive; the outgoing resolver does not have to sign
cancel_unfunded_milestone Payer Cancels a milestone that was never funded — status becomes Cancelled, never Refunded (reserved for dispute refunds)
get_agreement Anyone Returns the full current state of an agreement (read-only)
get_total_amount Anyone Returns the agreement's total value — sum of all milestone amounts (read-only)
batch_lock_funds Payer Funds multiple milestones atomically in one transaction
get_milestone Anyone Returns a single milestone's state, or none if the agreement or milestone does not exist (read-only)
extend_agreement_ttl Anyone (caller must sign) Renews an agreement's ledger TTL to avoid archival; the signing caller is recorded in the event
set_milestone_deadline Payer Sets an optional deadline (ledger timestamp) on a still-Pending milestone
get_milestone_deadline Anyone Returns a milestone's deadline, if one is set (read-only)
expire_milestone Anyone After the deadline, closes a stalled milestone: a never-funded Pending one becomes Cancelled, a Funded one becomes Refunded with the locked funds returned to the payer
🧬 Native ScVal Encoding
The CLI builds Soroban ScVal arguments natively in Rust instead of relying on the stellar CLI's JSON-to-XDR conversion. Scalar arguments (addresses, i128 amounts, symbols) are encoded by the scalar encoder, and the milestone vector passed to init is encoded by a dedicated builder that mirrors the contract's exact #[contracttype] layout: each Milestone is a struct-of-fields map, amount is an ScVal::I128, and status is encoded as the EscrowStatus::Pending enum tag. The resulting Vec<Milestone> ScVal is cross-checked against what the stellar CLI produces for the same input.
📦 Storage Lifetime
Soroban archives persistent ledger entries once their TTL expires, so an agreement that is never touched would eventually be lost. Every state-mutating entrypoint renews the agreement's TTL to ~30 days automatically. Agreements that stay idle longer than that — a long delivery window, a stalled dispute — need extend_agreement_ttl called before the TTL runs out; any address may call it — `caller` must sign its own call — and the caller pays the rent. Use trellis extend-ttl for a one-off renewal; the scheduled TTL keeper described in DEPLOYMENT.md renews tracked agreements weekly. Soroban archives persistent ledger entries once their TTL expires, so an agreement that is never touched would eventually be lost. Every state-mutating entrypoint renews the agreement's TTL to ~30 days automatically, and the view functions (get_agreement, get_milestone, get_total_amount) renew it as well whenever a read finds the remaining TTL below the threshold — so a read is not strictly side-effect free, and the caller pays for the extension. Reading keeps a watched agreement alive between transitions rather than leaving it to expire. Agreements that stay idle longer than that — a long delivery window, a stalled dispute — need extend_agreement_ttl called before the TTL runs out; any address may call it — `caller` must sign its own call — and the caller pays the rent.
⏱️ Milestone Deadlines
A milestone can carry an optional deadline so an unresponsive counterparty cannot stall it forever. The payer sets it with set_milestone_deadline while the milestone is still Pending, so the payee sees it before any funds are locked or work starts. It is a ledger timestamp (Unix seconds) and must be in the future. Once env.ledger().timestamp() is past the deadline, anyone (payer, payee or a keeper) may call expire_milestone:
  • Pending (never funded) → Refunded; no tokens move.
  • Funded (payee never submitted work) → Refunded; the locked amount returns to the payer.
  • WorkSubmitted and Disputed are never expired: once the payee has delivered, a silent payer must not win by default, and a timeout must not cut arbitration short. Use raise_dispute / resolve_dispute instead.

Tradeoffs: timestamps are used instead of ledger sequences because deadlines are agreed in wall-clock terms and ledger close times vary; validators bound timestamp drift, so this is precise to within seconds, not to the ledger. Expiry is an explicit call rather than automatic, because Soroban has no scheduler. Deadlines live in a separate storage entry (not a Milestone field), so the init argument layout and every existing caller are unchanged. That entry has its TTL renewed alongside the agreement's.

🔁 Rotating the Dispute Resolver
The dispute resolver is chosen at init and is otherwise fixed for the agreement's lifetime. Because resolve_dispute and resolve_dispute_split are gated solely by that address, a resolver whose key is lost or compromised — or which simply stops answering — would leave every Disputed milestone permanently unsettleable. rotate_resolver closes that single point of failure:
rotate_resolver (agreement_id, new_resolver)

Both the payer and the payee must sign. That is what stops the rotation becoming an attack in its own right: with a single-party gate, either side could install a resolver it controls and then have that resolver rule on the resulting dispute. The outgoing resolver is deliberately not asked to sign — the situations this entrypoint exists for are exactly those where its signature is unavailable or worthless. The incoming resolver does not sign either; it gains the authority to settle disputes the moment the rotation lands.

Rotation only changes dispute_resolver. Milestone statuses, escrowed balances, total_amount, release ledgers and deadlines are untouched and no tokens move, so it is safe at any point in the agreement's life — including while a milestone is actively Disputed. That is the rescue path: rotation hands the ruling over, it does not substitute for one, so the dispute stays open and the replacement settles it. Milestones already ruled on are final and are never re-opened.

The new resolver must not be the payer or the payee — the same neutrality invariant init enforces, re-checked on every rotation. Each rotation emits a resolver_rotated event carrying both the old and the new address, so the full resolver history of an agreement is reconstructible from the event log even though get_agreement only ever exposes the incumbent.

Tech Stack

Category Technologies
Smart Contract Soroban · soroban-sdk 22.x · Rust (#![no_std] → WASM)
CLI clap 4 · clap_complete · serde + serde_json · dotenvy
Frontend React 19 · Vite · TypeScript · Tailwind CSS · React Router
Stellar SDK @stellar/stellar-sdk · @stellar/freighter-api · Soroban RPC

Quickstart

Prerequisites

  • Rust (stable toolchain)
  • wasm32-unknown-unknown target: rustup target add wasm32-unknown-unknown
  • stellar CLI 26.x+
  • Node.js 20+ (for the frontend)

🐳 One-command local stack (Docker)

docker compose up   # or: make dev-up

Starts a local Stellar node, builds and deploys the contract, writes frontend/.env.local, and serves the frontend at http://localhost:5173. Point the CLI at it with --network custom --rpc-url http://localhost:8000/soroban/rpc. The manual steps below remain available as an alternative.

🖥️ Run the frontend locally

cd frontend
cp .env.example .env
npm install
npm run dev

The navbar network selector (testnet / mainnet / futurenet / custom) switches networks at runtime; the choice is saved in localStorage and defaults to your .env values. Open http://localhost:5173 to see the animated landing page with the particle network background, typewriter effects, live contract activity stats (agreements created / milestones locked, within the RPC event retention window), and full agreement management UI.

📡 Run the event indexer (optional)

By default each browser tab polls Soroban RPC for events, and history older than the RPC node's retention window (~7 days) is lost. The indexer in indexer/ stores every contract event in SQLite and pushes updates to the frontend over SSE:

cd indexer
RPC_URL=https://soroban-testnet.stellar.org \
CONTRACT_ID=CAUAO7CYKULE2K4EJMQ6LLRUHP7Y7JYOH6G2VBXKYG7PTETE3UZ3DU7Q \
npm start

Then set VITE_INDEXER_URL=http://localhost:8787 in frontend/.env. See DEPLOYMENT.md for production setup and the data-retention model.

🛠️ Build and test the contract

cd contracts/trellis_core
cargo test

The suite has 89 tests, all run in the Soroban sandbox: 63 example-based tests in test.rs (happy path, double-init protection, dispute resolution, resolver rotation, milestone cancellation, role checks, batch operations, TTL extension, and the get_agreement view function), 14 property-based proptest tests in test_properties.rs (balance conservation, invalid amounts, milestone isolation), 9 panic-boundary tests in test_panic_boundaries.rs, and 3 storage-layout tests in test_storage.rs.

📦 Build everything at once

make build

Runs the contract WASM build, CLI binary build, and frontend bundle in sequence. See make help for all available targets.

🔒 Dependency & supply-chain audit

The workspace's crate graph is scanned for known vulnerabilities, disallowed licences, and unexpected source registries. This runs automatically in CI (the supply-chain job in .github/workflows/contract-ci.yml) and can be run locally:

cargo install --locked cargo-deny cargo-audit
cargo deny check      # enforces deny.toml: advisories, licences, duplicate versions, sources
cargo audit           # RustSec advisory database check

Note: Keep Cargo.lock in sync with Cargo.toml. CI now verifies the lockfile with cargo check --locked --workspace and will fail if it's out of date. To update the lockfile locally after changing dependencies run:

cargo update -p <pkg>
# or to generate/update the lockfile explicitly:
cargo generate-lockfile

Policy lives in deny.toml at the repository root. A new RustSec advisory against any dependency (direct or transitive) fails the build.

Frontend npm dependencies are scanned by npm audit in CI (.github/workflows/npm-audit.yml) — run it locally with:

cd frontend
npm audit --audit-level=high

Known false positives are documented in frontend/.audit-allowlist.json.

Building the contract WASM

cd contracts/trellis_core
cargo rustc --manifest-path=Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release

See DEPLOYMENT.md for the full deployment walkthrough and why this command is used instead of stellar contract build.

⌨️ CLI Usage

cd cli/trellis_cli
cargo build --release

export STELLAR_RPC_URL="https://soroban-testnet.stellar.org"
export STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
export TRELLIS_CONTRACT_ID="CAUAO7CYKULE2K4EJMQ6LLRUHP7Y7JYOH6G2VBXKYG7PTETE3UZ3DU7Q"
export TRELLIS_SOURCE_KEY="<your stellar identity name>"
# Or, for a raw S… secret, keep it in a 0600 file instead of the environment:
# export TRELLIS_SOURCE_KEY_FILE=~/.trellis.key   (or pass --source-key-file)

# Create a new agreement
trellis init \
  --agreement-id <hex-id> \
  --payer <payer-address> \
  --payee <payee-address> \
  --token <token-contract-address> \
  --resolver <resolver-address> \
  --milestones "1000,2000"

# Check status
trellis status --agreement-id <hex-id>

# Total value of all milestones
trellis total-amount --agreement-id <hex-id>
# Check a single milestone's status
trellis milestone-status --agreement-id <hex-id> --milestone-id 0

# Fund the first milestone
trellis lock-funds --agreement-id <hex-id> --milestone-id 0

# Fund several milestones atomically in one transaction
trellis batch-lock-funds --agreement-id <hex-id> --milestone-ids "0,1,2"

# Set a milestone deadline (Unix seconds or RFC 3339; must be in the future)
trellis deadline set --agreement-id <hex-id> --milestone-id 0 --deadline 2026-01-01T00:00:00Z
trellis deadline get --agreement-id <hex-id> --milestone-id 0

# Close a stalled milestone once its deadline has passed
trellis expire --agreement-id <hex-id> --milestone-id 0 --caller <your-address>

All 11 CLI commands are implemented — init, lock-funds, batch-lock-funds, submit-work, approve-release, raise-dispute, resolve-dispute, cancel-milestone, extend-ttl, deadline set|get, expire, status, and total-amount. See DEPLOYMENT.md for the full command reference. All 9 escrow commands are implemented — init, lock-funds, submit-work, approve-release, raise-dispute, resolve-dispute, cancel-milestone, status, and milestone-status. The CLI also provides two utility commands: completion (see Shell Completions) and keys (manage secret keys in the OS keychain). See DEPLOYMENT.md for the full command reference.

trellis health checks the configured RPC endpoint natively (getHealth + getLatestLedger over HTTP). It needs only an RPC URL, with no stellar binary, contract ID or source key, and it supports --json / --human-readable / --quiet / --dry-run.

Global Output Flags

These flags work with every command and can be combined with .env/env-var configuration:

# Preview without submitting
trellis lock-funds --agreement-id <hex-id> --milestone-id 0 --dry-run

# Machine-parseable JSON
trellis status --agreement-id <hex-id> --json

# Suppress everything except final JSON (implies --json)
trellis status --agreement-id <hex-id> --quiet

# Colorized summary
trellis status --agreement-id <hex-id> --human-readable   # or -H

Read-only queries (status, milestone-status) decode the raw XDR ScVal returned by simulateTransaction natively in the CLI — no stellar binary is required for these commands. The decoded result is rendered through the same render_json/render_human paths as every other command, so --json, --human-readable, and --quiet all behave identically whether or not the Stellar CLI is installed.

Native transaction simulation is implemented in cli/trellis_cli/src/rpc.rs (RpcClient::simulate_transaction) against the Soroban JSON-RPC simulateTransaction method. For a base64 TransactionEnvelope XDR it returns a typed result containing the recommended minimum resource fee (minResourceFee), the resource fee and instruction / I/O-byte budgets embedded in the transactionData, and the fully parsed ledger footprint (its read-only and read-write LedgerKeys). For a read-only invocation the returned results[0].xdr ScVal is decoded directly, so a query value can be fetched without any signing key. A reverted host function call is surfaced as a typed contract error, kept distinct from network-level and JSON-RPC-level failures so callers can tell "the contract said no" apart from "the network was unreachable".

--dry-run prints the stellar contract invoke command that would be executed without actually running it or submitting anything on-chain. Because it never spawns the stellar binary, it works on machines where the Stellar CLI is not installed — useful for previewing command construction in CI or on a fresh checkout.

Milestone arguments passed to init (e.g. --milestones "1000,2000") are encoded to Soroban ScVal natively by the CLI, matching the contract's Vec<Milestone> layout — no stellar CLI conversion step is involved.

--json takes priority over --human-readable when both are passed.

--source-key-file <PATH> (or TRELLIS_SOURCE_KEY_FILE) reads the signing key from a file and takes precedence over TRELLIS_SOURCE_KEY. See DEPLOYMENT.md for details.

--as payer|payee|resolver signs with that role's key from TRELLIS_PAYER_KEY / TRELLIS_PAYEE_KEY / TRELLIS_RESOLVER_KEY (or the matching *_KEY_FILE variables). Keep all three in .env instead of re-exporting TRELLIS_SOURCE_KEY between commands:

trellis --as payee submit-work --agreement-id <hex-id> --milestone-id 0

Inspecting the Resolved Configuration

trellis config prints the configuration the CLI actually resolved from flags, env vars and .env, without making any RPC call. It shows the network preset, RPC URL, passphrase, contract ID, the source key and where it came from, and whether the stellar binary is on PATH:

trellis config                       # key: value lines
trellis config --human-readable      # colorized
trellis config --json                # {"status", "result": {...}, ...}
trellis --network futurenet config   # preview another preset

A raw S… secret seed is never printed. It is masked as S...***. Named stellar keys identities (e.g. alice) are not secret and are shown in full. The command exits 1 and lists the problems if any other command would reject this configuration, so it also works as a preflight check in scripts.

Agreement History

Every successful init, status and milestone-status call records the agreement ID (with its contract ID and a timestamp) in ~/.trellis/history.json, so you don't have to keep track of 64-char hex IDs yourself:

trellis history              # table, most recent first
trellis history --limit 5
trellis history --json       # {"status": "success", "result": [ ... ], ...}
trellis history --clear      # delete the cache

--dry-run calls are not recorded. Only public on-chain identifiers are stored, never keys. Set TRELLIS_HISTORY_FILE to use a different file, or TRELLIS_NO_HISTORY=1 to turn recording off. If the cache can't be written, the CLI prints a warning but the command itself still succeeds.

Network Passphrase Verification

Before any command runs, the CLI calls the configured RPC endpoint's getNetwork method and compares the returned passphrase against --network-passphrase (or STELLAR_NETWORK_PASSPHRASE). If they differ, the command fails early with an error naming both values, so a mismatched --rpc-url (e.g. mainnet RPC with a testnet passphrase) is caught immediately instead of surfacing as a confusing downstream failure. This check is skipped under --dry-run.

Loading Configuration From a Specific File

By default the CLI loads a .env file from the current directory (or the nearest ancestor). To switch between identities/environments without cd-ing or re-exporting variables, point it at an explicit file instead:

trellis status --agreement-id <hex-id> --env-file ~/trellis/payer.env

# or, for every command in the shell session
export TRELLIS_ENV_FILE=~/trellis/payer.env

--env-file overrides TRELLIS_ENV_FILE. When either is set, CWD discovery is skipped and a missing or unreadable file is an error. Exported environment variables and CLI flags still take priority over values in the file.

Verbose RPC Tracing

Use -v / --verbose (repeatable) to debug RPC retry behavior. Traces go to stderr, so --json output on stdout stays parseable:

# Each attempt's outcome, raw exit code, full error output, and why retrying stopped
trellis status --agreement-id <hex-id> -v

# Also the full `stellar contract invoke` command and per-attempt timing
trellis status --agreement-id <hex-id> -vv

Raw S… secret seeds are always shown as STELLAR_SECRET_KEY=<redacted>. --quiet overrides --verbose.

Shell Completions

# bash
trellis completion bash > /etc/bash_completion.d/trellis

# zsh
trellis completion zsh > "${fpath[1]}/_trellis"

# fish
trellis completion fish > ~/.config/fish/completions/trellis.fish

Supported shells: bash, zsh, fish, elvish, powershell.


Project Status

✅ Complete

  • Core Soroban escrow contract - all 18 entrypoints implemented and tested
  • Full state machine — happy path, dispute resolution, and cancellation paths
  • Contract test suite - 89 tests in the Soroban sandbox
  • Full CLI — all 8 commands wired end-to-end with JSON, dry-run, and human-readable output modes
  • Native ScVal encoding — scalar arguments and the Vec<Milestone> argument to init are built directly in Rust
  • Deployed live on Stellar testnet — init and status verified against the live contract
  • Frontend dashboard — 5 pages, 28 components, 12 custom hooks, animated particle network background
  • Wallet connect — Freighter wallet integration with connection states
  • Event feed — real-time on-chain event history per agreement (limited to the last ~100k ledgers, ~6 days, that RPC providers retain; full history awaits an event-indexing service, #496)
  • Shell completions — bash, zsh, fish, elvish, powershell
  • Native strkey codec — G.../S.../C... Stellar address encode/decode with CRC16 checksum validation (cli/trellis_cli/src/strkey.rs)

🚧 Open for Contribution

Area Description Difficulty
Frontend — Agreement Status page Polish and edge cases for live agreement state display Intermediate
Frontend — Create Agreement form Validation UX, milestone builder refinements Intermediate
Frontend — Milestone actions lock, submit, approve, dispute button workflows Intermediate
Frontend — Event feed enhancements Real-time updates, filtering, pagination Intermediate
Native RPC client Replace stellar CLI shell-out with native Rust HTTP client Advanced
Documentation CONTRIBUTING.md and contributor onboarding guide Beginner

See Issues for the full task list — each issue has exact requirements, acceptance criteria, a suggested branch name, and a timeframe.


Contributing

Trellis is built in the open and welcomes contributors of all experience levels — from documentation and frontend components to contract enhancements and tooling.

  1. Fork the repo and clone it locally
  2. Browse open issues tagged good first issue or help wanted
  3. Comment on the issue to claim it — wait for maintainer confirmation before starting
  4. Branch: git checkout -b feat/your-feature-name
  5. Verify: run cargo test in contracts/trellis_core
  6. PR: open a PR referencing the issue with Closes #X

New to Soroban? Start with the official Soroban docs.
New to React + Stellar? Read through frontend/src/lib/config.ts and the Stellar SDK docs.


License

MIT — see LICENSE for the full text.


Built by Allen · Trellis Ecosystem ✦ Trustless Escrow for the Global Workforce ✦

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages