Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,19 @@ contracts/
src/
lib.rs The authorize_as_current_contract pattern from docs/src/INTEGRATION.md
test.rs Validates that pattern against Tholos's real compiled wasm
demos/
freelance-escrow/ A real freelance milestone-payment app built on Tholos;
a pnpm/Vite/React project, not part of the Cargo
workspace, see its own README for setup
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
```

Additional demo apps should each live as their own directory under `demos/`,
following the same layout as `demos/freelance-escrow`.

`demo-consumer` and `asserter-consumer` exist to keep [INTEGRATION.md](docs/src/INTEGRATION.md)
honest: they're not products, they're compiled checks that the documented
integration patterns actually work. If you change Tholos's public interface,
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ contracts/
tholos/ The assertion and dispute contract
demo-consumer/ Minimal example contract that calls into Tholos,
validating the pattern documented in INTEGRATION.md
demos/
freelance-escrow/ A real freelance milestone-payment app built on
Tholos as its settlement layer, not a UI of buttons
calling contract functions; see its own README
scripts/
testnet-smoke.sh End-to-end check against real Stellar testnet infrastructure
.github/workflows/
Expand Down
5 changes: 5 additions & 0 deletions demos/freelance-escrow/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Overrides for the defaults baked into src/lib/config.ts. Leave unset to use
# the deployed Tholos testnet instance.
VITE_SOROBAN_RPC_URL=
VITE_NETWORK_PASSPHRASE=
VITE_THOLOS_CONTRACT_ID=
24 changes: 24 additions & 0 deletions demos/freelance-escrow/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
8 changes: 8 additions & 0 deletions demos/freelance-escrow/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
79 changes: 79 additions & 0 deletions demos/freelance-escrow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Freelance milestone escrow

A real freelance/gig milestone-payment app: a client and freelancer agree on
milestones, the freelancer marks a milestone done, funds release automatically
once the challenge window passes uncontested, or a resolver panel decides if
the client disputes it. Tholos is the settlement layer underneath, not
something this app puts on display: there are no "call assert_outcome" buttons
anywhere in the UI. Milestone actions read as ordinary product actions
(mark done, dispute, release), and Tholos's bonded assertion/dispute contract
is what actually backs the "funds release uncontested or a panel decides"
guarantee. See [docs/src/INTEGRATION.md](../../docs/src/INTEGRATION.md) for
the pattern this app follows (end user as asserter).

This app talks to a live Tholos instance, so every action (marking a milestone
done, disputing, voting, finalizing) is a real signed transaction, not a
simulation. You'll need testnet XLM in your Freighter wallet to post the bond;
get some from [Friendbot](https://friendbot.stellar.org/) if you're testing
with a fresh address.

## What maps to what

| App action | Tholos call |
| --- | --- |
| Freelancer marks a milestone done | `assert_outcome(freelancer, true)` |
| Nobody disputes within the challenge window | `finalize` releases the bond, milestone pays out |
| Client disputes a submitted milestone | `dispute(client, id)` |
| Resolver panel decides a dispute | `resolve(resolver, id, agrees_with_freelancer)` |

Job and milestone metadata (title, description, client, freelancer, the
milestone's face-value amount) lives entirely in this app, off-chain. Tholos
only ever sees a bonded assertion per milestone; the mapping from milestone to
assertion id is tracked client-side (see `assertionId` on `Milestone` in
`src/data/jobs.ts`), matching the "store that mapping on your side" guidance
in `INTEGRATION.md`.

The bond posted on-chain is a fixed amount set at deploy time (see
`docs/src/DEPLOYMENT.md`), not the milestone's face-value amount shown in the
UI: Tholos v1 has one bond size per instance, not a per-call bond, so the bond
is collateral that deters a bad-faith claim, separate from whatever the
client and freelancer actually agreed to pay for the milestone.

## Running it

Contract addresses are never committed to source (see CONTRIBUTING.md), so
point this at a deployed Tholos instance yourself:

```sh
cp .env.example .env.local
# fill in VITE_THOLOS_CONTRACT_ID with a deployed contract id
# (see docs/src/DEPLOYMENT.md to deploy one)

pnpm install
pnpm dev
```

Requires the [Freighter](https://www.freighter.app/) browser extension to
connect a wallet.

There's no role system of its own: Tholos only knows addresses, not "client"
or "freelancer," so this demo has a role switcher in the header letting the
connected wallet act as freelancer, client, or resolver to exercise all sides
of a dispute from one browser.

## Structure

```text
src/
lib/
config.ts RPC URL, network passphrase, and contract id (env-overridable)
tholos.ts Tholos contract client: assert_outcome/dispute/resolve/finalize/get_assertion_state
wallet.ts Freighter connect/detect
state/
JobsContext.tsx Job and milestone state, wired to the Tholos client
RoleContext.tsx Which side of the transaction the connected wallet is currently playing
components/
JobCard.tsx, MilestoneRow.tsx, PostJobForm.tsx, WalletButton.tsx, RoleSwitcher.tsx
data/
jobs.ts Seed jobs and the Job/Milestone types
```
13 changes: 13 additions & 0 deletions demos/freelance-escrow/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Freelance milestone escrow</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions demos/freelance-escrow/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "freelance-escrow",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"@stellar/freighter-api": "^6.0.1",
"@stellar/stellar-sdk": "^16.2.0",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/node": "^24.13.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.3.4",
"oxlint": "^1.75.0",
"typescript": "~5.6.2",
"vite": "^6.3.5"
},
"pnpm": {
"onlyBuiltDependencies": ["esbuild"]
}
}
Loading
Loading