Skip to content

Latest commit

 

History

History
339 lines (241 loc) · 14.2 KB

File metadata and controls

339 lines (241 loc) · 14.2 KB

Contributing to Sorokeep

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.

Table of Contents

Before You Start

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.

Quick Start

You need:

  • Node.js 22 or later
  • npm
  • Git

Clone and install:

git clone https://github.com/AbdulmalikAlayande/sorokeep.git
cd sorokeep
npm install

Verify 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.

Project Structure

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/.

Development Workflow

Test-Driven Development

We enforce strict test-driven development. Your PR will not be accepted without comprehensive tests.

The process is:

  1. 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).
  2. Write the minimum implementation to make the test pass (green).
  3. Refactor if needed, then run all tests to make sure nothing broke.

Running Tests

# 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-test

The 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:

  1. Verify the new output is correct by inspecting the diff shown by Vitest.
  2. Run npx vitest run -u to regenerate the .snap files.
  3. Commit the updated .snap files 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.

Flaky Tests

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):

  1. Do not ignore it. Flaky tests erode confidence in the test suite and block other contributors.
  2. Reproduce locally: Run the failing test repeatedly to reproduce the flakiness (e.g., using npx vitest run tests/your.test.ts).
  3. Identify the root cause: Common causes include race conditions, unmocked network or timer calls, or shared state leaking across tests.
  4. Fix it: Ensure the test passes 100% of the time. Avoid simply using test retries to mask the underlying issue.

Running the CLI During Development

Use tsx to run TypeScript directly without compiling:

npx tsx src/index.ts watch <contractId> --network testnet
npx tsx src/index.ts --help

Database

Sorokeep 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"

Linting and Type Checking

# Lint
npm run lint

# Type check (without emitting files)
npx tsc --noEmit

Run both before pushing to ensure CI passes.

Code Conventions

TypeScript

  • Strict mode is on (strict: true in tsconfig)
  • noUncheckedIndexedAccess is enabled — array access returns T | undefined
  • ESM modules ("type": "module" in package.json). See ADR-002.
  • Use import type for type-only imports
  • No console.log in core logic — use the pino logger for operational logging, and return data for the CLI layer to print

Naming

  • Files: kebab-case.ts
  • Functions: camelCase
  • Interfaces/Types: PascalCase
  • Database columns: snake_case
  • Constants: UPPER_SNAKE_CASE for true constants, camelCase for configuration

Imports

Order imports by:

  1. Node.js built-ins (node:fs, node:path)
  2. Third-party packages (vitest, better-sqlite3, commander)
  3. Internal modules (../../src/core/monitor.js)

Use explicit .js extensions for internal imports (ESM requirement). Type-only imports use import type.

Regex on File Content

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.

Error Handling

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 }

Commits

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

Branches

feature/short-description
fix/short-description
docs/short-description

Branch from main, PR back to main.

Architecture Decision Records

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.

E2E Sandbox Testing

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.

What Makes a Good Contribution

Good First Issues

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.

Larger Contributions

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

PR Checklist

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.log in core logic
  • ADR created if making a significant design decision
  • E2E sandbox tested (for changes affecting RPC or daemon interactions)

Getting Help

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.