Thank you for helping build trustless savings circles for the world. Every contribution matters — bug fixes, docs, contract improvements, or UI polish.
- Getting Started
- Development Setup
- How to Contribute
- Commit Style
- Pull Request Process
- Code Style
- Definition of Done
- How to use this section
- Scope A — Contract change (funds, guards, deadlines, protocol constants)
- Scope B — SDK or gating change (types, eligibility, error handling)
- Scope C — Indexer change (schema, events, API, migrations)
- Scope D — App or UI change (pages, components, wallet flow)
- Scope E — Identity or reputation change
- Scope F — Security or authorization change
- Scope G — Deployment or infrastructure change
- Evidence quick-reference
- Reporting Bugs
- Fork the repo on GitHub
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/CIRCLEUP - Create a feature branch:
git checkout -b feat/your-feature - Make your changes, following the Definition of Done for your scope
- Open a pull request against
main
# Install Node dependencies
npm install
# Build the SDK
npm run build:sdk
# Start the app (requires indexer + Postgres running)
npm run dev:app
# Start the indexer
npm run dev:indexer
# Run Rust contract tests (requires Rust + stellar-cli)
cd contracts && cargo test
# Run indexer tests (offline — no Postgres required for unit tests)
npm test --workspace=indexer
# Run incident reproduction tests only (fully offline)
npm run test:incidents --workspace=indexer
# Type-check the app
npx tsc --noEmit --project app/tsconfig.jsonSee the RUNBOOK for full environment variable setup.
Bug fixes — Open an issue first describing the bug, then submit a PR with a fix and a test if possible.
New features — Open an issue to discuss the feature before building it. For contract changes especially, discuss the design first.
Documentation — PRs that improve docs, examples, or comments are always welcome without prior discussion.
Tests — Additional unit tests for the Rust contracts or TypeScript SDK are always appreciated.
We follow Conventional Commits:
type(scope): short description
feat(circle): add grace period before mark_default
fix(indexer): handle null contractId in event response
docs(readme): update quick start steps
chore(ci): upgrade to Node 24
test(contracts): add edge case for zero collateral
Types: feat, fix, docs, test, chore, refactor, ci, build
- Make sure CI passes (TypeScript build + Rust compile + ESLint)
- Keep PRs focused — one feature or fix per PR
- Update
CHANGELOG.mdunder[Unreleased]with what you changed - Fill in the PR template, including the Definition of Done checklist for your scope
- Request a review from a maintainer
- PRs are squash-merged into
main
Rust — cargo fmt before committing. Follow existing patterns in the contracts.
TypeScript — ESLint + Prettier. Run npm run lint before committing.
React — Server components for data fetching, client components only where interactivity is needed. Keep "use client" boundaries minimal.
This section is the single source of validation requirements for changes that touch funds, identity, deadlines, or reputation. It is organized by change scope — each PR belongs to one or more scopes, and must satisfy all gates for every scope it touches.
If a gate does not apply to your change, write a one-line note explaining why (e.g. "no new contract entry-points added"). Do not leave gates blank.
Reviewers: your job is to verify that evidence exists for each gate, not to re-run commands yourself.
- Read through the scope descriptions below and identify every scope your PR touches.
- For each scope, work through its gate list and collect the evidence (command output, test names, migration filenames, etc.).
- Paste the evidence into the PR description under the corresponding scope heading.
- If you skip a gate, explain why in the PR description. Unexplained skips block merge.
A change can touch multiple scopes. A contract change that also requires an indexer migration must satisfy both Scope A and Scope C.
Applies to any modification of contracts/circle/src/lib.rs, contracts/circle_factory/src/lib.rs, contracts/reputation/src/lib.rs, or any of the shared Cargo.toml dependency versions.
A1 — All existing contract tests pass
cd contracts && cargo testEvidence required: test output showing 0 failures. If you added new tests, list their names.
A2 — Mutation guards cover any new financial path
For every guard you add, modify, or remove, there must be a corresponding test in contracts/circle/src/mutation_guards.rs that proves removal of the guard is detected. See the mutation guard catalogue for examples.
Evidence required: name of the new/updated mutation guard test, or an explicit statement that no new guard was added.
A3 — Property invariants hold
If you changed a numeric boundary (PENALTY_BPS, BPS_DENOM, COLLATERAL_MULTIPLIER, MIN/MAX_ROUND_DEADLINE_LEDGERS, MAX_MEMBERS), verify that all property-based tests in contracts/circle/src/prop_tests.rs still pass and update any invariant that changed semantics.
cd contracts && cargo test prop_testsEvidence required: prop_tests output, or confirmation that no protocol constants changed.
A4 — WASM compiles without warnings
cd contracts && cargo build --target wasm32-unknown-unknown --release 2>&1 | grep -i warnEvidence required: zero new warnings, or a justification for any warning present.
A5 — SDK types and app interfaces are updated
Any new or renamed entry-point, event field, or return type must be reflected in:
app/src/lib/stellar.ts(call helpers and result types)app/src/lib/gating.ts(if the change affects eligibility logic)app/src/lib/config.ts(if the change affects constants like USDC denomination)
Evidence required: list of files updated, or confirmation that no public interface changed.
A6 — Indexer handles any new event type
If you added a new contract event, there must be a handler in indexer/src/indexer.ts, a migration for any new columns, and a fixture in indexer/src/fixtures/incident-fixtures.ts for the new event class.
Evidence required: new event type name, handler function name, and migration filename; or confirmation that no new events were added.
A7 — Runbook updated
If the change alters observable behavior (new error message, new status, new deadline rule), update the corresponding troubleshooting section in docs/RUNBOOK.md.
Evidence required: runbook section updated, or confirmation that no user-visible behavior changed.
A8 — The failure case is tested
For every new guard, the test suite must include at least one test that puts the contract into the blocked state and asserts the correct panic message. Use #[should_panic(expected = "...")].
Evidence required: test name and the expected panic message.
A9 — Rollback plan documented
For changes that modify on-chain storage layout (new DataKey variants, changed value types), document the rollback plan: which ledgers would be affected, whether a re-deploy is required, and whether any indexer replay is needed.
Evidence required: rollback notes in the PR description, or confirmation that storage layout is unchanged.
Applies to modifications of app/src/lib/gating.ts, app/src/lib/stellar.ts, app/src/lib/config.ts, or any SDK package under sdk/.
B1 — Gating mutation tests pass
node --require ts-node/register --test app/src/lib/gating.mutation.test.tsEvidence required: test output, or an explanation of why the changed guard is not covered.
B2 — New guard has a mutation proof
If you added or tightened an eligibility guard in gating.ts, add a corresponding block in gating.mutation.test.ts that:
- Asserts the production function blocks the edge case.
- Demonstrates via a local mutant that removing the guard would allow it.
Evidence required: name of the new describe block in gating.mutation.test.ts.
B3 — TypeScript types are coherent
npx tsc --noEmit --project app/tsconfig.jsonEvidence required: zero type errors.
B4 — Telemetry instrumentation is correct
If you added a new stage to invokeContract or changed the transaction flow, update app/src/lib/telemetry.ts accordingly and verify that:
- The new stage emits using
emit(ctx, "<stage>")oremit(ctx, "<stage>", category). - No address, hash, XDR, ScVal, or user-entered value is passed to
emit. - The telemetry tests still pass:
node --require ts-node/register --test app/src/lib/telemetry.test.ts
Evidence required: stage name(s) added and test output, or confirmation that the transaction flow was not changed.
B5 — Error paths return the correct user-facing message
For every new error condition, confirm that formatContractError (in stellar.ts) returns a human-readable string — not a raw RPC or Freighter error. Add a test in telemetry.test.ts or a dedicated error formatting test if the mapping is new.
Evidence required: test name covering the new error path.
B6 — Stale snapshot gate is preserved
Any change that adds a new action to computeActionEligibility must check isSnapshotFresh first, before any other guard. Verify the ordering invariant test in gating.mutation.test.ts covers the new action.
Evidence required: confirmation that stale_snapshot is the first gate for the new action.
Applies to modifications of anything under indexer/src/.
C1 — All indexer tests pass
npm test --workspace=indexerEvidence required: test output showing 0 failures. Integration tests that require Postgres may be skipped in CI if DATABASE_URL is absent — note this explicitly if it applies.
C2 — Incident reproduction tests pass (offline)
npm run test:incidents --workspace=indexerEvidence required: test output. These tests must pass in any environment, including offline CI runners.
C3 — Schema health is clean after migrations
npm run migrate --workspace=indexer
npm run migrate:check --workspace=indexerEvidence required: migrate:check output shows Health state: clean.
C4 — New migration file follows naming convention
Migration files must be named NNN_short_description.sql where NNN is a zero-padded three-digit sequence number (e.g. 003_add_reputation_events.sql). The file must be idempotent on repeated application (use CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, etc.).
Evidence required: migration filename listed in the PR.
C5 — API shape changes are reflected in app types
Any change to a response field name, type, or added/removed field in api.ts must be reflected in the corresponding TypeScript interfaces in app/src/app/circles/[address]/CircleDetailClient.tsx or app/src/app/reputation/[member]/ReputationClient.tsx.
Evidence required: list of app interface files updated, or confirmation that the API shape is unchanged.
C6 — Drift scenario is tested
If you add a new migration file, add or update the SchemaDrift fixture in indexer/src/fixtures/incident-fixtures.ts to reflect the new filename so the drift reproduction test remains accurate.
Evidence required: updated fixture constant name, or confirmation that the existing fixture is still accurate.
C7 — Rollback procedure documented
For destructive migrations (DROP TABLE, DROP COLUMN, data backfills), document the rollback SQL in the PR description and note whether a full re-index is required. See Re-indexing from a Given Ledger in the runbook.
Evidence required: rollback SQL snippet in the PR description, or confirmation that the migration is additive-only (no rollback needed).
Applies to modifications of anything under app/src/app/ or app/src/components/.
D1 — TypeScript build passes
npx tsc --noEmit --project app/tsconfig.json
npm run lint --workspace=appEvidence required: zero errors and zero new lint warnings.
D2 — "use client" boundaries are correct
Pages and components that use React hooks, browser APIs, or Freighter must have "use client" at the top. Data fetching must remain in Server Components unless interaction is required.
Evidence required: list any new "use client" directives added and the reason, or confirmation that no new client components were added.
D3 — Gating logic lives in gating.ts, not in ad-hoc UI conditions
Any eligibility check that affects whether a wallet action is available must go through computeActionEligibility. It must not be implemented as an inline if in a component.
Evidence required: confirmation that no new eligibility logic was added to components directly, or a description of how the new condition was added to gating.ts.
D4 — New routes have metadata
Every new page.tsx must export a generateMetadata function with a descriptive title and description.
Evidence required: metadata export shown in the PR diff, or confirmation that no new routes were added.
D5 — UI failure states are handled
For any new wallet-triggered action:
- The loading state must be visible while the transaction is in flight.
- The error state must display a human-readable message from
invokeContract. - The success state must clear loading and show confirmation.
Verify that each state is reachable by reading through the component logic. A manual smoke test on testnet is recommended.
Evidence required: description of how each state (loading / error / success) is handled in the component.
D6 — Freighter not-installed case is handled
Any new button that triggers invokeContract must check isFreighterInstalled() before calling it and show the appropriate install prompt if Freighter is absent. See the WalletRejection.notInstalled fixture and Freighter not detected in the runbook.
Evidence required: confirmation that the not-installed path is guarded.
Applies to modifications of contracts/reputation/src/lib.rs, reputation-related indexer handlers, or any app page under app/src/app/reputation/.
E1 — Reputation authorization invariants hold
The five invariants documented at the top of contracts/reputation/src/lib.rs must all have passing tests:
- Admin-only management (add/remove authorized callers)
- Uniqueness (idempotent add)
- Permanent revocation
- Revocation outranks allowlist
- Monotonic scores
cd contracts && cargo test -p reputationEvidence required: test output, or confirmation that the invariants are unchanged.
E2 — Revocation permanence is tested
Any change that touches remove_authorized_caller or the revocation check in increment must include or update the guard_reputation_revocation_is_permanent test in contracts/circle/src/mutation_guards.rs.
Evidence required: test name and output.
E3 — Reputation score is non-decreasing
increment must only ever increase the score. If you are adding a new code path that modifies the score, verify that the monotonicity invariant (prop_tests.rs invariant 7) still passes.
Evidence required: prop_tests output, or confirmation that score modification logic is unchanged.
E4 — Unauthorized increment is blocked
The guard_reputation_unauthorized_caller_blocked test must pass unchanged. If you added a new caller type, add a corresponding test that verifies the new caller is correctly gated.
Evidence required: test name.
Applies to any change that modifies authentication, authorization, access control, or known-sensitive data handling in any layer.
F1 — Threat model is documented
Describe in the PR description: what the change protects, who the adversary is, and what they could do without this protection.
Evidence required: threat model paragraph in the PR description.
F2 — Both the allow and deny paths are tested
For any new authorization check, there must be:
- A test that verifies a legitimate caller succeeds.
- A test that verifies an unauthorized caller is rejected with the expected error.
Evidence required: both test names.
F3 — Telemetry does not leak sensitive data
If your change touches invokeContract, stellar.ts, or any place where wallet addresses, transaction hashes, or user-entered values are available, verify that:
- None of those values are passed to
emit()or any telemetry function. scrubPayloadwould catch them if they were accidentally included.
Run the privacy tests:
node --require ts-node/register --test app/src/lib/telemetry.test.ts 2>&1 | grep -E "pass|fail"Evidence required: all privacy tests pass; list of any new sensitive fields added to event payloads (there should be none).
F4 — No secrets in fixtures or tests
Verify that any new test fixtures, snapshots, or sample data follow the synthetic-data rules in indexer/src/fixtures/incident-fixtures.ts:
- All addresses are synthetic strkeys (all-zero byte pattern).
- All transaction hashes start with
0orf(synthetic pattern). - No real private keys (
S-addresses), mnemonics, or production contract IDs appear.
Run the cross-fixture invariant test:
npm run test:incidents --workspace=indexer 2>&1 | grep "no fixture"Evidence required: cross-fixture invariant tests pass.
F5 — Rollback does not reintroduce a vulnerability
If your PR fixes a security issue, describe in the PR description what the state of the system is if this commit is reverted (e.g. via a hotfix revert). If reversion would reintroduce the vulnerability, note this explicitly and propose a safe revert strategy.
Evidence required: revert-safety note in the PR description.
Applies to modifications of docker-compose.yml, Dockerfile (if added), CI workflow files under .github/, environment variable schemas (app/.env.example, indexer/.env.example), or the runbook (docs/RUNBOOK.md).
G1 — Environment variable changes are documented
Any new or renamed env var must appear in both the .env.example file for the affected package and the Environment Variables Reference table in the runbook.
Evidence required: .env.example diff and runbook section updated.
G2 — Migration health check passes after deployment
For any change to the database or indexer container:
npm run migrate:check --workspace=indexerEvidence required: Health state: clean output.
G3 — Runbook deployment steps are accurate
If deployment order, post-deployment configuration steps, or rollback procedures change, update Deployment and Contract Deployment Order in the runbook.
Evidence required: runbook sections updated, or confirmation that deployment steps are unchanged.
G4 — Rollback procedure is documented
For any infrastructure change that is not trivially reversible (container image upgrade, database volume change, contract re-deploy), document:
- How to roll back to the previous state.
- Whether any data migration or replay is required during rollback.
- The maximum acceptable downtime.
Evidence required: rollback procedure in the PR description.
G5 — Schema drift scenario is accounted for
If deployment involves applying a migration, verify the SchemaDrift incident fixture and test remain accurate. If the new migration is the latest in sequence, update SchemaDrift.seedFilesOnDisk() in indexer/src/fixtures/incident-fixtures.ts.
Evidence required: fixture updated or confirmed accurate.
This table maps each command to the gates it satisfies. Run the commands locally before opening a PR.
| Command | Gates satisfied |
|---|---|
cd contracts && cargo test |
A1, A3, A8, E1, E2, E3 |
cd contracts && cargo test -p circle mutation_guard |
A2, A8, E2, E4 |
cd contracts && cargo build --target wasm32-unknown-unknown --release |
A4 |
npx tsc --noEmit --project app/tsconfig.json |
B3, D1 |
npm run lint --workspace=app |
D1 |
node --require ts-node/register --test app/src/lib/gating.mutation.test.ts |
B1, B2, B6 |
node --require ts-node/register --test app/src/lib/telemetry.test.ts |
B4, F3 |
npm test --workspace=indexer |
C1, C2 |
npm run test:incidents --workspace=indexer |
C2, F4 |
npm run migrate --workspace=indexer |
C3 |
npm run migrate:check --workspace=indexer |
C3, G2, G5 |
Use the Bug Report issue template. For security vulnerabilities, see SECURITY.md — do not open a public issue.