diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 898a809..c09da6e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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,
diff --git a/README.md b/README.md
index 7262cc6..a22d470 100644
--- a/README.md
+++ b/README.md
@@ -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/
diff --git a/demos/freelance-escrow/.env.example b/demos/freelance-escrow/.env.example
new file mode 100644
index 0000000..bb4e4aa
--- /dev/null
+++ b/demos/freelance-escrow/.env.example
@@ -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=
diff --git a/demos/freelance-escrow/.gitignore b/demos/freelance-escrow/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/demos/freelance-escrow/.gitignore
@@ -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?
diff --git a/demos/freelance-escrow/.oxlintrc.json b/demos/freelance-escrow/.oxlintrc.json
new file mode 100644
index 0000000..6fa991d
--- /dev/null
+++ b/demos/freelance-escrow/.oxlintrc.json
@@ -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 }]
+ }
+}
diff --git a/demos/freelance-escrow/README.md b/demos/freelance-escrow/README.md
new file mode 100644
index 0000000..a04f350
--- /dev/null
+++ b/demos/freelance-escrow/README.md
@@ -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
+```
diff --git a/demos/freelance-escrow/index.html b/demos/freelance-escrow/index.html
new file mode 100644
index 0000000..b738e7c
--- /dev/null
+++ b/demos/freelance-escrow/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Freelance milestone escrow
+
+
+
+
+
+
diff --git a/demos/freelance-escrow/package.json b/demos/freelance-escrow/package.json
new file mode 100644
index 0000000..98cb347
--- /dev/null
+++ b/demos/freelance-escrow/package.json
@@ -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"]
+ }
+}
diff --git a/demos/freelance-escrow/pnpm-lock.yaml b/demos/freelance-escrow/pnpm-lock.yaml
new file mode 100644
index 0000000..8d44be4
--- /dev/null
+++ b/demos/freelance-escrow/pnpm-lock.yaml
@@ -0,0 +1,1702 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@stellar/freighter-api':
+ specifier: ^6.0.1
+ version: 6.0.1
+ '@stellar/stellar-sdk':
+ specifier: ^16.2.0
+ version: 16.2.0
+ react:
+ specifier: ^19.2.8
+ version: 19.2.8
+ react-dom:
+ specifier: ^19.2.8
+ version: 19.2.8(react@19.2.8)
+ devDependencies:
+ '@types/node':
+ specifier: ^24.13.3
+ version: 24.13.3
+ '@types/react':
+ specifier: ^19.2.17
+ version: 19.2.18
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.4(@types/react@19.2.18)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@6.4.3(@types/node@24.13.3))
+ oxlint:
+ specifier: ^1.75.0
+ version: 1.77.0
+ typescript:
+ specifier: ~5.6.2
+ version: 5.6.3
+ vite:
+ specifier: ^6.3.5
+ version: 6.4.3(@types/node@24.13.3)
+
+packages:
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.8':
+ resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.29.7':
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7':
+ resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7':
+ resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.8':
+ resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
+ engines: {node: ^22.20 || ^24.12 || >=25}
+ cpu: [x64]
+ os: [linux]
+
+ '@noble/ed25519@3.1.0':
+ resolution: {integrity: sha512-pfcObRY3CtvwfaG9Mt5XqZdKmAQppl37tHUeuBhDUbiwJBCVY4/A4lbMvb1xKhMDx96AqAqZpMWuBX1HulhX4g==}
+
+ '@noble/hashes@2.3.0':
+ resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==}
+ engines: {node: '>= 20.19.0'}
+
+ '@oxlint/binding-android-arm-eabi@1.77.0':
+ resolution: {integrity: sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxlint/binding-android-arm64@1.77.0':
+ resolution: {integrity: sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxlint/binding-darwin-arm64@1.77.0':
+ resolution: {integrity: sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint/binding-darwin-x64@1.77.0':
+ resolution: {integrity: sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint/binding-freebsd-x64@1.77.0':
+ resolution: {integrity: sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.77.0':
+ resolution: {integrity: sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm-musleabihf@1.77.0':
+ resolution: {integrity: sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-gnu@1.77.0':
+ resolution: {integrity: sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-musl@1.77.0':
+ resolution: {integrity: sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-ppc64-gnu@1.77.0':
+ resolution: {integrity: sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-gnu@1.77.0':
+ resolution: {integrity: sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-musl@1.77.0':
+ resolution: {integrity: sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-s390x-gnu@1.77.0':
+ resolution: {integrity: sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-gnu@1.77.0':
+ resolution: {integrity: sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-musl@1.77.0':
+ resolution: {integrity: sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-openharmony-arm64@1.77.0':
+ resolution: {integrity: sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxlint/binding-win32-arm64-msvc@1.77.0':
+ resolution: {integrity: sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint/binding-win32-ia32-msvc@1.77.0':
+ resolution: {integrity: sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxlint/binding-win32-x64-msvc@1.77.0':
+ resolution: {integrity: sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-beta.27':
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
+
+ '@rollup/rollup-android-arm-eabi@4.62.4':
+ resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.62.4':
+ resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.62.4':
+ resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.62.4':
+ resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.62.4':
+ resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.62.4':
+ resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.4':
+ resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.62.4':
+ resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.62.4':
+ resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.62.4':
+ resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.62.4':
+ resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-musl@4.62.4':
+ resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.62.4':
+ resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-musl@4.62.4':
+ resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.62.4':
+ resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.62.4':
+ resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.62.4':
+ resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.62.4':
+ resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.62.4':
+ resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openbsd-x64@4.62.4':
+ resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.62.4':
+ resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.62.4':
+ resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.62.4':
+ resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.62.4':
+ resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.62.4':
+ resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==}
+ cpu: [x64]
+ os: [win32]
+
+ '@stellar/freighter-api@6.0.1':
+ resolution: {integrity: sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==}
+
+ '@stellar/js-xdr@4.0.0':
+ resolution: {integrity: sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==}
+ engines: {node: '>=20.0.0', pnpm: '>=9.0.0'}
+
+ '@stellar/stellar-sdk@16.2.0':
+ resolution: {integrity: sha512-FV/Rm11QvrFzR5X9fIfb6Pg30KyYyrRkNezELGFmYDAYrzoPTjqo7vklnEPd2HEontzjJmZizWk8CjyIh5vp0w==}
+ engines: {node: '>=22.0.0'}
+ hasBin: true
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/node@24.13.3':
+ resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
+
+ '@types/react-dom@19.2.4':
+ resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.18':
+ resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
+
+ '@vitejs/plugin-react@4.7.0':
+ resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+ agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ axios@1.18.0:
+ resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==}
+
+ base32.js@0.1.0:
+ resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==}
+ engines: {node: '>=0.12.0'}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ baseline-browser-mapping@2.11.12:
+ resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ bignumber.js@11.1.5:
+ resolution: {integrity: sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==}
+
+ browserslist@4.28.7:
+ resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ caniuse-lite@1.0.30001806:
+ resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+ engines: {node: '>=20'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.402:
+ resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.2:
+ resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ eventsource-parser@3.1.0:
+ resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
+ engines: {node: '>=18.0.0'}
+
+ eventsource@4.1.1:
+ resolution: {integrity: sha512-D6bTRWh6KahHTK/m4WnjPQyEinNPf9eFLEZSEoj7d6fTibspnAVYfzHvirL7u/aoX5d9YYfIkBVAhmigUELk9w==}
+ engines: {node: '>=20.0.0'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ feaxios@0.0.23:
+ resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==}
+
+ follow-redirects@1.16.0:
+ resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ form-data@4.0.6:
+ resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
+ engines: {node: '>= 6'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.4:
+ resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+ engines: {node: '>= 0.4'}
+
+ https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+ is-retry-allowed@3.0.0:
+ resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==}
+ engines: {node: '>=12'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.17:
+ resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ node-releases@2.0.53:
+ resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
+ engines: {node: '>=18'}
+
+ oxlint@1.77.0:
+ resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ oxlint-tsgolint: '>=7.0.2001'
+ vite-plus: '*'
+ peerDependenciesMeta:
+ oxlint-tsgolint:
+ optional: true
+ vite-plus:
+ optional: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.25:
+ resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ proxy-from-env@2.1.0:
+ resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
+ engines: {node: '>=10'}
+
+ react-dom@19.2.8:
+ resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
+ peerDependencies:
+ react: ^19.2.8
+
+ react-refresh@0.17.0:
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
+ engines: {node: '>=0.10.0'}
+
+ react@19.2.8:
+ resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
+ engines: {node: '>=0.10.0'}
+
+ rollup@4.62.4:
+ resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.1:
+ resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ smol-toml@1.7.1:
+ resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==}
+ engines: {node: '>= 18'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ typescript@5.6.3:
+ resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ uint8array-extras@1.5.0:
+ resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
+ engines: {node: '>=18'}
+
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ vite@6.4.3:
+ resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.8
+ '@babel/types': 7.29.8
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.8':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.7
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.8
+ '@babel/types': 7.29.8
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.8
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.29.7': {}
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.8
+
+ '@babel/parser@7.29.8':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+
+ '@babel/traverse@7.29.8':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.8
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ optional: true
+
+ '@noble/ed25519@3.1.0': {}
+
+ '@noble/hashes@2.3.0': {}
+
+ '@oxlint/binding-android-arm-eabi@1.77.0':
+ optional: true
+
+ '@oxlint/binding-android-arm64@1.77.0':
+ optional: true
+
+ '@oxlint/binding-darwin-arm64@1.77.0':
+ optional: true
+
+ '@oxlint/binding-darwin-x64@1.77.0':
+ optional: true
+
+ '@oxlint/binding-freebsd-x64@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-musleabihf@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-gnu@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-musl@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-ppc64-gnu@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-gnu@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-musl@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-s390x-gnu@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-gnu@1.77.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-musl@1.77.0':
+ optional: true
+
+ '@oxlint/binding-openharmony-arm64@1.77.0':
+ optional: true
+
+ '@oxlint/binding-win32-arm64-msvc@1.77.0':
+ optional: true
+
+ '@oxlint/binding-win32-ia32-msvc@1.77.0':
+ optional: true
+
+ '@oxlint/binding-win32-x64-msvc@1.77.0':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
+
+ '@rollup/rollup-android-arm-eabi@4.62.4':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.62.4':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.62.4':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.62.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.62.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.62.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.62.4':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.62.4':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.62.4':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.62.4':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.62.4':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.62.4':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.62.4':
+ optional: true
+
+ '@stellar/freighter-api@6.0.1':
+ dependencies:
+ buffer: 6.0.3
+ semver: 7.7.1
+
+ '@stellar/js-xdr@4.0.0': {}
+
+ '@stellar/stellar-sdk@16.2.0':
+ dependencies:
+ '@noble/ed25519': 3.1.0
+ '@noble/hashes': 2.3.0
+ '@stellar/js-xdr': 4.0.0
+ axios: 1.18.0
+ base32.js: 0.1.0
+ bignumber.js: 11.1.5
+ buffer: 6.0.3
+ commander: 14.0.3
+ eventsource: 4.1.1
+ feaxios: 0.0.23
+ smol-toml: 1.7.1
+ uint8array-extras: 1.5.0
+ transitivePeerDependencies:
+ - debug
+ - supports-color
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@types/estree@1.0.9': {}
+
+ '@types/node@24.13.3':
+ dependencies:
+ undici-types: 7.18.2
+
+ '@types/react-dom@19.2.4(@types/react@19.2.18)':
+ dependencies:
+ '@types/react': 19.2.18
+
+ '@types/react@19.2.18':
+ dependencies:
+ csstype: 3.2.3
+
+ '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@24.13.3))':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
+ '@rolldown/pluginutils': 1.0.0-beta.27
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.17.0
+ vite: 6.4.3(@types/node@24.13.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ agent-base@6.0.2:
+ dependencies:
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ asynckit@0.4.0: {}
+
+ axios@1.18.0:
+ dependencies:
+ follow-redirects: 1.16.0
+ form-data: 4.0.6
+ https-proxy-agent: 5.0.1
+ proxy-from-env: 2.1.0
+ transitivePeerDependencies:
+ - debug
+ - supports-color
+
+ base32.js@0.1.0: {}
+
+ base64-js@1.5.1: {}
+
+ baseline-browser-mapping@2.11.12: {}
+
+ bignumber.js@11.1.5: {}
+
+ browserslist@4.28.7:
+ dependencies:
+ baseline-browser-mapping: 2.11.12
+ caniuse-lite: 1.0.30001806
+ electron-to-chromium: 1.5.402
+ node-releases: 2.0.53
+ update-browserslist-db: 1.2.3(browserslist@4.28.7)
+
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ caniuse-lite@1.0.30001806: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ commander@14.0.3: {}
+
+ convert-source-map@2.0.0: {}
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ delayed-stream@1.0.0: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.402: {}
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.2:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.4
+
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
+ escalade@3.2.0: {}
+
+ eventsource-parser@3.1.0: {}
+
+ eventsource@4.1.1:
+ dependencies:
+ eventsource-parser: 3.1.0
+
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
+ feaxios@0.0.23:
+ dependencies:
+ is-retry-allowed: 3.0.0
+
+ follow-redirects@1.16.0: {}
+
+ form-data@4.0.6:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.4
+ mime-types: 2.1.35
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.4
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.2
+
+ gopd@1.2.0: {}
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.4:
+ dependencies:
+ function-bind: 1.1.2
+
+ https-proxy-agent@5.0.1:
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ ieee754@1.2.1: {}
+
+ is-retry-allowed@3.0.0: {}
+
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json5@2.2.3: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ math-intrinsics@1.1.0: {}
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.17: {}
+
+ node-releases@2.0.53: {}
+
+ oxlint@1.77.0:
+ optionalDependencies:
+ '@oxlint/binding-android-arm-eabi': 1.77.0
+ '@oxlint/binding-android-arm64': 1.77.0
+ '@oxlint/binding-darwin-arm64': 1.77.0
+ '@oxlint/binding-darwin-x64': 1.77.0
+ '@oxlint/binding-freebsd-x64': 1.77.0
+ '@oxlint/binding-linux-arm-gnueabihf': 1.77.0
+ '@oxlint/binding-linux-arm-musleabihf': 1.77.0
+ '@oxlint/binding-linux-arm64-gnu': 1.77.0
+ '@oxlint/binding-linux-arm64-musl': 1.77.0
+ '@oxlint/binding-linux-ppc64-gnu': 1.77.0
+ '@oxlint/binding-linux-riscv64-gnu': 1.77.0
+ '@oxlint/binding-linux-riscv64-musl': 1.77.0
+ '@oxlint/binding-linux-s390x-gnu': 1.77.0
+ '@oxlint/binding-linux-x64-gnu': 1.77.0
+ '@oxlint/binding-linux-x64-musl': 1.77.0
+ '@oxlint/binding-openharmony-arm64': 1.77.0
+ '@oxlint/binding-win32-arm64-msvc': 1.77.0
+ '@oxlint/binding-win32-ia32-msvc': 1.77.0
+ '@oxlint/binding-win32-x64-msvc': 1.77.0
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.5: {}
+
+ postcss@8.5.25:
+ dependencies:
+ nanoid: 3.3.17
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ proxy-from-env@2.1.0: {}
+
+ react-dom@19.2.8(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+ scheduler: 0.27.0
+
+ react-refresh@0.17.0: {}
+
+ react@19.2.8: {}
+
+ rollup@4.62.4:
+ dependencies:
+ '@types/estree': 1.0.9
+ optionalDependencies:
+ '@napi-rs/lzma-linux-x64-gnu': 1.5.1
+ '@rollup/rollup-android-arm-eabi': 4.62.4
+ '@rollup/rollup-android-arm64': 4.62.4
+ '@rollup/rollup-darwin-arm64': 4.62.4
+ '@rollup/rollup-darwin-x64': 4.62.4
+ '@rollup/rollup-freebsd-arm64': 4.62.4
+ '@rollup/rollup-freebsd-x64': 4.62.4
+ '@rollup/rollup-linux-arm-gnueabihf': 4.62.4
+ '@rollup/rollup-linux-arm-musleabihf': 4.62.4
+ '@rollup/rollup-linux-arm64-gnu': 4.62.4
+ '@rollup/rollup-linux-arm64-musl': 4.62.4
+ '@rollup/rollup-linux-loong64-gnu': 4.62.4
+ '@rollup/rollup-linux-loong64-musl': 4.62.4
+ '@rollup/rollup-linux-ppc64-gnu': 4.62.4
+ '@rollup/rollup-linux-ppc64-musl': 4.62.4
+ '@rollup/rollup-linux-riscv64-gnu': 4.62.4
+ '@rollup/rollup-linux-riscv64-musl': 4.62.4
+ '@rollup/rollup-linux-s390x-gnu': 4.62.4
+ '@rollup/rollup-linux-x64-gnu': 4.62.4
+ '@rollup/rollup-linux-x64-musl': 4.62.4
+ '@rollup/rollup-openbsd-x64': 4.62.4
+ '@rollup/rollup-openharmony-arm64': 4.62.4
+ '@rollup/rollup-win32-arm64-msvc': 4.62.4
+ '@rollup/rollup-win32-ia32-msvc': 4.62.4
+ '@rollup/rollup-win32-x64-gnu': 4.62.4
+ '@rollup/rollup-win32-x64-msvc': 4.62.4
+ fsevents: 2.3.3
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.1: {}
+
+ smol-toml@1.7.1: {}
+
+ source-map-js@1.2.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
+ typescript@5.6.3: {}
+
+ uint8array-extras@1.5.0: {}
+
+ undici-types@7.18.2: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.7):
+ dependencies:
+ browserslist: 4.28.7
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ vite@6.4.3(@types/node@24.13.3):
+ dependencies:
+ esbuild: 0.25.12
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+ postcss: 8.5.25
+ rollup: 4.62.4
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.13.3
+ fsevents: 2.3.3
+
+ yallist@3.1.1: {}
diff --git a/demos/freelance-escrow/public/favicon.svg b/demos/freelance-escrow/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/demos/freelance-escrow/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/demos/freelance-escrow/public/icons.svg b/demos/freelance-escrow/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/demos/freelance-escrow/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/freelance-escrow/src/App.css b/demos/freelance-escrow/src/App.css
new file mode 100644
index 0000000..708e920
--- /dev/null
+++ b/demos/freelance-escrow/src/App.css
@@ -0,0 +1,430 @@
+.app {
+ max-width: 860px;
+ margin: 0 auto;
+ padding: var(--space-6) var(--space-5) 96px;
+ text-align: left;
+ width: 100%;
+}
+
+/* ---------- Header ---------- */
+
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-4);
+ flex-wrap: wrap;
+ padding-bottom: var(--space-5);
+ margin-bottom: var(--space-5);
+ border-bottom: 1px solid var(--border);
+}
+
+.brand {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.brand-name {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--text-h);
+ letter-spacing: -0.2px;
+}
+
+.brand-tagline {
+ font-size: 13px;
+ color: var(--text);
+}
+
+.app-header-controls {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+}
+
+.role-switcher {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ font-size: 13px;
+ color: var(--text);
+}
+
+.role-switcher select {
+ font: inherit;
+ padding: 6px 10px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--text-h);
+}
+
+/* ---------- Wallet button ---------- */
+
+.wallet-button {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: 8px 14px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--accent-border);
+ background: var(--accent-bg);
+ color: var(--accent);
+ font: inherit;
+ font-weight: 500;
+ font-size: 13px;
+ cursor: pointer;
+ text-decoration: none;
+ transition: background-color 0.15s ease;
+}
+
+.wallet-button:hover {
+ background: color-mix(in srgb, var(--accent-bg) 70%, var(--accent) 30%);
+}
+
+.wallet-button--connected {
+ cursor: default;
+ color: var(--text-h);
+}
+
+.wallet-button--connected:hover {
+ background: var(--accent-bg);
+}
+
+.wallet-button--warn {
+ border-color: var(--warn-bg);
+ background: var(--warn-bg);
+ color: var(--warn);
+}
+
+.wallet-network {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.3px;
+ text-transform: uppercase;
+ padding: 2px 6px;
+ border-radius: 999px;
+ background: var(--bg);
+ color: var(--text);
+}
+
+/* ---------- Job list ---------- */
+
+.jobs-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: var(--space-4);
+}
+
+.jobs-toolbar h1 {
+ font-size: 22px;
+ letter-spacing: -0.3px;
+ margin: 0;
+}
+
+.job-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+}
+
+.job-card {
+ border-radius: var(--radius-lg);
+ padding: var(--space-5);
+ background: var(--bg);
+ box-shadow: var(--shadow);
+ border: 1px solid var(--border);
+}
+
+.job-card-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--space-3);
+}
+
+.job-card-header h2 {
+ font-size: 17px;
+ margin: 0;
+}
+
+.job-total {
+ font-family: var(--mono);
+ font-size: 13px;
+ color: var(--text);
+ white-space: nowrap;
+}
+
+.job-description {
+ font-size: 14px;
+ color: var(--text);
+ margin: var(--space-2) 0 var(--space-3);
+}
+
+.job-parties {
+ display: flex;
+ gap: var(--space-4);
+ font-size: 12px;
+ color: var(--text);
+ margin-bottom: var(--space-4);
+ padding-bottom: var(--space-4);
+ border-bottom: 1px solid var(--border);
+}
+
+/* ---------- Milestones ---------- */
+
+.milestone-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.milestone {
+ padding: var(--space-3) 0;
+ border-bottom: 1px solid var(--border);
+}
+
+.milestone:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.milestone-main {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: var(--space-3);
+}
+
+.milestone-title {
+ font-weight: 500;
+ color: var(--text-h);
+ font-size: 14px;
+}
+
+.milestone-amount {
+ font-family: var(--mono);
+ font-size: 13px;
+ color: var(--text);
+ white-space: nowrap;
+}
+
+.milestone-meta {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin-top: 6px;
+ font-size: 12px;
+ color: var(--text);
+}
+
+.milestone-assertion {
+ font-family: var(--mono);
+}
+
+.status-badge {
+ padding: 2px 9px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.2px;
+}
+
+.status-badge--in_progress {
+ background: var(--code-bg);
+ color: var(--text);
+}
+
+.status-badge--submitted {
+ background: var(--info-bg);
+ color: var(--info);
+}
+
+.status-badge--disputed {
+ background: var(--danger-bg);
+ color: var(--danger);
+}
+
+.status-badge--released {
+ background: var(--success-bg);
+ color: var(--success);
+}
+
+.status-badge--returned {
+ background: var(--warn-bg);
+ color: var(--warn);
+}
+
+.milestone-actions {
+ margin-top: var(--space-3);
+ display: flex;
+ gap: var(--space-2);
+}
+
+/* ---------- Buttons ---------- */
+
+.milestone-actions button,
+.resolver-vote button,
+.post-job-actions button,
+.jobs-toolbar button,
+.milestone-fieldset > button {
+ font: inherit;
+ font-size: 13px;
+ font-weight: 500;
+ padding: 7px 14px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--accent-border);
+ background: var(--accent-bg);
+ color: var(--accent);
+ cursor: pointer;
+ transition: background-color 0.15s ease;
+}
+
+.milestone-actions button:hover,
+.resolver-vote button:hover,
+.post-job-actions button:hover,
+.jobs-toolbar button:hover,
+.milestone-fieldset > button:hover {
+ background: color-mix(in srgb, var(--accent-bg) 60%, var(--accent) 40%);
+}
+
+.milestone-actions button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.button--danger {
+ border-color: var(--danger-border) !important;
+ background: var(--danger-bg) !important;
+ color: var(--danger) !important;
+}
+
+.resolver-vote {
+ display: flex;
+ gap: var(--space-2);
+}
+
+.milestone-error {
+ margin-top: var(--space-2);
+ font-size: 12px;
+ color: var(--danger);
+}
+
+/* ---------- Post job form ---------- */
+
+.post-job-form {
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-5);
+ margin-bottom: var(--space-5);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+ background: var(--bg);
+ box-shadow: var(--shadow);
+}
+
+.post-job-form h2 {
+ font-size: 17px;
+ margin: 0;
+}
+
+.post-job-form label {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ font-size: 13px;
+ color: var(--text);
+}
+
+.post-job-form input,
+.post-job-form textarea {
+ font: inherit;
+ font-size: 14px;
+ padding: 8px 10px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+ background: var(--code-bg);
+ color: var(--text-h);
+}
+
+.post-job-form input:focus,
+.post-job-form textarea:focus {
+ outline: none;
+ border-color: var(--accent-border);
+ background: var(--bg);
+}
+
+.post-job-form textarea {
+ resize: vertical;
+ min-height: 60px;
+}
+
+.post-job-row {
+ display: flex;
+ gap: var(--space-3);
+}
+
+.post-job-row label {
+ flex: 1;
+}
+
+.milestone-fieldset {
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: var(--space-4);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+
+.milestone-fieldset legend {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text);
+ padding: 0 var(--space-1);
+}
+
+.milestone-draft-row {
+ display: flex;
+ gap: var(--space-2);
+}
+
+.milestone-draft-row input:first-child {
+ flex: 2;
+}
+
+.milestone-draft-row input:last-child {
+ flex: 1;
+}
+
+.milestone-fieldset > button {
+ align-self: flex-start;
+ margin-top: var(--space-1);
+}
+
+.button--icon {
+ border: none !important;
+ background: none !important;
+ color: var(--text) !important;
+ padding: 6px 8px !important;
+ font-size: 12px !important;
+}
+
+.button--icon:hover {
+ color: var(--danger) !important;
+}
+
+.post-job-actions {
+ display: flex;
+ gap: var(--space-2);
+ justify-content: flex-end;
+}
+
+.post-job-actions button:first-child {
+ border-color: var(--border);
+ background: var(--bg);
+ color: var(--text);
+}
diff --git a/demos/freelance-escrow/src/App.tsx b/demos/freelance-escrow/src/App.tsx
new file mode 100644
index 0000000..503970b
--- /dev/null
+++ b/demos/freelance-escrow/src/App.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+import "./App.css";
+import { JobCard } from "./components/JobCard";
+import { PostJobForm } from "./components/PostJobForm";
+import { RoleSwitcher } from "./components/RoleSwitcher";
+import { WalletButton } from "./components/WalletButton";
+import { JobsProvider } from "./state/JobsContext";
+import { useJobs } from "./state/useJobs";
+import { RoleProvider } from "./state/RoleContext";
+
+function AppShell() {
+ const { jobs } = useJobs();
+ const [showPostJob, setShowPostJob] = useState(false);
+
+ return (
+
+
+
+
+
+
Open jobs
+ setShowPostJob(true)}>Post a job
+
+
+ {showPostJob && setShowPostJob(false)} />}
+
+
+ {jobs.map((job) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/demos/freelance-escrow/src/components/JobCard.tsx b/demos/freelance-escrow/src/components/JobCard.tsx
new file mode 100644
index 0000000..ddb7a58
--- /dev/null
+++ b/demos/freelance-escrow/src/components/JobCard.tsx
@@ -0,0 +1,30 @@
+import type { Job } from "../data/jobs";
+import { MilestoneRow } from "./MilestoneRow";
+
+export function JobCard({ job }: { job: Job }) {
+ const total = job.milestones.reduce((sum, m) => sum + Number(m.amount), 0);
+ const paid = job.milestones
+ .filter((m) => m.status === "released")
+ .reduce((sum, m) => sum + Number(m.amount), 0);
+
+ return (
+
+
+ {job.title}
+
+ ${paid} / ${total} {job.token}
+
+
+ {job.description}
+
+ Client: {job.client}
+ Freelancer: {job.freelancer}
+
+
+ {job.milestones.map((milestone) => (
+
+ ))}
+
+
+ );
+}
diff --git a/demos/freelance-escrow/src/components/MilestoneRow.tsx b/demos/freelance-escrow/src/components/MilestoneRow.tsx
new file mode 100644
index 0000000..e9a7145
--- /dev/null
+++ b/demos/freelance-escrow/src/components/MilestoneRow.tsx
@@ -0,0 +1,102 @@
+import { useState } from "react";
+import type { Milestone } from "../data/jobs";
+import { useJobs } from "../state/useJobs";
+import { useRole } from "../state/useRole";
+import { useWallet } from "../hooks/useWallet";
+
+const STATUS_LABEL: Record = {
+ in_progress: "In progress",
+ submitted: "Awaiting review",
+ disputed: "Disputed",
+ released: "Paid out",
+ returned: "Returned to client",
+};
+
+export function MilestoneRow({ jobId, milestone }: { jobId: string; milestone: Milestone }) {
+ const { wallet } = useWallet();
+ const [role] = useRole();
+ const { submitMilestone, disputeMilestone, voteOnMilestone, finalizeMilestone } = useJobs();
+ const [busy, setBusy] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ const address = wallet.status === "connected" ? wallet.address : null;
+
+ async function run(action: () => Promise) {
+ if (!address) {
+ setErrorMessage("Connect a wallet first.");
+ return;
+ }
+ setBusy(true);
+ setErrorMessage(null);
+ try {
+ await action();
+ } catch (err) {
+ setErrorMessage(err instanceof Error ? err.message : "Something went wrong.");
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+
+ {milestone.title}
+ ${milestone.amount}
+
+
+
+ {STATUS_LABEL[milestone.status]}
+
+ {milestone.submittedAt && milestone.status === "submitted" && (
+
+ submitted {new Date(milestone.submittedAt).toLocaleDateString()}
+
+ )}
+ {milestone.assertionId && (
+ assertion #{milestone.assertionId}
+ )}
+
+
+
+ {milestone.status === "in_progress" && role === "freelancer" && (
+
run(() => submitMilestone(jobId, milestone.id, address!))}>
+ Mark milestone done
+
+ )}
+
+ {milestone.status === "submitted" && role === "client" && (
+
run(() => disputeMilestone(jobId, milestone.id, address!))}
+ >
+ Dispute
+
+ )}
+
+ {milestone.status === "submitted" && (
+
run(() => finalizeMilestone(jobId, milestone.id, address!))}>
+ Finalize and release
+
+ )}
+
+ {milestone.status === "disputed" && role === "resolver" && (
+
+ run(() => voteOnMilestone(jobId, milestone.id, address!, true))}>
+ Freelancer is right
+
+ run(() => voteOnMilestone(jobId, milestone.id, address!, false))}
+ >
+ Client is right
+
+
+ )}
+
+
+ {errorMessage && {errorMessage}
}
+
+ );
+}
diff --git a/demos/freelance-escrow/src/components/PostJobForm.tsx b/demos/freelance-escrow/src/components/PostJobForm.tsx
new file mode 100644
index 0000000..d8a48a6
--- /dev/null
+++ b/demos/freelance-escrow/src/components/PostJobForm.tsx
@@ -0,0 +1,120 @@
+import { useState } from "react";
+import { useJobs } from "../state/useJobs";
+
+interface DraftMilestone {
+ title: string;
+ amount: string;
+}
+
+const EMPTY_MILESTONE: DraftMilestone = { title: "", amount: "" };
+
+export function PostJobForm({ onDone }: { onDone: () => void }) {
+ const { createJob } = useJobs();
+ const [title, setTitle] = useState("");
+ const [description, setDescription] = useState("");
+ const [client, setClient] = useState("");
+ const [freelancer, setFreelancer] = useState("");
+ const [milestones, setMilestones] = useState([{ ...EMPTY_MILESTONE }]);
+
+ function updateMilestone(index: number, patch: Partial) {
+ setMilestones((current) => current.map((m, i) => (i === index ? { ...m, ...patch } : m)));
+ }
+
+ function addMilestone() {
+ setMilestones((current) => [...current, { ...EMPTY_MILESTONE }]);
+ }
+
+ function removeMilestone(index: number) {
+ setMilestones((current) => current.filter((_, i) => i !== index));
+ }
+
+ const canSubmit =
+ title.trim() &&
+ client.trim() &&
+ freelancer.trim() &&
+ milestones.length > 0 &&
+ milestones.every((m) => m.title.trim() && Number(m.amount) > 0);
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!canSubmit) {
+ return;
+ }
+ createJob({
+ title: title.trim(),
+ description: description.trim(),
+ client: client.trim(),
+ freelancer: freelancer.trim(),
+ token: "USDC",
+ milestones,
+ });
+ onDone();
+ }
+
+ return (
+
+ );
+}
diff --git a/demos/freelance-escrow/src/components/RoleSwitcher.tsx b/demos/freelance-escrow/src/components/RoleSwitcher.tsx
new file mode 100644
index 0000000..a39bf28
--- /dev/null
+++ b/demos/freelance-escrow/src/components/RoleSwitcher.tsx
@@ -0,0 +1,24 @@
+import { useRole } from "../state/useRole";
+import type { Role } from "../state/role-context";
+
+const ROLES: { value: Role; label: string }[] = [
+ { value: "freelancer", label: "Freelancer" },
+ { value: "client", label: "Client" },
+ { value: "resolver", label: "Resolver" },
+];
+
+export function RoleSwitcher() {
+ const [role, setRole] = useRole();
+ return (
+
+ Viewing as
+ setRole(e.target.value as Role)}>
+ {ROLES.map((r) => (
+
+ {r.label}
+
+ ))}
+
+
+ );
+}
diff --git a/demos/freelance-escrow/src/components/WalletButton.tsx b/demos/freelance-escrow/src/components/WalletButton.tsx
new file mode 100644
index 0000000..be6f50f
--- /dev/null
+++ b/demos/freelance-escrow/src/components/WalletButton.tsx
@@ -0,0 +1,34 @@
+import { useWallet } from "../hooks/useWallet";
+import { shortenAddress } from "../lib/wallet";
+
+export function WalletButton() {
+ const { wallet, connecting, connect } = useWallet();
+
+ if (wallet.status === "unavailable") {
+ return (
+
+ Install Freighter
+
+ );
+ }
+
+ if (wallet.status === "connected") {
+ return (
+
+ {shortenAddress(wallet.address)}
+ {wallet.network}
+
+ );
+ }
+
+ return (
+
+ {connecting ? "Connecting..." : "Connect wallet"}
+
+ );
+}
diff --git a/demos/freelance-escrow/src/data/jobs.ts b/demos/freelance-escrow/src/data/jobs.ts
new file mode 100644
index 0000000..c42e3af
--- /dev/null
+++ b/demos/freelance-escrow/src/data/jobs.ts
@@ -0,0 +1,110 @@
+export type MilestoneStatus =
+ | "in_progress"
+ | "submitted"
+ | "disputed"
+ | "released"
+ | "returned";
+
+export interface Milestone {
+ id: string;
+ title: string;
+ amount: string;
+ status: MilestoneStatus;
+ /** Set once the freelancer has submitted; drives the challenge-window countdown. */
+ submittedAt?: string;
+ /**
+ * The Tholos assertion id backing this milestone, once assert_outcome has
+ * been called. Job/milestone metadata itself never touches the contract;
+ * this is the one piece of state that maps this app's data to Tholos's,
+ * kept client-side per the pattern in docs/src/INTEGRATION.md.
+ */
+ assertionId?: string;
+}
+
+export interface Job {
+ id: string;
+ title: string;
+ description: string;
+ client: string;
+ freelancer: string;
+ token: string;
+ milestones: Milestone[];
+}
+
+export const jobs: Job[] = [
+ {
+ id: "job-1",
+ title: "Landing page redesign",
+ description:
+ "Redesign the marketing site landing page: new hero, pricing table, and mobile layout.",
+ client: "Nova Analytics",
+ freelancer: "Priya Chen",
+ token: "USDC",
+ milestones: [
+ {
+ id: "job-1-m1",
+ title: "Wireframes and content outline",
+ amount: "250",
+ status: "released",
+ },
+ {
+ id: "job-1-m2",
+ title: "Hero and pricing section build",
+ amount: "600",
+ status: "in_progress",
+ },
+ {
+ id: "job-1-m3",
+ title: "Mobile layout and QA pass",
+ amount: "400",
+ status: "in_progress",
+ },
+ ],
+ },
+ {
+ id: "job-2",
+ title: "Onboarding email sequence",
+ description:
+ "Write and implement a 5-part onboarding email sequence in the existing ESP template.",
+ client: "Fernbank Studio",
+ freelancer: "Diego Ramirez",
+ token: "USDC",
+ milestones: [
+ {
+ id: "job-2-m1",
+ title: "Copy draft for all 5 emails",
+ amount: "300",
+ status: "in_progress",
+ },
+ {
+ id: "job-2-m2",
+ title: "ESP implementation and send test",
+ amount: "200",
+ status: "in_progress",
+ },
+ ],
+ },
+ {
+ id: "job-3",
+ title: "API rate-limit middleware",
+ description:
+ "Add a configurable rate-limit middleware to the existing Node API gateway, with tests.",
+ client: "Harrow Logistics",
+ freelancer: "Amara Okafor",
+ token: "USDC",
+ milestones: [
+ {
+ id: "job-3-m1",
+ title: "Middleware implementation",
+ amount: "500",
+ status: "released",
+ },
+ {
+ id: "job-3-m2",
+ title: "Load test and tuning",
+ amount: "350",
+ status: "in_progress",
+ },
+ ],
+ },
+];
diff --git a/demos/freelance-escrow/src/hooks/useWallet.ts b/demos/freelance-escrow/src/hooks/useWallet.ts
new file mode 100644
index 0000000..72df683
--- /dev/null
+++ b/demos/freelance-escrow/src/hooks/useWallet.ts
@@ -0,0 +1,22 @@
+import { useCallback, useEffect, useState } from "react";
+import { type WalletState, connectWallet, detectWallet } from "../lib/wallet";
+
+export function useWallet() {
+ const [wallet, setWallet] = useState({ status: "disconnected" });
+ const [connecting, setConnecting] = useState(false);
+
+ useEffect(() => {
+ detectWallet().then(setWallet);
+ }, []);
+
+ const connect = useCallback(async () => {
+ setConnecting(true);
+ try {
+ setWallet(await connectWallet());
+ } finally {
+ setConnecting(false);
+ }
+ }, []);
+
+ return { wallet, connecting, connect };
+}
diff --git a/demos/freelance-escrow/src/index.css b/demos/freelance-escrow/src/index.css
new file mode 100644
index 0000000..b92e495
--- /dev/null
+++ b/demos/freelance-escrow/src/index.css
@@ -0,0 +1,142 @@
+:root {
+ --text: #6b6375;
+ --text-h: #08060d;
+ --bg: #fff;
+ --border: #e5e4e7;
+ --code-bg: #f4f3ec;
+ --accent: #aa3bff;
+ --accent-bg: rgba(170, 59, 255, 0.1);
+ --accent-border: rgba(170, 59, 255, 0.5);
+ --social-bg: rgba(244, 243, 236, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 24px;
+ --space-6: 32px;
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+
+ --danger: #dc3545;
+ --danger-bg: rgba(220, 53, 69, 0.1);
+ --danger-border: rgba(220, 53, 69, 0.4);
+ --success: #1a9c53;
+ --success-bg: rgba(26, 156, 83, 0.1);
+ --info: #2f7cf6;
+ --info-bg: rgba(47, 124, 246, 0.12);
+ --warn: #b7791f;
+ --warn-bg: rgba(217, 158, 45, 0.15);
+
+ font: 18px/145% var(--sans);
+ letter-spacing: 0.18px;
+ color-scheme: light dark;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --text: #9ca3af;
+ --text-h: #f3f4f6;
+ --bg: #16171d;
+ --border: #2e303a;
+ --code-bg: #1f2028;
+ --accent: #c084fc;
+ --accent-bg: rgba(192, 132, 252, 0.15);
+ --accent-border: rgba(192, 132, 252, 0.5);
+ --social-bg: rgba(47, 48, 58, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+
+ --danger: #f27686;
+ --danger-bg: rgba(242, 118, 134, 0.15);
+ --danger-border: rgba(242, 118, 134, 0.4);
+ --success: #4ade80;
+ --success-bg: rgba(74, 222, 128, 0.15);
+ --info: #7aa8fb;
+ --info-bg: rgba(122, 168, 251, 0.15);
+ --warn: #e8b654;
+ --warn-bg: rgba(232, 182, 84, 0.15);
+ }
+
+ #social .button-icon {
+ filter: invert(1) brightness(2);
+ }
+}
+
+#root {
+ width: 100%;
+ max-width: 100%;
+ min-height: 100svh;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+}
+
+h1,
+h2 {
+ font-family: var(--heading);
+ font-weight: 500;
+ color: var(--text-h);
+}
+
+h1 {
+ font-size: 56px;
+ letter-spacing: -1.68px;
+ margin: 32px 0;
+ @media (max-width: 1024px) {
+ font-size: 36px;
+ margin: 20px 0;
+ }
+}
+h2 {
+ font-size: 24px;
+ line-height: 118%;
+ letter-spacing: -0.24px;
+ margin: 0 0 8px;
+ @media (max-width: 1024px) {
+ font-size: 20px;
+ }
+}
+p {
+ margin: 0;
+}
+
+code,
+.counter {
+ font-family: var(--mono);
+ display: inline-flex;
+ border-radius: 4px;
+ color: var(--text-h);
+}
+
+code {
+ font-size: 15px;
+ line-height: 135%;
+ padding: 4px 8px;
+ background: var(--code-bg);
+}
diff --git a/demos/freelance-escrow/src/lib/config.ts b/demos/freelance-escrow/src/lib/config.ts
new file mode 100644
index 0000000..21ecaa8
--- /dev/null
+++ b/demos/freelance-escrow/src/lib/config.ts
@@ -0,0 +1,10 @@
+/**
+ * Every value here is env-configured, the same way DEPLOYMENT.md's own deploy
+ * walkthrough and scripts/testnet-smoke.sh treat a deployed contract id: never
+ * committed to source, always supplied at build/run time. See README.md for
+ * how to set VITE_THOLOS_CONTRACT_ID.
+ */
+export const RPC_URL = import.meta.env.VITE_SOROBAN_RPC_URL ?? "https://soroban-testnet.stellar.org";
+export const NETWORK_PASSPHRASE =
+ import.meta.env.VITE_NETWORK_PASSPHRASE ?? "Test SDF Network ; September 2015";
+export const THOLOS_CONTRACT_ID: string = import.meta.env.VITE_THOLOS_CONTRACT_ID ?? "";
diff --git a/demos/freelance-escrow/src/lib/tholos.ts b/demos/freelance-escrow/src/lib/tholos.ts
new file mode 100644
index 0000000..d2fc337
--- /dev/null
+++ b/demos/freelance-escrow/src/lib/tholos.ts
@@ -0,0 +1,161 @@
+import {
+ Address,
+ BASE_FEE,
+ Contract,
+ TransactionBuilder,
+ nativeToScVal,
+ scValToNative,
+ rpc,
+} from "@stellar/stellar-sdk";
+import { signTransaction } from "@stellar/freighter-api";
+import { NETWORK_PASSPHRASE, RPC_URL, THOLOS_CONTRACT_ID } from "./config";
+
+export type AssertionStatus = "Pending" | "Disputed" | "Resolved";
+
+export interface AssertionState {
+ asserter: string;
+ outcome: boolean;
+ finalOutcome: boolean | null;
+ bond: bigint;
+ openedAt: bigint;
+ status: AssertionStatus;
+ disputer: string | null;
+ votesForOutcome: number;
+ votesAgainstOutcome: number;
+ resolvers: string[];
+ finalizer: string | null;
+}
+
+function server(): rpc.Server {
+ return new rpc.Server(RPC_URL);
+}
+
+function contract(): Contract {
+ return new Contract(THOLOS_CONTRACT_ID);
+}
+
+function addrArg(value: string) {
+ return nativeToScVal(Address.fromString(value), { type: "address" });
+}
+
+/** Builds, signs (via Freighter), submits, and awaits one contract call. */
+async function submit(method: string, args: ReturnType[], sourcePublicKey: string) {
+ const rpcServer = server();
+ const account = await rpcServer.getAccount(sourcePublicKey);
+ const tx = new TransactionBuilder(account, {
+ fee: BASE_FEE,
+ networkPassphrase: NETWORK_PASSPHRASE,
+ })
+ .addOperation(contract().call(method, ...args))
+ .setTimeout(30)
+ .build();
+
+ const prepared = await rpcServer.prepareTransaction(tx);
+
+ const { signedTxXdr, error } = await signTransaction(prepared.toXDR(), {
+ networkPassphrase: NETWORK_PASSPHRASE,
+ });
+ if (error || !signedTxXdr) {
+ throw new Error(`Wallet declined to sign the ${method} transaction.`);
+ }
+
+ const signedTx = TransactionBuilder.fromXDR(signedTxXdr, NETWORK_PASSPHRASE);
+ const sent = await rpcServer.sendTransaction(signedTx);
+ if (sent.status === "ERROR") {
+ throw new Error(`${method} was rejected before submission.`);
+ }
+
+ return pollForResult(rpcServer, sent.hash, method);
+}
+
+async function pollForResult(rpcServer: rpc.Server, hash: string, method: string) {
+ for (let attempt = 0; attempt < 20; attempt++) {
+ const result = await rpcServer.getTransaction(hash);
+ if (result.status === rpc.Api.GetTransactionStatus.SUCCESS) {
+ return result.resultMetaXdr ? scValToNative(result.returnValue!) : undefined;
+ }
+ if (result.status === rpc.Api.GetTransactionStatus.FAILED) {
+ throw new Error(`${method} failed on-chain (tx ${hash}).`);
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+ }
+ throw new Error(`Timed out waiting for ${method} (tx ${hash}) to confirm.`);
+}
+
+export async function assertOutcome(asserter: string, outcome: boolean): Promise {
+ const id = await submit("assert_outcome", [addrArg(asserter), nativeToScVal(outcome)], asserter);
+ return BigInt(id);
+}
+
+export async function disputeAssertion(disputer: string, id: bigint): Promise {
+ await submit("dispute", [addrArg(disputer), nativeToScVal(id, { type: "u64" })], disputer);
+}
+
+export async function resolveAssertion(
+ resolver: string,
+ id: bigint,
+ agreesWithAsserter: boolean,
+): Promise {
+ const result = await submit(
+ "resolve",
+ [addrArg(resolver), nativeToScVal(id, { type: "u64" }), nativeToScVal(agreesWithAsserter)],
+ resolver,
+ );
+ return result ?? null;
+}
+
+export async function finalizeAssertion(caller: string, id: bigint): Promise {
+ return submit("finalize", [addrArg(caller), nativeToScVal(id, { type: "u64" })], caller);
+}
+
+/**
+ * Read-only lookup. Simulation still needs a funded source account to build
+ * against, so this piggybacks on whichever wallet address is currently
+ * connected rather than requiring a second, app-owned account.
+ */
+export async function getAssertionState(id: bigint, readAs: string): Promise {
+ const rpcServer = server();
+ const account = await rpcServer.getAccount(readAs);
+ const tx = new TransactionBuilder(account, {
+ fee: BASE_FEE,
+ networkPassphrase: NETWORK_PASSPHRASE,
+ })
+ .addOperation(contract().call("get_assertion_state", nativeToScVal(id, { type: "u64" })))
+ .setTimeout(30)
+ .build();
+
+ const sim = await rpcServer.simulateTransaction(tx);
+ if (rpc.Api.isSimulationError(sim)) {
+ throw new Error(sim.error);
+ }
+ if (!rpc.Api.isSimulationSuccess(sim) || !sim.result) {
+ throw new Error(`Simulation for get_assertion_state(${id}) didn't return a value.`);
+ }
+
+ return mapAssertion(scValToNative(sim.result.retval));
+}
+
+function mapAssertion(raw: Record): AssertionState {
+ return {
+ asserter: String(raw.asserter),
+ outcome: Boolean(raw.outcome),
+ finalOutcome: raw.final_outcome == null ? null : Boolean(raw.final_outcome),
+ bond: BigInt(raw.bond as string | number | bigint),
+ openedAt: BigInt(raw.opened_at as string | number | bigint),
+ status: statusFromRaw(raw.status),
+ disputer: raw.disputer == null ? null : String(raw.disputer),
+ votesForOutcome: Number(raw.votes_for_outcome),
+ votesAgainstOutcome: Number(raw.votes_against_outcome),
+ resolvers: Array.isArray(raw.resolvers) ? raw.resolvers.map(String) : [],
+ finalizer: raw.finalizer == null ? null : String(raw.finalizer),
+ };
+}
+
+function statusFromRaw(status: unknown): AssertionStatus {
+ // soroban-client decodes a Rust unit-variant enum to its tag string.
+ const tag = typeof status === "string" ? status : Object.keys(status as object)[0];
+ if (tag === "Pending" || tag === "Disputed" || tag === "Resolved") {
+ return tag;
+ }
+ throw new Error(`Unrecognized assertion status: ${String(status)}`);
+}
diff --git a/demos/freelance-escrow/src/lib/wallet.ts b/demos/freelance-escrow/src/lib/wallet.ts
new file mode 100644
index 0000000..c1197a3
--- /dev/null
+++ b/demos/freelance-escrow/src/lib/wallet.ts
@@ -0,0 +1,56 @@
+import {
+ isConnected,
+ isAllowed,
+ requestAccess,
+ getAddress,
+ getNetwork,
+} from "@stellar/freighter-api";
+
+export type WalletState =
+ | { status: "unavailable" }
+ | { status: "disconnected" }
+ | { status: "connected"; address: string; network: string };
+
+/** Freighter must be installed and unlocked before any other call succeeds. */
+export async function detectWallet(): Promise {
+ const connected = await isConnected();
+ if (connected.error || !connected.isConnected) {
+ return { status: "unavailable" };
+ }
+
+ const allowed = await isAllowed();
+ if (allowed.error || !allowed.isAllowed) {
+ return { status: "disconnected" };
+ }
+
+ return resolveConnectedState();
+}
+
+export async function connectWallet(): Promise {
+ const access = await requestAccess();
+ if (access.error) {
+ return { status: "disconnected" };
+ }
+ return resolveConnectedState();
+}
+
+async function resolveConnectedState(): Promise {
+ const [addressResult, networkResult] = await Promise.all([
+ getAddress(),
+ getNetwork(),
+ ]);
+
+ if (addressResult.error || !addressResult.address) {
+ return { status: "disconnected" };
+ }
+
+ return {
+ status: "connected",
+ address: addressResult.address,
+ network: networkResult.network ?? "UNKNOWN",
+ };
+}
+
+export function shortenAddress(address: string): string {
+ return `${address.slice(0, 4)}...${address.slice(-4)}`;
+}
diff --git a/demos/freelance-escrow/src/main.tsx b/demos/freelance-escrow/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/demos/freelance-escrow/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/demos/freelance-escrow/src/state/JobsContext.tsx b/demos/freelance-escrow/src/state/JobsContext.tsx
new file mode 100644
index 0000000..6b53f8d
--- /dev/null
+++ b/demos/freelance-escrow/src/state/JobsContext.tsx
@@ -0,0 +1,111 @@
+import { useCallback, useMemo, useState, type ReactNode } from "react";
+import { jobs as seedJobs, type Job, type Milestone, type MilestoneStatus } from "../data/jobs";
+import { assertOutcome, disputeAssertion, finalizeAssertion, resolveAssertion } from "../lib/tholos";
+import { JobsContext, type JobsContextValue, type NewJobInput } from "./jobs-context";
+
+function updateMilestone(
+ jobs: Job[],
+ jobId: string,
+ milestoneId: string,
+ patch: Partial,
+): Job[] {
+ return jobs.map((job) =>
+ job.id !== jobId
+ ? job
+ : {
+ ...job,
+ milestones: job.milestones.map((milestone) =>
+ milestone.id !== milestoneId ? milestone : { ...milestone, ...patch },
+ ),
+ },
+ );
+}
+
+function findMilestone(jobs: Job[], jobId: string, milestoneId: string): Milestone | undefined {
+ return jobs.find((job) => job.id === jobId)?.milestones.find((m) => m.id === milestoneId);
+}
+
+export function JobsProvider({ children }: { children: ReactNode }) {
+ const [jobs, setJobs] = useState(seedJobs);
+
+ const createJob = useCallback((input: NewJobInput) => {
+ const jobId = `job-${crypto.randomUUID()}`;
+ const job: Job = {
+ id: jobId,
+ title: input.title,
+ description: input.description,
+ client: input.client,
+ freelancer: input.freelancer,
+ token: input.token,
+ milestones: input.milestones.map((m, index) => ({
+ id: `${jobId}-m${index + 1}`,
+ title: m.title,
+ amount: m.amount,
+ status: "in_progress",
+ })),
+ };
+ setJobs((current) => [job, ...current]);
+ }, []);
+
+ const submitMilestone = useCallback(async (jobId: string, milestoneId: string, signerAddress: string) => {
+ const assertionId = (await assertOutcome(signerAddress, true)).toString();
+ setJobs((current) =>
+ updateMilestone(current, jobId, milestoneId, {
+ status: "submitted" satisfies MilestoneStatus,
+ submittedAt: new Date().toISOString(),
+ assertionId,
+ }),
+ );
+ }, []);
+
+ const disputeMilestone = useCallback(async (jobId: string, milestoneId: string, signerAddress: string) => {
+ const milestone = findMilestone(jobs, jobId, milestoneId);
+ if (!milestone?.assertionId) {
+ return;
+ }
+ await disputeAssertion(signerAddress, BigInt(milestone.assertionId));
+ setJobs((current) => updateMilestone(current, jobId, milestoneId, { status: "disputed" }));
+ }, [jobs]);
+
+ const voteOnMilestone = useCallback(
+ async (jobId: string, milestoneId: string, resolverAddress: string, agreesWithFreelancer: boolean) => {
+ const milestone = findMilestone(jobs, jobId, milestoneId);
+ if (!milestone?.assertionId) {
+ return;
+ }
+ const decided = await resolveAssertion(resolverAddress, BigInt(milestone.assertionId), agreesWithFreelancer);
+ if (decided === null) {
+ return;
+ }
+ setJobs((current) =>
+ updateMilestone(current, jobId, milestoneId, {
+ status: decided ? "released" : "returned",
+ }),
+ );
+ },
+ [jobs],
+ );
+
+ const finalizeMilestone = useCallback(async (jobId: string, milestoneId: string, callerAddress: string) => {
+ const milestone = findMilestone(jobs, jobId, milestoneId);
+ if (!milestone?.assertionId) {
+ return;
+ }
+ await finalizeAssertion(callerAddress, BigInt(milestone.assertionId));
+ setJobs((current) => updateMilestone(current, jobId, milestoneId, { status: "released" }));
+ }, [jobs]);
+
+ const value = useMemo(
+ () => ({
+ jobs,
+ createJob,
+ submitMilestone,
+ disputeMilestone,
+ voteOnMilestone,
+ finalizeMilestone,
+ }),
+ [jobs, createJob, submitMilestone, disputeMilestone, voteOnMilestone, finalizeMilestone],
+ );
+
+ return {children} ;
+}
diff --git a/demos/freelance-escrow/src/state/RoleContext.tsx b/demos/freelance-escrow/src/state/RoleContext.tsx
new file mode 100644
index 0000000..0e2b705
--- /dev/null
+++ b/demos/freelance-escrow/src/state/RoleContext.tsx
@@ -0,0 +1,7 @@
+import { useState, type ReactNode } from "react";
+import { RoleContext, type Role } from "./role-context";
+
+export function RoleProvider({ children }: { children: ReactNode }) {
+ const state = useState("freelancer");
+ return {children} ;
+}
diff --git a/demos/freelance-escrow/src/state/jobs-context.ts b/demos/freelance-escrow/src/state/jobs-context.ts
new file mode 100644
index 0000000..9e5a1bb
--- /dev/null
+++ b/demos/freelance-escrow/src/state/jobs-context.ts
@@ -0,0 +1,27 @@
+import { createContext } from "react";
+import type { Job } from "../data/jobs";
+
+export interface NewJobInput {
+ title: string;
+ description: string;
+ client: string;
+ freelancer: string;
+ token: string;
+ milestones: { title: string; amount: string }[];
+}
+
+export interface JobsContextValue {
+ jobs: Job[];
+ createJob: (input: NewJobInput) => void;
+ submitMilestone: (jobId: string, milestoneId: string, signerAddress: string) => Promise;
+ disputeMilestone: (jobId: string, milestoneId: string, signerAddress: string) => Promise;
+ voteOnMilestone: (
+ jobId: string,
+ milestoneId: string,
+ resolverAddress: string,
+ agreesWithFreelancer: boolean,
+ ) => Promise;
+ finalizeMilestone: (jobId: string, milestoneId: string, callerAddress: string) => Promise;
+}
+
+export const JobsContext = createContext(null);
diff --git a/demos/freelance-escrow/src/state/role-context.ts b/demos/freelance-escrow/src/state/role-context.ts
new file mode 100644
index 0000000..cebe775
--- /dev/null
+++ b/demos/freelance-escrow/src/state/role-context.ts
@@ -0,0 +1,11 @@
+import { createContext } from "react";
+
+/**
+ * A real deployment would derive role from who's logged in. This demo has no
+ * backend or auth of its own (Tholos itself doesn't know "client" or
+ * "freelancer", only addresses), so the connected wallet plays whichever
+ * role you pick here to exercise all three sides of a dispute.
+ */
+export type Role = "freelancer" | "client" | "resolver";
+
+export const RoleContext = createContext<[Role, (role: Role) => void] | null>(null);
diff --git a/demos/freelance-escrow/src/state/useJobs.ts b/demos/freelance-escrow/src/state/useJobs.ts
new file mode 100644
index 0000000..7a63ea8
--- /dev/null
+++ b/demos/freelance-escrow/src/state/useJobs.ts
@@ -0,0 +1,10 @@
+import { useContext } from "react";
+import { JobsContext, type JobsContextValue } from "./jobs-context";
+
+export function useJobs(): JobsContextValue {
+ const ctx = useContext(JobsContext);
+ if (!ctx) {
+ throw new Error("useJobs must be used within a JobsProvider");
+ }
+ return ctx;
+}
diff --git a/demos/freelance-escrow/src/state/useRole.ts b/demos/freelance-escrow/src/state/useRole.ts
new file mode 100644
index 0000000..9446691
--- /dev/null
+++ b/demos/freelance-escrow/src/state/useRole.ts
@@ -0,0 +1,10 @@
+import { useContext } from "react";
+import { RoleContext } from "./role-context";
+
+export function useRole() {
+ const ctx = useContext(RoleContext);
+ if (!ctx) {
+ throw new Error("useRole must be used within a RoleProvider");
+ }
+ return ctx;
+}
diff --git a/demos/freelance-escrow/tsconfig.app.json b/demos/freelance-escrow/tsconfig.app.json
new file mode 100644
index 0000000..4971bc8
--- /dev/null
+++ b/demos/freelance-escrow/tsconfig.app.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/demos/freelance-escrow/tsconfig.json b/demos/freelance-escrow/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/demos/freelance-escrow/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/demos/freelance-escrow/tsconfig.node.json b/demos/freelance-escrow/tsconfig.node.json
new file mode 100644
index 0000000..8b471a2
--- /dev/null
+++ b/demos/freelance-escrow/tsconfig.node.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/demos/freelance-escrow/vite.config.ts b/demos/freelance-escrow/vite.config.ts
new file mode 100644
index 0000000..8b0f57b
--- /dev/null
+++ b/demos/freelance-escrow/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})