From aa6611795e7ef1f2b9f0dd28f0b21390c36401cf Mon Sep 17 00:00:00 2001 From: graceuvala-collab Date: Thu, 30 Jul 2026 10:20:20 +0000 Subject: [PATCH] docs: add ADRs, API error reference, troubleshooting and git workflow guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four contributor-facing documentation sets: - Architecture Decision Records under docs/adr/ — a template plus five initial records covering the off-chain listener architecture, Soroban on Stellar, SQLite persistence, TypeScript for the listener, and the event deduplication strategy. - API error reference documenting every error response the listener returns, grounded in events-server.ts, template-routes.ts, rate-limiter.ts and batch-validator.ts. - Contributor troubleshooting guide for build, test, database, Git and CI problems, scoped to complement the existing setup and deployment troubleshooting docs. - Git workflow guide covering the fork model, branch naming, commit conventions and the PR process. README.md and CONTRIBUTING.md now link to all four, including ADRs. Closes #516 Closes #511 Closes #513 Closes #512 Co-Authored-By: Claude Opus 5 --- CONTRIBUTING.md | 12 + README.md | 9 + docs/API_ERROR_REFERENCE.md | 416 ++++++++++++++++ docs/CONTRIBUTOR_TROUBLESHOOTING.md | 451 ++++++++++++++++++ docs/GIT_WORKFLOW.md | 378 +++++++++++++++ docs/adr/0000-template.md | 98 ++++ .../0001-off-chain-listener-architecture.md | 112 +++++ docs/adr/0002-soroban-smart-contracts.md | 110 +++++ docs/adr/0003-sqlite-for-local-persistence.md | 128 +++++ .../0004-typescript-for-listener-service.md | 118 +++++ docs/adr/0005-event-deduplication-strategy.md | 124 +++++ docs/adr/README.md | 37 ++ 12 files changed, 1993 insertions(+) create mode 100644 docs/API_ERROR_REFERENCE.md create mode 100644 docs/CONTRIBUTOR_TROUBLESHOOTING.md create mode 100644 docs/GIT_WORKFLOW.md create mode 100644 docs/adr/0000-template.md create mode 100644 docs/adr/0001-off-chain-listener-architecture.md create mode 100644 docs/adr/0002-soroban-smart-contracts.md create mode 100644 docs/adr/0003-sqlite-for-local-persistence.md create mode 100644 docs/adr/0004-typescript-for-listener-service.md create mode 100644 docs/adr/0005-event-deduplication-strategy.md create mode 100644 docs/adr/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90ef20ff..46c36fc7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,12 @@ Thank you for your interest in contributing to NotifyChain! This document provid **Start here instead (recommended):** [`CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md`](CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md) +**Companion guides:** +- [`docs/GIT_WORKFLOW.md`](docs/GIT_WORKFLOW.md) — the full branching strategy, commit conventions, and PR process, with a command cheat sheet +- [`docs/CONTRIBUTOR_TROUBLESHOOTING.md`](docs/CONTRIBUTOR_TROUBLESHOOTING.md) — solutions for common build, test, database, and Git problems +- [`docs/API_ERROR_REFERENCE.md`](docs/API_ERROR_REFERENCE.md) — every error response the listener API returns +- [`docs/adr/README.md`](docs/adr/README.md) — Architecture Decision Records + ## Code of Conduct - Be respectful and inclusive @@ -93,6 +99,8 @@ Example: - `fix/resolve-event-deduplication-bug` - `docs/update-contributing-guide` +> For the complete branching strategy — including naming rules, what to avoid, and how to recover from common mistakes — see [`docs/GIT_WORKFLOW.md`](docs/GIT_WORKFLOW.md). + ### 2. Make Your Changes - Write clean, readable code @@ -100,6 +108,10 @@ Example: - Add comments for complex logic - Update documentation as needed +**Making a significant architectural change?** Read the [Architecture Decision Records](docs/adr/README.md) first — they document why the current design is what it is. If your change alters one of those decisions, add a new ADR using [`docs/adr/0000-template.md`](docs/adr/0000-template.md) and reference it in your PR. + +**Hit a problem?** Check [`docs/CONTRIBUTOR_TROUBLESHOOTING.md`](docs/CONTRIBUTOR_TROUBLESHOOTING.md) before opening an issue. + ### 3. Write and Run Tests All new features and bug fixes must include tests. diff --git a/README.md b/README.md index e68705b1..3a6612fe 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,12 @@ The project enables developers to build reactive decentralized applications with > - [Notification Failure Recovery](NOTIFICATION_FAILURE_RECOVERY.md) — retry lifecycle, configuration, and troubleshooting. > > **Event reference**: [Smart Contract Event Reference Guide](CONTRACT_EVENT_REFERENCE.md) — all emitted events, parameters, data types, and usage recommendations for indexers and listeners. +> +> **API errors**: [API Error Reference](docs/API_ERROR_REFERENCE.md) — every error response the listener API returns, with causes and resolutions. +> +> **Architecture decisions**: [Architecture Decision Records](docs/adr/README.md) — the *why* behind the project's significant technical choices, including the [off-chain listener architecture](docs/adr/0001-off-chain-listener-architecture.md), [Soroban on Stellar](docs/adr/0002-soroban-smart-contracts.md), [SQLite persistence](docs/adr/0003-sqlite-for-local-persistence.md), [TypeScript for the listener](docs/adr/0004-typescript-for-listener-service.md), and the [event deduplication strategy](docs/adr/0005-event-deduplication-strategy.md). +> +> **Contributor guides**: [Git Workflow](docs/GIT_WORKFLOW.md) · [Contributor Troubleshooting](docs/CONTRIBUTOR_TROUBLESHOOTING.md) --- @@ -638,6 +644,9 @@ See [`frontend/src/components/SubscriptionForm.tsx`](frontend/src/components/Sub Contributions are welcome! Please follow these steps (or start with the canonical workflow guide): - [`CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md`](CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md) +- [`docs/GIT_WORKFLOW.md`](docs/GIT_WORKFLOW.md) — branching strategy, commit conventions, and the PR process +- [`docs/CONTRIBUTOR_TROUBLESHOOTING.md`](docs/CONTRIBUTOR_TROUBLESHOOTING.md) — fixes for common build, test, and workflow problems +- [`docs/adr/README.md`](docs/adr/README.md) — architecture decision records; read these before proposing a significant design change - [`docs/ROADMAP.md`](docs/ROADMAP.md) — planned features and milestones 1. Fork the repository diff --git a/docs/API_ERROR_REFERENCE.md b/docs/API_ERROR_REFERENCE.md new file mode 100644 index 00000000..591ee29d --- /dev/null +++ b/docs/API_ERROR_REFERENCE.md @@ -0,0 +1,416 @@ +# API Error Reference + +Complete reference for error responses returned by the NotifyChain listener HTTP API. + +Every error response is JSON. Use this document to map a status code and body to a cause and a concrete fix. + +**Related:** [API Contract and Event Reference](../listener/API_CONTRACT_EVENT_REFERENCE.md) · [API Versioning](api-versioning.md) · [Contributor Troubleshooting](CONTRIBUTOR_TROUBLESHOOTING.md) + +--- + +## Table of Contents + +1. [Error Response Format](#1-error-response-format) +2. [Diagnostic Headers](#2-diagnostic-headers) +3. [HTTP Status Codes](#3-http-status-codes) +4. [400 — Bad Request](#4-400--bad-request) +5. [401 — Unauthorized](#5-401--unauthorized) +6. [404 — Not Found](#6-404--not-found) +7. [409 — Conflict](#7-409--conflict) +8. [413 — Payload Too Large](#8-413--payload-too-large) +9. [429 — Too Many Requests](#9-429--too-many-requests) +10. [500 — Internal Server Error](#10-500--internal-server-error) +11. [503 — Service Unavailable](#11-503--service-unavailable) +12. [Batch Validation Error Codes](#12-batch-validation-error-codes) +13. [Quick Reference Table](#13-quick-reference-table) + +--- + +## 1. Error Response Format + +Most errors return a single `error` field: + +```json +{ + "error": "Template not found" +} +``` + +Rate-limit responses add a human-readable `message`: + +```json +{ + "error": "Too Many Requests", + "message": "Rate limit exceeded. Try again in 42 seconds." +} +``` + +Batch validation returns a structured, per-item error array instead of a flat `error` string — see [Section 12](#12-batch-validation-error-codes): + +```json +{ + "valid": false, + "processedCount": 0, + "errors": [ + { "index": 2, "code": "MISSING_FIELD", "message": "Field 'recipient' is required." } + ] +} +``` + +> **Note:** the API does not currently emit a stable machine-readable error code at the top level for non-batch errors. Match on the HTTP status code first, and treat the `error` string as human-readable. Only the batch validation `code` values in Section 12 are stable identifiers safe to branch on. + +--- + +## 2. Diagnostic Headers + +Every response — success or error — carries these headers. Include them in any bug report. + +| Header | Description | +|--------|-------------| +| `X-Request-Id` | Unique ID generated per request. Appears in listener logs for the same request. | +| `X-Correlation-Id` | Echoes an inbound `X-Correlation-Id` if supplied, otherwise a new UUID. Use it to trace one logical operation across services. | +| `X-API-Version` | Active API version — currently `v1`. | + +Rate-limited endpoints additionally return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and — on a 429 — `Retry-After`. + +To correlate a failure with server logs, send your own correlation ID: + +```bash +curl -i -H "X-Correlation-Id: debug-run-001" http://localhost:8787/api/events +``` + +--- + +## 3. HTTP Status Codes + +| Code | Meaning | Retry? | +|------|---------|--------| +| `200` | Success | — | +| `201` | Resource created (scheduled notification, template) | — | +| `202` | Accepted — webhook received, processed asynchronously | — | +| `204` | No content — CORS preflight (`OPTIONS`) | — | +| `400` | Bad Request — malformed or invalid input | No — fix the request | +| `401` | Unauthorized — missing or invalid credentials/signature | No — fix credentials | +| `404` | Not Found — unknown route or missing resource | No | +| `409` | Conflict — resource already exists | No — use a different key | +| `413` | Payload Too Large — body exceeds the size limit | No — shrink the payload | +| `429` | Too Many Requests — rate limit exceeded | Yes — after `Retry-After` | +| `500` | Internal Server Error — unhandled server failure | Yes — with backoff | +| `503` | Service Unavailable — an optional subsystem is not enabled | No — enable the subsystem | + +--- + +## 4. 400 — Bad Request + +The request reached the right handler but the input was rejected. + +### Missing required fields + +```json +{ "error": "Missing required fields: executeAt, payload, targetRecipient" } +``` + +**Cause:** `POST /api/schedule` was called without all three required fields. + +**Resolution:** Supply every listed field. + +```bash +curl -X POST http://localhost:8787/api/schedule \ + -H "Content-Type: application/json" \ + -d '{ + "executeAt": "2026-01-01T12:00:00Z", + "payload": { "message": "Reminder" }, + "targetRecipient": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + }' +``` + +### Invalid date + +```json +{ "error": "executeAt is not a valid date" } +``` + +**Cause:** `executeAt` could not be parsed as a date. + +**Resolution:** Use an ISO 8601 timestamp, e.g. `2026-01-01T12:00:00Z`. A Unix epoch number or a locale-formatted string will be rejected. + +### Malformed JSON body + +```json +{ + "valid": false, + "processedCount": 0, + "errors": [ + { "index": -1, "code": "PARSE_ERROR", "message": "Request body must be valid JSON." } + ] +} +``` + +**Cause:** The body of `POST /api/notifications/validate-batch` was not valid JSON. + +**Resolution:** Verify the payload parses, and that `Content-Type: application/json` is set. A common cause is shell quoting — prefer `--data-binary @file.json` over an inline heredoc. + +### Failed to read request body + +```json +{ "error": "Failed to read request body" } +``` + +**Cause:** The connection dropped or the stream errored while `POST /api/webhooks` was reading the body. + +**Resolution:** Retry. If it persists, check for a proxy timeout between client and listener. + +### Template validation errors + +```json +{ "error": "Missing required fields" } +``` +```json +{ "error": "Invalid template ID" } +``` +```json +{ "error": "Missing unique key" } +``` + +**Cause:** A template request omitted required fields, or passed a non-numeric/malformed template ID in the path. + +**Resolution:** Check the request against [`listener/src/api/template-routes.ts`](../listener/src/api/template-routes.ts). Template IDs are numeric; `/api/templates/abc` is rejected. + +--- + +## 5. 401 — Unauthorized + +Returned by `POST /api/webhooks` (signature verification) and the scheduler admin routes. + +| Response | Cause | Resolution | +|----------|-------|------------| +| `{"error": "Missing signature header"}` | No signature header on the webhook request. | Sign the raw body and send the signature header. | +| `{"error": "Missing key-id header"}` | No key-id header supplied. | Send the key-id identifying which shared secret was used. | +| `{"error": "Unknown key-id"}` | The key-id is not registered with the listener. | Verify the key-id matches one configured in the listener's webhook key set. | +| `{"error": "Request signature expired"}` | The signed timestamp is outside the allowed freshness window. | Sign and send within the window. Check for clock skew — sync with NTP. | +| `{"error": "Invalid signature"}` | The computed HMAC does not match the supplied signature. | See below. | +| `{"error": "Unauthorized"}` | Scheduler admin route called without valid credentials. | Supply the configured API key. | + +**Debugging `Invalid signature`** — the three usual causes, in order of likelihood: + +1. **Signing the parsed body rather than the raw bytes.** Re-serialising JSON changes key order and whitespace, producing a different digest. Sign the exact bytes you transmit. +2. **A secret mismatch** between sender and listener — check for a trailing newline in the `.env` value. +3. **Encoding mismatch** — confirm hex vs. base64 matches what the verifier expects. + +See [`listener/src/services/webhook-verifier.ts`](../listener/src/services/webhook-verifier.ts) for the exact verification logic. + +--- + +## 6. 404 — Not Found + +```json +{ "error": "Not found" } +``` + +**Cause:** No route matched the method and path. + +**Resolution:** +- Check the method — `GET /api/schedule` does not exist; scheduling is `POST /api/schedule`. +- Check the version prefix. Both `/api/events` and `/api/v1/events` work (`/api/v1/*` is rewritten to `/api/*`), but `/v1/events` does not. +- Confirm the subsystem is registered. Template routes only exist when a `templateService` is configured; without it, `/api/templates` returns 404 rather than 503. + +```json +{ "error": "Template not found" } +``` + +**Cause:** The template ID or unique key does not exist. + +**Resolution:** List templates with `GET /api/templates` and confirm the identifier. Note a deleted template returns 404 on subsequent reads. + +--- + +## 7. 409 — Conflict + +```json +{ "error": "Template with this unique key already exists" } +``` + +**Cause:** `POST /api/templates` used a unique key already taken. + +**Resolution:** Choose a different unique key, or update the existing template instead of creating a new one. To check first: + +```bash +curl http://localhost:8787/api/templates +``` + +--- + +## 8. 413 — Payload Too Large + +```json +{ "error": "Notification payload of 98304 bytes exceeds the 65536-byte limit. Reduce the payload size and retry." } +``` + +**Cause:** The notification payload exceeded the maximum size — **64 KB (65,536 bytes)** by default. + +**Resolution:** +- Move large content out of the payload and reference it by URL. +- Split a batch into smaller requests. +- Strip redundant nesting or whitespace before sending. + +Retrying unchanged will fail identically. See [`listener/src/utils/payload-size-validator.ts`](../listener/src/utils/payload-size-validator.ts). + +--- + +## 9. 429 — Too Many Requests + +```json +{ + "error": "Too Many Requests", + "message": "Rate limit exceeded. Try again in 42 seconds." +} +``` + +**Cause:** The client exceeded its allowed request count within the sliding window. + +**Response headers:** + +| Header | Meaning | +|--------|---------| +| `X-RateLimit-Limit` | Maximum requests permitted per window. | +| `X-RateLimit-Remaining` | Requests left in the current window. | +| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets. | +| `Retry-After` | Seconds to wait before retrying. | + +**Resolution:** Honour `Retry-After` — do not retry immediately. Use exponential backoff with jitter for automated clients, and read `X-RateLimit-Remaining` proactively rather than waiting to be blocked. + +`GET /api/rate-limit/metrics` is **exempt** from rate limiting by design, so you can always inspect the metrics that explain why you are being throttled: + +```bash +curl http://localhost:8787/api/rate-limit/metrics +``` + +--- + +## 10. 500 — Internal Server Error + +```json +{ "error": "Internal server error" } +``` + +Some handlers surface the underlying exception message instead: + +```json +{ "error": "SQLITE_BUSY: database is locked" } +``` + +**Cause:** An unhandled exception — commonly a database failure, a downstream provider timeout, or a bug. + +**Resolution:** + +1. Capture the `X-Request-Id` from the response. +2. Grep the listener logs for that ID — the full stack trace is logged server-side even when the response body is generic. +3. For `SQLITE_BUSY`, check whether another process holds the database file; see [Contributor Troubleshooting](CONTRIBUTOR_TROUBLESHOOTING.md). +4. Retry with exponential backoff — many 500s here are transient. + +If reproducible, open an issue including the request ID, the request, and the surrounding log lines. + +--- + +## 11. 503 — Service Unavailable + +A 503 from this API almost always means **an optional subsystem is not enabled**, not that the server is overloaded. Retrying will not help — enable the subsystem. + +| Response | Subsystem | Resolution | +|----------|-----------|------------| +| `{"error": "Scheduler not enabled"}` | Scheduled notifications | Configure and enable the scheduler; all `/api/schedule*` routes return this until it is running. | +| `{"error": "Rate limiting not enabled"}` | Rate limiter | Enable rate limiting to use `/api/rate-limit/metrics`. | +| `{"error": "Health monitor not configured or no report yet"}` | Notification health monitor | Configure the monitor, or wait for its first report — this also appears briefly right after startup. | +| `{"error": "Metrics history store unavailable"}` | Analytics history | Enable the metrics history store for `/api/analytics/history`. | +| `{"error": "Analytics aggregator unavailable"}` | Analytics aggregator | Enable the aggregator for `/api/analytics`. | + +See [`ENVIRONMENT_VARIABLES_AND_SECRETS.md`](../ENVIRONMENT_VARIABLES_AND_SECRETS.md) for the configuration each subsystem requires. + +--- + +## 12. Batch Validation Error Codes + +`POST /api/notifications/validate-batch` returns a structured error array. These `code` values are stable and safe to branch on. + +```json +{ + "valid": false, + "processedCount": 0, + "errors": [ + { "index": 0, "code": "MISSING_FIELD", "message": "Field 'recipient' is required." }, + { "index": 3, "code": "INVALID_CHANNEL", "message": "Channel 'carrier-pigeon' is not supported." } + ] +} +``` + +`index` is the zero-based position in the submitted array; `-1` means the error applies to the request as a whole. + +| Code | Meaning | Resolution | +|------|---------|------------| +| `PARSE_ERROR` | Body is not valid JSON. Always `index: -1`. | Validate the JSON and set `Content-Type: application/json`. | +| `INVALID_STRUCTURE` | Top-level payload is not the expected shape. | Send an array of notification objects (or the documented wrapper). | +| `EMPTY_BATCH` | The batch contains no items. | Include at least one notification. | +| `INVALID_ITEM` | An item is not an object. | Remove nulls, strings, or primitives from the array. | +| `MISSING_FIELD` | A required field is absent. | Add the field named in `message`. | +| `EMPTY_FIELD` | A required field is present but empty or whitespace-only. | Provide a non-empty value. | +| `INVALID_CHANNEL` | The channel is not supported. | Use a supported channel identifier. | +| `DUPLICATE_RECIPIENT` | The same recipient appears more than once in the batch. | Deduplicate before sending — see [ADR-0005](adr/0005-event-deduplication-strategy.md). | +| `VALIDATION_ERROR` | General validation failure from the batch service. | Read `message` for the specific constraint violated. | + +Validate a batch before submitting it: + +```bash +curl -X POST http://localhost:8787/api/notifications/validate-batch \ + -H "Content-Type: application/json" \ + --data-binary @batch.json +``` + +You can also run the validator locally without a server: + +```bash +cd listener +npm run validate:batch +``` + +--- + +## 13. Quick Reference Table + +| Status | Response `error` | Endpoint(s) | Fix | +|--------|------------------|-------------|-----| +| 400 | `Missing required fields: executeAt, payload, targetRecipient` | `POST /api/schedule` | Add all three fields | +| 400 | `executeAt is not a valid date` | `POST /api/schedule` | Use ISO 8601 | +| 400 | `Failed to read request body` | `POST /api/webhooks` | Retry; check proxy timeouts | +| 400 | `Missing required fields` | `POST /api/templates` | Add required template fields | +| 400 | `Invalid template ID` | `/api/templates/:id` | Use a numeric ID | +| 400 | `Missing unique key` | `/api/templates` | Supply the unique key | +| 401 | `Missing signature header` | `POST /api/webhooks` | Sign the request | +| 401 | `Missing key-id header` | `POST /api/webhooks` | Send the key-id | +| 401 | `Unknown key-id` | `POST /api/webhooks` | Register/correct the key-id | +| 401 | `Request signature expired` | `POST /api/webhooks` | Fix clock skew; re-sign | +| 401 | `Invalid signature` | `POST /api/webhooks` | Sign raw bytes, verify secret | +| 401 | `Unauthorized` | `/api/schedule` admin | Supply the API key | +| 404 | `Not found` | any | Check method, path, version prefix | +| 404 | `Template not found` | `/api/templates/*` | Verify the identifier | +| 409 | `Template with this unique key already exists` | `POST /api/templates` | Use a different key | +| 413 | `…exceeds the 65536-byte limit…` | notification endpoints | Shrink the payload | +| 429 | `Too Many Requests` | rate-limited routes | Wait `Retry-After` seconds | +| 500 | `Internal server error` | any | Trace `X-Request-Id` in logs | +| 503 | `Scheduler not enabled` | `/api/schedule*` | Enable the scheduler | +| 503 | `Rate limiting not enabled` | `/api/rate-limit/metrics` | Enable rate limiting | +| 503 | `Health monitor not configured or no report yet` | `/api/notifications/health` | Configure the monitor | +| 503 | `Metrics history store unavailable` | `/api/analytics/history` | Enable the history store | +| 503 | `Analytics aggregator unavailable` | `/api/analytics` | Enable the aggregator | + +--- + +## Reporting an Undocumented Error + +If you hit an error not covered here, open an issue with: + +1. The full request (method, path, headers, body — **redact secrets**). +2. The full response, including status code and the `X-Request-Id` / `X-Correlation-Id` headers. +3. Listener log lines matching that request ID. +4. The listener version or commit SHA. + +Source of truth for these responses: [`listener/src/api/events-server.ts`](../listener/src/api/events-server.ts), [`listener/src/api/template-routes.ts`](../listener/src/api/template-routes.ts), [`listener/src/api/rate-limiter.ts`](../listener/src/api/rate-limiter.ts), and [`listener/src/utils/batch-validator.ts`](../listener/src/utils/batch-validator.ts). diff --git a/docs/CONTRIBUTOR_TROUBLESHOOTING.md b/docs/CONTRIBUTOR_TROUBLESHOOTING.md new file mode 100644 index 00000000..1e8722d4 --- /dev/null +++ b/docs/CONTRIBUTOR_TROUBLESHOOTING.md @@ -0,0 +1,451 @@ +# Contributor Troubleshooting Guide + +Common problems contributors hit while working on NotifyChain, and how to resolve them. + +This guide covers the **contribution workflow** — building, testing, linting, running services locally, Git, and CI. For other problem domains: + +| Guide | Covers | +|-------|--------| +| [`TROUBLESHOOTING.md`](../TROUBLESHOOTING.md) | First-time environment setup — installing Rust, Stellar CLI, Node | +| [`DEPLOYMENT_TROUBLESHOOTING.md`](../DEPLOYMENT_TROUBLESHOOTING.md) | Deployment and staging failures | +| [API Error Reference](API_ERROR_REFERENCE.md) | HTTP error responses from the listener API | +| [Git Workflow Guide](GIT_WORKFLOW.md) | Branching, commits, and the PR process | + +--- + +## Table of Contents + +1. [Start Here — Triage](#1-start-here--triage) +2. [Install and Dependency Issues](#2-install-and-dependency-issues) +3. [TypeScript and Build Failures](#3-typescript-and-build-failures) +4. [Test Failures](#4-test-failures) +5. [Running the Listener Locally](#5-running-the-listener-locally) +6. [Database and Migration Issues](#6-database-and-migration-issues) +7. [Dashboard Issues](#7-dashboard-issues) +8. [Smart Contract (Rust) Issues](#8-smart-contract-rust-issues) +9. [Git and Pull Request Issues](#9-git-and-pull-request-issues) +10. [CI Failures](#10-ci-failures) +11. [Still Stuck?](#11-still-stuck) + +--- + +## 1. Start Here — Triage + +Before diving into a specific section, three checks resolve a large share of problems. + +**Are you on the right Node version?** CI runs **Node 22**. A different major version is the most common source of "works locally, fails in CI". + +```bash +node --version +``` + +**Are you in the right directory?** Each component has its own `package.json`. Running `npm test` at the repo root does nothing useful — you must be in `listener/` or `dashboard/`. + +**Is your branch current?** A failure caused by a stale branch disappears after syncing: + +```bash +git fetch upstream && git merge upstream/main +``` + +--- + +## 2. Install and Dependency Issues + +### `npm ci` fails with a lockfile mismatch + +``` +npm ERR! `npm ci` can only install packages when your package.json and +npm ERR! package-lock.json are in sync. +``` + +**Cause:** `package.json` was edited without regenerating the lockfile — often by hand-editing a version, or by a merge that resolved `package.json` but not `package-lock.json`. + +**Fix:** + +```bash +cd listener # or dashboard +npm install # regenerates package-lock.json +``` + +Commit the updated `package-lock.json`. Never delete it to make an error disappear — CI uses `npm ci`, which requires it. + +### `Cannot find module` after switching branches + +Branches can carry different dependency sets. Reinstall: + +```bash +cd listener +npm install +``` + +If that doesn't resolve it, clear and reinstall: + +```bash +rm -rf node_modules +npm install +``` + +### Merge conflict in `package-lock.json` + +Don't hand-resolve it. Take one side, then regenerate: + +```bash +git checkout --theirs package-lock.json +npm install +git add package-lock.json +``` + +--- + +## 3. TypeScript and Build Failures + +In this repo `npm run lint` and `npm run typecheck` are **both** `tsc --noEmit` for the listener — a lint failure is a type error. + +### `npm run lint` fails in `listener/` + +```bash +cd listener +npm run typecheck +``` + +Read the first error, not the last. TypeScript errors cascade: one bad type produces a dozen downstream complaints that vanish when the first is fixed. + +### `Property 'x' does not exist on type 'y'` + +**Cause:** Usually a domain type in `listener/src/types/` was changed without updating every consumer — exactly the class of bug TypeScript is here to catch (see [ADR-0004](adr/0004-typescript-for-listener-service.md)). + +**Fix:** Update the type definition *or* the call site so they agree. Don't reach for `as any` — it silences the check and moves the failure to runtime. + +### Build succeeds but `npm start` runs stale code + +`npm start` runs `dist/`, not `src/`. If you didn't rebuild, you're running the previous compile: + +```bash +cd listener +npm run build +npm start +``` + +For development use `npm run dev` instead — it runs TypeScript directly through `ts-node`, no build step. + +### Dashboard build fails on lint warnings + +The dashboard lints with `--max-warnings=0`, so a warning fails the build: + +```bash +cd dashboard +npm run lint +``` + +Fix the warnings. Don't raise the threshold — CI enforces zero. + +--- + +## 4. Test Failures + +Run tests from the component directory: + +```bash +cd listener && npm test +cd dashboard && npm test +``` + +### Run a single test file while iterating + +```bash +cd listener +npm test -- src/api/events-server.test.ts +``` + +Filter by test name: + +```bash +npm test -- -t "deduplication" +``` + +### Tests pass individually but fail together + +**Cause:** Shared state leaking between tests — a module-level cache, a database file, or a timer that outlives its test. + +**Fix:** Reset state in `beforeEach`/`afterEach`. For the deduplicator and similar caches, construct a fresh instance per test rather than reusing a module-level singleton. + +### Tests hang or time out + +**Cause:** An open handle — a server, database connection, or interval that was never closed. + +**Fix:** Close what you opened in `afterEach`/`afterAll`. To find the culprit: + +```bash +cd listener +npm test -- --detectOpenHandles +``` + +### Timing-dependent tests fail intermittently + +Deduplication and rate-limiting logic is time-windowed. Tests that use real wall-clock time are flaky by construction. + +**Fix:** Inject the clock rather than reading it. `NotificationDeduplicator` accepts a `now: () => number` option precisely for this — pass a controllable function instead of relying on `Date.now()`. + +### Snapshot mismatches + +If the change is intentional: + +```bash +npm test -- -u +``` + +Review the updated snapshots in the diff before committing. An unreviewed `-u` can silently bless a regression. + +--- + +## 5. Running the Listener Locally + +### Setup + +```bash +cd listener +cp .env.example .env # then edit +npm install +npm run dev +``` + +### `EADDRINUSE: address already in use` + +The events API defaults to port **8787**. + +Find and stop the process holding it: + +```bash +lsof -i :8787 +kill +``` + +Or run on a different port by setting `EVENTS_API_PORT` in `listener/.env`. + +### The listener starts but no events arrive + +Work through these in order: + +1. **Is the contract address correct?** Check it in `listener/.env` against the deployed contract. +2. **Is the RPC endpoint reachable?** A wrong or unreachable URL usually surfaces as repeated poll errors in the logs. +3. **Are you on the right network?** A testnet contract address against a mainnet RPC returns nothing — no error, just silence. +4. **Have the events already been consumed?** Deduplication suppresses events seen within the window. Restarting clears the in-memory cache (see [ADR-0005](adr/0005-event-deduplication-strategy.md)). + +### An endpoint returns 503 + +A 503 means an optional subsystem isn't enabled, not that the server is broken. `Scheduler not enabled` on `/api/schedule*` is the common case — it's off by default. See [Section 11 of the API Error Reference](API_ERROR_REFERENCE.md#11-503--service-unavailable). + +### Everything returns 404 + +Check the path and method. `/api/events` and `/api/v1/events` both work; `/v1/events` does not. Scheduling is `POST /api/schedule`, not `GET`. + +--- + +## 6. Database and Migration Issues + +The listener uses SQLite, defaulting to `./data/notifications.db` (override with `DATABASE_PATH`). See [ADR-0003](adr/0003-sqlite-for-local-persistence.md). + +### `SQLITE_BUSY: database is locked` + +**Cause:** Two processes have the database file open — commonly a stray `npm run dev` from a previous session, or a DB browser you left connected. + +**Fix:** Find and stop the other process, or point your test run at a separate `DATABASE_PATH`. + +### Migration errors on startup + +Check migration status: + +```bash +cd listener +npm run check-migrations +``` + +Apply pending migrations: + +```bash +npm run migrate +``` + +### Schema out of sync after switching branches + +If a branch added a migration that another branch doesn't have, the schema can end up in a state neither expects. Locally, the fastest fix is a clean database: + +```bash +rm -f data/notifications.db +npm run migrate +``` + +> This **deletes all local data**. Only do it in development, never against anything you need to keep. + +### `SQLITE_CANNOT_OPEN` + +The parent directory doesn't exist. Create it: + +```bash +mkdir -p data +``` + +--- + +## 7. Dashboard Issues + +### Dashboard loads but shows no data + +The dashboard reads from the listener API. Confirm, in order: + +1. **The listener is running** — `curl http://localhost:8787/health`. +2. **The dashboard points at the right URL** — check `dashboard/.env` against the listener's actual port. +3. **No CORS errors in the browser console** — the listener sets CORS headers; a mismatch usually means the wrong origin or port. + +### Vite dev server won't start + +Port already taken — stop the other process or start Vite on another port: + +```bash +cd dashboard +npm run dev -- --port 5174 +``` + +### Wallet (Freighter) not detected + +Wallet-specific problems are covered in the **Freighter Troubleshooting** section of [`README.md`](../README.md#freighter-troubleshooting). + +--- + +## 8. Smart Contract (Rust) Issues + +### `error[E0463]: can't find crate for 'core'` when building + +The WebAssembly target isn't installed: + +```bash +rustup target add wasm32-unknown-unknown +``` + +### `cargo test` fails after pulling contract changes + +Clear stale build artifacts: + +```bash +cd contract/contracts/hello-world +cargo clean +cargo test +``` + +### `stellar: command not found` + +The Stellar CLI isn't installed or isn't on `PATH`: + +```bash +cargo install --locked stellar-cli --features opt +``` + +If it installs but isn't found, ensure `~/.cargo/bin` is on your `PATH`. + +### Contract builds locally but the WASM is rejected + +Build with the release profile and the correct target — a debug build produces a much larger artifact that may exceed limits. Check the `Makefile` in `contract/contracts/hello-world/` for the canonical build command. + +--- + +## 9. Git and Pull Request Issues + +Full workflow details are in the [Git Workflow Guide](GIT_WORKFLOW.md). The failures that come up most often: + +### Your PR shows commits you didn't write + +**Cause:** You branched off another feature branch instead of a synced `main`. + +**Fix:** Re-create the branch from an up-to-date `main` and move only your commits across. See [Git Workflow §10](GIT_WORKFLOW.md#10-after-your-pr-merges). + +### You committed to `main` by accident + +```bash +git branch feature/my-work +git reset --hard upstream/main +git checkout feature/my-work +``` + +> `--hard` discards uncommitted work. Run `git status` first. + +### `Updates were rejected because the remote contains work you do not have` + +Someone (or you, elsewhere) pushed to that branch. Integrate before pushing: + +```bash +git pull --rebase origin +git push +``` + +Don't force-push a branch that's under review unless a reviewer asks — it makes incremental re-review much harder. + +### Permission denied when pushing + +You're pushing to `upstream` instead of `origin`. Contributors push to their fork only: + +```bash +git remote -v # confirm origin is YOUR fork +git push origin +``` + +### Your PR has conflicts with `main` + +```bash +git checkout main +git fetch upstream && git merge upstream/main +git checkout +git merge main +# resolve, then: +git add . && git commit && git push +``` + +Resolve by understanding both sides — if upstream changed a signature you also touched, your code needs to adapt to the new one. + +--- + +## 10. CI Failures + +### Reproduce CI locally before pushing again + +CI runs lint, typecheck/build, and tests per component. Run the same gates: + +```bash +cd listener && npm run lint && npm test +cd ../dashboard && npm run lint && npm run build && npm test +``` + +Run the checks for **every component you touched**. A green listener says nothing about the dashboard. + +### Passes locally, fails in CI + +The usual causes, in order: + +1. **Node version** — CI uses Node 22. +2. **`npm install` vs `npm ci`** — CI uses `npm ci`, which installs strictly from the lockfile. If your lockfile is stale, CI sees different dependencies than you do. +3. **Uncommitted files** — a file that exists locally but was never `git add`ed. Check `git status`. +4. **Case-sensitive imports** — macOS is case-insensitive, CI's Linux is not. `import './Foo'` resolves locally and fails in CI when the file is `foo.ts`. +5. **Test ordering or timing** — see [Section 4](#4-test-failures). + +### CI didn't run on your PR + +The CI workflow is path-filtered on pull requests — it triggers on changes under `listener/src/migrations/`, `listener/src/database/`, `listener/src/scripts/`, and `listener/package.json`. A PR touching only documentation legitimately runs no jobs. That's expected, not a failure. + +--- + +## 11. Still Stuck? + +Before opening an issue, gather: + +1. **What you ran** — the exact command and directory. +2. **What happened** — the full error output, not a paraphrase. +3. **Environment** — `node --version`, `npm --version`, and OS. +4. **Branch state** — `git status` and `git log --oneline -3`. +5. **What you already tried.** + +Then: + +- **Search existing issues first** — [Issue tracker](https://github.com/Core-Foundry/Notify-Chain/issues). Most contributor-facing problems have been hit before. +- **Comment on the issue you're working on** if it's specific to that work. +- **Open a new issue** if the problem is reproducible and undocumented — and consider a PR adding it to this guide. + +If a fix here is wrong or out of date, that's a bug in the docs. Please fix it. diff --git a/docs/GIT_WORKFLOW.md b/docs/GIT_WORKFLOW.md new file mode 100644 index 00000000..4bb0383d --- /dev/null +++ b/docs/GIT_WORKFLOW.md @@ -0,0 +1,378 @@ +# Git Workflow Guide + +The branching strategy, commit conventions, and pull request process for NotifyChain contributors. + +NotifyChain uses a **fork-and-pull-request** model. Nobody pushes directly to `main` on the upstream repository — all changes arrive via reviewed pull requests from forks. + +**Related:** [`CONTRIBUTING.md`](../CONTRIBUTING.md) · [`CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md`](../CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md) · [Contributor Troubleshooting](CONTRIBUTOR_TROUBLESHOOTING.md) + +--- + +## Table of Contents + +1. [Repository Model](#1-repository-model) +2. [One-Time Setup](#2-one-time-setup) +3. [Keeping Your Fork in Sync](#3-keeping-your-fork-in-sync) +4. [Branch Naming](#4-branch-naming) +5. [Commit Messages](#5-commit-messages) +6. [The Day-to-Day Loop](#6-the-day-to-day-loop) +7. [Opening a Pull Request](#7-opening-a-pull-request) +8. [Review and Merge](#8-review-and-merge) +9. [Handling Conflicts](#9-handling-conflicts) +10. [After Your PR Merges](#10-after-your-pr-merges) +11. [Command Cheat Sheet](#11-command-cheat-sheet) + +--- + +## 1. Repository Model + +``` + Core-Foundry/Notify-Chain ← upstream (read-only for contributors) + │ + │ fork + ▼ + your-username/Notify-Chain ← origin (you push here) + │ + │ clone + ▼ + local working copy ← you work here +``` + +Two remotes, two distinct roles: + +| Remote | Points at | You do | +|--------|-----------|--------| +| `origin` | your fork | push branches, never push to `main` | +| `upstream` | `Core-Foundry/Notify-Chain` | fetch only, never push | + +**`main` is a mirror, not a workspace.** Your local `main` exists solely to track upstream. Never commit to it directly — if you do, syncing becomes a conflict-resolution exercise every time. + +--- + +## 2. One-Time Setup + +Fork the repository on GitHub, then: + +```bash +git clone https://github.com/your-username/Notify-Chain.git +cd Notify-Chain +git remote add upstream https://github.com/Core-Foundry/Notify-Chain.git +``` + +Verify — you should see exactly four lines: + +```bash +git remote -v +``` + +``` +origin https://github.com/your-username/Notify-Chain.git (fetch) +origin https://github.com/your-username/Notify-Chain.git (push) +upstream https://github.com/Core-Foundry/Notify-Chain.git (fetch) +upstream https://github.com/Core-Foundry/Notify-Chain.git (push) +``` + +If `origin` points at `Core-Foundry`, you cloned the upstream repo rather than your fork. Fix it without re-cloning: + +```bash +git remote set-url origin https://github.com/your-username/Notify-Chain.git +``` + +Optionally protect yourself from accidental upstream pushes: + +```bash +git remote set-url --push upstream DISABLED +``` + +--- + +## 3. Keeping Your Fork in Sync + +Do this **before starting every new branch**. Most merge conflicts trace back to branching off a stale `main`. + +```bash +git checkout main +git fetch upstream +git merge upstream/main +git push origin main +``` + +If `git merge upstream/main` reports anything other than a fast-forward, you have commits on local `main` that upstream doesn't. See [Section 9](#9-handling-conflicts). + +--- + +## 4. Branch Naming + +Format: `/` + +| Prefix | Use for | +|--------|---------| +| `feature/` | New features | +| `fix/` | Bug fixes | +| `docs/` | Documentation changes | +| `refactor/` | Code restructuring with no behaviour change | +| `test/` | Adding or modifying tests | +| `chore/` | Maintenance, dependencies, tooling | + +### Examples + +```bash +feature/add-slack-notifications +feature/webhook-retry-queue +fix/resolve-event-deduplication-bug +fix/scheduler-timezone-offset +docs/update-contributing-guide +docs/api-error-reference +refactor/extract-notification-dispatcher +test/add-rate-limiter-coverage +chore/bump-stellar-sdk +``` + +### Guidelines + +- Lowercase, hyphen-separated. No spaces, no underscores, no camelCase. +- Describe the change, not the ticket: `fix/resolve-event-deduplication-bug`, not `fix/issue-488`. +- Aim for three to five words — long enough to be meaningful in a branch list, short enough to type. +- One branch per issue. If you find an unrelated bug mid-branch, open a separate branch for it. + +### Avoid + +| Don't | Why | +|-------|-----| +| `patch-1` | GitHub's web-editor default; says nothing | +| `my-changes` | Meaningless to a reviewer | +| `fix` | No description, and collides immediately | +| `Feature/Add-Thing` | Inconsistent casing | +| `fix/issue-488` | Identifies the ticket, not the change | + +--- + +## 5. Commit Messages + +NotifyChain follows [Conventional Commits](https://www.conventionalcommits.org/). + +``` +: +``` + +| Type | Use for | +|------|---------| +| `feat:` | New feature | +| `fix:` | Bug fix | +| `docs:` | Documentation | +| `test:` | Test additions or changes | +| `refactor:` | Restructuring without behaviour change | +| `chore:` | Maintenance tasks | + +### Examples + +```bash +git commit -m "feat: add retry queue for failed notifications" +git commit -m "fix: resolve event parsing issue in listener" +git commit -m "docs: update README with setup instructions" +git commit -m "test: cover rate limiter window expiry" +git commit -m "refactor: extract fingerprint generation into helper" +``` + +### Guidelines + +- **Imperative mood** — "add retry queue", not "added" or "adds". Read it as *"this commit will… add retry queue"*. +- **No trailing period**, lowercase after the type prefix. +- **Around 72 characters** for the summary line. +- **Explain *why* in the body** when the change isn't self-evident. The diff shows what changed; the body should say what it fixes and why this approach. + +```bash +git commit -m "fix: prevent duplicate Discord sends after listener restart" -m " +The dedup cache is in-memory, so a restart cleared it and events in +flight across the boundary were re-delivered. Seed the cache from the +last processed ledger on startup. + +Closes #123 +" +``` + +- **Reference the issue** with `Closes #123` / `Fixes #123` in the body or the PR description — either will auto-close the issue on merge. +- **Commit in logical units.** One commit per coherent change beats one giant commit, and also beats fifteen "wip" commits. + +--- + +## 6. The Day-to-Day Loop + +**Claim the issue first.** Comment `I would like to work on this issue.` and wait for a maintainer to assign it. Do not open a PR for an unassigned issue — see the issue-claiming process in [`CONTRIBUTING.md`](../CONTRIBUTING.md). + +```bash +# 1. Sync main +git checkout main +git fetch upstream +git merge upstream/main + +# 2. Branch +git checkout -b feature/add-slack-notifications + +# 3. Work, then stage and review before committing +git add listener/src/services/slack-sender.ts +git diff --staged + +# 4. Commit +git commit -m "feat: add Slack notification sender" + +# 5. Verify locally — the same gates CI runs +cd listener && npm run lint && npm test && cd .. + +# 6. Push +git push -u origin feature/add-slack-notifications +``` + +`-u` is only needed on the first push; afterwards `git push` suffices. + +> Run the checks for **every component you touched**. CI runs lint, typecheck/build, and tests for `dashboard/`, `listener/`, and the Rust contracts independently — a green listener does not tell you the dashboard still compiles. + +--- + +## 7. Opening a Pull Request + +Push, then open a PR from your branch to `Core-Foundry/Notify-Chain:main`. + +### Title + +Same format as commit messages: + +``` +feat: add Slack notification sender +fix: standardize error messages across contracts +docs: add API error reference +``` + +### Description + +Include: + +1. **Overview** — what the PR does and why. +2. **Related issue** — `Closes #123`. Use one `Closes` line per issue if the PR covers several. +3. **Changes** — what was added, removed, or modified. +4. **Verification** — which tests you ran and their results. +5. **How to test** — steps a reviewer can follow. + +### Checklist + +- [ ] Code follows the project's style guidelines +- [ ] Tests added or updated, and passing +- [ ] Documentation updated +- [ ] All tests pass locally +- [ ] Branch is up to date with `main` + +### Scope + +Keep a PR to a single issue or feature. A PR that fixes a bug, renames a module, and bumps a dependency is three PRs — reviewers cannot evaluate them independently, and a problem in one blocks the other two. + +Open a **draft PR** early if you want feedback on direction before the work is finished. + +--- + +## 8. Review and Merge + +**Responding to feedback:** push follow-up commits to the same branch — the PR updates automatically. Don't force-push mid-review unless asked; it makes incremental re-review harder. + +```bash +git add . +git commit -m "fix: address review feedback on error handling" +git push +``` + +Reply to each review comment, and resolve threads once addressed. If you disagree with a suggestion, say so with your reasoning — review is a conversation, not a checklist. + +**CI must be green before merge.** Fix failures on your branch and push; do not merge around a red build. + +Maintainers merge. Don't merge your own PR unless you are one. + +--- + +## 9. Handling Conflicts + +### Your branch conflicts with `main` + +Sync `main`, then merge it into your branch: + +```bash +git checkout main +git fetch upstream +git merge upstream/main + +git checkout feature/your-branch +git merge main +# resolve conflicts in your editor, then: +git add +git commit +git push +``` + +Resolve conflicts by understanding both sides — don't blindly take yours. If upstream changed a function you also modified, your change may need to adapt to the new signature. + +### You accidentally committed to `main` + +Move the commits onto a branch: + +```bash +git branch feature/my-work # save current main (with your commits) +git reset --hard upstream/main # restore main to upstream state +git checkout feature/my-work # continue on the branch +``` + +> `git reset --hard` discards uncommitted changes. Run `git status` first and commit or stash anything you want to keep. + +### You need to undo the last commit + +```bash +git reset --soft HEAD~1 # undo the commit, keep changes staged +git reset HEAD~1 # undo the commit, keep changes unstaged +``` + +Avoid rewriting history that is already pushed and under review. + +--- + +## 10. After Your PR Merges + +```bash +git checkout main +git fetch upstream +git merge upstream/main +git push origin main + +git branch -d feature/add-slack-notifications # delete locally +git push origin --delete feature/add-slack-notifications # delete on your fork +``` + +Start each new issue from a freshly synced `main` — never from a previous feature branch, or your PR will contain the other branch's commits. + +--- + +## 11. Command Cheat Sheet + +| Task | Command | +|------|---------| +| Add upstream remote | `git remote add upstream https://github.com/Core-Foundry/Notify-Chain.git` | +| Check remotes | `git remote -v` | +| Sync fork | `git checkout main && git fetch upstream && git merge upstream/main && git push origin main` | +| New branch | `git checkout -b feature/my-feature` | +| Check status | `git status` | +| Review staged changes | `git diff --staged` | +| Commit | `git commit -m "feat: description"` | +| First push | `git push -u origin feature/my-feature` | +| Subsequent pushes | `git push` | +| Update branch from main | `git checkout feature/my-feature && git merge main` | +| List branches | `git branch -a` | +| Switch branch | `git checkout branch-name` | +| Delete local branch | `git branch -d branch-name` | +| Delete remote branch | `git push origin --delete branch-name` | +| Undo last commit, keep changes | `git reset --soft HEAD~1` | +| Stash work in progress | `git stash` / `git stash pop` | +| Compact history | `git log --oneline --graph --decorate -10` | + +--- + +## Questions? + +- Workflow question → comment on the issue you're working on +- Something broken → [Contributor Troubleshooting](CONTRIBUTOR_TROUBLESHOOTING.md) +- Setup problem → [`TROUBLESHOOTING.md`](../TROUBLESHOOTING.md) diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md new file mode 100644 index 00000000..9dee8b19 --- /dev/null +++ b/docs/adr/0000-template.md @@ -0,0 +1,98 @@ +# ADR-0000: Template + +**Date:** YYYY-MM-DD +**Status:** Proposed | Accepted | Superseded | Deprecated | Rejected +**Deciders:** (list the people or teams involved in the decision) +**Supersedes:** (ADR number and link, if this replaces an earlier decision) +**Superseded by:** (ADR number and link, if a later ADR replaces this one) + +--- + +## Context + +Describe the situation or problem that led to this decision. Include any technical constraints, business requirements, or forces at play. Be specific enough that someone unfamiliar with the codebase can understand the problem without reading other documents. + +--- + +## Decision Drivers + +- Driver 1 (e.g., performance requirement, operational simplicity) +- Driver 2 +- Driver 3 + +--- + +## Options Considered + +### Option A — (short name) + +Brief description of the option. + +**Pros:** +- Pro 1 +- Pro 2 + +**Cons:** +- Con 1 +- Con 2 + +--- + +### Option B — (short name) + +Brief description of the option. + +**Pros:** +- Pro 1 + +**Cons:** +- Con 1 + +--- + +### Option C — (short name, if applicable) + +Brief description of the option. + +**Pros:** +- Pro 1 + +**Cons:** +- Con 1 + +--- + +## Decision + +State the chosen option clearly: + +> We will use **Option X** because … + +Explain the primary reason(s) for the choice, referencing the decision drivers above. + +--- + +## Consequences + +### Positive + +- What becomes easier or better as a result. +- What risks are mitigated. + +### Negative / Trade-offs + +- What becomes harder, more complex, or constrained. +- Known limitations or future debt introduced. + +### Neutral / Notes + +- Follow-up work required. +- Decisions this record leaves open. + +--- + +## Links + +- Related issue or PR: # +- Related documentation: (link) +- Prior art / reference: (link) diff --git a/docs/adr/0001-off-chain-listener-architecture.md b/docs/adr/0001-off-chain-listener-architecture.md new file mode 100644 index 00000000..92ba4267 --- /dev/null +++ b/docs/adr/0001-off-chain-listener-architecture.md @@ -0,0 +1,112 @@ +# ADR-0001: Off-Chain Listener Architecture + +**Date:** 2024-01-15 +**Status:** Accepted +**Deciders:** Core-Foundry maintainers + +--- + +## Context + +Soroban smart contracts on Stellar emit events when their state changes (e.g., a task is created, a payment is made). Applications and users need to react to these events in near real-time — for example, by sending Discord notifications, updating a dashboard, or triggering webhooks. + +The question is: how should event consumption and notification delivery be structured? + +Options range from purely on-chain logic (cost-prohibitive for notification delivery) to pure off-chain polling, to event-driven architectures using dedicated relay infrastructure. + +--- + +## Decision Drivers + +- Soroban contracts cannot make outbound HTTP calls — external notifications must be off-chain. +- Polling the Stellar RPC from every client independently would be wasteful and error-prone. +- The system must support multiple downstream consumers (Discord, webhooks, dashboard) from a single event stream. +- The solution should be runnable locally by contributors with minimal infrastructure. +- Event deduplication must be handled somewhere to prevent duplicate notifications. + +--- + +## Options Considered + +### Option A — Each consumer polls the RPC directly + +Every notification channel (Discord, dashboard, webhooks) independently polls the Stellar RPC endpoint for contract events. + +**Pros:** +- No shared service to maintain. +- Each consumer is fully autonomous. + +**Cons:** +- Duplicate RPC calls; each consumer re-fetches the same raw events. +- Deduplication must be reimplemented in every consumer. +- No central event log for debugging or replay. +- Harder to add new consumers without duplicating polling logic. + +--- + +### Option B — Centralised off-chain listener service (chosen) + +A single Node.js listener service polls the Stellar RPC, deduplicates events, stores them in an in-memory registry, and exposes them via an HTTP API. Notification channels (Discord, webhooks) and the dashboard all consume from this single service. + +**Pros:** +- Single source of truth for event state. +- Deduplication logic lives in one place. +- Adding a new consumer (e.g., Slack, email) requires no RPC polling changes. +- The HTTP events API allows the dashboard and external tools to query history. +- Contributors can run everything locally with one service. + +**Cons:** +- Single point of failure if the listener service goes down. +- In-memory storage means events are lost on restart (addressed by optional SQLite persistence). + +--- + +### Option C — Managed event streaming platform (e.g., Kafka, SQS) + +Route Stellar events through a managed streaming platform. + +**Pros:** +- Battle-tested reliability and replay capabilities. +- Scales horizontally. + +**Cons:** +- Significant operational overhead for an open-source project. +- Overkill for the current scale and contributor base. +- Creates a hard infrastructure dependency that blocks local development. + +--- + +## Decision + +> We will use **Option B** — a centralised off-chain listener service. + +The listener service is the simplest design that satisfies all current requirements: it eliminates duplicate polling, centralises deduplication, and provides a stable API for all consumers. The single-point-of-failure risk is acceptable at current scale and can be addressed incrementally with persistence (SQLite) and health-check monitoring. + +--- + +## Consequences + +### Positive + +- All event consumers share one polling connection to the Stellar RPC. +- The `/api/events` HTTP endpoint gives the dashboard and any future consumer a clean interface. +- Deduplication is implemented once in `NotificationDeduplicator`. +- The listener can be run standalone, making it easy to test and contribute to in isolation. + +### Negative / Trade-offs + +- Events held in memory are lost on listener restart unless SQLite persistence is enabled. +- A second deployment (listener + dashboard) is required for the full system. + +### Neutral / Notes + +- The listener's persistence layer (`listener/src/database/`) uses SQLite when `DATABASE_PATH` is configured, providing durability without external dependencies. +- Future work may add a replay endpoint to re-emit stored events to new consumers. + +--- + +## Links + +- Architecture diagram: [`SYSTEM_ARCHITECTURE.md`](../../SYSTEM_ARCHITECTURE.md) +- Listener source: [`listener/src/services/event-subscriber.ts`](../../listener/src/services/event-subscriber.ts) +- Deduplicator: [`listener/src/services/notification-deduplicator.ts`](../../listener/src/services/notification-deduplicator.ts) diff --git a/docs/adr/0002-soroban-smart-contracts.md b/docs/adr/0002-soroban-smart-contracts.md new file mode 100644 index 00000000..de257552 --- /dev/null +++ b/docs/adr/0002-soroban-smart-contracts.md @@ -0,0 +1,110 @@ +# ADR-0002: Soroban Smart Contracts on Stellar + +**Date:** 2024-01-15 +**Status:** Accepted +**Deciders:** Core-Foundry maintainers + +--- + +## Context + +NotifyChain needs an on-chain layer to serve as the authoritative source of truth for events and state. The choice of smart contract platform determines the programming language, tooling, event model, and the developer experience for all contributors working on contracts. + +--- + +## Decision Drivers + +- The on-chain layer must emit structured, queryable events that the listener service can consume. +- Contracts must be auditable and trustless — logic should be transparent and verifiable. +- The platform should have a growing ecosystem and active maintenance. +- Rust is a strong preference among core contributors for safety and performance. +- The contract platform should support a rich event schema (not just simple logs). + +--- + +## Options Considered + +### Option A — Ethereum / EVM-compatible chain (Solidity) + +Deploy contracts on Ethereum, Polygon, or a compatible L2 using Solidity. + +**Pros:** +- Largest existing developer ecosystem and tooling (Hardhat, Foundry, etc.). +- Extensive documentation and community support. + +**Cons:** +- Gas costs on Ethereum mainnet are prohibitive for frequent small transactions. +- Solidity lacks the memory safety guarantees of Rust. +- EVM chains were not the platform vision for this project. + +--- + +### Option B — Soroban on Stellar (chosen) + +Write contracts in Rust compiled to WebAssembly, deployed on Stellar's Soroban smart contract platform. + +**Pros:** +- Rust provides strong type safety and memory safety at compile time. +- Soroban events are structured and queryable via the Stellar RPC — ideal for the listener service. +- Stellar's transaction fees are significantly lower than Ethereum. +- The Stellar SDK supports event subscription natively. +- Aligns with Stellar's growing focus on DeFi and dApp development. + +**Cons:** +- Smaller ecosystem than EVM chains. +- Soroban is newer; some tooling is still maturing. +- Contributors unfamiliar with Rust face a steeper onboarding curve. + +--- + +### Option C — NEAR Protocol (Rust) + +Write contracts in Rust for the NEAR Protocol. + +**Pros:** +- Also uses Rust, similar safety guarantees. +- Good developer tooling. + +**Cons:** +- Not aligned with the project's Stellar-centric vision. +- Different event model from Soroban. + +--- + +## Decision + +> We will use **Option B** — Soroban smart contracts on Stellar, written in Rust. + +Soroban's structured event model is a natural fit for the listener's event-polling architecture. Rust's safety guarantees reduce the risk of contract bugs. The lower transaction fees on Stellar make the system more accessible for frequent interactions (task creation, payments, submissions). + +--- + +## Consequences + +### Positive + +- Contracts are written in Rust, benefiting from compile-time correctness checks. +- Soroban events map cleanly to the listener's event schema. +- The `stellar-cli` toolchain enables reproducible local builds and testnet deployments. +- Contributors learn Rust and Soroban, which have strong demand in the blockchain ecosystem. + +### Negative / Trade-offs + +- Contributors must install Rust and the `wasm32-unknown-unknown` target to work on contracts. +- The Soroban ecosystem is less mature than EVM; some patterns require custom implementation. +- Cross-contract calls have limitations compared to EVM chains. + +### Neutral / Notes + +- Both the AutoShare contract (`contract/contracts/hello-world/`) and the TaskBounty contract (`Documents/Task Bounty/`) use this approach. +- The `stellar contract build` command handles the WebAssembly compilation target automatically. + +--- + +## Links + +- Local development guide: [`LOCAL_DEVELOPMENT.md`](../../LOCAL_DEVELOPMENT.md) +- AutoShare contract: [`contract/contracts/hello-world/src/`](../../contract/contracts/hello-world/src/) +- TaskBounty contract: [`Documents/Task Bounty/src/`](../../Documents/Task%20Bounty/src/) +- Contract event reference: [`CONTRACT_EVENT_REFERENCE.md`](../../CONTRACT_EVENT_REFERENCE.md) +- Stellar Soroban docs: https://developers.stellar.org/docs/build/smart-contracts diff --git a/docs/adr/0003-sqlite-for-local-persistence.md b/docs/adr/0003-sqlite-for-local-persistence.md new file mode 100644 index 00000000..6e104d5e --- /dev/null +++ b/docs/adr/0003-sqlite-for-local-persistence.md @@ -0,0 +1,128 @@ +# ADR-0003: SQLite for Local Notification Persistence + +**Date:** 2024-03-10 +**Status:** Accepted +**Deciders:** Core-Foundry maintainers + +--- + +## Context + +The listener service initially held all processed events and scheduled notifications in memory. While this is simple, a restart wipes all state. Contributors and operators running the service long-term need durability: scheduled notifications that survive restarts, a queryable event history, and an audit trail for delivery attempts. + +The persistence layer must work without external infrastructure — no managed databases, no Docker Compose requirement — so any contributor can run the full stack with `npm run dev`. + +--- + +## Decision Drivers + +- Scheduled notifications must survive listener restarts. +- Delivery history and retry state must be queryable. +- Zero external infrastructure dependencies for local development. +- The solution must be embeddable in the Node.js process. +- Migration management must be simple enough for contributors to run with a single command. + +--- + +## Options Considered + +### Option A — In-memory store only + +Keep all state in JavaScript objects / Maps with no persistence. + +**Pros:** +- Zero setup, zero dependencies. +- Simplest possible implementation. + +**Cons:** +- All scheduled notifications lost on restart. +- No queryable history for debugging. +- Not viable for any production-like deployment. + +--- + +### Option B — SQLite via `better-sqlite3` / `sqlite3` (chosen) + +Embed a SQLite database file in the listener's working directory, managed via simple migration scripts. + +**Pros:** +- No external process or daemon required — the database is a single file. +- Full SQL query support for history, search, and analytics queries. +- Works identically in local dev, CI, and self-hosted deployments. +- Migration state is tracked in a `migrations` table, making schema evolution predictable. +- Well-supported in the Node.js ecosystem. + +**Cons:** +- Not horizontally scalable (single writer). +- Performance degrades with very large datasets (mitigated by archiving). +- Requires native bindings (`npm rebuild sqlite3` if Node.js version changes). + +--- + +### Option C — PostgreSQL + +Use a managed relational database. + +**Pros:** +- Horizontally scalable. +- Full ACID guarantees. +- Rich tooling and monitoring. + +**Cons:** +- Requires a running PostgreSQL instance — breaks the "single `npm run dev`" contributor experience. +- Significantly more complex setup for local development and CI. +- Overkill for the current scale. + +--- + +### Option D — Redis + +Use Redis for event queues and notification state. + +**Pros:** +- Fast in-memory operations with optional persistence. +- Natural fit for queues and pub/sub. + +**Cons:** +- Another external process required. +- Less natural for relational queries (history, search, audit). +- Adds operational complexity. + +--- + +## Decision + +> We will use **Option B** — SQLite, configured via the `DATABASE_PATH` environment variable. + +SQLite satisfies every requirement: zero external dependencies, full SQL querying, file-based persistence, and contributor-friendly setup. The single-writer limitation is not a concern at current scale. If the project outgrows SQLite, this ADR will be superseded by a decision to migrate to PostgreSQL or a managed database. + +--- + +## Consequences + +### Positive + +- Scheduled notifications survive listener restarts. +- Contributors can inspect the database file directly with any SQLite client. +- History, retry state, and audit logs are queryable with standard SQL. +- CI runs with an in-memory or temp-file database without additional setup. + +### Negative / Trade-offs + +- Native module rebuild required when switching Node.js versions. +- Not suitable for multi-process deployments without a connection proxy. +- Database file must be excluded from version control (already in `.gitignore`). + +### Neutral / Notes + +- `DATABASE_PATH` defaults to `./data/notifications.db`. +- Migrations live in `listener/src/migrations/` and are applied with `npm run migrate`. +- The archiving service (`listener/src/services/`) periodically moves old records to reduce database size. + +--- + +## Links + +- Environment variable reference: [`ENVIRONMENT_VARIABLES_AND_SECRETS.md`](../../ENVIRONMENT_VARIABLES_AND_SECRETS.md) +- Troubleshooting database errors: [`TROUBLESHOOTING.md`](../../TROUBLESHOOTING.md#listener-service-nodejs) +- Migrations directory: [`listener/src/migrations/`](../../listener/src/migrations/) diff --git a/docs/adr/0004-typescript-for-listener-service.md b/docs/adr/0004-typescript-for-listener-service.md new file mode 100644 index 00000000..1b68099f --- /dev/null +++ b/docs/adr/0004-typescript-for-listener-service.md @@ -0,0 +1,118 @@ +# ADR-0004: TypeScript for Listener Service + +**Date:** 2024-02-08 +**Status:** Accepted +**Deciders:** Core-Foundry maintainers + +--- + +## Context + +The off-chain listener (see [ADR-0001](0001-off-chain-listener-architecture.md)) is the largest non-contract component in the project. It polls the Stellar RPC, normalises Soroban event payloads, deduplicates them, persists them, and exposes an HTTP API consumed by the dashboard and external integrators. + +Soroban event payloads arrive as loosely-typed `ScVal` structures. They are converted into domain objects (`DisplayEvent`, `ScheduledNotification`, `NotificationTemplate`) that flow through many layers — subscriber, deduplicator, repository, HTTP handler — before reaching a consumer. A field renamed in one layer and missed in another produces a silent runtime failure that only shows up as a malformed notification in production. + +The language choice for this service determines how much of that class of bug is caught before merge, and how approachable the codebase is to contributors. + +--- + +## Decision Drivers + +- Event payload shapes are the core domain model and change often as contracts evolve; renames must be caught mechanically, not by review. +- The dashboard is already a TypeScript React app — sharing type definitions across the boundary avoids drift. +- The Stellar SDK (`@stellar/stellar-sdk`) ships first-class TypeScript definitions. +- Contributors are frequently new to the project; editor autocomplete over domain types materially lowers the ramp-up cost. +- The project must stay runnable with `npm install && npm run dev` — no additional toolchain beyond Node. + +--- + +## Options Considered + +### Option A — Plain JavaScript (Node + JSDoc) + +Write the listener in JavaScript, optionally annotating types via JSDoc comments. + +**Pros:** +- No build step; `node src/index.js` runs directly. +- Lowest barrier for contributors unfamiliar with typed languages. +- No compiler configuration to maintain. + +**Cons:** +- `ScVal` conversion errors surface only at runtime, typically as a malformed notification already delivered to a user. +- JSDoc annotations are advisory — nothing fails when they drift from reality. +- No shared type contract with the TypeScript dashboard; the HTTP response shape must be kept in sync by hand. +- Refactoring across the subscriber → store → API chain becomes grep-driven and error-prone. + +--- + +### Option B — TypeScript (chosen) + +Write the listener in TypeScript, compiled with `tsc`, with domain types centralised under `listener/src/types/`. + +**Pros:** +- Event and notification shapes are enforced at compile time across every layer. +- `npm run typecheck` (`tsc --noEmit`) acts as a fast, dependency-free lint gate in CI. +- Domain types can be shared conceptually with the dashboard, which is already TypeScript. +- The Stellar SDK's bundled types make RPC and `ScVal` handling self-documenting. +- Editor autocomplete over `DisplayEvent`, `ScheduledNotification`, etc. speeds up onboarding. + +**Cons:** +- Adds a compile step (`npm run build`) between source and `dist/`. +- `ts-node` in development is slower to start than plain `node`. +- Contributors unfamiliar with TypeScript face an initial learning curve. +- Type definitions for the loosely-typed `ScVal` boundary require deliberate care; `any` at that seam can give false confidence. + +--- + +### Option C — Rust for the listener as well + +Reuse the contract language for the off-chain service, giving the project a single language. + +**Pros:** +- One language across contracts and services. +- Strongest compile-time guarantees and runtime performance. + +**Cons:** +- Shrinks the contributor pool sharply — most web contributors can write TypeScript but not Rust. +- Notification-provider ecosystem (Discord, webhooks, templating) is far richer in the Node ecosystem. +- Rebuild-per-change cycle is significantly slower for an I/O-bound service where performance is not the constraint. +- The listener is I/O-bound on RPC polling and HTTP delivery; Rust's performance advantage is largely irrelevant here. + +--- + +## Decision + +> We will use **Option B** — TypeScript for the listener service. + +The listener's dominant risk is *shape drift* in event payloads as contracts evolve, not throughput. TypeScript targets exactly that risk while keeping the service approachable to the web contributors who make up most of the project. Option C's guarantees are real but purchased at a contributor-pool cost the project cannot absorb, and the performance it buys does not apply to an I/O-bound service. Option A's simplicity is not worth shipping payload-shape bugs to users. + +--- + +## Consequences + +### Positive + +- Domain types under `listener/src/types/` are a single source of truth for event and notification shapes. +- `npm run typecheck` catches cross-layer breakage before tests run, with no extra dependency. +- The dashboard and listener speak the same conceptual types across the HTTP boundary. +- New contributors get autocomplete-driven discovery of the domain model. + +### Negative / Trade-offs + +- A build step (`npm run build` → `dist/`) is required before `npm start`. +- Development startup via `ts-node` is measurably slower than plain Node. +- The `ScVal` → domain-object boundary still needs runtime validation; types alone do not make untrusted RPC input safe. + +### Neutral / Notes + +- `npm run lint` is currently aliased to `tsc --noEmit`; adding ESLint with typed rules remains open follow-up work. +- Contract code stays in Rust — this decision covers the listener service only (see [ADR-0002](0002-soroban-smart-contracts.md)). + +--- + +## Links + +- Listener entry point: [`listener/src/index.ts`](../../listener/src/index.ts) +- Domain types: [`listener/src/types/`](../../listener/src/types/) +- `ScVal` conversion: [`listener/src/utils/scval-format.ts`](../../listener/src/utils/scval-format.ts) +- Related: [ADR-0001](0001-off-chain-listener-architecture.md), [ADR-0002](0002-soroban-smart-contracts.md) diff --git a/docs/adr/0005-event-deduplication-strategy.md b/docs/adr/0005-event-deduplication-strategy.md new file mode 100644 index 00000000..574b5bd3 --- /dev/null +++ b/docs/adr/0005-event-deduplication-strategy.md @@ -0,0 +1,124 @@ +# ADR-0005: Event Deduplication Strategy + +**Date:** 2024-04-22 +**Status:** Accepted +**Deciders:** Core-Foundry maintainers + +--- + +## Context + +The listener polls the Stellar RPC for Soroban contract events on an interval. That polling loop delivers the same event more than once under several ordinary conditions: + +- **Overlapping ledger ranges** — a poll window that re-reads a ledger already processed. +- **Retries after a transient RPC or network failure** — the request succeeded server-side but the response was lost. +- **Listener restarts** — the service resumes from a checkpoint that predates events it already handled. +- **Chain reorganisations** — an event is re-observed at a different ledger. + +Every duplicate that reaches the delivery layer is a duplicate Discord message or webhook call to a real user. Notification delivery is not idempotent from the recipient's point of view, so the listener must suppress duplicates before dispatch. + +The open question is *what identity means* for an event, and *where* that check lives. + +--- + +## Decision Drivers + +- Duplicate suppression must happen once, centrally — reimplementing it per channel guarantees drift (a driver already established in [ADR-0001](0001-off-chain-listener-architecture.md)). +- The check sits in the hot path of every polled event; it must be O(1) and allocation-light. +- Memory must be bounded — an unbounded set of seen events is a slow leak in a long-running service. +- Duplicates arriving far apart (hours later, after a reorg) are rarer and less costly than the near-term duplicates that polling produces constantly. +- The strategy must be observable: operators need to know how often it fires and whether it is over- or under-suppressing. + +--- + +## Options Considered + +### Option A — Compare raw event IDs in a plain `Set` + +Store the raw `eventId` string for every event seen, and reject on membership. + +**Pros:** +- Trivial to implement and reason about. +- No hashing cost. + +**Cons:** +- Event IDs are not guaranteed unique *across* contracts; two contracts can collide on the same ID and the second event is silently dropped. +- Raw IDs are variable-length, making memory use per entry unpredictable. +- No natural expiry — the set grows without bound for the life of the process. + +--- + +### Option B — Bounded fingerprint cache with a time window (chosen) + +Hash `contractAddress:eventId` with SHA-256 into a fixed-width fingerprint, and keep fingerprints in a cache bounded by both a maximum entry count and a time window. Entries older than the window expire; when the cache is full, the oldest entry is evicted. + +**Pros:** +- Namespacing by contract address eliminates the cross-contract collision in Option A. +- SHA-256 gives collision resistance and a uniform, fixed-width key regardless of event ID length. +- The dual bound (max size *and* time window) caps memory deterministically. +- Lookup and insert stay O(1) in the polling hot path. +- Naturally exposes metrics — accepted, skipped, evicted, expired, hit ratio. + +**Cons:** +- Duplicates arriving after the window expires are not caught. +- Under sustained load the size cap can evict entries before their window elapses, opening a narrow re-delivery gap. +- In-memory only: the cache is empty after a restart, so events straddling a restart can re-deliver once. +- SHA-256 per event is a small but non-zero CPU cost. + +--- + +### Option C — Persist every processed event ID in SQLite and query per event + +Use the existing SQLite persistence layer ([ADR-0003](0003-sqlite-for-local-persistence.md)) as the deduplication index. + +**Pros:** +- Survives restarts — no re-delivery window after a crash. +- Unbounded history; duplicates are caught no matter how late they arrive. + +**Cons:** +- A synchronous disk read on every polled event, in the hot path. +- The index grows without bound and needs its own pruning policy — reintroducing the same expiry question one layer down. +- Couples deduplication to persistence being enabled, which is optional. + +--- + +## Decision + +> We will use **Option B** — a bounded SHA-256 fingerprint cache with a time window. + +The duplicates the listener actually produces are overwhelmingly *near-term*: overlapping poll windows and immediate retries, all landing within seconds of each other. A time-windowed cache catches essentially all of them at O(1) cost with a hard memory ceiling. Option C's durability guarantee addresses the rarer restart case but pays a disk read on every event in the hot path and defers rather than solves the growth problem. Option A's cross-contract collision risk is disqualifying: silently dropping a legitimate notification is worse than occasionally sending a duplicate. + +Defaults are a **10,000-entry** cache over a **60-second** window, both configurable via `NotificationDeduplicatorOptions`. + +--- + +## Consequences + +### Positive + +- Deduplication lives in one place — `NotificationDeduplicator` — and every channel inherits it. +- Memory is bounded by construction; the service can run indefinitely without a dedup-driven leak. +- Fingerprints are namespaced per contract, so multi-contract deployments cannot collide. +- Metrics (`acceptedRequests`, `skippedDuplicates`, `evictedEntries`, `expiredEntries`, `hitRatio`) let operators tune the window against observed duplicate rates. + +### Negative / Trade-offs + +- Duplicates separated by more than the window are delivered again; the window is a tuning knob, not a guarantee. +- A restart clears the cache, so events in flight across the restart may deliver twice. +- High event volume can force size-based eviction before the time window elapses, shortening the effective window. + +### Neutral / Notes + +- `generateExtendedFingerprint()` additionally folds in `eventType` and `ledgerNumber`, for callers that must distinguish the same event re-observed at a different ledger after a reorg. +- Consumers that need exactly-once semantics should layer idempotency keys on top — see `listener/src/services/idempotency-key-service.ts`. +- Persisting fingerprints to SQLite to close the restart gap remains open follow-up work; it would complement, not replace, this cache. + +--- + +## Links + +- Deduplicator: [`listener/src/services/notification-deduplicator.ts`](../../listener/src/services/notification-deduplicator.ts) +- Deduplication service: [`listener/src/services/event-deduplication-service.ts`](../../listener/src/services/event-deduplication-service.ts) +- Event subscriber: [`listener/src/services/event-subscriber.ts`](../../listener/src/services/event-subscriber.ts) +- Idempotency keys: [`listener/src/services/idempotency-key-service.ts`](../../listener/src/services/idempotency-key-service.ts) +- Related: [ADR-0001](0001-off-chain-listener-architecture.md), [ADR-0003](0003-sqlite-for-local-persistence.md) diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..ca97855f --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,37 @@ +# Architecture Decision Records (ADR) + +This directory contains Architecture Decision Records for NotifyChain. An ADR documents a significant architectural or technical decision, the context that led to it, the options considered, and the reasoning behind the chosen approach. + +## Why ADRs? + +ADRs give future contributors the *why* behind design choices, not just the *what*. When you read code and wonder "why was it built this way?", the relevant ADR should answer that question. + +## How to Use This Directory + +- **Reading an ADR**: Each record is self-contained. Start with the status and context, then read the decision and consequences. +- **Writing a new ADR**: Copy [`0000-template.md`](0000-template.md), increment the number, fill in all sections, and open a PR. +- **Superseding an ADR**: Mark the old ADR status as `Superseded by ADR-XXXX` and reference the new one. + +## ADR Lifecycle + +| Status | Meaning | +|--------|---------| +| `Proposed` | Under discussion — not yet accepted | +| `Accepted` | Agreed upon and actively guiding the project | +| `Superseded` | Replaced by a newer decision (link provided) | +| `Deprecated` | No longer relevant but kept for historical record | +| `Rejected` | Considered and explicitly declined | + +## Index + +| ADR | Title | Status | +|-----|-------|--------| +| [ADR-0001](0001-off-chain-listener-architecture.md) | Off-Chain Listener Architecture | Accepted | +| [ADR-0002](0002-soroban-smart-contracts.md) | Soroban Smart Contracts on Stellar | Accepted | +| [ADR-0003](0003-sqlite-for-local-persistence.md) | SQLite for Local Notification Persistence | Accepted | +| [ADR-0004](0004-typescript-for-listener-service.md) | TypeScript for Listener Service | Accepted | +| [ADR-0005](0005-event-deduplication-strategy.md) | Event Deduplication Strategy | Accepted | + +--- + +New ADRs should be numbered sequentially. When in doubt, open a GitHub Discussion or tag a maintainer before writing a full ADR.