Skip to content

Latest commit

 

History

History
782 lines (598 loc) · 30.9 KB

DEPLOYMENT.md

File metadata and controls

782 lines (598 loc) · 30.9 KB

Trellis — Testnet Deployment

Working directory: Run deployment commands from the repository root. Each step begins with cd "$(git rev-parse --show-toplevel)" so it can be run independently from any directory inside the repository. Paths to build artifacts below are relative to the repository root.

Live Contract

Network Contract ID
Stellar Testnet CAUAO7CYKULE2K4EJMQ6LLRUHP7Y7JYOH6G2VBXKYG7PTETE3UZ3DU7Q

Explorer Links


Network Configuration

Value
Network Stellar Testnet
RPC URL https://soroban-testnet.stellar.org
Network Passphrase Test SDF Network ; September 2015
Soroban SDK Version 22.x (Protocol 26 compatible)
Explorer https://stellar.expert/explorer/testnet

Verifying Release Artifacts

Each GitHub Release publishes a .sha256 checksum and a .asc GPG detached signature alongside every WASM/CLI binary artifact.

  1. Download the artifact, its .sha256 file, and its .asc file from the Releases page.
  2. Verify the checksum:
    sha256sum -c trellis-x86_64-unknown-linux-gnu.sha256
  3. Verify the GPG signature (import the maintainers' public key first):
    gpg --verify trellis-x86_64-unknown-linux-gnu.asc trellis-x86_64-unknown-linux-gnu

Deploying Your Own Instance

Prerequisites

Step 1 — Create and fund a testnet identity

cd "$(git rev-parse --show-toplevel)"
stellar keys generate trellis-deployer --network testnet
stellar keys fund trellis-deployer --network testnet --rpc-url https://soroban-testnet.stellar.org

Step 2 — Build the contract WASM

cd "$(git rev-parse --show-toplevel)"
cargo rustc --manifest-path=contracts/trellis_core/Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release

Note: Use cargo rustc with --target=wasm32-unknown-unknown rather than stellar contract build — the latter targets wasm32v1-none which requires soroban-sdk 22.x+ and a newer toolchain. The manual cargo rustc command gives you full control over the target.

The compiled WASM will be at:

target/wasm32-unknown-unknown/release/trellis_core.wasm

Step 3 — Deploy to testnet

cd "$(git rev-parse --show-toplevel)"
stellar contract deploy \
  --wasm target/wasm32-unknown-unknown/release/trellis_core.wasm \
  --source trellis-deployer \
  --network testnet

This returns a C... contract address. Save it — you'll need it for all CLI commands.

Step 4 — Set environment variables

cd "$(git rev-parse --show-toplevel)"
export STELLAR_RPC_URL="https://soroban-testnet.stellar.org"
export STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
export TRELLIS_CONTRACT_ID="<your contract ID>"
export TRELLIS_SOURCE_KEY="<your stellar identity name>"

Keeping a raw secret key out of the environment

If you sign with a raw S… secret seed instead of a named stellar keys identity, put it in a file instead of exporting it. Environment variables end up in /proc/<pid>/environ, shell history and .env files:

umask 077
printf '%s\n' 'S...your-secret...' > ~/.trellis-payer.key   # mode 0600

export TRELLIS_SOURCE_KEY_FILE=~/.trellis-payer.key
# or per invocation:
trellis --source-key-file ~/.trellis-payer.key status --agreement-id <hex>

The source key is resolved in this order (first match wins):

  1. --source-key-file <PATH>
  2. TRELLIS_SOURCE_KEY_FILE
  3. TRELLIS_SOURCE_KEY

A configured key file always wins. If TRELLIS_SOURCE_KEY is also set, the CLI prints a note saying it was ignored. If a configured key file is missing or empty, the command fails instead of falling back to TRELLIS_SOURCE_KEY. On Linux/macOS a warning is printed when the key file is readable by the group or other users (anything other than 0600/0400). Whitespace and a trailing newline around the key are trimmed.

One config for every role (--as)

Escrow commands have to be signed by different parties: the payer, the payee or the dispute resolver. You don't need to re-export TRELLIS_SOURCE_KEY before each one. Put all three identities in .env and pick one per command with --as:

# .env
TRELLIS_PAYER_KEY=trellis-payer          # stellar identity name or S… seed
TRELLIS_PAYEE_KEY=trellis-payee
TRELLIS_RESOLVER_KEY=trellis-resolver
# Or point at key files instead (they take precedence over the *_KEY vars):
# TRELLIS_PAYER_KEY_FILE=~/.trellis-payer.key
trellis --as payer lock-funds --agreement-id <hex> --milestone-id 0
--as Key file variable (checked first) Key variable
payer TRELLIS_PAYER_KEY_FILE TRELLIS_PAYER_KEY
payee TRELLIS_PAYEE_KEY_FILE TRELLIS_PAYEE_KEY
resolver TRELLIS_RESOLVER_KEY_FILE TRELLIS_RESOLVER_KEY

If the chosen role's variables aren't set, --as fails. It never falls back to TRELLIS_SOURCE_KEY, so a missing entry can't make a command sign with the wrong identity. Without --as, the CLI uses TRELLIS_SOURCE_KEY exactly as before. --as cannot be combined with --source-key-file.

Step 5 — Build the CLI

cd "$(git rev-parse --show-toplevel)"
cargo build --release --manifest-path=cli/trellis_cli/Cargo.toml

Binary will be at target/release/trellis.exe (Windows) or target/release/trellis (Linux/Mac), relative to the repository root.


CLI Usage

Create an agreement

trellis --as payer init \
  --agreement-id <64-char hex string> \
  --payer <payer G... address> \
  --payee <payee G... address> \
  --token <USDC SAC contract address> \
  --resolver <dispute resolver G... address> \
  --milestones "1000,2000"

The --milestones flag accepts comma-separated amounts in stroops. Multiple milestones: --milestones "1000,2000,3000"

Check agreement status

trellis status --agreement-id <hex>

Check an agreement's total value

trellis total-amount --agreement-id <hex>

Read-only; no signature or confirmation needed. Returns the sum of all milestone amounts (an i128 in the token's base unit, printed as a quoted string). The frontend's agreement status page shows the same value as Total in the Milestones header.

Fund a milestone

trellis --as payer lock-funds --agreement-id <hex> --milestone-id 0

Must be signed by the payer identity (--as payer)

Fund several milestones at once

trellis batch-lock-funds --agreement-id <hex> --milestone-ids "0,1,2"

Must be signed by the payer identity. The batch is atomic: if any listed milestone is out of range or not Pending, the whole transaction reverts and no tokens move. --milestone-ids takes comma-separated zero-based indices; empty input, empty fields ("0,,1"), non-integers and duplicate IDs are rejected by the CLI before anything is submitted.

Submit work proof

trellis --as payee submit-work \
  --agreement-id <hex> \
  --milestone-id 0 \
  --proof-uri "ipfs://your-proof-hash"

Must be signed by the payee identity (--as payee)

--proof-uri is optional — omit it to mark the milestone submitted without a proof link. Do not pass an empty string: the contract stores proof_uri as an Option, so "no proof" is the absent flag, not "". The web UI follows the same rule: leaving the Proof URI field blank submits None, not an empty string.

Approve and release payment

trellis --as payer approve-release --agreement-id <hex> --milestone-id 0

Must be signed by the payer identity (--as payer)

Raise a dispute

trellis --as payer raise-dispute \
  --agreement-id <hex> \
  --milestone-id 0 \
  --caller <payer address>

Can be called by either the payer or the payee. Use --as payee with the payee's address as --caller to raise it from the payee side.

In the web UI, the Dispute button only appears when the connected wallet is the agreement's payer or payee; any other wallet sees an explanatory note instead.

Resolve a dispute

trellis --as resolver resolve-dispute \
  --agreement-id <hex> \
  --milestone-id 0 \
  --refund-to-payer true

Must be signed by the dispute resolver identity (--as resolver). --refund-to-payer true returns funds to payer. --refund-to-payer false releases funds to payee.

Cancel an unfunded milestone

trellis --as payer cancel-milestone --agreement-id <hex> --milestone-id 0

Must be signed by the payer (--as payer). Only works on milestones still in Pending status.

Extend an agreement's ledger TTL

trellis extend-ttl --agreement-id <hex> --caller <your G... address>

Any address may call this — no party signature is required — but the source account pays the rent. --caller is recorded in the ttl_extended event; pass the source key's public address. Prompts for confirmation unless --yes is given.

Check RPC connectivity

trellis health
trellis --json health   # {"status":"success","result":{"health":{...},"latest_ledger":{"hash","protocol_version","sequence"}},...}

Calls the Soroban getHealth and getLatestLedger JSON-RPC methods directly over HTTP. The stellar binary, TRELLIS_CONTRACT_ID and TRELLIS_SOURCE_KEY are all optional here, since only the RPC URL is used. Exits non-zero if the endpoint is unreachable or does not report healthy. --dry-run prints the JSON-RPC requests without sending them.


Testnet Token (USDC)

The USDC Stellar Asset Contract address on testnet:

CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA

To derive it yourself:

stellar contract id asset \
  --network testnet \
  --asset USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5

To get testnet USDC, visit the Stellar testnet faucet: https://developers.stellar.org/docs/tools/quickstart#friendbot


Verified Test Agreement

A live test agreement was created on testnet during initial deployment verification:

Field Value
Agreement ID 0101010101010101010101010101010101010101010101010101010101010101
Payer GDTFKGGBI4GGHSZ6ILTTJJTXOA4CIKCT6J2DRC3Q3OT3YMWGB7TBLXMK
Payee GCGX32T5547AE57PUIAX5CDQWKHHEQFYOPIGXH7O3CENKDX3FR6U67RY
Milestone 0 1000 stroops, status: Pending

Note — testnet state is ephemeral: Stellar resets testnet ledger state periodically (roughly quarterly). When that happens, the contract instance above and this agreement ID are wiped, and trellis status will return agreement not found. If the command below fails, see Testnet Reset to redeploy and create fresh test data.

Verify it live:

trellis status --agreement-id 0101010101010101010101010101010101010101010101010101010101010101

Mainnet Deployment

Everything above is testnet-only. A mainnet deployment moves real funds and some decisions cannot be undone, so add these precautions:

  • Key management: use a dedicated hardware-backed or offline-generated deployer identity (stellar keys add trellis-mainnet-deployer --seed-phrase / ledger). Never reuse testnet keys, never store the secret in .env, CI variables, or shell history.
  • Review gates: deploy only a WASM built from a tagged, reviewed release (see "Verifying Release Artifacts"); compare its sha256 against the release attestation. make deploy-mainnet prints the hash and requires typing mainnet to confirm.
  • Irreversibility: init parameters and any upgrade/admin decisions are permanent once submitted. Dry-run the full flow on testnet first, and confirm the admin and fee settings with a second reviewer before submitting.
  • Config: start from frontend/.env.mainnet.example; the CLI uses --network mainnet.
make build-contract
make deploy-mainnet MAINNET_IDENTITY=trellis-mainnet-deployer

CI approval gate

.github/workflows/deploy-mainnet.yml is the manual, environment: mainnet-gated deploy workflow.

Any workflow that targets mainnet must declare environment: mainnet. In Settings → Environments → New environment → mainnet, enable Required reviewers (at least two maintainers), restrict deployment to tags/master, and store mainnet secrets only as environment secrets. Jobs then pause until a reviewer manually approves.


Testnet Reset

Stellar's testnet is periodically reset, wiping all deployed contract instances and stored ledger entries (including every Trellis agreement). When this happens, the contract ID in Live Contract and the agreement ID in Verified Test Agreement both stop resolving.

How to tell testnet was reset

A scheduled workflow checks this for you. Testnet Health runs daily (07:00 UTC, or on demand via Actions → Testnet Health → Run workflow) and calls trellis status against the Live Contract and Verified Test Agreement:

  • Reset detected — the contract or agreement no longer exists on-chain (e.g. Error(Contract, #3) / AgreementNotFound, or a missing contract instance). The workflow opens an issue labelled testnet-reset, or comments on the existing open one, and the run fails. Check for that issue before debugging "agreement not found" yourself.
  • Inconclusive — the probe failed for another reason (RPC outage, Friendbot down, build failure). The run fails so it is visible, but no issue is filed, so a flaky RPC never raises a false reset alarm.
  • Healthy — any open testnet-reset issue is closed automatically.

Each run's probe output is in its job summary. The same check runs locally:

TRELLIS_CONTRACT_ID=<contract ID> TRELLIS_SOURCE_KEY=<identity> \
  TRELLIS_BIN=target/release/trellis ./scripts/check-testnet-health.sh [agreement ID]

It exits 0 (healthy), 10 (reset suspected) or 1 (inconclusive).

Signs you can check manually:

  • trellis status --agreement-id <any known ID> returns agreement not found even though the ID was previously valid.
  • The contract ID no longer resolves on Stellar Expert.

Recovering

  1. Redeploy the contract (Steps 1–3 under Deploying Your Own Instance) to get a new contract ID.
  2. Update TRELLIS_CONTRACT_ID (CLI) and VITE_CONTRACT_ID (frontend/.env) to the new contract ID.
  3. Create a fresh test agreement with trellis init (see CLI Usage) and note the new agreement ID for testing.
  4. Optionally run scripts/reset-testnet.sh, which automates steps 1–3: it rebuilds and redeploys the contract, builds the CLI, creates a new test agreement, and then writes the new values back into the repo (see below).
./scripts/reset-testnet.sh

After a successful reset the script updates, in place:

  • DEPLOYMENT.md — the blocks between <!-- reset-testnet:begin … --> and <!-- reset-testnet:end … --> markers in Live Contract and Verified Test Agreement are regenerated with the new contract ID, agreement ID, and payer/payee addresses. Keep those markers intact; content between them is overwritten on every reset. The script fails before writing anything if a marker is missing.
  • frontend/.env — VITE_CONTRACT_ID is set to the new contract ID (other variables are preserved; the file is created from frontend/.env.example if it does not exist). Set FRONTEND_ENV_FILE to patch a different file instead.
  • frontend/.env.example — VITE_CONTRACT_ID is updated so new checkouts default to the live contract.

Nothing is written if any earlier step (build, deploy, trellis init, or the verification below) fails.

Verifying a reset

A reset is only done once the new agreement can actually move funds — an agreement can be created successfully with a token address that no lock_funds call can ever transfer from. scripts/reset-testnet.sh checks this itself before updating any files, and exits non-zero if any check fails:

  1. Token is a token contract. The test agreement uses the native XLM Stellar Asset Contract (stellar contract id asset --asset native), which must resolve to a C… contract ID. A G… account/issuer address is not a token and will make every lock_funds fail.
  2. Agreement reads back. trellis status returns the new agreement and it references the expected payer, payee, resolver and token.
  3. Funds actually move. trellis lock-funds funds milestone 0 as trellis-payer, trellis milestone-status reports Funded, and the token's balance for the new contract equals the milestone amount.

As a result the test agreement in Verified Test Agreement is left with milestone 0 Funded, ready for submit-work / approve-release testing.

If you reset manually (steps 1–3 above) instead, run the same checks by hand:

TOKEN_ID=$(stellar contract id asset --asset native --network testnet)
# ... trellis init --token "$TOKEN_ID" ... (see CLI Usage)

trellis status --agreement-id <new-id>
TRELLIS_SOURCE_KEY=<payer identity> trellis lock-funds --agreement-id <new-id> --milestone-id 0
trellis milestone-status --agreement-id <new-id> --milestone-id 0   # expect Funded
stellar contract invoke --id "$TOKEN_ID" --network testnet --source <any identity> \
  -- balance --id <new contract id>                                 # expect the milestone amount

Review and commit the changes with git diff DEPLOYMENT.md frontend/.env.example (frontend/.env is git-ignored).

The script requires the same prerequisites as manual deployment (Rust, wasm32-unknown-unknown target, stellar CLI) and a funded trellis-deployer identity — it will create and fund one via Friendbot if it does not already exist.


Agreement TTL Keeper

Soroban archives persistent entries whose TTL runs out. Every state-mutating call renews an agreement's TTL to ~30 days, but an agreement that sits idle longer than that (a long delivery window, a stalled dispute) needs extend_agreement_ttl called on it. The repo ships a minimal keeper for this:

  • scripts/ttl-keeper.sh runs trellis extend-ttl --yes for every agreement listed in .github/keeper-agreements.txt (or in TRELLIS_KEEPER_AGREEMENT_IDS, comma-separated, when set). It attempts every agreement and exits non-zero if any failed.
  • .github/workflows/ttl-keeper.yml runs the script weekly (Wednesdays 05:00 UTC) and on manual dispatch, where an agreement_ids input can override the file for a one-off run. The run's output is written to the job summary.

Setup (maintainers). The workflow is skipped until these are configured in the repository settings:

Name Kind Value
TRELLIS_KEEPER_SECRET_KEY Secret S… secret key of a funded testnet account that pays the rent
TRELLIS_KEEPER_ADDRESS Variable The matching G… public address (recorded as caller; the contract requires it to sign, so it must match the secret key)
TRELLIS_CONTRACT_ID Variable (optional) Contract to target; defaults to the Live Contract

Use a dedicated low-balance account — never a payer, payee or resolver key.

Running it locally / as a cron job:

export TRELLIS_CONTRACT_ID="<contract ID>"
export TRELLIS_SOURCE_KEY="<funded identity name>"
export TRELLIS_KEEPER_CALLER="$(stellar keys address <funded identity name>)"
TRELLIS_BIN=target/release/trellis ./scripts/ttl-keeper.sh

Assumptions and limitations. This is a stopgap, not a production keeper network:

  • It only renews agreements someone has added to the tracked list — there is no on-chain discovery, so new long-lived agreements must be added by PR.
  • It depends on GitHub's scheduler (runs can be delayed, and scheduled workflows are disabled after 60 days without repository activity) and on a single funded key. If that key runs out of XLM, every extension fails.
  • A failed run is visible only as a failed workflow run; nothing pages anyone. The weekly cadence leaves roughly three missed runs of headroom before a 30-day TTL lapses.
  • After a Testnet Reset the listed IDs no longer exist and every extension fails with agreement not found until the list (and TRELLIS_CONTRACT_ID) is updated.

Verified CLI Commands

Command Status Notes
init ✅ Verified on testnet Agreement created on-chain
status ✅ Verified on testnet Returns full agreement state as JSON
total-amount ✅ Implemented Returns the agreement's total value via get_total_amount (Issue #492)
lock-funds ✅ Verified on testnet Funds successfully locked in escrow (Issue #7)
batch-lock-funds ✅ Implemented Atomically funds multiple milestones in one transaction (Issue #493)
submit-work ✅ Verified on testnet Work proof submitted and milestone transitioned to WorkSubmitted (Issue #8)
approve-release ✅ Verified on testnet Funds released to payee, milestone marked Completed (Issue #8)
raise-dispute ✅ Verified on testnet Dispute raised and milestone transitioned to Disputed (Issue #9)
resolve-dispute ✅ Verified on testnet (both paths) Resolves to both Refunded (payer) and Completed (payee) states (Issue #9)
cancel-milestone ✅ Implemented Cancels unfunded milestones
extend-ttl ✅ Implemented Renews an agreement's ledger TTL; used by the TTL keeper (Issue #494)


End-to-End Test Flows (Verified on Testnet)

Flow 1: Happy Path — Lock → Submit → Approve (Issues #7 & #8)

Objective: Verify the complete payment flow from initial funding through approval and release.

Steps:

  1. Create agreement with payer, payee, resolver identities
  2. Fund milestone 0 (payer calls lock-funds)
  3. Submit work proof (payee calls submit-work)
  4. Approve and release (payer calls approve-release)

Verification:

  • ✅ Milestone transitions: Pending → Funded → WorkSubmitted → Completed
  • ✅ Token transfer confirmed from payer to contract, then to payee
  • ✅ Events emitted at each stage (queryable via event filters)
  • ✅ status returns Completed state with proof URI recorded

Flow 2: Dispute Path — Raise → Resolve as Refund (Issue #9)

Objective: Verify dispute resolution when resolver rules in payer's favor.

Steps:

  1. Create and fund agreement as above
  2. Dispute raised by payee (payer calls raise-dispute with payee caller)
  3. Resolver ruling: refund to payer (resolver calls resolve-dispute --refund-to-payer true)

Verification:

  • ✅ Milestone transitions: Funded → Disputed → Refunded
  • ✅ Funds returned to payer's address
  • ✅ Payee does not receive payment
  • ✅ Events show dispute resolution with refund outcome

Flow 3: Dispute Path — Raise → Resolve as Release (Issue #9)

Objective: Verify dispute resolution when resolver rules in payee's favor.

Steps:

  1. Create and fund agreement
  2. Work submitted, then dispute raised by payer (payer calls raise-dispute with caller=payer)
  3. Resolver ruling: release to payee (resolver calls resolve-dispute --refund-to-payer false)

Verification:

  • ✅ Milestone transitions: WorkSubmitted → Disputed → Completed
  • ✅ Funds released to payee's address
  • ✅ Payer does not recover funds
  • ✅ Events show dispute resolution with payee-win outcome

Frontend SPA Routing

The frontend is a single-page app using client-side routing (React Router). A direct load or refresh on a nested route (e.g. /agreement/123) must be served index.html by the web server, not a 404 — the router only resolves the path once the JS bundle loads.

  • Vite dev server: already configured via appType: 'spa' in frontend/vite.config.ts; no extra setup needed.

  • Netlify / Vercel / other static hosts that honor _redirects: frontend/public/_redirects ships a catch-all (/* /index.html 200) that is copied into dist/ on build.

  • nginx: add a fallback to the location / block serving the built dist/ directory:

    location / {
        try_files $uri $uri/ /index.html;
    }
  • Apache: enable mod_rewrite and add to dist/.htaccess:

    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.html [L]

Security headers and CSP

npm run build emits dist/_headers (honoured by Netlify and Cloudflare Pages) carrying a Content-Security-Policy plus X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options: DENY and a restrictive Permissions-Policy. The same CSP (minus frame-ancestors, which browsers ignore in a <meta> tag) is injected into dist/index.html as a fallback. The policy is generated by frontend/security-headers.ts:

  • connect-src is 'self' plus the origins of VITE_RPC_URL and, when set, VITE_INDEXER_URL — set those before building or RPC calls will be blocked.
  • script-src 'self' with no 'unsafe-inline' or 'unsafe-eval'; the build (and the frontend CI job) fails if 'unsafe-eval' is ever added.
  • Freighter needs no allowance: it talks to the page via postMessage.

For nginx or Apache, copy the values from dist/_headers into add_header … always; / Header always set … directives so frame-ancestors 'none' is enforced too.


Event Indexer & Real-Time Relay

indexer/ is an optional service that sits between the frontend and Soroban RPC (#495, #496). It:

  • polls getEvents for the contract once, server-side, and stores every event in a SQLite database;
  • serves stored history over HTTP (/events, /stats);
  • pushes new events to connected browsers over Server-Sent Events (/stream/events, /stream/stats).

Without it, each open browser tab polls RPC on its own, and agreement history disappears once events age out of the RPC node's retention window.

Deploying

Requirements: Node 22.13+ (for the built-in node:sqlite), a persistent disk for the database file, and one long-running process. There are no npm dependencies, so no install step is needed.

cd indexer
RPC_URL=https://soroban-testnet.stellar.org \
CONTRACT_ID=CAUAO7CYKULE2K4EJMQ6LLRUHP7Y7JYOH6G2VBXKYG7PTETE3UZ3DU7Q \
DB_PATH=/var/lib/trellis/events.db \
CORS_ORIGIN=https://your-frontend.example \
PORT=8787 \
npm start

Then build the frontend with VITE_INDEXER_URL=https://indexer.your-domain.example. See indexer/README.md for every option and endpoint.

  • Run exactly one instance per contract. The relay keeps its subscriber list in memory and the SQLite file is single-writer, so don't scale it horizontally. One instance handles thousands of SSE connections.
  • Reverse proxies: SSE needs response buffering off and a long read timeout. The service sends X-Accel-Buffering: no and a heartbeat comment every 15 s. For nginx, also set proxy_read_timeout 1h; and proxy_http_version 1.1; on the /stream/ location.
  • Health checks: use GET /health. It reports status: "degraded" while RPC polls are failing, and compares rpcLatestLedger with latestIndexedLedger so you can see lag.
  • Redeploying the contract: each database is tied to one contract ID, and the service refuses to start if CONTRACT_ID doesn't match the one already stored in DB_PATH. Point DB_PATH at a new file for the new contract.
  • Fallback: if VITE_INDEXER_URL is unset, or the stream can't connect within 5 s, the frontend polls RPC directly as before. An indexer outage makes the app slower to update, but it keeps working.

Data-retention model

  • The RPC node is a rolling window. Public testnet RPC keeps roughly the last 7 days of events (getHealth → ledgerRetentionWindow). Anything older is gone from that node.
  • The indexer keeps events indefinitely. It never prunes. Events are keyed by their RPC event id, so re-ingesting an overlapping page is a no-op.
  • First start: ingestion begins at the node's oldestLedger, or at START_LEDGER if set. Events older than that window can't be recovered from this node. To get full history for an older contract, set START_LEDGER and point RPC_URL at a node or archive that still holds those ledgers.
  • Downtime: the ingest cursor is stored in the database, so a restart resumes where it stopped. If the indexer is down longer than the RPC retention window, the cursor expires. The indexer then resumes from the node's oldestLedger, logs a warning, and records the gap under lastGap in /health. Events inside that gap are missing until backfilled from another source.
  • The database is the only copy of old history. Back up DB_PATH regularly (e.g. sqlite3 events.db ".backup events.bak"). Losing it means falling back to whatever the RPC node still retains.

Known Issues & Notes

  • Auth per command: Each CLI command must be signed by the correct identity. Pass --as payer|payee|resolver to pick the required signer per operation (payer for init, lock-funds, approve-release, cancel-milestone; payee for submit-work; resolver for resolve-dispute). See One config for every role.

  • WASM build target: Use wasm32-unknown-unknown via cargo rustc rather than stellar contract build until the project upgrades to a toolchain that fully supports wasm32v1-none.

  • Windows paths: On Windows Git Bash, use the full path to the binary: C:\Users\<you>\Documents\Trellis\target\release\trellis.exe

  • Native RPC client (#11): The current implementation falls back to the stellar CLI for transaction signing and submission. A full native Rust implementation would require integrating the Stellar SDK for key management, transaction envelope building, and signing. The structure is in place for future native implementation in cli/trellis_cli/src/rpc.rs.