Sorokeep is an open-source project and contributions are welcome. This document explains how the project works, how to set up your environment, and what we expect from contributions.
- Before You Start
- Quick Start
- Project Structure
- Development Workflow
- Code Conventions
- Architecture Decision Records
- E2E Sandbox Testing
- What Makes a Good Contribution
- Getting Help
Read the README to understand what Sorokeep does and how it's structured. The short version: Sorokeep monitors Soroban smart contract TTLs and alerts developers before their contract state expires. It's a TypeScript CLI that reads from the Stellar RPC and stores data in local SQLite.
If you want to work on something, check the open issues first. If there's no issue for what you want to do, open one and describe the change before writing code. This prevents wasted effort on changes that don't fit the project direction.
For how the pieces actually work together at runtime (the daemon cycle, fault isolation, where a new alert channel or command plugs in), read docs/ARCHITECTURE.md. The Project Structure section below covers the directory layout; ARCHITECTURE.md covers the data flow.
If you're reporting a security issue (key leakage, unintended transactions, signature bypass), see SECURITY.md instead of opening a public issue. This project is also governed by a Code of Conduct.
You need:
- Node.js 22 or later
- npm
- Git
Clone and install:
git clone https://github.com/AbdulmalikAlayande/sorokeep.git
cd sorokeep
npm installVerify everything works:
# Run all tests
npm test
# Run the CLI
npx tsx src/index.ts --help
# Watch a real contract on testnet (optional, requires internet)
npx tsx src/index.ts watch CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC --network testnet --name "XLM Test"If all tests pass and the CLI prints its help screen, you're ready.
sorokeep/
├── src/
│ ├── index.ts # CLI entry point (Commander.js)
│ ├── commands/ # CLI command handlers (parse args, call core, format output)
│ ├── core/ # Business logic (no CLI dependencies, no side effects)
│ ├── rpc/ # Stellar RPC client wrapper
│ ├── db/ # SQLite schema, connection, and data access functions
│ ├── alerts/ # Alert dispatcher (webhook, Slack)
│ ├── daemon/ # Monitoring loop and lifecycle
│ ├── logging/ # Structured logging with pino
│ └── utils/ # Formatting helpers, config loading
├── tests/ # Mirrors src/ — same folder names, .test.ts suffix
│ ├── commands/
│ ├── core/
│ ├── alerts/
│ ├── daemon/
│ ├── rpc/
│ ├── db/
│ └── utils/
├── docs/ # Documentation
│ ├── adr/ # Architecture Decision Records
│ └── e2e-sandbox.md # E2E sandbox setup guide
├── .github/workflows/ # CI (test + type-check) and publish
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── CONTRIBUTING.md
Key architectural rule: core logic never depends on CLI or presentation code. The commands/ layer is a thin wrapper that calls functions from core/, which do all the real work. This means the daemon can reuse the same core functions without importing CLI code.
If you're adding a new feature, the logic goes in core/, the CLI wiring goes in commands/, and tests go in tests/core/.
We enforce strict test-driven development. Your PR will not be accepted without comprehensive tests.
The process is:
- Write the test first. Define what the function should do, what inputs it takes, and what outputs it returns. Run the test — it should fail (red).
- Write the minimum implementation to make the test pass (green).
- Refactor if needed, then run all tests to make sure nothing broke.
# All tests
npm test
# Specific file
npx vitest run tests/core/monitor.test.ts
# Watch mode (re-runs on file changes)
npx vitest
# With coverage
npx vitest run --coverage
# Update all snapshot files after intentional formatting changes
npx vitest run -u
# Update snapshot for a specific file
npx vitest run tests/commands/status.test.ts -u
# Mutation testing (src/core/ only)
npm run mutation-testThe baseline mutation score for src/core/ is 62.33%. We aim to improve or maintain this score. If a surviving mutant reveals a genuinely weak test, strengthen the test rather than gaming the source code.
All tests use in-memory SQLite databases and mocked RPC responses — no network calls, no filesystem side effects.
Snapshot tests (toMatchSnapshot()) capture the ANSI-stripped rendered output of CLI commands such as status, costs, and alerts list. If you intentionally change the formatting of a command's output — column alignment, labels, colors, or field order — the snapshot tests will fail. To update them:
- Verify the new output is correct by inspecting the diff shown by Vitest.
- Run
npx vitest run -uto regenerate the.snapfiles. - Commit the updated
.snapfiles alongside your formatting changes.
Snapshot tests strip ANSI color codes and normalize timestamps before comparing, so diffs focus on content and alignment rather than escape sequences.
To maintain trust in our CI pipeline, we run a scheduled workflow to detect intermittent test failures (flaky tests). A test must pass reliably.
If your PR is flagged as introducing a flaky test (or if a scheduled workflow opens an issue assigned to you):
- Do not ignore it. Flaky tests erode confidence in the test suite and block other contributors.
- Reproduce locally: Run the failing test repeatedly to reproduce the flakiness (e.g., using
npx vitest run tests/your.test.ts). - Identify the root cause: Common causes include race conditions, unmocked network or timer calls, or shared state leaking across tests.
- Fix it: Ensure the test passes 100% of the time. Avoid simply using test retries to mask the underlying issue.
Use tsx to run TypeScript directly without compiling:
npx tsx src/index.ts watch <contractId> --network testnet
npx tsx src/index.ts --helpSorokeep uses SQLite stored at ~/.sorokeep/sorokeep.db. The schema is in src/db/schema.sql.
Tests use an in-memory SQLite database (getDatabaseForTesting()) so they're fast and don't touch your local state.
If you need to reset your local database during development:
# Linux/macOS
rm ~/.sorokeep/sorokeep.db
# Windows PowerShell
Remove-Item "$HOME\.sorokeep\sorokeep.db"# Lint
npm run lint
# Type check (without emitting files)
npx tsc --noEmitRun both before pushing to ensure CI passes.
- Strict mode is on (
strict: truein tsconfig) noUncheckedIndexedAccessis enabled — array access returnsT | undefined- ESM modules (
"type": "module"in package.json). See ADR-002. - Use
import typefor type-only imports - No
console.login core logic — use the pino logger for operational logging, and return data for the CLI layer to print
- Files:
kebab-case.ts - Functions:
camelCase - Interfaces/Types:
PascalCase - Database columns:
snake_case - Constants:
UPPER_SNAKE_CASEfor true constants,camelCasefor configuration
Order imports by:
- Node.js built-ins (
node:fs,node:path) - Third-party packages (
vitest,better-sqlite3,commander) - Internal modules (
../../src/core/monitor.js)
Use explicit .js extensions for internal imports (ESM requirement). Type-only imports use import type.
Avoid . in a regex meant to match "anything up to a line break" (e.g. /--.*\n/) — JavaScript's . excludes all line terminator characters, including \r. On a CRLF-checked-out file, a line ending in \r\n leaves a trailing \r that . won't consume, so the pattern silently fails to match and no error is thrown. Use a negated character class instead, e.g. /--[^\n]*\n/, which consumes everything up to (but not including) the newline regardless of a preceding \r.
Catch errors and return structured results (like WatchResult) instead of throwing from core functions. Let the CLI layer decide how to present errors.
// Core function returns a result type, doesn't throw
function doSomething(input: string): { ok: true; value: number } | { ok: false; error: string }Follow conventional commit format:
feat: add slack alert integration
fix: handle archived WASM entries in monitor cycle
test: add boundary tests for TTL threshold detection
docs: update README with daemon usage
refactor: extract RPC response mapping into helper
Types: feat, fix, test, docs, refactor, chore
feature/short-description
fix/short-description
docs/short-description
Branch from main, PR back to main.
Significant design decisions are documented as Architecture Decision Records (ADRs) in docs/adr/. Each ADR explains the context, options considered, and rationale for the chosen approach.
| ADR | Title | Description |
|---|---|---|
| ADR-001 | Use SQLite for Local Storage | Why SQLite over PostgreSQL or JSON files |
| ADR-002 | Use ESM (ECMAScript Modules) | Why ESM over CommonJS |
| ADR-003 | Use Commander.js for CLI Framework | Why Commander over oclif or yargs |
| ADR-004 | Polling Daemon Architecture | Why polling over event-driven |
| ADR-005 | Use TypeScript (Not Rust) | Why TypeScript over Rust for this tool |
| ADR-006 | In-Memory SQLite for Testing | Why tests use in-memory databases |
| ADR-007 | Use a Plugin Registry for Alert Channels | Why a Map-based registry over hardcoded channel maps or dynamic plugins |
| ADR-008 | Application-Layer Channel Type Validation | Why channel_type CHECK was relaxed from a fixed SQL enum to a non-empty-string guard, validated by the alert channel registry |
Before making a significant new design decision, write an ADR. This helps future contributors understand why things are the way they are.
We provide a complete guide for setting up an end-to-end sandbox environment with a local Stellar network using Docker. See docs/e2e-sandbox.md for:
- Running a local Soroban-enabled Stellar network
- Creating and funding test accounts
- Deploying test contracts
- Configuring Sorokeep to monitor local contracts
- An automated E2E test script for CI usage
- Troubleshooting common issues
The sandbox lets you test Sorokeep against a real Stellar RPC without touching public testnet or mainnet.
If you're new to the project, look for issues tagged good first issue. These are typically:
- Adding a new alert channel (e.g. Matrix, Microsoft Teams, email) — see docs/adding-an-alert-channel.md, it's a self-contained plugin registration, not a core change
- CLI UX improvements (better error messages, colored output)
- Documentation improvements
- Adding test coverage for edge cases
New Contributor? Check out our First PR Tutorial where we walk through picking a real trivial issue, writing the failing test, implementing the fix, and creating the PR.
For anything beyond small fixes, open an issue first to discuss the approach. This is especially important for:
- New CLI commands
- Database schema changes
- Changes to the monitor cycle logic
- New RPC client methods
Before submitting a PR, verify:
- Tests pass (
npm test) - Type check passes (
npx tsc --noEmit) - Lint passes (
npm run lint) - Tests cover the new functionality (TDD preferred)
- No unnecessary dependencies added
- Commit messages follow conventional format
- Code matches the project's style and conventions
- No
console.login core logic - ADR created if making a significant design decision
- E2E sandbox tested (for changes affecting RPC or daemon interactions)
Troubleshooting production daemon issues? See docs/troubleshooting.md for a complete runbook covering common failure modes (hung cycles, alerts not firing, auto-extension blocked, RPC errors) with diagnostic commands and resolution steps.
If you're stuck or have questions about the codebase, open an issue or reach out on X (@The_good_man02). We'd rather answer questions early than review a PR that went in the wrong direction.