diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f36cc00 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +**/.git +**/node_modules +**/dist +**/build +**/.next +**/coverage +**/*.log +**/*.db +**/*.db-journal +**/data +**/.env +**/.env.* +!**/.env.example +contract/target diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..449ed0a --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Root .env — values here are picked up by docker compose via ${VAR} substitution. +# Copy to .env and fill in your values: +# cp .env.example .env + +# ── Stellar connectivity ────────────────────────────────────────────────────── +STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443 +STELLAR_NETWORK=testnet +STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 + +# Replace with your deployed contract ID +CONTRACT_ADDRESSES=[{"address":"CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","events":["*"]}] + +# ── Dashboard ───────────────────────────────────────────────────────────────── +# Vite embeds this at BUILD time — change requires `docker compose up --build` +VITE_EVENTS_API_URL=http://localhost:8787/api/events +VITE_STELLAR_NETWORK=TESTNET + +# ── Optional: Discord notifications ────────────────────────────────────────── +# DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN +# DISCORD_WEBHOOK_ID=YOUR_ID + +# ── Logging ─────────────────────────────────────────────────────────────────── +LOG_LEVEL=info diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..929dc28 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,85 @@ +version: 2 + +updates: + # Dashboard (frontend) npm dependencies + - package-ecosystem: "npm" + directory: "/dashboard" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "UTC" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "frontend" + commit-message: + prefix: "chore(dashboard)" + groups: + react: + patterns: + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" + testing: + patterns: + - "@testing-library/*" + - "jest" + - "jest-*" + - "ts-jest" + - "@types/jest*" + typescript-eslint: + patterns: + - "@typescript-eslint/*" + - "eslint*" + - "typescript" + vite: + patterns: + - "vite" + - "@vitejs/*" + + # Listener npm dependencies + - package-ecosystem: "npm" + directory: "/listener" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "UTC" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "listener" + commit-message: + prefix: "chore(listener)" + + # Rust/Cargo contract dependencies + - package-ecosystem: "cargo" + directory: "/contract" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "UTC" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "contract" + commit-message: + prefix: "chore(contract)" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "UTC" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore(ci)" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a0ed3b9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +## Overview + + + +## Related Issue + +Closes # + +## Changes + + + +## Verification + + + +```bash +# e.g. +cd dashboard && npm test +cd listener && npm run typecheck && npm test +cd contract/contracts/hello-world && cargo test +``` + +## How to Test + + + +## Checklist + +- [ ] Branch is up to date with `main` +- [ ] Tests added/updated and all pass locally +- [ ] `cargo fmt --all` run (if Rust changes) +- [ ] `npm run lint` passes (if TypeScript changes) +- [ ] Documentation updated if behavior changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90ef20f..17ad28c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,213 +1,243 @@ # Contributing to NotifyChain -Thank you for your interest in contributing to NotifyChain! This document provides guidelines and instructions for contributing to the project. +Welcome, and thanks for your interest in contributing. This document is your starting point — it covers the contribution workflow, coding standards, and PR guidelines. Deeper references are linked throughout. -**Start here instead (recommended):** [`CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md`](CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md) +--- + +## Documentation Map + +| Document | Purpose | +|---|---| +| **This file** | Workflow, standards, PR guidelines | +| [`CONTRIBUTOR_SETUP.md`](CONTRIBUTOR_SETUP.md) | Full local environment setup from scratch | +| [`CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md`](CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md) | End-to-end workflow reference | +| [`CONTRIBUTOR_ARCHITECTURE_DEEP_DIVE.md`](CONTRIBUTOR_ARCHITECTURE_DEEP_DIVE.md) | System architecture and component internals | +| [`ARCHITECTURE_OVERVIEW.md`](ARCHITECTURE_OVERVIEW.md) | High-level architecture walkthrough | + +--- ## Code of Conduct - Be respectful and inclusive -- Provide constructive feedback -- Focus on what is best for the community -- Show empathy towards other contributors - -## Getting Started - -### Prerequisites - -To contribute to NotifyChain, make sure you have: -- [Rust](https://www.rust-lang.org/tools/install) installed with WebAssembly target (`rustup target add wasm32-unknown-unknown`) -- [Stellar CLI](https://developers.stellar.org/docs/build/sdks-and-libraries/cli/) installed -- [Node.js](https://nodejs.org/) (for listener and dashboard components) -- Basic understanding of Soroban smart contracts, Git, and GitHub - -### Setup (Fork Workflow) - -To set up a local development environment, follow this fork-and-clone workflow: - -1. **Fork the Repository**: Visit [Notify-Chain](https://github.com/Core-Foundry/Notify-Chain) and click the **Fork** button to create a copy of the repository under your GitHub account. -2. **Clone your Fork**: - ```bash - git clone https://github.com/your-username/Notify-Chain.git - cd Notify-Chain - ``` -3. **Configure Upstream Remote**: Keep your fork updated by pointing to the upstream repository: - ```bash - git remote add upstream https://github.com/Core-Foundry/Notify-Chain.git - ``` -4. **Verify Remotes**: Run `git remote -v` to ensure your configuration is correct: - ```bash - 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) - ``` - -### Syncing Your Fork - -Before starting any new work or creating a branch, always pull the latest changes from the upstream `main` branch to prevent merge conflicts: +- Give constructive, specific feedback +- Show empathy — everyone is learning + +--- + +## Prerequisites + +Before you start, make sure you have: + +- **Rust** (stable) + WebAssembly target: `rustup target add wasm32-unknown-unknown` +- **Stellar CLI**: `cargo install --locked stellar-cli --features opt` +- **Node.js 22** (used by both listener and dashboard in CI) +- **Git** + +For a detailed walkthrough including platform-specific notes, see [`CONTRIBUTOR_SETUP.md`](CONTRIBUTOR_SETUP.md). + +--- + +## Quick Setup + +### With Docker (easiest) + +Requires [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or Docker Engine + Compose on Linux). No Node.js install needed. ```bash -git checkout main -git fetch upstream -git merge upstream/main -git push origin main +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 + +cp .env.example .env +# Edit .env — set CONTRACT_ADDRESSES to your deployed contract ID + +docker compose up --build +``` + +Dashboard → http://localhost:5173 · Listener API → http://localhost:8787 + +### Without Docker + +```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 + +cd listener && npm install && cp .env.example .env && npm run migrate +cd ../dashboard && npm install && cp .env.example .env +``` + +Minimum `listener/.env`: +```bash +STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443 +CONTRACT_ADDRESSES=[{"address":"YOUR_CONTRACT_ID","events":["*"]}] ``` -## Issue Claiming Process +Full setup details: [`CONTRIBUTOR_SETUP.md`](CONTRIBUTOR_SETUP.md) -To ensure that efforts are not duplicated and contributors can work on tasks they are interested in, we use the following issue claiming process: +--- -1. **Browse Open Issues**: Explore the [GitHub Issue Tracker](https://github.com/Core-Foundry/Notify-Chain/issues) to find tasks. Look for issues labeled `good first issue` if you are new to the codebase. -2. **Claim an Issue**: - * Comment on the issue stating: `I would like to work on this issue.` - * Wait for a maintainer to assign the issue to you. Once assigned, your username will appear under "Assignees" on GitHub. - * Do **not** begin working on an issue or open a Pull Request for it unless it has been formally assigned to you. This prevents two developers from working on the same problem. -3. **Active Work & SLA**: - * Once assigned, you are expected to submit a draft PR or progress update within **5 days**. - * If there is no activity or communication on the issue after 5 days, the issue may be unassigned and made available for other contributors. - * If you need more time, simply post an update on the issue so the maintainers know you are still active. +## Issue Claiming +1. Browse [open issues](https://github.com/Core-Foundry/Notify-Chain/issues). New? Look for `good first issue`. +2. Comment: `I would like to work on this issue.` +3. Wait to be assigned — don't open a PR for unassigned work. +4. Once assigned, submit a draft PR or progress update within **5 days**. Post an update if you need more time. + +--- ## Development Workflow -### 1. Create a Branch +### 1. Sync and branch + +Always start from an up-to-date `main`: -Always start from an up‑to‑date `main`: ```bash git checkout main -git pull upstream main +git fetch upstream +git merge upstream/main +git push origin main git checkout -b ``` -Use descriptive branch names following these conventions: -- `feature/` for new features -- `fix/` for bug fixes -- `docs/` for documentation -- `refactor/` for code refactoring -- `test/` for adding or modifying tests -- `chore/` for maintenance tasks - -Example: -- `feature/add-slack-notifications` -- `fix/resolve-event-deduplication-bug` -- `docs/update-contributing-guide` +Branch naming: -### 2. Make Your Changes +| Prefix | Use | +|---|---| +| `feature/` | New features | +| `fix/` | Bug fixes | +| `docs/` | Documentation | +| `refactor/` | Refactoring | +| `test/` | Tests only | +| `chore/` | Maintenance | -- Write clean, readable code -- Follow existing code style in each directory -- Add comments for complex logic -- Update documentation as needed +### 2. Make changes -### 3. Write and Run Tests +- Follow the existing code style in each component directory. +- Add comments for non-obvious logic. +- Update docs when behavior changes. +- Write tests for all new logic and bug fixes. -All new features and bug fixes must include tests. +### 3. Run tests before pushing -#### Contract Tests (Rust) +**Contracts (Rust)** ```bash -cd contract/contracts/hello-world -cargo test +cd contract +cargo fmt --all # format +cargo fmt --all -- --check # verify clean +cd contracts/hello-world && cargo test # unit tests ``` -#### Listener Tests (TypeScript) +**Listener (TypeScript)** ```bash cd listener -npm install +npm run lint +npm run typecheck npm test ``` -#### Dashboard Tests (TypeScript) +**Dashboard (TypeScript)** ```bash cd dashboard -npm install +npm run lint +npm run build # includes TypeScript check npm test ``` -### 4. Commit Changes +### 4. Commit -Write clear, descriptive commit messages following [Conventional Commits](https://www.conventionalcommits.org/): -- `feat:` new feature -- `fix:` bug fix -- `docs:` documentation changes -- `test:` test additions or changes -- `refactor:` code refactoring -- `chore:` maintenance tasks +Follow [Conventional Commits](https://www.conventionalcommits.org/): -Example: +``` +feat: new feature +fix: bug fix +docs: documentation only +test: tests only +refactor: no behavior change +chore: maintenance +``` + +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: add payload validation edge cases" ``` -### 5. Push and Create PR +### 5. Push and open a PR + ```bash git push -u origin ``` -Then create a Pull Request on GitHub! +Then open a PR on GitHub against `main`. GitHub will pre-fill the PR template — fill it out completely. + +--- + +## Coding Standards + +### Rust (contracts) + +- Format with `cargo fmt --all` before every commit +- Add `///` doc comments on all public functions and structs +- Use `#[contracterror]` for custom errors +- Every public function needs a test + +### TypeScript (listener + dashboard) + +- `npm run lint` must pass with zero warnings +- Use TypeScript — no `any` unless genuinely unavoidable +- Unit test all new service logic +- Follow existing file and naming conventions in the directory you're editing + +--- ## Pull Request Guidelines -### PR Title -Use the same format as commit messages: -- `feat: add retry queue for notifications` -- `fix: standardize error messages across contracts` - -### PR Description -Include: -1. **Overview**: What changes does this PR introduce? -2. **Related Issue**: Link to GitHub issue(s) this PR addresses -3. **Changes**: What was added/removed/modified -4. **Verification Results**: What tests passed, coverage, etc. -5. **How to Test**: Instructions for testing your changes - -### PR Checklist -- [ ] Code follows project style guidelines -- [ ] Tests added/updated and passing -- [ ] Documentation updated -- [ ] All tests pass locally +### Title + +Match commit convention: `feat: add slack notification channel` + +### Description + +The PR template will prompt you for: +1. Overview of what changed and why +2. Linked issue number +3. Key files modified +4. Verification commands you ran +5. Manual test instructions + +### Before submitting + - [ ] Branch is up to date with `main` +- [ ] All tests pass locally +- [ ] Lint/format checks pass +- [ ] Docs updated if behavior changed +- [ ] PR scope is focused on a single issue -## Code Style Guidelines +### Review process -### Rust (Soroban Contracts) -Follow existing patterns in `contract/contracts/hello-world/`: -- Format with `cargo fmt` -- Add `///` documentation comments for public functions/structs -- Use `#[contracterror]` for custom errors -- Test all functionality +- CI runs automatically — wait for green before requesting review. +- Address feedback promptly and push to the same branch (the PR updates automatically). +- Keep the scope tight — don't mix unrelated changes in one PR. +- Reviewers will test locally for significant changes. -### TypeScript (Listener/Dashboard) -Follow the existing style in `listener/` and `dashboard/`: -- Run `npm run lint` before committing -- Use TypeScript for type safety -- Write unit tests for all new logic -- Follow existing naming conventions +--- -## Review Process +## Automated Dependency Updates -### For Contributors -1. Ensure all tests pass locally -2. Address reviewer feedback promptly -3. Keep PR scope focused on a single issue or feature -4. Be open to suggestions +[Dependabot](https://docs.github.com/en/code-security/dependabot) opens PRs weekly (Mondays) for outdated dependencies across all four ecosystems. Config is in [`.github/dependabot.yml`](.github/dependabot.yml). -### For Reviewers -1. Review code thoroughly -2. Test locally if needed -3. Provide constructive feedback -4. Approve when ready +When reviewing Dependabot PRs: check the changelog for breaking changes, wait for CI to pass, and review the migration guide for major version bumps. -## Questions? +--- -- Open an issue for bugs or feature requests -- Check existing issues and PRs first -- Join discussions on GitHub +## Questions -## License +- Search [existing issues](https://github.com/Core-Foundry/Notify-Chain/issues) first. +- Open a new issue for bugs or feature requests. +- Join discussions on GitHub. -By contributing, you agree that your contributions will be licensed under the MIT License. +--- -Thank you for contributing to NotifyChain! 🎉 +By contributing, you agree your work will be licensed under the MIT License. diff --git a/CONTRIBUTOR_SETUP.md b/CONTRIBUTOR_SETUP.md index 1ee2496..7065945 100644 --- a/CONTRIBUTOR_SETUP.md +++ b/CONTRIBUTOR_SETUP.md @@ -1,13 +1,6 @@ # Contributor Environment Setup Guide -> **Goal**: A new contributor should be able to set up NotifyChain from scratch -> on a clean machine without asking maintainers for help. - -This guide walks you through setting up a local development environment for -NotifyChain. By the end, you will have the listener service, the dashboard, -the frontend analytics app, and the smart contracts building and running on -your machine. -This guide walks you through setting up a local development environment for NotifyChain. By the end, you will have the listener service, the dashboard, and the smart contracts building and running on your machine. +> Goal: a new contributor should be able to set up NotifyChain from scratch on a clean machine without asking maintainers for help. --- @@ -17,446 +10,326 @@ This guide walks you through setting up a local development environment for Noti 2. [Clone the Repository](#2-clone-the-repository) 3. [Listener Service Setup](#3-listener-service-setup) 4. [Dashboard Setup](#4-dashboard-setup) -5. [Frontend (Next.js Analytics) Setup](#5-frontend-nextjs-analytics-setup) -6. [Smart Contracts Setup](#6-smart-contracts-setup) -7. [Environment Variables Reference](#7-environment-variables-reference) -8. [Full-Stack Verification Checklist](#8-full-stack-verification-checklist) -9. [Running Tests](#9-running-tests) -10. [VS Code Setup (Recommended)](#10-vs-code-setup-recommended) -11. [Troubleshooting & FAQ](#11-troubleshooting--faq) 5. [Smart Contracts Setup](#5-smart-contracts-setup) 6. [Environment Variables Reference](#6-environment-variables-reference) 7. [Running Tests](#7-running-tests) -8. [VS Code Setup (Recommended)](#8-vs-code-setup-recommended) +8. [VS Code Setup](#8-vs-code-setup) 9. [Troubleshooting & FAQ](#9-troubleshooting--faq) --- ## 1. Required Dependencies -### Essential Toolchain +| Dependency | Minimum Version | Install | Used By | +|---|---|---|---| +| Rust (stable) | stable | [rustup.rs](https://rustup.rs) | Smart contracts | +| `wasm32-unknown-unknown` | — | `rustup target add wasm32-unknown-unknown` | Soroban contracts | +| Stellar CLI | latest | `cargo install --locked stellar-cli --features opt` | Contract build/deploy | +| Node.js | **22** | [nodejs.org](https://nodejs.org) or `nvm` | Listener, Dashboard | +| Git | — | your package manager | Version control | -| Dependency | Minimum Version | Install Method | Used By | -|----------------|-----------------|-----------------------------------------|--------------------| -| Rust | stable | [rustup.rs](https://rustup.rs) | Smart contracts | -| `wasm32-unknown-unknown` | — | `rustup target add wasm32-unknown-unknown` | Soroban contracts | -| Stellar CLI | latest | `cargo install stellar-cli` | Contract build/deploy | -| Node.js | **18** (dashboard), **20** (listener) | [nodejs.org](https://nodejs.org) or `nvm` | Listener, Dashboard | -| npm | comes with Node | — | Package management | -| Git | — | Your package manager or [git-scm.com](https://git-scm.com) | Version control | +### Platform notes -### Platform Notes +- **macOS**: Install Xcode Command Line Tools first: `xcode-select --install` +- **Linux**: Install build tools first: `sudo apt install build-essential` +- **Windows**: Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) with the "C++ build tools" workload (needed for native `sqlite3` bindings) -- **macOS**: Install Xcode Command Line Tools (`xcode-select --install`) before Rust. -- **Linux**: Install `build-essential` (`sudo apt install build-essential`) before Rust. -- **Windows**: Use [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) with "C++ build tools" workload for native `sqlite3` bindings. +### Quick install (Rust + Stellar CLI) -### Node.js Version Management +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +rustup target add wasm32-unknown-unknown +cargo install --locked stellar-cli --features opt -The project uses **two different Node.js versions** across its components: +# Verify +rustc --version && cargo --version && stellar --version +``` + +--- + +## Docker Setup (Recommended) -| Workspace | Node Version | CI Reference | -|-------------|-------------|--------------| -| `listener/` | **20** | `.github/workflows/ci.yml` — `node-version: 20` | -| `dashboard/` | **18** | `.github/workflows/ci.yml` — `node-version: 18` | +Docker removes the need to install Node.js or manage per-service env files. All you need is [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or Docker Engine + Compose on Linux). -**Recommendation**: Use [nvm](https://github.com/nvm-sh/nvm) (Node Version Manager) to switch between versions: +### Start everything ```bash -# Install nvm (macOS/Linux) -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash +# 1. Copy the root env file (only needed once) +cp .env.example .env -# Restart your terminal, then install both versions -nvm install 18 -nvm install 20 +# 2. Edit .env — at minimum set CONTRACT_ADDRESSES to your deployed contract ID -# Use Node 20 for the listener (the more demanding component) -nvm use 20 +# 3. Build images and start listener + dashboard +docker compose up --build ``` -For most local development, **Node 20 is sufficient** for both components. If you encounter CI-related issues, test against the specific version used in CI. +| Service | URL | +|---|---| +| Dashboard (Vite HMR) | http://localhost:5173 | +| Listener API | http://localhost:8787 | +| Listener health check | http://localhost:8787/health | -### Quick Install Commands +The SQLite database is stored in a named Docker volume (`listener_data`) and persists across container restarts. -```bash -# ── Rust + WebAssembly + Stellar CLI ── -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source "$HOME/.cargo/env" -rustup target add wasm32-unknown-unknown -cargo install --locked stellar-cli --features opt +### Common commands -# Verify -rustc --version && cargo --version && stellar --version +```bash +docker compose up --build # rebuild images and start (needed after code changes) +docker compose up # start with existing images +docker compose down # stop and remove containers +docker compose down -v # stop and delete the database volume (full reset) +docker compose logs -f listener # tail listener logs +docker compose logs -f dashboard # tail dashboard logs +docker compose restart listener # restart one service after env change ``` ---- +### Change configuration -## 2. Clone the Repository +Edit `.env` at the repo root, then: ```bash -git clone https://github.com/CollinsC1O/Notify-Chain.git -git clone https://github.com/Core-Foundry/Notify-Chain.git -cd Notify-Chain +docker compose restart listener # for most listener settings +docker compose up --build dashboard # required if VITE_* vars changed (baked in at build time) +``` + +### Reset the database + +```bash +docker compose down -v # removes listener_data volume +docker compose up # fresh start — migrations run automatically on boot ``` -If you plan to contribute, fork the repository first, then clone your fork: +### What doesn't run in Docker + +The **Rust/Soroban contract** is a build-only component — no runtime container exists for it. Build and test it locally following [Smart Contracts Setup](#5-smart-contracts-setup). + +--- + +## Manual Setup (Without Docker) + +Follow sections 2–9 below to set up each component directly on your machine. + +--- + +## 2. Clone the Repository + +Fork the repo on GitHub first, then: ```bash git clone https://github.com/YOUR-USERNAME/Notify-Chain.git cd Notify-Chain -git remote add upstream https://github.com/CollinsC1O/Notify-Chain.git git remote add upstream https://github.com/Core-Foundry/Notify-Chain.git ``` +Verify remotes: +```bash +git remote -v +# origin https://github.com/YOUR-USERNAME/Notify-Chain.git (fetch) +# upstream https://github.com/Core-Foundry/Notify-Chain.git (fetch) +``` + --- ## 3. Listener Service Setup The listener is the core off-chain service that polls the Stellar network, processes contract events, and delivers notifications. -### 3.1 Install Dependencies +### 3.1 Install dependencies ```bash cd listener npm install ``` -> **Note**: The `sqlite3` package includes native bindings. If the install fails, see the [Troubleshooting](#9-troubleshooting--faq) section. +> If `npm install` fails with `node-gyp` or `sqlite3` errors, see [Troubleshooting](#9-troubleshooting--faq). -### 3.2 Configure Environment +### 3.2 Configure environment ```bash cp .env.example .env ``` -Open `listener/.env` in your editor. At minimum, set: +Minimum required values in `listener/.env`: ```bash STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443 CONTRACT_ADDRESSES=[{"address":"YOUR_CONTRACT_ID","events":["*"]}] ``` -If you plan to use Discord notifications, also set: +For Discord notifications, also add: ```bash DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_TOKEN DISCORD_WEBHOOK_ID=YOUR_WEBHOOK_ID ``` -> **Note**: Both `DISCORD_WEBHOOK_URL` and `DISCORD_WEBHOOK_ID` are required -> together. If either is missing, Discord notifications are disabled. +Full variable reference: [Environment Variables Reference](#6-environment-variables-reference). -For a full reference of every variable, see [Environment Variables Reference](#7-environment-variables-reference). -For a full reference of every variable, see [Environment Variables Reference](#6-environment-variables-reference). - -### 3.3 Initialize the Database +### 3.3 Initialize the database ```bash npm run migrate ``` -This creates the SQLite database file (default location: `listener/data/notifications.db`) and runs all schema migrations. +This creates `listener/data/notifications.db` and runs all schema migrations. -### 3.4 Run the Listener +### 3.4 Run the listener ```bash npm run dev ``` -The listener starts and immediately begins polling the configured contracts. You should see log output showing poll cycles. - -**Verify it's running:** +Verify it's working: ```bash -curl http://localhost:8787/health # Health check -curl http://localhost:8787/api/events # Event feed (may be empty initially) +curl http://localhost:8787/health +curl http://localhost:8787/api/events ``` -### 3.5 Useful Listener Commands +### 3.5 Listener commands -| Command | Purpose | -|-------------------------|--------------------------------------------------| -| `npm run dev` | Start in development mode (ts-node, hot reload) | -| `npm run build` | Compile TypeScript to JavaScript | -| `npm start` | Run the compiled production build | -| `npm test` | Run all tests | -| `npm run typecheck` | TypeScript type checking (no emit) | -| `npm run migrate` | Initialize or update the SQLite database schema | -| `npm run lint` | Alias for `typecheck` | +| Command | Purpose | +|---|---| +| `npm run dev` | Start in dev mode (ts-node, hot reload) | +| `npm run build` | Compile TypeScript | +| `npm start` | Run compiled production build | +| `npm test` | Run all tests | +| `npm run typecheck` | TypeScript type check (no emit) | +| `npm run lint` | ESLint | +| `npm run migrate` | Initialize or update SQLite schema | --- ## 4. Dashboard Setup -The dashboard is a React + Vite application that visualizes events from the listener's API. +The dashboard is a React + Vite app that visualizes events from the listener's API. -### 4.1 Install Dependencies +### 4.1 Install dependencies ```bash cd dashboard npm install ``` -### 4.2 Configure Environment +### 4.2 Configure environment ```bash cp .env.example .env ``` -The default `.env.example` is pre-configured for local development: +Default `.env.example` works for local development: ```bash VITE_EVENTS_API_URL=http://localhost:8787/api/events VITE_STELLAR_NETWORK=TESTNET ``` -Change `VITE_EVENTS_API_URL` if your listener runs on a different port. - -### 4.3 Run the Dashboard +### 4.3 Run the dashboard ```bash npm run dev ``` -Open [http://localhost:5173](http://localhost:5173) in your browser. The dashboard fetches events from the listener API and displays them in real-time. +Open [http://localhost:5173](http://localhost:5173). The dashboard fetches from the listener API. -### 4.4 Useful Dashboard Commands - -| Command | Purpose | -|------------------|--------------------------------------------------| -| `npm run dev` | Start Vite dev server with hot module replacement | -| `npm run build` | TypeScript check + Vite production build | -| `npm test` | Run all tests | -| `npm run lint` | ESLint with zero-tolerance for warnings | -| `npm run preview` | Preview the production build locally | -| `npm run benchmark` | Run performance rendering benchmarks | - ---- - -## 5. Frontend (Next.js Analytics) Setup - -> **Note**: The frontend is a legacy Next.js app for analytics visualization. -> Most contributors only need the **Dashboard** (section 4). Skip this section -> unless you are working on analytics features. - -The frontend is a Next.js 14 application with Chart.js for analytics -dashboards and Tailwind CSS for styling. - -### 5.1 Install Dependencies - -```bash -cd frontend -npm install -``` - -### 5.2 Configure Environment - -The frontend reads from the listener's API and does not require its own -`.env` file for basic operation. If you need to override defaults, create: - -```bash -echo "NEXT_PUBLIC_API_URL=http://localhost:8787" > .env.local -``` - -### 5.3 Run the Frontend - -```bash -npm run dev -``` - -Opens at [http://localhost:3000](http://localhost:3000). - -### 5.4 Useful Frontend Commands +### 4.4 Dashboard commands | Command | Purpose | -|---------|---------| -| `npm run dev` | Start Next.js dev server | -| `npm run build` | Production build | -| `npm start` | Start production server | -| `npm run lint` | Run Next.js linting | +|---|---| +| `npm run dev` | Start Vite dev server | +| `npm run build` | TypeScript check + Vite production build | +| `npm test` | Run all tests | +| `npm run lint` | ESLint (zero warnings) | +| `npm run preview` | Preview production build locally | +| `npm run benchmark` | Run rendering performance benchmarks | --- -## 6. Smart Contracts Setup - -The project contains two Soroban smart contracts. Building them is optional — you only need to do this if you are modifying contracts or deploying your own instances. - -### 6.1 Build Contracts ## 5. Smart Contracts Setup -The project contains two Soroban smart contracts. Building them is optional — you only need to do this if you are modifying contracts or deploying your own instances. +Only needed if you're modifying or deploying contracts. -### 5.1 Build Contracts - -**AutoShare Contract** (`contract/`): +### 5.1 Build ```bash cd contract stellar contract build ``` -The compiled `.wasm` file is written to `contract/target/wasm32-unknown-unknown/release/`. +Output goes to `contract/target/wasm32-unknown-unknown/release/`. -**TaskBounty Contract** (`Documents/Task Bounty/`): +### 5.2 Run tests ```bash -cd Documents/Task\ Bounty -stellar contract build -``` - -### 6.2 Run Contract Tests -### 5.2 Run Contract Tests - -```bash -# AutoShare cd contract/contracts/hello-world cargo test - -# TaskBounty -cd Documents/Task\ Bounty -cargo test ``` -### 6.3 Deploy to Testnet (Optional) -### 5.3 Deploy to Testnet (Optional) - -This requires a funded Stellar testnet identity: +### 5.3 Deploy to testnet (optional) ```bash -# Generate and fund a test identity +# Generate and fund a testnet identity stellar keys generate my-identity --network testnet stellar keys fund my-identity --network testnet -# Deploy the AutoShare contract +# Deploy cd contract stellar contract deploy \ --wasm target/wasm32-unknown-unknown/release/hello_world.wasm \ --source my-identity \ --network testnet - -# The contract ID is printed after successful deployment -``` - -### 6.4 Initialize a Deployed Contract - -Most contracts require initialization after deployment. For the AutoShare -contract: - -```bash -# Find your wallet address -stellar keys address my-identity - -# Initialize the contract with your admin address -stellar contract invoke \ - --id YOUR_CONTRACT_ID \ - --source my-identity \ - --network testnet \ - -- \ - initialize_admin \ - --admin YOUR_WALLET_ADDRESS -``` - -For the TaskBounty contract (see `Documents/Task Bounty/SETUP.md` for -detailed configuration): - -```bash -stellar contract invoke \ - --id YOUR_CONTRACT_ID \ - --source my-identity \ - --network testnet \ - -- \ - initialize +# Contract ID is printed on success ``` -After initializing, add the contract ID to your `listener/.env` -`CONTRACT_ADDRESSES` so the listener knows to monitor it. - -### 6.5 Useful Contract Commands -### 5.4 Useful Contract Commands +### 5.4 Contract commands -| Command | Purpose | -|--------------------------------------------------|-----------------------------| -| `stellar contract build` | Build all contracts | -| `cargo test` | Run contract tests | -| `stellar contract deploy --wasm --source --network testnet` | Deploy to testnet | -| `stellar contract invoke --id --source --network testnet -- [ARGS]` | Call a contract function | -| `stellar contract optimize --wasm ` | Optimize Wasm for production | -| `stellar contract inspect --wasm ` | Inspect contract interface | -| `cargo fmt --all` | Format Rust code | +| Command | Purpose | +|---|---| +| `stellar contract build` | Build all contracts | +| `cargo test` | Run contract unit tests | +| `cargo fmt --all` | Format Rust code | +| `cargo fmt --all -- --check` | Verify formatting (used in CI) | +| `stellar contract deploy ...` | Deploy to testnet | +| `stellar contract invoke ...` | Call a contract function | --- -## 7. Environment Variables Reference - -### 7.1 Listener (`listener/.env`) ## 6. Environment Variables Reference -### 6.1 Listener (`listener/.env`) +### Listener (`listener/.env`) | Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| **Stellar Network** | | | | -| `STELLAR_NETWORK` | No | `testnet` | Network passphrase (`testnet`, `pubnet`, or custom) | -| `STELLAR_RPC_URL` | **Yes** | `https://soroban-testnet.stellar.org:443` | Soroban RPC endpoint to poll for events | -| **Contracts** | | | | -| `CONTRACT_ADDRESSES` | **Yes** | `[]` | JSON array of contract configs. Format: `[{"address":"C...","events":["*"]}]`. Each entry has `address` (string, the Stellar contract ID) and `events` (string array of event names to filter on, or `["*"]` for all). | -| **Polling** | | | | -| `POLL_INTERVAL_MS` | No | `30000` | Time between RPC polls (milliseconds) | -| `MAX_RECONNECT_ATTEMPTS` | No | `5` | Max consecutive RPC failures before stopping | -| `RECONNECT_DELAY_MS` | No | `5000` | Base delay between reconnection attempts (exponential backoff) | -| **API Server** | | | | -| `EVENTS_API_PORT` | No | `8787` | HTTP server port for `/health` and `/api/events` | -| `EVENTS_API_CORS_ORIGIN` | No | `http://localhost:5173` | Allowed CORS origin (dashboard URL) | -| `WEBHOOK_SECRETS` | No | `[]` | JSON array of webhook secrets for HMAC verification. Format: `[{"id":"default","secret":"whsec_..."}]` | -| **Discord Notifications** | | | | -| `DISCORD_WEBHOOK_URL` | No | — | Discord webhook URL for sending notifications | -| `DISCORD_WEBHOOK_ID` | No | — | Discord webhook ID (required together with webhook URL) | -| `NOTIFICATION_DEDUPLICATION_WINDOW_MS` | No | `60000` | In-memory dedup window for Discord notifications (ms) | -| `NOTIFICATION_DEDUPLICATION_MAX_SIZE` | No | `10000` | Max entries in in-memory dedup cache | -| `DISCORD_WEBHOOK_ID` | No | — | Discord webhook ID (required if webhook URL is set) | -| **Retry Queue** | | | | -| `RETRY_BASE_DELAY_MS` | No | `5000` | Base delay for notification retry exponential backoff | -| `RETRY_MAX_RETRIES` | No | `5` | Max retry attempts for failed Discord notifications | -| **Event Processing Queue** | | | | -| `EVENT_QUEUE_MAX_CONCURRENCY` | No | `1` | Max events to process concurrently (1 = ordered) | -| `EVENT_QUEUE_MAX_RETRIES` | No | `3` | Max retry attempts per event before permanent failure | -| `EVENT_QUEUE_BASE_DELAY_MS` | No | `2000` | Base delay for event retry exponential backoff | -| `EVENT_QUEUE_POLL_INTERVAL_MS` | No | `1000` | How often the queue checks for due events (ms) | -| **Database** | | | | -| `DATABASE_PATH` | No | `./data/notifications.db` | SQLite database file path | -| **Scheduler** | | | | -| `SCHEDULER_ENABLED` | No | `true` | Enable the notification scheduler | -| `SCHEDULER_POLL_INTERVAL_MS` | No | `10000` | How often the scheduler polls for due notifications | -| `SCHEDULER_LOCK_TIMEOUT_MS` | No | `60000` | Distributed lock timeout for scheduler | -| `SCHEDULER_PROCESSOR_ID` | No | auto-generated | Unique ID for this scheduler instance (multi-instance setups) | -| `SCHEDULER_BATCH_SIZE` | No | `10` | Max notifications to process per poll cycle | -| `SCHEDULER_TIMING_BUFFER_MS` | No | `60000` | Buffer to prevent premature notification delivery | -| **Rate Limiting** | | | | +|---|---|---|---| +| `STELLAR_RPC_URL` | Yes | `https://soroban-testnet.stellar.org:443` | Soroban RPC endpoint | +| `CONTRACT_ADDRESSES` | Yes | `[]` | JSON array: `[{"address":"C...","events":["*"]}]` | +| `STELLAR_NETWORK` | No | `testnet` | Network passphrase | +| `POLL_INTERVAL_MS` | No | `30000` | Time between RPC polls (ms) | +| `MAX_RECONNECT_ATTEMPTS` | No | `5` | Max RPC failures before stopping | +| `RECONNECT_DELAY_MS` | No | `5000` | Base delay between reconnect attempts | +| `EVENTS_API_PORT` | No | `8787` | HTTP server port | +| `EVENTS_API_CORS_ORIGIN` | No | `http://localhost:5173` | Allowed CORS origin | +| `DATABASE_PATH` | No | `./data/notifications.db` | SQLite file path | +| `DISCORD_WEBHOOK_URL` | No | — | Discord webhook URL | +| `DISCORD_WEBHOOK_ID` | No | — | Discord webhook ID (required with URL) | +| `RETRY_BASE_DELAY_MS` | No | `5000` | Base delay for notification retry backoff | +| `RETRY_MAX_RETRIES` | No | `5` | Max retry attempts for failed notifications | +| `SCHEDULER_ENABLED` | No | `true` | Enable notification scheduler | +| `SCHEDULER_POLL_INTERVAL_MS` | No | `10000` | Scheduler poll frequency (ms) | +| `SCHEDULER_BATCH_SIZE` | No | `10` | Max notifications per scheduler cycle | | `RATE_LIMIT_ENABLED` | No | `true` | Enable HTTP API rate limiting | -| `RATE_LIMIT_WINDOW_MS` | No | `60000` | Rate limit window (milliseconds) | | `RATE_LIMIT_MAX_REQUESTS` | No | `60` | Max requests per window per client | -| `RATE_LIMIT_CLIENT_OVERRIDES` | No | `{}` | JSON object of per-client rate limit overrides | -| **Cleanup** | | | | -| `CLEANUP_INTERVAL_MS` | No | `3600000` | How often to run cleanup jobs (1 hour) | -| `NOTIFICATION_RETENTION_MS` | No | `604800000` | Retain completed notifications (7 days) | -| `RATE_LIMIT_EVENT_RETENTION_MS` | No | `86400000` | Retain rate limit audit events (1 day) | -| `EVENT_RETENTION_MS` | No | `86400000` | Retain in-memory events (1 day) | -| **Logging** | | | | -| `LOG_LEVEL` | No | `info` | Winston log level (`error`, `warn`, `info`, `debug`) | -| `NODE_ENV` | No | — | Set to `production` for newline-delimited JSON log output | - -### 7.2 Dashboard (`dashboard/.env`) -### 6.2 Dashboard (`dashboard/.env`) +| `LOG_LEVEL` | No | `info` | Winston log level (`error`,`warn`,`info`,`debug`) | +| `NODE_ENV` | No | — | Set to `production` for JSON log output | -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `VITE_EVENTS_API_URL` | No | `http://localhost:8787/api/events` | Listener API endpoint for fetching events | -| `VITE_STELLAR_NETWORK` | No | `TESTNET` | Stellar network for wallet integration (`TESTNET` or `MAINNET`) | +### Dashboard (`dashboard/.env`) -### 7.3 Minimum Viable Configuration -### 6.3 Minimum Viable Configuration +| Variable | Required | Default | Description | +|---|---|---|---| +| `VITE_EVENTS_API_URL` | No | `http://localhost:8787/api/events` | Listener API endpoint | +| `VITE_STELLAR_NETWORK` | No | `TESTNET` | Stellar network (`TESTNET` or `MAINNET`) | -To run the listener with no optional features: +### Minimum viable listener config ```bash STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443 @@ -466,110 +339,26 @@ EVENTS_API_CORS_ORIGIN=http://localhost:5173 DATABASE_PATH=./data/notifications.db ``` -To add Discord notifications, also set: - -```bash -DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_TOKEN -DISCORD_WEBHOOK_ID=YOUR_WEBHOOK_ID -``` - ---- - -## 8. Full-Stack Verification Checklist - -Run these checks to confirm every component is working correctly. - -### 8.1 Listener Health - -```bash -# Check the health endpoint -curl http://localhost:8787/health - -# Expected: {"status":"ok","services":{"stellarRpc":{"status":"ok"},"eventRegistry":{"status":"ok","eventCount":0}}} -``` - -### 8.2 Event Feed - -```bash -# Check the events API (may be empty if no contracts are active) -curl http://localhost:8787/api/events - -# Expected: {"events":[],"total":0} (or populated with events) -``` - -### 8.3 Scheduler Stats - -```bash -# Check scheduler status -curl http://localhost:8787/api/schedule/stats - -# Expected: {"pending":0,"processing":0,"completed":0,"failed":0,"overdue":0} -``` - -### 8.4 Dashboard - -Open [http://localhost:5173](http://localhost:5173) in your browser and -confirm: -- The page loads without errors -- The events feed renders (may be empty) -- No CORS errors in the browser console (F12 → Console) - -### 8.5 Frontend Analytics - -If you set up the frontend, open -[http://localhost:3000](http://localhost:3000) and confirm: -- The analytics dashboard loads -- Charts render without errors - -### 8.6 Database Inspection - -```bash -# Verify the database exists and has the correct schema -sqlite3 listener/data/notifications.db ".tables" - -# Expected: notification_execution_log notification_templates polling_cursors -# notification_template_audit_log processed_events rate_limit_events -# scheduled_notifications -``` - -### 8.7 Clean Up and Start Fresh - -If you need to reset everything: - -```bash -# Reset listener database -rm -f listener/data/notifications.db -cd listener && npm run migrate - -# Clear listener event registry (restart the process) -# Stop with Ctrl+C, then npm run dev again -``` - ---- - -## 9. Running Tests -``` - --- ## 7. Running Tests -Always run tests before submitting a pull request. +Run these before opening any PR. ### Contracts ```bash -# AutoShare -cd contract/contracts/hello-world && cargo test - -# TaskBounty -cd Documents/Task\ Bounty && cargo test +cd contract/contracts/hello-world +cargo fmt --all -- --check # must be clean +cargo test ``` ### Listener ```bash cd listener +npm run typecheck +npm run lint npm test ``` @@ -577,49 +366,41 @@ npm test ```bash cd dashboard +npm run lint +npm run build npm test ``` -### CI Validation (What GitHub Actions Runs) - -The CI pipeline executes these checks on every pull request: +### What CI runs on every PR ```bash -# Listener -cd listener -npm run typecheck # TypeScript strict type checking -npm test # Jest test suite - # Dashboard -cd dashboard -npm run lint # ESLint (zero warnings) -npm run build # TypeScript check + Vite build -npm test # Jest test suite +npm run lint && npm run build && npm test + +# Listener +npm run lint && npm run typecheck && npm test # Contracts -cd contract -cargo fmt --all -- --check # Rust formatting check +cargo fmt --all -- --check cargo test --workspace --all-features --verbose +cargo test fuzz_ --verbose -- --nocapture ``` --- -## 10. VS Code Setup (Recommended) -## 8. VS Code Setup (Recommended) +## 8. VS Code Setup -### Extensions +### Recommended extensions -Install these VS Code extensions: +1. [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) +2. [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) — Rust debugger +3. [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) +4. [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +5. [Better TOML](https://marketplace.visualstudio.com/items?itemName=bungcip.better-toml) -1. [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) — Rust language support -2. [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) — Debugger for Rust -3. [Better TOML](https://marketplace.visualstudio.com/items?itemName=bungcip.better-toml) — TOML file support -4. [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) — TypeScript linting -5. [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) — Code formatter +### `.vscode/settings.json` -### Settings - -Add this to `.vscode/settings.json` (already present in the repository): +The repo includes this already: ```json { @@ -628,100 +409,63 @@ Add this to `.vscode/settings.json` (already present in the repository): } ``` -This configures `rust-analyzer` to use the `wasm32` target and avoids false-positive type errors from non-Wasm platform checks. +This prevents false-positive errors from non-Wasm platform checks. --- -## 11. Troubleshooting & FAQ ## 9. Troubleshooting & FAQ -> A more extensive troubleshooting guide is available at [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md). This section covers the most common setup issues. - -### Node.js & npm - -**Q: `npm install` fails with `node-gyp` or `sqlite3` errors.** +### `npm install` fails with `node-gyp` / `sqlite3` errors -Native `sqlite3` bindings must be compiled for your platform and Node.js version. +Native `sqlite3` bindings must be compiled for your platform: ```bash -# Rebuild native bindings npm rebuild sqlite3 - -# If that fails, reinstall +# If that fails: npm uninstall sqlite3 && npm install ``` -On Windows, ensure you have Visual Studio Build Tools installed with the -"C++ build tools" workload. On macOS, install Xcode Command Line Tools: -`xcode-select --install`. -On Windows, ensure you have Visual Studio Build Tools installed with the "C++ build tools" workload. +On Windows, ensure Visual Studio Build Tools are installed with the "C++ build tools" workload. --- -**Q: Which Node.js version should I use?** - -The listener CI runs on Node 20, the dashboard CI runs on Node 18. Use **Node 20** for local development (it is forward-compatible with the dashboard). Use `nvm` to switch if needed. +### No events appear in the listener ---- - -### Stellar RPC & Network - -**Q: The listener starts but no events appear.** - -1. Verify the contract ID in `CONTRACT_ADDRESSES` is correct and deployed on the same network as `STELLAR_RPC_URL`. -2. Confirm the RPC endpoint is reachable: - ```bash - curl -X POST https://soroban-testnet.stellar.org:443 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' - ``` -3. Check listener logs for poll cycle output: - ```bash - cd listener && npm run dev - ``` - Look for lines containing `"Received events"` or `"Processing event"`. -4. Verify the API is responding: +1. Confirm the contract ID in `CONTRACT_ADDRESSES` is deployed on the same network as `STELLAR_RPC_URL`. +2. Check the RPC is reachable: ```bash - curl http://localhost:8787/health - curl http://localhost:8787/api/events + curl -X POST https://soroban-testnet.stellar.org:443 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' ``` +3. Check listener logs for poll output — look for `"Received events"`. --- -**Q: `stellar: command not found`** +### `stellar: command not found` ```bash -# Ensure cargo's bin directory is in your PATH source "$HOME/.cargo/env" - -# Or reinstall +# Or reinstall: cargo install --locked stellar-cli --features opt ``` --- -### Database - -**Q: `Error: Database not initialized` or `SQLITE_ERROR: no such table`** +### `SQLITE_ERROR: no such table` or `Database not initialized` ```bash cd listener -npm run migrate -``` - -If the data directory is missing: - -```bash -mkdir -p listener/data +mkdir -p data npm run migrate ``` --- -### Port Conflicts - -**Q: `EADDRINUSE: address already in use :::8787`** +### Port conflict: `EADDRINUSE :::8787` ```bash -# Find the process using the port (macOS/Linux) +# macOS/Linux — find and kill the process lsof -i :8787 kill -9 @@ -731,19 +475,15 @@ EVENTS_API_PORT=8788 --- -### Dashboard - -**Q: Dashboard shows a blank page or "Failed to fetch"** +### Dashboard shows blank page or "Failed to fetch" 1. Is the listener running? (`npm run dev` in `listener/`) -2. Does `VITE_EVENTS_API_URL` in `dashboard/.env` match the listener's port? +2. Does `VITE_EVENTS_API_URL` in `dashboard/.env` match the listener port? 3. Restart the Vite dev server after editing `.env`. --- -### Contracts - -**Q: `error: toolchain 'stable' does not support target 'wasm32-unknown-unknown'`** +### `error: toolchain 'stable' does not support target 'wasm32-unknown-unknown'` ```bash rustup target add wasm32-unknown-unknown @@ -751,49 +491,52 @@ rustup target add wasm32-unknown-unknown --- -**Q: `error[E0463]: can't find crate for 'std'`** +### After `git pull` things stop working + +```bash +cd listener && npm install && npm run migrate +cd dashboard && npm install +cd contract && stellar contract build +``` -Build with the correct target: +With Docker, rebuild after pulling: ```bash -cargo build --target wasm32-unknown-unknown --release -# Or use Stellar CLI (handles the target automatically): -stellar contract build +docker compose up --build ``` --- -### After a `git pull` +### Docker: dashboard shows "Failed to fetch" or blank page -If things stop working after pulling latest changes, run the full refresh: -If things stop working after pulling latest changes: +`VITE_EVENTS_API_URL` is baked in at image build time. If you changed it in `.env`, you need to rebuild: ```bash -# Contracts -cd contract && stellar contract build +docker compose up --build dashboard +``` -# Listener -cd listener && npm install && npm run migrate +Also confirm the listener is healthy before the dashboard starts: -# Dashboard -cd dashboard && npm install +```bash +docker compose ps # check listener status shows "healthy" +docker compose logs listener +``` + +--- -# Frontend -cd frontend && npm install +### Docker: `listener_data` volume has stale schema + +```bash +docker compose down -v # removes the volume +docker compose up --build # fresh start, migrations run automatically ``` --- -### Still Stuck? +### Still stuck? -1. Search [open issues](https://github.com/CollinsC1O/Notify-Chain/issues) — your problem may already be reported. -1. Search [open issues](https://github.com/Core-Foundry/Notify-Chain/issues) — your problem may already be reported. -2. Read the detailed [Troubleshooting Guide](TROUBLESHOOTING.md). -3. Open a new issue with: - - Your OS and version - - Output of `rustc --version`, `node --version`, `stellar --version` - - The full error message and stack trace - - Steps you have already tried +1. Search [open issues](https://github.com/Core-Foundry/Notify-Chain/issues). +2. Open a new issue with: your OS, output of `rustc --version && node --version && stellar --version`, the full error and stack trace, and steps already tried. --- @@ -801,42 +544,44 @@ cd frontend && npm install ``` Notify-Chain/ -├── contract/ # Soroban smart contract workspace -│ ├── contracts/hello-world/ # AutoShare contract (active) -│ └── Cargo.toml # Workspace configuration +├── contract/ # Soroban smart contract workspace +│ ├── contracts/hello-world/ # AutoShare notification contract +│ │ └── src/ # Rust source + tests +│ └── Cargo.toml # Workspace config │ -├── listener/ # Off-chain listener service +├── listener/ # Off-chain listener service (Node.js/TypeScript) +│ ├── Dockerfile # Multi-stage Docker image +│ ├── .dockerignore │ ├── src/ -│ │ ├── api/ # HTTP API (events, health, templates) -│ │ ├── services/ # Core: subscriber, dedup, notifier, scheduler -│ │ ├── store/ # In-memory event registry + preferences -│ │ ├── database/ # SQLite schema and client -│ │ ├── types/ # TypeScript type definitions -│ │ ├── utils/ # Logging, formatting, helpers -│ │ └── index.ts # Entry point -│ └── src/__tests__/ # Integration / E2E tests +│ │ ├── api/ # HTTP API (events, health, schedule) +│ │ ├── services/ # Core: subscriber, dedup, notifier, scheduler +│ │ ├── store/ # SQLite repositories + in-memory registry +│ │ ├── database/ # Schema and client +│ │ ├── types/ # TypeScript type definitions +│ │ └── index.ts # Entry point +│ └── src/__tests__/ # Integration tests │ -├── dashboard/ # React + Vite event dashboard +├── dashboard/ # React + Vite event dashboard +│ ├── Dockerfile # Multi-stage Docker image (dev + production) +│ ├── .dockerignore │ └── src/ -│ ├── components/ # UI components (EventCard, filters, etc.) -│ ├── services/ # API client -│ ├── store/ # Zustand state management -│ └── pages/ # Page components -│ -├── Documents/Task Bounty/ # TaskBounty contract (Soroban) +│ ├── components/ # UI components +│ ├── services/ # API client +│ ├── store/ # Zustand state +│ └── pages/ # Page components │ -├── frontend/ # Next.js analytics dashboard (Chart.js) -│ └── app/ -│ ├── analytics/ # Analytics pages with charts -│ └── page.tsx # Landing page -├── frontend/ # Legacy Next.js frontend (not actively maintained) +├── docker-compose.yml # Orchestrates listener + dashboard +├── .env.example # Root env template for Docker Compose +├── .dockerignore # Root-level build context exclusions │ -├── scripts/ # Helper scripts (health check, etc.) +├── .github/ +│ ├── workflows/ci.yml # CI pipeline +│ ├── dependabot.yml # Automated dependency updates +│ └── pull_request_template.md # PR description template │ -├── CONTRIBUTOR_SETUP.md # This file -├── CONTRIBUTING.md # Contribution guidelines & PR workflow -├── TROUBLESHOOTING.md # Detailed troubleshooting reference -├── ARCHITECTURE_OVERVIEW.md # High-level architecture walkthrough -├── SYSTEM_ARCHITECTURE.md # Visual architecture with Mermaid diagrams -└── README.md # Project overview and quick start +├── CONTRIBUTING.md # Workflow, standards, PR guidelines ← start here +├── CONTRIBUTOR_SETUP.md # This file — local environment setup +├── CONTRIBUTOR_DEVELOPMENT_WORKFLOW_GUIDE.md +├── CONTRIBUTOR_ARCHITECTURE_DEEP_DIVE.md +└── ARCHITECTURE_OVERVIEW.md ``` diff --git a/contract/contracts/hello-world/src/lib.rs b/contract/contracts/hello-world/src/lib.rs index f834d9b..e73fb8f 100644 --- a/contract/contracts/hello-world/src/lib.rs +++ b/contract/contracts/hello-world/src/lib.rs @@ -655,74 +655,46 @@ pub mod test_utils { mod tests { #[path = "tests/test_utils_test.rs"] mod test_utils_test; - #[path = "tests/storage_optimization_test.rs"] mod storage_optimization_test; - #[path = "tests/preferences_test.rs"] mod preferences_test; - #[path = "tests/autoshare_test.rs"] -#[cfg(test)] -#[path = "tests/preferences_test.rs"] -mod preferences_test; - -#[cfg(test)] -mod tests { - #[path = "../tests/autoshare_test.rs"] mod autoshare_test; - #[path = "tests/pause_test.rs"] mod pause_test; - #[path = "tests/mock_token_test.rs"] mod mock_token_test; - #[path = "tests/version_test.rs"] mod version_test; - #[path = "tests/notification_test.rs"] mod notification_test; - #[path = "tests/expiration_test.rs"] mod expiration_test; - #[path = "tests/revocation_test.rs"] mod revocation_test; - #[path = "tests/ownership_transfer_test.rs"] mod ownership_transfer_test; - #[path = "../tests/notification_validation_test.rs"] + #[path = "tests/notification_validation_test.rs"] mod notification_validation_test; - #[path = "../tests/category_registry_test.rs"] + #[path = "tests/category_registry_test.rs"] mod category_registry_test; - - #[path = "../tests/expiration_test.rs"] - mod expiration_test; - - #[path = "../tests/batch_notification_test.rs"] + #[path = "tests/batch_notification_test.rs"] mod batch_notification_test; - - #[path = "../tests/audit_log_test.rs"] + #[path = "tests/audit_log_test.rs"] mod audit_log_test; - - #[path = "../tests/payload_validation_test.rs"] + #[path = "tests/payload_validation_test.rs"] mod payload_validation_test; - - #[path = "../tests/revocation_test.rs"] - mod revocation_test; - - #[path = "../tests/batch_ack_test.rs"] + #[path = "tests/batch_ack_test.rs"] mod batch_ack_test; - #[path = "../tests/fuzz_test.rs"] + #[path = "tests/fuzz_test.rs"] mod fuzz_test; - - #[path = "../tests/schema_version_test.rs"] + #[path = "tests/schema_version_test.rs"] mod schema_version_test; - - #[path = "../tests/access_log_test.rs"] + #[path = "tests/access_log_test.rs"] mod access_log_test; - - #[path = "../tests/subscription_cancellation_test.rs"] + #[path = "tests/subscription_cancellation_test.rs"] mod subscription_cancellation_test; + #[path = "tests/extended_coverage_test.rs"] + mod extended_coverage_test; } diff --git a/contract/contracts/hello-world/src/tests/extended_coverage_test.rs b/contract/contracts/hello-world/src/tests/extended_coverage_test.rs new file mode 100644 index 0000000..4401a0d --- /dev/null +++ b/contract/contracts/hello-world/src/tests/extended_coverage_test.rs @@ -0,0 +1,642 @@ +//! Extended coverage tests (AGENTS.md acceptance criteria). +//! +//! Fills gaps not covered by existing test files: +//! +//! Payload validation: +//! - configure_notification_limits boundary validation +//! - batch per-entry TTL overflow rejection +//! - audit helpers reject unknown notification ids +//! - delivery attempt / failure on non-existent notification +//! +//! Event type filtering: +//! - CategoryRegistered event carries category and priority topics +//! - NotificationRevoked event has Notification category +//! - AuditRecordAppended event carries Notification category +//! - Every emitted event has at least 2 trailing topics (category + priority) +//! +//! Audit logging: +//! - delivery attempt on a revoked notification is rejected +//! - delivery failure on a non-existent notification is rejected +//! - acknowledgment on an expired notification is rejected +//! - audit log grows monotonically across independent notifications + +use crate::base::events::{AuditAction, NotificationCategory, NotificationPriority}; +use crate::test_utils::setup_test_env; +use crate::AutoShareContractClient; + +use soroban_sdk::testutils::{Address as _, Events, Ledger}; +use soroban_sdk::{Address, BytesN, Env, String, Symbol, TryFromVal, Val, Vec}; + +const ONE_HOUR: u64 = 3_600; + +// ── helpers ───────────────────────────────────────────────────────────────── + +fn make_id(env: &Env, tag: u8) -> BytesN<32> { + let mut b = [0u8; 32]; + b[0] = tag; + b[1] = 0xEC; // namespace: extended_coverage + BytesN::from_array(env, &b) +} + +fn title(env: &Env) -> String { + String::from_str(env, "EC Test") +} + +fn set_ts(env: &Env, ts: u64) { + env.ledger().set_timestamp(ts); +} + +/// Returns all topics of the most recently emitted event named `name`. +fn topics_of(env: &Env, name: &str) -> Option> { + let target = Symbol::new(env, name); + let mut found: Option> = None; + for (_addr, topics, _data) in env.events().all().iter() { + if topics.is_empty() { + continue; + } + if let Ok(sym) = Symbol::try_from_val(env, &topics.get(0).unwrap()) { + if sym == target { + found = Some(topics); + } + } + } + found +} + +fn last_category(env: &Env) -> Option { + let (_addr, topics, _data) = env.events().all().last()?; + let n = topics.len(); + if n < 2 { + return None; + } + NotificationCategory::try_from_val(env, &topics.get(n - 2)?).ok() +} + +fn last_priority(env: &Env) -> Option { + let (_addr, topics, _data) = env.events().all().last()?; + NotificationPriority::try_from_val(env, &topics.last()?).ok() +} + +// ============================================================================ +// Payload validation — configure_notification_limits +// ============================================================================ + +#[test] +fn test_configure_limits_valid_values_accepted() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + + // Valid: min < max, all non-zero + client.configure_notification_limits( + &test_env.admin, + &1024u32, // max_payload_size + &86_400u64, // max_expiration_seconds (1 day) + &60u64, // min_expiration_seconds (1 min) + &50u32, // max_batch_size + ); + + let limits = client.get_notification_limits(); + assert_eq!(limits.max_payload_size, 1024); + assert_eq!(limits.max_expiration_seconds, 86_400); + assert_eq!(limits.min_expiration_seconds, 60); + assert_eq!(limits.max_batch_size, 50); +} + +#[test] +fn test_configure_limits_emits_event_with_admin_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + + client.configure_notification_limits( + &test_env.admin, + &512u32, + &3_600u64, + &1u64, + &10u32, + ); + + assert!( + topics_of(&test_env.env, "notification_limits_configured").is_some(), + "notification_limits_configured event must be emitted" + ); + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Admin), + "limits config event must carry Admin category" + ); +} + +#[test] +fn test_configure_limits_non_admin_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let non_admin = Address::generate(&test_env.env); + + let result = client.try_configure_notification_limits( + &non_admin, + &1024u32, + &3_600u64, + &1u64, + &10u32, + ); + assert!(result.is_err(), "non-admin must not configure limits"); +} + +#[test] +fn test_configure_limits_min_greater_than_max_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + + // min_expiration_seconds > max_expiration_seconds — invalid + let result = client.try_configure_notification_limits( + &test_env.admin, + &1024u32, + &60u64, // max = 60s + &3_600u64, // min = 1h — greater than max + &10u32, + ); + assert!(result.is_err(), "min > max expiration must be rejected"); +} + +#[test] +fn test_configure_limits_zero_batch_size_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + + let result = client.try_configure_notification_limits( + &test_env.admin, + &1024u32, + &3_600u64, + &1u64, + &0u32, // zero batch size invalid + ); + assert!(result.is_err(), "zero max_batch_size must be rejected"); +} + +// ============================================================================ +// Payload validation — batch per-entry TTL overflow +// ============================================================================ + +#[test] +fn test_batch_ttl_overflow_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + + // Set a non-zero timestamp so u64::MAX + timestamp overflows. + set_ts(&test_env.env, 1_000); + + let mut ids: Vec> = Vec::new(&test_env.env); + let mut ttls: Vec = Vec::new(&test_env.env); + let mut titles: Vec = Vec::new(&test_env.env); + ids.push_back(make_id(&test_env.env, 1)); + ttls.push_back(u64::MAX); // will overflow when added to timestamp + titles.push_back(title(&test_env.env)); + + let result = client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles); + assert!( + result.is_err(), + "batch with overflow TTL must be rejected" + ); +} + +// ============================================================================ +// Payload validation — audit helpers on unknown notifications +// ============================================================================ + +#[test] +fn test_record_delivery_attempt_on_unknown_id_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let actor = test_env.users.get(0).unwrap().clone(); + + let ghost = make_id(&test_env.env, 10); + let result = client.try_record_delivery_attempt(&ghost, &actor); + assert!( + result.is_err(), + "delivery attempt on unknown notification must be rejected" + ); +} + +#[test] +fn test_record_delivery_failure_on_unknown_id_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let actor = test_env.users.get(0).unwrap().clone(); + + let ghost = make_id(&test_env.env, 11); + let result = client.try_record_delivery_failure(&ghost, &actor); + assert!( + result.is_err(), + "delivery failure on unknown notification must be rejected" + ); +} + +#[test] +fn test_record_acknowledgment_on_unknown_id_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let actor = test_env.users.get(0).unwrap().clone(); + + let ghost = make_id(&test_env.env, 12); + let result = client.try_record_acknowledgment(&ghost, &actor); + assert!( + result.is_err(), + "acknowledgment on unknown notification must be rejected" + ); +} + +// ============================================================================ +// Payload validation — audit helpers on revoked / expired notifications +// ============================================================================ + +#[test] +fn test_delivery_attempt_on_revoked_notification_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let relay = test_env.users.get(1).unwrap().clone(); + + let id = make_id(&test_env.env, 20); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.revoke_notification(&id, &creator); + + let result = client.try_record_delivery_attempt(&id, &relay); + assert!( + result.is_err(), + "delivery attempt on revoked notification must be rejected" + ); +} + +#[test] +fn test_acknowledgment_on_expired_notification_rejected() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let recipient = test_env.users.get(2).unwrap().clone(); + + set_ts(&test_env.env, 1_000); + let id = make_id(&test_env.env, 21); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + + // Advance past expiry and finalise. + set_ts(&test_env.env, 1_000 + ONE_HOUR); + client.expire_notification(&id); + + let result = client.try_record_acknowledgment(&id, &recipient); + assert!( + result.is_err(), + "acknowledgment on expired notification must be rejected" + ); +} + +// ============================================================================ +// Event type filtering — CategoryRegistered carries category + priority +// ============================================================================ + +#[test] +fn test_category_registered_event_carries_category_and_priority() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + + client.register_category(&test_env.admin, &NotificationCategory::Group); + + let topics = topics_of(&test_env.env, "category_registered") + .expect("category_registered event must be emitted"); + + // topics: [0] name, [1] admin, [2] category, [3] priority + assert_eq!(topics.len(), 4, "category_registered must have 4 topics"); + + let category = + NotificationCategory::try_from_val(&test_env.env, &topics.get(2).unwrap()) + .expect("topic[2] must be a NotificationCategory"); + assert_eq!( + category, + NotificationCategory::Group, + "registered category must match" + ); + + let _priority = + NotificationPriority::try_from_val(&test_env.env, &topics.get(3).unwrap()) + .expect("topic[3] must be a NotificationPriority"); +} + +// ============================================================================ +// Event type filtering — NotificationRevoked has Notification category +// ============================================================================ + +#[test] +fn test_revoke_notification_event_has_notification_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + + let id = make_id(&test_env.env, 30); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.revoke_notification(&id, &creator); + + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Notification), + "notification_revoked must carry Notification category" + ); +} + +// ============================================================================ +// Event type filtering — AuditRecordAppended carries Notification category +// ============================================================================ + +#[test] +fn test_audit_record_appended_event_has_notification_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let relay = test_env.users.get(1).unwrap().clone(); + + let id = make_id(&test_env.env, 40); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + + // The AuditRecordAppended event is emitted alongside NotificationScheduled. + // After a delivery attempt it should again carry the Notification category. + client.record_delivery_attempt(&id, &relay); + + let topics = topics_of(&test_env.env, "audit_record_appended") + .expect("audit_record_appended must be emitted"); + + // topics: [0] name, [1] notification_id, [2] action, [3] category + assert_eq!(topics.len(), 4); + let category = + NotificationCategory::try_from_val(&test_env.env, &topics.get(3).unwrap()) + .expect("topic[3] must be NotificationCategory"); + assert_eq!( + category, + NotificationCategory::Notification, + "audit_record_appended must carry Notification category" + ); +} + +#[test] +fn test_audit_record_appended_carries_correct_action_topic() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let relay = test_env.users.get(1).unwrap().clone(); + + let id = make_id(&test_env.env, 41); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.record_delivery_failure(&id, &relay); + + let topics = topics_of(&test_env.env, "audit_record_appended").unwrap(); + let action = + AuditAction::try_from_val(&test_env.env, &topics.get(2).unwrap()) + .expect("topic[2] must be AuditAction"); + // Most recent audit_record_appended should be for the delivery failure. + assert_eq!( + action, + AuditAction::DeliveryFailed, + "most recent audit event action must be DeliveryFailed" + ); +} + +// ============================================================================ +// Event type filtering — all emitted events have category + priority topics +// ============================================================================ + +#[test] +fn test_ownership_transfer_events_have_admin_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let new_owner = Address::generate(&test_env.env); + + client.initiate_ownership_transfer(&test_env.admin, &new_owner); + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Admin), + "ownership_transfer_initiated must carry Admin category" + ); + assert_eq!( + last_priority(&test_env.env), + Some(NotificationPriority::Critical), + "ownership transfer must be Critical priority" + ); + + client.accept_ownership(&new_owner); + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Admin), + "ownership_transferred must carry Admin category" + ); + assert_eq!( + last_priority(&test_env.env), + Some(NotificationPriority::Critical), + ); +} + +#[test] +fn test_notification_expired_event_has_notification_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + + set_ts(&test_env.env, 2_000); + let id = make_id(&test_env.env, 50); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + + set_ts(&test_env.env, 2_000 + ONE_HOUR); + client.expire_notification(&id); + + let topics = topics_of(&test_env.env, "notification_expired") + .expect("notification_expired event must be emitted"); + // topics: [0] name, [1] notification_id, [2] category, [3] priority + let category = + NotificationCategory::try_from_val(&test_env.env, &topics.get(2).unwrap()).unwrap(); + assert_eq!(category, NotificationCategory::Notification); +} + +#[test] +fn test_notification_delivered_event_has_notification_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + + let id = make_id(&test_env.env, 51); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.confirm_notification_delivery(&id, &creator); + + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Notification), + "notification_delivered must carry Notification category" + ); +} + +#[test] +fn test_notification_recalled_event_has_notification_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + + let id = make_id(&test_env.env, 52); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.recall_notification(&id, &creator); + + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Notification), + "notification_recalled must carry Notification category" + ); +} + +// ============================================================================ +// Audit logging — log grows monotonically across multiple notifications +// ============================================================================ + +#[test] +fn test_audit_log_grows_across_independent_notifications() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let relay = test_env.users.get(1).unwrap().clone(); + + // Interleave operations on three independent notifications. + let ids: [BytesN<32>; 3] = [ + make_id(&test_env.env, 60), + make_id(&test_env.env, 61), + make_id(&test_env.env, 62), + ]; + + for id in &ids { + client.schedule_notification(id, &creator, &ONE_HOUR, &title(&test_env.env)); + } + // 3 Created records. + assert_eq!(client.get_audit_log().len(), 3); + + client.record_delivery_attempt(&ids[0], &relay); + client.record_delivery_attempt(&ids[1], &relay); + // 5 records total. + assert_eq!(client.get_audit_log().len(), 5); + + client.record_delivery_failure(&ids[0], &relay); + client.record_acknowledgment(&ids[2], &creator); + // 7 records total. + assert_eq!(client.get_audit_log().len(), 7); + + // Sequence numbers are strictly ascending. + let log = client.get_audit_log(); + for i in 1..log.len() { + assert!(log.get(i).unwrap().seq > log.get(i - 1).unwrap().seq); + } +} + +#[test] +fn test_audit_log_seq_starts_at_one() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + + let id = make_id(&test_env.env, 70); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + + let log = client.get_audit_log(); + assert_eq!(log.len(), 1); + assert_eq!(log.get(0).unwrap().seq, 1, "first seq must be 1"); +} + +#[test] +fn test_audit_records_per_notification_match_full_log_subset() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let relay = test_env.users.get(1).unwrap().clone(); + + let id_a = make_id(&test_env.env, 80); + let id_b = make_id(&test_env.env, 81); + + client.schedule_notification(&id_a, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id_b, &creator, &ONE_HOUR, &title(&test_env.env)); + client.record_delivery_attempt(&id_a, &relay); + client.record_delivery_failure(&id_a, &relay); + client.record_delivery_attempt(&id_b, &relay); + + // Full log: 5 records total. + assert_eq!(client.get_audit_log().len(), 5); + + // Per-notification filter must match a subset of the full log. + let records_a = client.get_notification_audit(&id_a); + assert_eq!(records_a.len(), 3); // Created + Attempt + Failed + + let records_b = client.get_notification_audit(&id_b); + assert_eq!(records_b.len(), 2); // Created + Attempt + + // Every record must belong to the queried notification. + for r in records_a.iter() { + assert_eq!(r.notification_id, id_a); + } + for r in records_b.iter() { + assert_eq!(r.notification_id, id_b); + } +} + +// ============================================================================ +// Payload validation — consumers can filter via category on every event type +// ============================================================================ + +#[test] +fn test_subscription_cancelled_has_group_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let token = test_env.mock_tokens.get(0).unwrap().clone(); + + crate::test_utils::mint_tokens(&test_env.env, &token, &creator, 1_000_000); + let id = crate::test_utils::create_test_group( + &test_env.env, + &test_env.autoshare_contract, + &creator, + &Vec::new(&test_env.env), + 1, + &token, + ); + + client.cancel_subscription(&id, &creator); + + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Group), + "subscription_cancelled must carry Group category" + ); +} + +#[test] +fn test_schema_version_set_has_admin_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + + client.set_schema_version(&test_env.admin, &1u32); + + assert_eq!( + last_category(&test_env.env), + Some(NotificationCategory::Admin), + "schema_version_set must carry Admin category" + ); +} + +#[test] +fn test_notification_accessed_has_notification_category() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let accessor = test_env.users.get(1).unwrap().clone(); + + let id = make_id(&test_env.env, 90); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.record_notification_access(&id, &accessor); + + let topics = topics_of(&test_env.env, "notification_accessed") + .expect("notification_accessed event must be emitted"); + // topics: [0] name, [1] notification_id, [2] accessor, [3] category + assert_eq!(topics.len(), 4); + let category = + NotificationCategory::try_from_val(&test_env.env, &topics.get(3).unwrap()).unwrap(); + assert_eq!(category, NotificationCategory::Notification); +} diff --git a/dashboard/.dockerignore b/dashboard/.dockerignore new file mode 100644 index 0000000..75699eb --- /dev/null +++ b/dashboard/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.env +.env.* +!.env.example +coverage +*.log diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile new file mode 100644 index 0000000..2ab358e --- /dev/null +++ b/dashboard/Dockerfile @@ -0,0 +1,37 @@ +FROM node:22-slim AS base +WORKDIR /app + +# ── deps ────────────────────────────────────────────────────────────────────── +FROM base AS deps +COPY package*.json ./ +RUN npm ci + +# ── dev: live-reloading dev server ──────────────────────────────────────────── +# Vite VITE_* vars are embedded at build time — pass them as build args. +FROM deps AS dev +ARG VITE_EVENTS_API_URL=http://localhost:8787/api/events +ARG VITE_STELLAR_NETWORK=TESTNET +ENV VITE_EVENTS_API_URL=$VITE_EVENTS_API_URL +ENV VITE_STELLAR_NETWORK=$VITE_STELLAR_NETWORK +COPY . . +EXPOSE 5173 +# --host binds to 0.0.0.0 so the port is reachable from outside the container +CMD ["node", "./node_modules/vite/bin/vite.js", "--host"] + +# ── build: production static assets ────────────────────────────────────────── +FROM deps AS build +ARG VITE_EVENTS_API_URL=http://localhost:8787/api/events +ARG VITE_STELLAR_NETWORK=TESTNET +ENV VITE_EVENTS_API_URL=$VITE_EVENTS_API_URL +ENV VITE_STELLAR_NETWORK=$VITE_STELLAR_NETWORK +COPY . . +RUN npm run build + +# ── production: serve built assets via Vite preview ────────────────────────── +FROM node:22-slim AS production +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=build /app/dist ./dist +EXPOSE 4173 +CMD ["node", "./node_modules/vite/bin/vite.js", "preview", "--host", "--port", "4173"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bba6a71 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +services: + + # ── Listener: off-chain event processor & REST API (port 8787) ────────────── + listener: + build: + context: ./listener + target: production + ports: + - "8787:8787" + environment: + # Stellar connectivity — update CONTRACT_ADDRESSES with your deployed contract ID + STELLAR_RPC_URL: ${STELLAR_RPC_URL:-https://soroban-testnet.stellar.org:443} + STELLAR_NETWORK: ${STELLAR_NETWORK:-testnet} + STELLAR_NETWORK_PASSPHRASE: ${STELLAR_NETWORK_PASSPHRASE:-Test SDF Network ; September 2015} + CONTRACT_ADDRESSES: ${CONTRACT_ADDRESSES:-[{"address":"CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","events":["*"]}]} + # API server + EVENTS_API_PORT: "8787" + EVENTS_API_CORS_ORIGIN: "http://localhost:5173" + # Database — stored in the named volume below + DATABASE_PATH: /app/data/notifications.db + # Logging + LOG_LEVEL: ${LOG_LEVEL:-info} + # Optional Discord delivery (leave blank to disable) + DISCORD_WEBHOOK_URL: ${DISCORD_WEBHOOK_URL:-} + DISCORD_WEBHOOK_ID: ${DISCORD_WEBHOOK_ID:-} + volumes: + # Persist SQLite database across container restarts + - listener_data:/app/data + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:8787/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s + + # ── Dashboard: React + Vite dev server with HMR (port 5173) ───────────────── + # + # NOTE: Vite VITE_* variables are embedded at BUILD time, not at runtime. + # To change the API URL, set VITE_EVENTS_API_URL in your shell before running + # `docker compose up --build`, or export it in a .env file at the repo root. + dashboard: + build: + context: ./dashboard + target: dev + args: + # Passed as a build arg so Vite can embed it — defaults to the listener + # container reachable via the host-mapped port. + VITE_EVENTS_API_URL: ${VITE_EVENTS_API_URL:-http://localhost:8787/api/events} + VITE_STELLAR_NETWORK: ${VITE_STELLAR_NETWORK:-TESTNET} + ports: + - "5173:5173" + volumes: + # Mount source for hot module replacement — changes reflect without rebuild + - ./dashboard/src:/app/src:ro + - ./dashboard/index.html:/app/index.html:ro + - ./dashboard/vite.config.ts:/app/vite.config.ts:ro + depends_on: + listener: + condition: service_healthy + restart: unless-stopped + +volumes: + listener_data: + driver: local diff --git a/listener/.dockerignore b/listener/.dockerignore new file mode 100644 index 0000000..a1aa013 --- /dev/null +++ b/listener/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +data +*.db +*.db-journal +.env +.env.* +!.env.example +coverage +*.log diff --git a/listener/Dockerfile b/listener/Dockerfile new file mode 100644 index 0000000..b41c0e3 --- /dev/null +++ b/listener/Dockerfile @@ -0,0 +1,43 @@ +FROM node:22-slim AS base +WORKDIR /app + +# Build tools required for native sqlite3 bindings +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# ── deps: install all deps (including devDeps needed to compile) ────────────── +FROM base AS deps +COPY package*.json ./ +RUN npm ci + +# ── build: compile TypeScript to dist/ ─────────────────────────────────────── +FROM deps AS build +COPY tsconfig.json ./ +COPY src/ ./src/ +RUN npm run build + +# ── production: minimal runtime image ──────────────────────────────────────── +FROM node:22-slim AS production +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY package*.json ./ +RUN npm ci --omit=dev + +COPY --from=build /app/dist ./dist + +# Persistent SQLite data directory — mount a named volume here +RUN mkdir -p /app/data + +EXPOSE 8787 + +# Migrate then start. Migration script is compiled to dist/scripts/migrate-db.js +CMD ["sh", "-c", "node dist/scripts/migrate-db.js && node dist/index.js"]