These scripts demonstrate, end-to-end and on-chain, that the trustless-work-core API correctly integrates with the v2 Soroban escrow contracts. Each public contract function is reached through its corresponding API endpoint, the returned transaction is signed by a real wallet, and the result is submitted and verified on Stellar testnet.
They are written so an auditor can read them and follow exactly what happens:
every HTTP request, the unsigned XDR returned by the API, the local signature,
and the full send-transaction response are printed to the terminal.
Scope: single-release v2 (
single-release-v2/) and multi-release v2 (multi-release-v2/). Both cover all 14 public contract functions.
The API never holds keys. For every operation the integration is a three-step loop, and these scripts exercise all three steps against the live service:
build sign send
┌────────────┐ unsigned ┌────────────┐ signed ┌──────────────────────┐
│ POST │──── XDR ───▶│ wallet │── XDR ───▶│ POST /stellar/ │──▶ on-chain
│ /escrow/...│ │ (stellar-cli)│ │ send-transaction │
└────────────┘ └────────────┘ └──────────────────────┘
If a flow completes and its assertions pass, it proves the API builds a
transaction the contract accepts, that the contract executes the intended state
transition, and that send-transaction reports it back correctly.
All 14 public functions of the single-release v2 contract, each mapped 1:1 to an endpoint and proven by at least one script:
| API endpoint | Method | Contract function | Proven by |
|---|---|---|---|
/escrow/single-release/v2/deploy |
POST | tw_new_single_release_escrow + initialize_escrow |
01,02,03,04,05,06 |
/escrow/single-release/v2/fund |
POST | fund_escrow |
01,02,03,05,06 |
/escrow/single-release/v2/change-milestone-status |
POST | change_milestone_status |
01 |
/escrow/single-release/v2/approve-milestones |
POST | approve_milestones |
01 |
/escrow/single-release/v2/approve-and-release-milestones |
POST | approve_and_release_milestones |
02 |
/escrow/single-release/v2/release-funds |
POST | release_funds |
01 |
/escrow/single-release/v2/dispute |
POST | dispute_escrow |
03,05 |
/escrow/single-release/v2/resolve-dispute |
POST | resolve_dispute |
03,05 |
/escrow/single-release/v2/withdraw-remaining-funds |
POST | withdraw_remaining_funds |
05 |
/escrow/single-release/v2/update |
PUT | update_escrow |
04 |
/escrow/single-release/v2/manage-milestones |
POST | manage_milestones |
04 |
/escrow/single-release/v2/extend-ttl |
POST | extend_contract_ttl |
04 |
/escrow/single-release/v2/{contractId} |
GET | get_escrow |
all |
/escrow/single-release/v2/escrow-balances |
GET | get_multiple_escrow_balances |
06 |
Multi-release v2 (multi-release-v2/) covers the same 14 functions under
/escrow/multi-release/v2/…, with per-milestone semantics: each milestone has
its own amount + receiver, and release-funds, dispute-milestones and
resolve-dispute take a milestoneIndexes array so milestones are
released/disputed/resolved independently. The flow scripts (01–06) mirror the
single-release ones; dispute-milestones replaces dispute.
tw-v2-audit-scripts/
├── README.md
├── .env.example # copy to .env and fill in
├── lib/
│ ├── common.sh # config, HTTP, build→sign→send, logging, assertions
│ └── stellar.sh # wallets, friendbot, XLM→USDC swap, SAC, deploy payload
├── single-release-v2/
│ ├── 00_setup_check.sh # run first — verifies tooling, key, DEX liquidity
│ ├── 01_release_funds.sh # deploy→fund→status→approve→release
│ ├── 02_approve_and_release.sh # deploy→fund→approve-and-release
│ ├── 03_dispute_and_resolve.sh # deploy→fund→dispute→resolve
│ ├── 04_admin_management.sh # update, manage-milestones, extend-ttl
│ ├── 05_withdraw_remaining_funds.sh# residual sweep → withdraw-remaining-funds
│ ├── 06_reads.sh # get-escrow, escrow-balances
│ └── run_all.sh # runs 01→06 in order
└── multi-release-v2/ # same 6 flows, per-milestone (release/dispute/resolve by index)
├── 00_setup_check.sh
├── 01_release_funds.sh … 06_reads.sh
└── run_all.sh
Each flow ends with a summary listing every escrow contract it created, in order, with its stellar.expert link and the operations executed on it.
- stellar-cli ≥ 22 —
curl -sSf https://stellar.org/install.sh | bash(orbrew install stellar-cli) - curl and jq
- A valid API key for the deployed API.
- Internet access to the API, Horizon, the Soroban RPC and friendbot (all testnet).
Check your stellar-cli version with stellar --version.
cd tw-v2-audit-scripts
cp .env.example .envOpen .env and paste your API key into API_KEY. That is the only place
the key is needed — every script reads it from there and sends it as the
x-api-key header.
There is no treasury and no faucet. Every run generates fresh role wallets, funds them with testnet XLM via friendbot, and then swaps XLM → USDC on the testnet DEX (a path payment) so they hold the USDC needed to fund escrows. All of this happens inside the scripts; the only thing you configure is the API key in step 1.
bash single-release-v2/00_setup_check.shGreen all the way down means you are ready.
# single-release v2 — one flow, or everything in order
bash single-release-v2/01_release_funds.sh
bash single-release-v2/run_all.sh
# multi-release v2 — same idea
bash multi-release-v2/00_setup_check.sh
bash multi-release-v2/run_all.shEach script provisions its own fresh wallets, runs the flow, prints every
request/response, and finishes with PASSED (or aborts at the first failed
assertion). The escrow contract id is printed so you can inspect it on
stellar.expert (testnet).
The contract enforces that admin and disputeResolvers never overlap with
the other roles, while approver / serviceProvider / releaseSigner / receiver /
platform may all be the same address. Each run therefore uses three wallets:
| Wallet | Roles it holds | Signs |
|---|---|---|
| MAIN (W1) | approver, serviceProvider, releaseSigner, receiver, platform | deploy, fund, change-status, approve, release, approve-and-release, dispute |
| ADMIN (W2) | admin | update, manage-milestones, extend-ttl |
| RESOLVER (W3) | disputeResolver | resolve-dispute, withdraw-remaining-funds |
On testnet the Trustless Work fee wallet is a classic Stellar account that holds
a USDC trustline only. release_funds, resolve_dispute and
withdraw_remaining_funds pay the protocol fee to that wallet via the token's
Soroban contract; a transfer to a classic account traps unless that account
trusts the asset. A self-issued asset would make those settlement calls fail, so
every escrow is denominated in Circle's testnet USDC, which the fee wallet
already trusts. This is configurable in .env (USDC_ASSET).
- HTTP 401 /
AUTH_API_KEY_MISSING—API_KEYis wrong or empty in.env. Deploy did not return a contractId— the indexer was lagging when the deploy confirmed; the response carriedSTELLAR_TX_SUBMITTED_INDEXER_LAGGING. Re-run; the transaction itself succeeded.Signing produced empty output— stellar-cli too old (need ≥ 22) or the wallet name is unknown to the keystore.XLM->USDC swap failed— testnet DEX liquidity was momentarily thin, or the wallet's friendbot XLM had not settled. Re-run; if it persists, lowerESCROW_AMOUNT/SEED_USDC_AMOUNTin.env.DistributionsMustEqualEscrowBalance— the resolve/withdraw distribution sum must equal the live on-chain balance; the scripts read it just before the call, so this should not happen unless the balance changed underneath.
- The scripts make no assumptions hidden in code: payloads are built inline
with
jqand printed before sending. - The only thing done off-API is plumbing that the API has no endpoint for: generating + XLM-funding wallets, the USDC trustline, the XLM→USDC swap, and (only in script 05) a direct SAC transfer used to create a residual balance for the withdraw demo. Everything that touches an escrow goes through the API.
- All on-chain authorization is enforced by the contract's
require_auth; the scripts simply sign with the correct role wallet for each call. - Balance is verified against on-chain truth. The
get-escrowresponse'sbalancefield is served from an indexer that can lag behind the ledger, so the scripts assert balances against theescrow-balancesendpoint instead, which performs a liveget_multiple_escrow_balancescontract read. Lifecycle flags (released,dispute.*) and milestones come from the liveget_escrowcontract struct. Every assertion polls until the on-chain state is reflected.