The contract now has a verified synthetic testnet demonstration. The hosted browser prototype is public. Signed browser business flows remain unverified. No real pilot or production readiness is claimed.
A small web app for paying school fees on Stellar testnet. A school records a fee obligation against an opaque reference, a payer settles it in any number of installments, and anyone can read the record. The contract never holds money: payments move straight from the payer's balance to the school's.
Status: hosted testnet prototype, synthetic contract demo, no pilot or audit. The landing page opens the school or payer workspace. Wallet signing is explicit; signed browser business flows still need verification.
Part of the schoolfees project, which is three repositories:
schoolfees-contracts
(the Rust contract), schoolfees-app (this one) and
schoolfees-docs (the book).
| Page | Who | What it does |
|---|---|---|
| Landing / workspace | anyone | Explains the flow; Pay a fee, Create a fee and Open workspace lead to the relevant task |
| Create a fee | school | Records a fee: token, opaque reference, total, due date |
| View a fee | anyone | Looks a fee up by id and shows everything stored, plus its status |
| Pay | payer | Pays part or all of a fee, straight to the school |
| School actions | school | Refunds a payer from the school's own balance; closes a settled fee |
Contract functions are called exactly as named in src/lib.rs in
schoolfees-contracts: create_fee, pay, close_fee, refund, get_fee,
status. No other contract function exists in v0.
- A visible "TESTNET - no real money" banner on every screen, including the configuration-error screen.
- The app refuses to operate on any network other than testnet: the network
comes from
.env, the passphrase is pinned insrc/lib/network.ts, and the wallet's own network is re-read before every write. - Wallets sign; the app never asks for, receives, stores or logs a secret key or seed phrase. It only ever sees a public address and a signed XDR.
- Every action that produces a transaction shows the transaction hash and an explorer link.
- No analytics, trackers, injected third-party scripts or backend. The app calls the configured RPC, loads wallet modules locally and uses local picker icons. The selected wallet provider may make its own requests during connection. No browser network capture has verified all wallet traffic.
- The reference field accepts only a 32-byte opaque value (64 hex characters) and says in plain words that names, phone numbers, emails and student or member ids must never be entered.
Requires Node.js 24 (what CI and the maintainer's machine use).
cp .env.example .env # then fill in the values
npm install
npm run dev.env is git-ignored and must never be committed. Every value the app reads is
documented in .env.example, and it is read in exactly one place,
src/config.ts.
npm run lint # oxlint
npm run typecheck # tsc -b (strict)
npm test # pure logic, hook, component and page regressions
npm run build # tsc -b && vite buildAll four run in CI (.github/workflows/web.yml).
├── src/
│ ├── config.ts # the only place .env is read
│ ├── lib/ # pure logic + unit tests
│ │ ├── network.ts # config resolution, testnet-only refusal
│ │ ├── amount.ts # amount parsing and formatting
│ │ ├── datetime.ts # Unix seconds <-> local input
│ │ ├── reference.ts # opaque reference validation + the warning
│ │ ├── validation.ts # address, contract id and fee id checks
│ │ ├── contractErrors.ts # error code -> ERRORS.md wording
│ │ ├── fee.ts # Fee record helpers and status rules
│ │ ├── scval.ts # JS <-> xdr.ScVal conversions
│ │ ├── contract.ts # the Soroban client (build/simulate/submit)
│ │ ├── wallet.ts # Stellar Wallets Kit adapter
│ │ └── explorer.ts # explorer links
│ ├── hooks/ # useWallet, useAction
│ ├── components/ # UI only
│ └── pages/ # one per flow
├── docs/
│ ├── decisions/ # 0001: why Vite + React, not Scaffold Stellar
│ ├── issue-drafts/ # everything not built, as drafts
│ └── contract-errors.md # vendored copy of ERRORS.md, used by the tests
└── scripts/deploy-testnet.sh # written, never run by an agent
Error wording is never written in this repo. src/lib/contractErrors.ts
maps the contract's numeric error codes to the "user-facing message" column of
ERRORS.md in schoolfees-contracts, and
src/lib/contractErrors.test.ts fails if a
variant in the vendored copy of that table has no mapped message — in either
direction. If ERRORS.md changes, re-copy
docs/contract-errors.md in the same commit.
Read this before trusting the app with anything.
Verified locally on 2026-10-07:
- 26 test files, 168 tests passed, including pure validation and SDK ScVal conversion, mocked page flows, automated axe checks, synchronous duplicate guards and abandoned wallet/action request regressions.
- Lint and strict TypeScript checks passed. Current build evidence is in docs/TESTING.md. These are local working-tree checks; this update has not been pushed or verified on GitHub CI.
Assumed — never exercised:
- A synthetic contract demo is deployed, but signed browser flows remain unverified. RPC calls
in
src/lib/contract.tsis unproven. - Signed browser business flows are unverified. Connection screenshots do not prove signing, payment or network refusal.
- No signed browser transaction has been verified from this app, so the submit/poll
path, the archived-record refusal, and the "extract the code from a host error
string" assumption (
Error(Contract, #N)) are untested against a real network. - The contract's
status()return value is read defensively (symbol, single-element vec, or object) because unit-variant enums were not verified against a deployed contract; an unrecognised value shows asUnknownrather than a guess. - Accessibility is built in and machine-checked, not audited: semantic
landmarks, labels, focus styles and
aria-describedbyare used throughout, and an axe-core check now runs over every rendered component and page — but axe is a floor, not an audit: no screen-reader or keyboard audit has been done, and colour contrast is verified against the design tokens rather than rendered styles.
The published CLI demonstration verifies the contract, not the browser flow. Treat the chain-facing code as unverified.
A narrow maintainer-authorized synthetic testnet contract demonstration exists.
A real school pilot remains separately gated; see the linked deployment record.
scripts/deploy-testnet.sh refuses to run without
PILOT_CONFIRMED=yes. It was written, not run: no agent should run it.
- docs/decisions/0001 — why Vite + React + TypeScript instead of Scaffold Stellar, with the versions used.
- ROADMAP.md — what is next, and what is deliberately not built.
- docs/issue-drafts — the same gaps as drafts.
- The engineering standards this repo is held to (Build Arsenal crypto profile +
Flowtick), and the audits run against it on 2026-10-01:
SECURITY, TESTING,
DESIGN_GUIDELINES,
ACCESSIBILITY,
DEPLOYMENT_CHECKLIST,
PRODUCTION_QUALITY,
RESOURCES. The across-repo gap map and audit reports live
in
schoolfees-docs/docs/.
- The wallet adapter now imports eight Stellar modules lazily instead of the broad default module bundle. The installed dependency tree still has 19 audit findings (13 low, 6 moderate); see docs/RESOURCES.md.
- Amounts are whole numbers in the token's smallest unit; the app does not convert decimal places yet (draft 03).
- Browsing the app does not keep a fee record alive. Reads are simulations
(
simulateTransaction), so the TTL extension the contract performs while serving a read is never persisted; only submitted transactions extend TTL. A fee nobody writes to can still archive and show the archived-record refusal. Decided 2026-10-02 as option A in draft 18.
MIT — see LICENSE.
The default screen is now a public landing page. Pay a fee opens the payer task, Create a fee opens the school form, and Open workspace starts with lookup. The workspace retains all existing contract operations and wallet/network refusals; the brand button and Home task return to the landing page.
Local lint, typecheck, 168 tests and production build passed. Offline browser layout/accessibility checks passed 29 scenes; details and limits are in docs/TESTING.md. Connected design screenshots use mocked public-wallet and fee fixtures. They do not establish signing or a live browser payment. The hosted link above is the current public prototype; the new design's publication is a separate maintainer step.