diff --git a/.kiro/specs/notify-chain-improvements/.config.kiro b/.kiro/specs/notify-chain-improvements/.config.kiro new file mode 100644 index 0000000..8d19582 --- /dev/null +++ b/.kiro/specs/notify-chain-improvements/.config.kiro @@ -0,0 +1 @@ +{"specId": "16f73cd2-f721-43da-8f45-21b0feb74e4c", "workflowType": "design-first", "specType": "feature"} diff --git a/.kiro/specs/notify-chain-improvements/design.md b/.kiro/specs/notify-chain-improvements/design.md new file mode 100644 index 0000000..57633ed --- /dev/null +++ b/.kiro/specs/notify-chain-improvements/design.md @@ -0,0 +1,431 @@ +# Design Document: Notify-Chain Improvements + +## Overview + +This document covers three independent improvements to the Notify-Chain repository: + +1. **Task 1 — Secure Credential Management**: Harden the repository against accidental secret leakage by closing a `.gitignore` gap in the dashboard, completing missing entries in `listener/.env.example`, and adding a unified Configuration reference section to the root `README.md`. + +2. **Task 2 — Gas Consumption Tracking**: Introduce a reproducible gas-measurement test module for the AutoShare Soroban contract, persist the results as a JSON snapshot file, document human-readable cost figures in `GAS_USAGE.md`, and add a CI gate that fails when any function regresses beyond 10 % of its baseline. + +3. **Task 3 — Subscription Edge-Case Tests**: Expand the Rust test suite for the `topup_subscription` and `reduce_usage` functions to cover the eight missing edge cases identified during review, and register the new test module in `lib.rs`. + +Each task is self-contained and can be committed to its own branch independently. + +--- + +## Architecture + +```mermaid +graph TD + subgraph Task1["Task 1 · Credential Management"] + A1["dashboard/.gitignore
add .env"] + A2["listener/.env.example
add missing vars"] + A3["README.md
add Config section"] + end + + subgraph Task2["Task 2 · Gas Tracking"] + B1["gas_snapshot_test.rs
measure budget per fn"] + B2["contract/gas-snapshots/autoshare.json
baseline values"] + B3["GAS_USAGE.md
human docs"] + B4[".github/workflows/ci.yml
regression gate step"] + B1 -->|writes| B2 + B2 -->|read by| B4 + B1 -->|documents| B3 + end + + subgraph Task3["Task 3 · Subscription Edge Cases"] + C1["subscription_edge_cases_test.rs
8 new test functions"] + C2["lib.rs
register module"] + C1 -->|registered via| C2 + end +``` + +--- + +## Components and Interfaces + +See per-task subsections below. + +## Data Models + +See per-task subsections below. + +## Error Handling + +See per-task subsections below. + +## Testing Strategy + +See per-task subsections below. + +--- + +## Task 1 — Secure Credential Management + +### Components and Interfaces + +#### Component: `dashboard/.gitignore` + +**Purpose**: Prevent the dashboard's `.env` file from being committed. + +**Change**: Append `.env` to the existing ignore list. + +**Current state**: +``` +node_modules +dist +.DS_Store +``` + +**Target state** (additions only): +``` +.env +.env.local +.env.*.local +``` + +#### Component: `listener/.env.example` + +**Purpose**: Document every environment variable the listener reads so operators can configure a new deployment without reading source code. + +**Missing variables** (identified by diffing `config.ts` against `.env.example`): + +| Variable | Type | Default | Description | +|---|---|---|---| +| `DISCORD_WEBHOOK_ID` | string | — | The numeric ID portion of the Discord webhook URL; required when `DISCORD_WEBHOOK_URL` is set | +| `RATE_LIMIT_ENABLED` | boolean | `true` | Set to `false` to disable the API rate limiter | +| `RATE_LIMIT_WINDOW_MS` | integer | `60000` | Rolling window length for rate-limit counting (ms) | +| `RATE_LIMIT_MAX_REQUESTS` | integer | `60` | Max requests per client within the window | +| `RATE_LIMIT_CLIENT_OVERRIDES` | JSON object | `{}` | Per-client rate-limit overrides, keyed by client ID | + +These are already parsed by `loadConfig()` in `listener/src/config.ts`; the example file simply lacks corresponding documentation entries. + +#### Component: `README.md` (root) + +**Purpose**: Provide a single, authoritative reference for all environment variables so contributors do not need to read source files. + +**New section**: `## Configuration / Environment Variables` + +The section will contain two sub-sections: + +- **Listener (`listener/.env`)** — all variables from `listener/.env.example` with type, default, and description +- **Dashboard (`dashboard/.env`)** — all variables from `dashboard/.env.example` with type, default, and description + +### Data Models + +No new data structures are introduced. The change is purely to non-code files. + +### Error Handling + +Not applicable — these are documentation and configuration changes. + +### Testing Strategy + +**Unit Testing Approach**: Not applicable for `.gitignore` and `.env.example` edits. + +**Property-Based Testing Approach**: Not applicable. + +**Integration Testing Approach**: Manual smoke-test by verifying `git status` does not surface a created `.env` file in the dashboard directory after the `.gitignore` change is applied. + +--- + +## Task 2 — Gas Consumption Tracking + +### Architecture + +```mermaid +sequenceDiagram + participant CI as CI Job (cargo test) + participant GasTest as gas_snapshot_test.rs + participant Budget as env.budget() + participant Snapshot as autoshare.json + participant Gate as CI regression step + + CI->>GasTest: cargo test --all-features + GasTest->>Budget: reset_unlimited() before call + GasTest->>Budget: measure cpu_insns + mem_bytes after call + GasTest->>Snapshot: write baseline (first run) + CI->>Gate: read autoshare.json + compare + Gate-->>CI: fail if delta > 10% +``` + +### Components and Interfaces + +#### Component: `gas_snapshot_test.rs` + +**Purpose**: Measure and assert Soroban budget consumption for each critical function under representative inputs. + +**Location**: `contract/contracts/hello-world/src/tests/gas_snapshot_test.rs` + +**Key functions measured**: + +```rust +// Pseudocode — actual Soroban testutils API +fn measure_budget(env: &Env, call: impl FnOnce()) -> BudgetSnapshot { + env.budget().reset_unlimited(); + call(); + BudgetSnapshot { + cpu_insns: env.budget().cpu_insns_consumed(), + mem_bytes: env.budget().mem_bytes_consumed(), + } +} +``` + +Each test function follows this pattern: + +```rust +#[test] +fn snapshot_create() { + let test_env = setup_test_env(); + let token = test_env.mock_tokens.get(0).unwrap(); + let creator = test_env.users.get(0).unwrap(); + mint_tokens(&test_env.env, &token, &creator, 10_000_000); + + test_env.env.budget().reset_unlimited(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + client.create(&id, &name, &creator, &10u32, &token); + + let cpu = test_env.env.budget().cpu_insns_consumed(); + let mem = test_env.env.budget().mem_bytes_consumed(); + + // Assert values are non-zero and within plausible range + assert!(cpu > 0); + assert!(mem > 0); + // Print for snapshot capture: println!("create: cpu={cpu}, mem={mem}"); +} +``` + +Functions covered: +- `create` — baseline: 1 group, 10 usages, mock token, no members yet +- `topup_subscription` — baseline: existing group, 10 additional usages, same payer +- `update_members` — baseline: group with 3 members replacing existing empty list +- `withdraw` — baseline: admin withdraws 100 tokens to recipient + +#### Component: `contract/gas-snapshots/autoshare.json` + +**Purpose**: Machine-readable baseline that the CI regression gate reads. + +**Schema**: + +```json +{ + "version": 1, + "functions": { + "create": { + "cpu_insns": 0, + "mem_bytes": 0, + "description": "Create a new AutoShare group (10 usages, no members)" + }, + "topup_subscription": { + "cpu_insns": 0, + "mem_bytes": 0, + "description": "Top up existing group with 10 additional usages" + }, + "update_members": { + "cpu_insns": 0, + "mem_bytes": 0, + "description": "Set 3 members with equal percentage split" + }, + "withdraw": { + "cpu_insns": 0, + "mem_bytes": 0, + "description": "Admin withdraws 100 tokens" + } + } +} +``` + +Values are initialised to `0` in the committed skeleton; the first successful CI run (or a local `cargo test -- --nocapture` run) populates them with real measurements that are then committed. + +#### Component: `GAS_USAGE.md` + +**Purpose**: Human-readable explanation of what each function costs and how to interpret or update snapshots. + +**Sections**: +- Overview of Soroban resource metering +- Table of function baselines (populated after first measurement run) +- How to update the snapshot (`cargo test gas_snapshot`) +- CI regression policy (10 % tolerance) + +#### Component: CI regression gate + +**Purpose**: Detect performance regressions before merge. + +**Mechanism**: After the existing `cargo test` step, add a new step that: +1. Re-runs only the gas snapshot tests with `-- --nocapture` to capture printed measurements +2. Parses the output lines (format: `FUNCTION: cpu=NNN mem=NNN`) +3. Compares against `autoshare.json` baselines +4. Exits non-zero if any metric exceeds `baseline * 1.10` + +The step uses a small inline shell script (no external action required) that reads the JSON with `jq` and performs integer arithmetic. + +```yaml +- name: Gas regression check + working-directory: contract + run: | + output=$(cargo test --all-features gas_snapshot -- --nocapture 2>&1) + echo "$output" + baseline=$(cat contracts/hello-world/gas-snapshots/autoshare.json) + # parse and compare using jq + awk + # exit 1 if any function exceeds 110% of baseline +``` + +### Data Models + +``` +BudgetSnapshot { + cpu_insns: u64, + mem_bytes: u64, +} +``` + +This is an ephemeral struct used only within the test module — it is never stored on-chain. + +### Error Handling + +- If the snapshot JSON is malformed, `jq` will exit non-zero and the CI step will fail with a clear parse error. +- If the snapshot file is missing, the gate step skips comparison and emits a warning (does not fail — first-run scenario). + +### Testing Strategy + +**Unit Testing Approach**: Each snapshot test asserts that `cpu_insns > 0` and `mem_bytes > 0`, acting as a sanity check that measurement is working. + +**Property-Based Testing Approach**: Not applicable — gas measurement is deterministic for a given input, so PBT adds no value here. + +**Integration Testing Approach**: The CI gate is itself an integration test between the test output and the baseline JSON. + +--- + +## Task 3 — Subscription Edge-Case Tests + +### Architecture + +All new tests live in a single new file and are registered in the existing `lib.rs` `tests` module block. No production code changes are required — the edge cases exercise existing contract logic. + +```mermaid +graph LR + LIB["lib.rs\n#[path] mod subscription_edge_cases_test"] --> ECT["subscription_edge_cases_test.rs"] + ECT -->|uses| TU["test_utils.rs\nsetup_test_env, create_test_group, mint_tokens"] + ECT -->|calls| CLIENT["AutoShareContractClient\ntopup_subscription, reduce_usage, create, get"] +``` + +### Components and Interfaces + +#### Component: `subscription_edge_cases_test.rs` + +**Purpose**: Cover the eight missing edge cases for subscription lifecycle. + +**Location**: `contract/contracts/hello-world/src/tests/subscription_edge_cases_test.rs` + +**Test inventory**: + +| # | Test name | Expected behaviour | Error variant | +|---|---|---|---| +| 1 | `test_duplicate_create_error_type` | `create` with same ID panics with `AlreadyExists` | `Error::AlreadyExists` | +| 2 | `test_topup_nonexistent_group` | `topup_subscription` on unknown ID panics | `Error::NotFound` | +| 3 | `test_topup_inactive_group_succeeds` | `topup_subscription` on deactivated group **succeeds**; `usage_count` and `total_usages_paid` both increase | — | +| 4 | `test_multiple_sequential_topups` | Three consecutive topups accumulate correctly in `usage_count` and `total_usages_paid` | — | +| 5 | `test_reduce_to_zero_then_reduce_again` | `reduce_usage` N times to reach zero, next call panics | `Error::NoUsagesRemaining` | +| 6 | `test_large_usage_count` | `topup_subscription` with 10 000 usages; verify arithmetic exact | — | +| 7 | `test_topup_different_payer` | Third-party address (not the creator) successfully tops up | — | +| 8 | `test_topup_insufficient_balance` | Payer has 0 tokens; call panics with token transfer failure | token panic | + +**Key design decisions**: + +- **Test 3 (inactive group topup)**: The `topup_subscription` logic in `autoshare_logic.rs` does **not** check `is_active` — it only checks that the group exists. Therefore, topping up a deactivated group succeeds. The test must verify this is intentional by asserting the state after the call. + +- **Test 5 (reduce to zero then again)**: Uses `reduce_usage` N times in a loop (N = initial `usage_count`), then asserts the (N+1)-th call panics. The `#[should_panic]` attribute applies to the final call. + +- **Test 8 (insufficient balance)**: Does not mint any tokens for the payer before calling `topup_subscription`. The MockToken `transfer` will panic when the sender has insufficient balance. + +**Pattern for each test** (following repo conventions): + +```rust +use crate::test_utils::{create_test_group, mint_tokens, setup_test_env}; +use crate::AutoShareContractClient; +use soroban_sdk::{testutils::Address as _, Address, BytesN}; + +#[test] +fn test_topup_inactive_group_succeeds() { + 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(); + + let id = create_test_group(&test_env.env, &test_env.autoshare_contract, &creator, + &soroban_sdk::Vec::new(&test_env.env), 5, &token); + client.deactivate_group(&id, &creator); + assert!(!client.is_group_active(&id)); + + // top up from the same creator (has balance from create_test_group) + mint_tokens(&test_env.env, &token, &creator, 1000); + client.topup_subscription(&id, &10u32, &token, &creator); + + let details = client.get(&id); + assert_eq!(details.usage_count, 15); + assert_eq!(details.total_usages_paid, 15); + assert!(!client.is_group_active(&id)); // still inactive +} +``` + +#### Component: `lib.rs` (registration) + +Add to the `mod tests` block: + +```rust +#[path = "../tests/subscription_edge_cases_test.rs"] +mod subscription_edge_cases_test; +``` + +### Data Models + +No new data structures. Tests use `AutoShareDetails` fields `usage_count` and `total_usages_paid` directly. + +### Error Handling + +Tests rely on Soroban's `#[should_panic]` attribute. Where possible, test names and comments call out the specific `Error` variant expected (e.g., `NotFound`, `NoUsagesRemaining`) so future developers can tighten assertions if the SDK exposes typed panic matching. + +### Testing Strategy + +**Unit Testing Approach**: Each test is a `#[test]` function that exercises one scenario. Success tests use direct `assert_eq!` / `assert!` on retrieved state. Panic tests use `#[should_panic]`. + +**Property-Based Testing Approach**: Not applicable — the scenarios are specific boundary conditions, not universal properties across arbitrary inputs. + +**Integration Testing Approach**: The new tests are integrated into the existing `cargo test --workspace --all-features` invocation in CI with no CI changes needed for this task. + +--- + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Topup accumulates usage counts correctly + +For any existing group with initial `usage_count = U` and `total_usages_paid = P`, after calling `topup_subscription` with `additional_usages = N`, the group's `usage_count` becomes `U + N` and `total_usages_paid` becomes `P + N`. + +**Validates: Requirements 11.2, 11.3, 13.3, 13.4** + +### Property 2: Topup succeeds regardless of group active status + +For any existing group (whether active or inactive), `topup_subscription` with a valid token and sufficient payer balance succeeds and updates `usage_count` and `total_usages_paid`. + +**Validates: Requirements 10.2, 10.3, 10.4, 10.5** + +### Property 3: reduce_usage terminates at zero + +For any group with `usage_count = N`, after exactly N calls to `reduce_usage` the count reaches zero, and the (N+1)-th call raises `NoUsagesRemaining`. + +**Validates: Requirements 12.2, 12.3** + +### Property 4: Gas baselines are non-trivially positive + +For any measured contract function, `cpu_insns > 0` and `mem_bytes > 0` after a successful call, confirming the budget measurement is operational. + +**Validates: Requirements 4.6** + +### Property 5: Snapshot regression is bounded + +For any contract function whose baseline is recorded in `autoshare.json`, re-measuring on the same code produces a value within 110 % of the stored baseline. + +**Validates: Requirements 6.2, 6.3** diff --git a/.kiro/specs/notify-chain-improvements/requirements.md b/.kiro/specs/notify-chain-improvements/requirements.md new file mode 100644 index 0000000..30addd9 --- /dev/null +++ b/.kiro/specs/notify-chain-improvements/requirements.md @@ -0,0 +1,227 @@ +# Requirements Document + +## Introduction + +This document captures the formal requirements for three independent improvements to the Notify-Chain repository. The improvements address a credential-management gap in the dashboard, introduce gas-consumption tracking for the AutoShare Soroban contract, and expand the Rust test suite with subscription edge-case coverage. + +Each requirement group is independently deliverable and can be merged as a separate pull request. + +--- + +## Glossary + +- **AutoShare Contract**: The Soroban/Rust smart contract located in `contract/contracts/hello-world/`. +- **Dashboard**: The React/Vite frontend located in `dashboard/`. +- **Listener**: The Node.js/TypeScript off-chain event listener located in `listener/`. +- **Gas Snapshot**: A JSON file containing baseline CPU instruction and memory byte counts for measured contract functions. +- **Budget API**: The Soroban testutils `env.budget()` interface that exposes `cpu_insns_consumed()` and `mem_bytes_consumed()` after a contract call. +- **Group**: An AutoShare subscription group identified by a `BytesN<32>` ID; has `usage_count` (remaining) and `total_usages_paid` (cumulative) fields. +- **Topup**: A call to `topup_subscription` that increases a group's `usage_count` and `total_usages_paid` by `additional_usages`. +- **Reduce**: A call to `reduce_usage` that decrements a group's `usage_count` by 1. +- **CI**: The GitHub Actions continuous integration pipeline defined in `.github/workflows/ci.yml`. + +--- + +## Requirements + +### Requirement 1: Prevent Accidental `.env` Commits in the Dashboard + +**User Story:** As a contributor, I want the dashboard directory to exclude `.env` files from version control, so that I cannot accidentally commit secrets by running a standard `git add .`. + +#### Acceptance Criteria + +1. THE Dashboard `.gitignore` SHALL contain `.env` as an entry so that a file named `.env` created in the `dashboard/` directory is not tracked by Git. +2. THE Dashboard `.gitignore` SHALL contain `.env.local` and `.env.*.local` as entries to cover common Vite-flavored environment file variants. + +--- + +### Requirement 2: Complete the Listener `.env.example` + +**User Story:** As an operator setting up a new Notify-Chain deployment, I want the listener's example environment file to document every variable the listener reads, so that I do not have to read TypeScript source code to find missing configuration keys. + +#### Acceptance Criteria + +1. THE `listener/.env.example` SHALL contain a documented entry for `DISCORD_WEBHOOK_ID` that explains it is the numeric webhook identifier required alongside `DISCORD_WEBHOOK_URL`. +2. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_ENABLED` with a default value of `true` and a description of its effect. +3. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_WINDOW_MS` with a default value of `60000` and a unit annotation (milliseconds). +4. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_MAX_REQUESTS` with a default value of `60` and a description. +5. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_CLIENT_OVERRIDES` with a default value of `{}` and a JSON-format example showing the per-client override schema. + +--- + +### Requirement 3: Add a Configuration Reference to the Root README + +**User Story:** As a new contributor, I want a single place in the repository to find every environment variable for both the listener and the dashboard, so that I can configure my local development environment without reading multiple source files. + +#### Acceptance Criteria + +1. THE root `README.md` SHALL contain a section headed `## Configuration / Environment Variables`. +2. WITHIN the Configuration section, THE `README.md` SHALL document all environment variables read by the Listener, including each variable's name, type, default value, and a short description. +3. WITHIN the Configuration section, THE `README.md` SHALL document all environment variables read by the Dashboard, including each variable's name, type, default value, and a short description. +4. WHEN a variable has no default (i.e., it is required), THE `README.md` SHALL mark it as required rather than listing a default. + +--- + +### Requirement 4: Gas Snapshot Test Module + +**User Story:** As a contract developer, I want automated tests that measure the CPU instruction and memory byte cost of critical contract functions, so that I have a reproducible performance baseline. + +#### Acceptance Criteria + +1. THE AutoShare Contract SHALL have a test module at `contract/contracts/hello-world/src/tests/gas_snapshot_test.rs` that measures gas usage using the Soroban Budget API. +2. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `create` function under the baseline scenario (1 group, 10 usages, mock token, no members). +3. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `topup_subscription` function under the baseline scenario (existing group, 10 additional usages, same token). +4. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `update_members` function under the baseline scenario (group with 3 members replacing an empty list). +5. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `withdraw` function under the baseline scenario (admin withdraws 100 tokens to a recipient). +6. WHEN a gas snapshot test runs, THE test SHALL assert that `cpu_insns_consumed() > 0` and `mem_bytes_consumed() > 0`, confirming that measurement is active. +7. THE gas snapshot test module SHALL print each measurement to stdout in the format `FUNCTION: cpu=NNN mem=NNN` so that CI scripts can parse the values. +8. THE `lib.rs` test module block SHALL register the gas snapshot test module with `#[path = "../tests/gas_snapshot_test.rs"] mod gas_snapshot_test;`. + +--- + +### Requirement 5: Gas Snapshot Baseline File + +**User Story:** As a CI engineer, I want a machine-readable JSON file that records the baseline gas costs, so that the regression gate has a stable reference to compare against. + +#### Acceptance Criteria + +1. THE repository SHALL contain a gas snapshot file at `contract/contracts/hello-world/gas-snapshots/autoshare.json`. +2. THE snapshot file SHALL contain a top-level `"version"` field set to `1`. +3. THE snapshot file SHALL contain a `"functions"` object with entries for `"create"`, `"topup_subscription"`, `"update_members"`, and `"withdraw"`. +4. EACH function entry in the snapshot file SHALL contain `"cpu_insns"` and `"mem_bytes"` numeric fields and a `"description"` string field. +5. WHEN the snapshot values are zero (initial skeleton), THE CI regression gate SHALL skip comparison and emit a warning rather than failing. + +--- + +### Requirement 6: CI Gas Regression Gate + +**User Story:** As a maintainer, I want CI to block merges that increase any function's gas cost by more than 10 %, so that performance regressions are caught before they reach the main branch. + +#### Acceptance Criteria + +1. THE `.github/workflows/ci.yml` SHALL contain a step in the `rust` job that re-runs only the gas snapshot tests after the main `cargo test` step. +2. WHEN the gas regression step runs and the snapshot file contains non-zero baselines, THE CI step SHALL parse the printed measurements and compare each value against the corresponding baseline. +3. IF any measured `cpu_insns` or `mem_bytes` value exceeds its snapshot baseline by more than 10 %, THEN THE CI step SHALL exit with a non-zero status code and print which function exceeded its budget. +4. IF the snapshot file contains only zero baseline values, THEN THE CI step SHALL print a warning and exit with status zero (skip regression check). +5. THE CI regression gate step SHALL use only standard Unix tools (`jq`, `awk`, `bash`) available on the `ubuntu-latest` runner without additional installation steps. + +--- + +### Requirement 7: Gas Usage Documentation + +**User Story:** As a developer unfamiliar with Soroban resource accounting, I want a human-readable document that explains what each function costs and how to update the baselines, so that I can reason about the contract's on-chain economics. + +#### Acceptance Criteria + +1. THE repository SHALL contain a `GAS_USAGE.md` file at the repository root. +2. THE `GAS_USAGE.md` SHALL contain a section explaining Soroban CPU instruction and memory byte metering in plain language. +3. THE `GAS_USAGE.md` SHALL contain a table of the four measured functions listing their baseline `cpu_insns`, `mem_bytes`, and the scenario used for measurement. +4. THE `GAS_USAGE.md` SHALL contain instructions for how to update the snapshot (run the gas snapshot tests with `--nocapture`, copy printed values into `autoshare.json`). +5. THE `GAS_USAGE.md` SHALL document the 10 % regression tolerance policy. + +--- + +### Requirement 8: Duplicate Create Error-Type Test + +**User Story:** As a contract developer, I want a test that verifies the exact error type returned when `create` is called with a duplicate group ID, so that regressions in error classification are caught. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_duplicate_create_error_type` in `subscription_edge_cases_test.rs` that calls `create` twice with the same `BytesN<32>` ID. +2. WHEN `create` is called with a duplicate ID, THE test SHALL assert that the call panics (corresponding to `Error::AlreadyExists`). + +--- + +### Requirement 9: Topup on Non-Existent Group Test + +**User Story:** As a contract developer, I want a test that verifies `topup_subscription` panics when the target group ID does not exist, so that the NotFound guard is confirmed. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_topup_nonexistent_group` that calls `topup_subscription` with a `BytesN<32>` ID that was never passed to `create`. +2. WHEN `topup_subscription` is called with an unknown group ID, THE test SHALL assert that the call panics. + +--- + +### Requirement 10: Topup on Inactive Group Test + +**User Story:** As a contract developer, I want a test that confirms `topup_subscription` intentionally succeeds on a deactivated group and correctly updates usage counts, so that this permissive behaviour is explicitly documented as tested. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_topup_inactive_group_succeeds` that creates a group, deactivates it, and then calls `topup_subscription` on it. +2. WHEN `topup_subscription` is called on an inactive group with a funded payer, THE test SHALL assert that the call succeeds. +3. AFTER a successful topup on an inactive group, THE test SHALL assert that `usage_count` equals the initial count plus the additional usages. +4. AFTER a successful topup on an inactive group, THE test SHALL assert that `total_usages_paid` equals the initial total plus the additional usages. +5. AFTER a successful topup on an inactive group, THE test SHALL assert that `is_group_active` still returns `false`. + +--- + +### Requirement 11: Multiple Sequential Topups Test + +**User Story:** As a contract developer, I want a test that performs multiple consecutive topups and verifies cumulative accounting, so that off-by-one or reset bugs are detected. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_multiple_sequential_topups` that calls `topup_subscription` at least three times on the same group. +2. AFTER each topup, THE test SHALL assert that `usage_count` and `total_usages_paid` equal the expected cumulative sum. +3. AFTER all topups, THE test SHALL assert that `total_usages_paid` equals the initial usages plus the sum of all topup amounts. + +--- + +### Requirement 12: Reduce to Zero Then Over-Reduce Test + +**User Story:** As a contract developer, I want a test that reduces a group's usage count to zero and then attempts one more reduction, so that the NoUsagesRemaining guard is confirmed. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_reduce_to_zero_then_reduce_again` that creates a group with a small fixed `usage_count` (e.g., 3) and calls `reduce_usage` exactly that many times. +2. AFTER reducing to zero, THE test SHALL assert that `get_remaining_usages` returns `0`. +3. WHEN `reduce_usage` is called one more time after the count reaches zero, THE test SHALL assert that the call panics. + +--- + +### Requirement 13: Large Usage Count Arithmetic Test + +**User Story:** As a contract developer, I want a test that tops up with a very large number of usages (e.g., 10 000) and verifies that the arithmetic is exact, so that potential integer overflow or truncation bugs are caught. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_large_usage_count` that calls `topup_subscription` with `additional_usages = 10_000`. +2. THE test SHALL mint sufficient tokens to the payer to cover `10_000 * usage_fee` before calling `topup_subscription`. +3. AFTER the topup, THE test SHALL assert that `usage_count` equals exactly `initial_count + 10_000`. +4. AFTER the topup, THE test SHALL assert that `total_usages_paid` equals exactly `initial_total + 10_000`. + +--- + +### Requirement 14: Third-Party Payer Topup Test + +**User Story:** As a contract developer, I want a test that verifies any funded address (not just the group creator) can top up a subscription, so that the permissive payer model is explicitly tested. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_topup_different_payer` that generates a new address distinct from the group creator and mints tokens to that address. +2. WHEN `topup_subscription` is called by the non-creator payer, THE test SHALL assert that the call succeeds. +3. AFTER the topup, THE test SHALL assert that `usage_count` and `total_usages_paid` are updated correctly. + +--- + +### Requirement 15: Topup with Insufficient Balance Test + +**User Story:** As a contract developer, I want a test that verifies `topup_subscription` panics when the payer has no tokens, so that the token transfer failure path is confirmed. + +#### Acceptance Criteria + +1. THE test suite SHALL contain a test named `test_topup_insufficient_balance` that generates a payer address without minting any tokens to it. +2. WHEN `topup_subscription` is called by the zero-balance payer, THE test SHALL assert that the call panics. + +--- + +### Requirement 16: Register Subscription Edge-Case Module in `lib.rs` + +**User Story:** As a contributor, I want the subscription edge-case tests to be compiled and run as part of `cargo test --workspace --all-features`, so that CI executes them automatically. + +#### Acceptance Criteria + +1. THE `lib.rs` `mod tests` block SHALL contain the declaration `#[path = "../tests/subscription_edge_cases_test.rs"] mod subscription_edge_cases_test;`. +2. WHEN `cargo test --workspace --all-features` is run, THE Command SHALL execute all tests in `subscription_edge_cases_test.rs` without compilation errors. diff --git a/.kiro/specs/notify-chain-improvements/tasks.md b/.kiro/specs/notify-chain-improvements/tasks.md new file mode 100644 index 0000000..d8f4630 --- /dev/null +++ b/.kiro/specs/notify-chain-improvements/tasks.md @@ -0,0 +1,269 @@ +# Implementation Plan: Notify-Chain Improvements + +## Overview + +Three independent task groups, each committable to its own branch. Tasks within each group build on one another; groups are fully independent. + +--- + +## Task Dependency Graph + +```json +{ + "waves": [ + { + "wave": 1, + "tasks": ["1", "5", "11"], + "description": "First task of each independent group — can all start in parallel" + }, + { + "wave": 2, + "tasks": ["2", "6", "12"], + "description": "Builds on wave 1 within each group" + }, + { + "wave": 3, + "tasks": ["3", "7", "13"], + "description": "Builds on wave 2 within each group" + }, + { + "wave": 4, + "tasks": ["4", "8", "14"], + "description": "Checkpoints and final registrations" + }, + { + "wave": 5, + "tasks": ["9", "15"], + "description": "Documentation and final verification" + }, + { + "wave": 6, + "tasks": ["10"], + "description": "Task Group 2 final checkpoint" + } + ] +} +``` + +The three groups (Tasks 1–4, Tasks 5–10, Tasks 11–15) are fully independent and can be executed in parallel on separate branches. + +--- + +## Tasks + + + + + +- [ ] 1. Add `.env` to `dashboard/.gitignore` + - Open `dashboard/.gitignore` and append `.env`, `.env.local`, and `.env.*.local` entries. + - Verify that creating a `dashboard/.env` file and running `git status` does not show the file as untracked. + - _Requirements: 1.1, 1.2_ + +- [ ] 2. Complete `listener/.env.example` with missing variables + - Add a `# Discord Webhook Configuration` comment block with `DISCORD_WEBHOOK_ID=` including an inline description explaining it is the numeric webhook ID paired with `DISCORD_WEBHOOK_URL`. + - Add a `# Rate Limiting Configuration` comment block containing: + - `RATE_LIMIT_ENABLED=true` with description + - `RATE_LIMIT_WINDOW_MS=60000` with unit annotation (milliseconds) + - `RATE_LIMIT_MAX_REQUESTS=60` with description + - `RATE_LIMIT_CLIENT_OVERRIDES={}` with a JSON schema example comment showing `{"client-id": {"maxRequests": 30, "windowMs": 60000}}` + - Verify the file parses correctly (each line is a valid `KEY=value` or comment). + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_ + +- [ ] 3. Add Configuration reference section to root `README.md` + - Locate or create an appropriate position in the root `README.md` (after the existing introduction/setup sections). + - Add `## Configuration / Environment Variables` heading. + - Add a `### Listener (\`listener/.env\`)` sub-section with a Markdown table containing columns: Variable, Type, Default, Required, Description — populated from all variables in `listener/.env.example`. + - Add a `### Dashboard (\`dashboard/.env\`)` sub-section with the same table format — populated from all variables in `dashboard/.env.example`. + - Mark variables with no default (e.g. `CONTRACT_ADDRESSES`, `DISCORD_WEBHOOK_URL`, `DISCORD_WEBHOOK_ID`) as Required = Yes. + - _Requirements: 3.1, 3.2, 3.3, 3.4_ + +- [ ] 4. Checkpoint — Task 1 complete + - Confirm `dashboard/.gitignore` includes `.env`. + - Confirm `listener/.env.example` includes all five new variables. + - Confirm root `README.md` contains the Configuration section with both sub-sections. + - Ensure all tests pass, ask the user if questions arise. + + + + + +- [ ] 5. Create the gas snapshot baseline JSON skeleton + - Create directory `contract/contracts/hello-world/gas-snapshots/`. + - Create `contract/contracts/hello-world/gas-snapshots/autoshare.json` with the following skeleton (all numeric values set to `0`): + ```json + { + "version": 1, + "functions": { + "create": { "cpu_insns": 0, "mem_bytes": 0, "description": "Create a new AutoShare group (10 usages, no members)" }, + "topup_subscription": { "cpu_insns": 0, "mem_bytes": 0, "description": "Top up existing group with 10 additional usages" }, + "update_members": { "cpu_insns": 0, "mem_bytes": 0, "description": "Set 3 members with equal percentage split" }, + "withdraw": { "cpu_insns": 0, "mem_bytes": 0, "description": "Admin withdraws 100 tokens to recipient" } + } + } + ``` + - _Requirements: 5.1, 5.2, 5.3, 5.4_ + +- [ ] 6. Implement `gas_snapshot_test.rs` + - Create `contract/contracts/hello-world/src/tests/gas_snapshot_test.rs`. + - Add imports: `use crate::test_utils::{create_test_group, mint_tokens, setup_test_env}; use crate::AutoShareContractClient; use soroban_sdk::{testutils::Address as _, Address, BytesN, String, Vec};` + - Implement four `#[test]` functions following the pattern below. Each test MUST: + 1. Call `test_env.env.budget().reset_unlimited()` immediately before the contract call being measured. + 2. Perform the contract call. + 3. Read `test_env.env.budget().cpu_insns_consumed()` and `test_env.env.budget().mem_bytes_consumed()`. + 4. Assert both values are `> 0`. + 5. Print `println!("SNAPSHOT {name}: cpu={cpu} mem={mem}");` so the CI gate can parse output. + - **`snapshot_create`**: mint 10 000 tokens to creator, call `client.create(...)` with 10 usages, measure budget. + - **`snapshot_topup_subscription`**: use `create_test_group(...)` to set up a group, mint extra tokens to creator, call `client.topup_subscription(...)` with 10 additional usages, measure budget. + - **`snapshot_update_members`**: create a group (0 members), build a `Vec` with 3 members at 34/33/33 %, call `client.update_members(...)`, measure budget. + - **`snapshot_withdraw`**: create a group (funds land in contract), call `client.withdraw(admin, token, 100, recipient)`, measure budget. + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7_ + +- [ ]* 6.1 Verify gas snapshot tests compile and run + - Run `cargo test --all-features gas_snapshot -- --nocapture` in `contract/`. + - Confirm all four tests pass and each prints a `SNAPSHOT ...: cpu=NNN mem=NNN` line with non-zero values. + - Copy the printed values into `autoshare.json` (replacing the `0` skeletons) and commit. + - _Requirements: 4.6, 4.7, 5.4_ + +- [ ] 7. Register `gas_snapshot_test` module in `lib.rs` + - In `contract/contracts/hello-world/src/lib.rs`, inside the `mod tests { ... }` block, add: + ```rust + #[path = "../tests/gas_snapshot_test.rs"] + mod gas_snapshot_test; + ``` + - Run `cargo build --all-features` in `contract/` to confirm no compilation errors. + - _Requirements: 4.8_ + +- [ ] 8. Add CI gas regression gate step to `.github/workflows/ci.yml` + - After the existing `Run tests` step in the `rust` job, add a new step named `Gas regression check`. + - The step runs in `working-directory: contract`. + - Shell script logic: + 1. Run `cargo test --all-features gas_snapshot -- --nocapture 2>&1` and capture output. + 2. Check whether the snapshot file contains all-zero baselines; if so, print a warning and exit 0. + 3. For each of the four function names, parse the `cpu_insns` and `mem_bytes` from the captured output using `grep` and `awk`. + 4. Read the baseline values from `contracts/hello-world/gas-snapshots/autoshare.json` using `jq`. + 5. For each metric, compute `threshold = baseline * 110 / 100` using integer arithmetic in `awk`. + 6. If any measured value exceeds its threshold, print the function name and values, then exit 1. + - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_ + +- [ ] 9. Create `GAS_USAGE.md` at the repository root + - Create `GAS_USAGE.md` with the following sections: + - `## Overview` — 2–3 sentences explaining Soroban CPU instruction and memory byte metering (what they measure, where limits are enforced). + - `## Function Baselines` — a Markdown table with columns: Function, Scenario, CPU Instructions, Memory Bytes; rows for `create`, `topup_subscription`, `update_members`, `withdraw`. Populate from values committed in task 6.1. + - `## How to Update the Snapshot` — step-by-step: run `cargo test --all-features gas_snapshot -- --nocapture`, note printed values, update `autoshare.json`. + - `## Regression Policy` — document the 10 % tolerance and how the CI gate enforces it. + - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ + +- [ ] 10. Checkpoint — Task 2 complete + - Confirm snapshot JSON has non-zero values. + - Confirm `gas_snapshot_test.rs` is registered in `lib.rs`. + - Confirm CI `ci.yml` has the new regression step. + - Confirm `GAS_USAGE.md` exists with all required sections. + - Ensure all tests pass, ask the user if questions arise. + + + + + +- [ ] 11. Create `subscription_edge_cases_test.rs` with error-path tests + - Create `contract/contracts/hello-world/src/tests/subscription_edge_cases_test.rs`. + - Add standard imports at the top of the file: + ```rust + use crate::test_utils::{create_test_group, mint_tokens, setup_test_env}; + use crate::AutoShareContractClient; + use soroban_sdk::{testutils::Address as _, Address, BytesN, Vec}; + ``` + - Implement the following `#[should_panic]` tests: + - **`test_duplicate_create_error_type`**: call `create_test_group` twice with the same manually-constructed `BytesN<32>` ID (e.g., `[1u8; 32]`). The second call must panic. Add comment `// Expects Error::AlreadyExists`. + - **`test_topup_nonexistent_group`**: construct a random `BytesN<32>` that was never used in `create`, call `client.topup_subscription(...)` on it. Must panic. Add comment `// Expects Error::NotFound`. + - **`test_topup_insufficient_balance`**: create a group normally, generate a new `payer` address, do NOT mint any tokens, call `client.topup_subscription(...)` with that payer. Must panic. Add comment `// Expects token transfer failure`. + - _Requirements: 8.1, 8.2, 9.1, 9.2, 15.1, 15.2_ + +- [ ]* 11.1 Write property test for topup accumulation (Property 1) + - **Property 1: Topup accumulates usage counts correctly** + - In the same file, add a parameterized helper `assert_topup_accumulates(initial: u32, topup: u32)` that: + 1. Creates a group with `initial` usages. + 2. Mints enough tokens. + 3. Calls `topup_subscription` with `topup` usages. + 4. Asserts `usage_count == initial + topup` and `total_usages_paid == initial + topup`. + - Call it from a `#[test]` named `test_topup_accumulation_property` with representative pairs: `(1, 1)`, `(5, 10)`, `(100, 200)`, `(1, 10_000)`. + - **Validates: Requirements 11.2, 11.3, 13.3, 13.4** + +- [ ] 12. Implement success-path edge-case tests + - In `subscription_edge_cases_test.rs`, implement the following success-path `#[test]` functions: + - **`test_topup_inactive_group_succeeds`**: + 1. Create a group with `initial_usages = 5`. + 2. Call `client.deactivate_group(...)`. + 3. Assert `!client.is_group_active(...)`. + 4. Mint extra tokens to `creator`. + 5. Call `client.topup_subscription(id, 10, token, creator)`. + 6. Assert `client.get(&id).usage_count == 15`. + 7. Assert `client.get(&id).total_usages_paid == 15`. + 8. Assert `!client.is_group_active(...)` (still inactive). + - **`test_multiple_sequential_topups`**: + 1. Create a group with `initial_usages = 5`. + 2. Perform three topups of `10`, `20`, and `30` usages (mint tokens before each). + 3. After each topup, assert `usage_count` and `total_usages_paid` equal the expected cumulative sum. + 4. After all topups, assert `total_usages_paid == 5 + 10 + 20 + 30 == 65`. + - **`test_large_usage_count`**: + 1. Create a group with `initial_usages = 1`. + 2. Mint `10_000 * usage_fee + buffer` tokens to the creator. + 3. Call `client.topup_subscription(id, 10_000, token, creator)`. + 4. Assert `usage_count == 10_001` and `total_usages_paid == 10_001`. + - **`test_topup_different_payer`**: + 1. Create a group with `initial_usages = 5`. + 2. Generate a new `payer` address distinct from `creator`. + 3. Mint `10 * usage_fee` tokens to `payer`. + 4. Call `client.topup_subscription(id, 10, token, payer)`. + 5. Assert `usage_count == 15` and `total_usages_paid == 15`. + - _Requirements: 10.1–10.5, 11.1–11.3, 13.1–13.4, 14.1–14.3_ + +- [ ]* 12.1 Write property test for inactive-group topup (Property 2) + - **Property 2: Topup succeeds regardless of group active status** + - Add a `#[test]` named `test_inactive_topup_property` that calls `assert_topup_accumulates` helper (from task 11.1) after deactivating the group, with pairs: `(5, 5)`, `(1, 100)`. + - Assert `is_group_active` returns `false` after topup in both cases. + - **Validates: Requirements 10.2, 10.3, 10.4, 10.5** + +- [ ] 13. Implement `test_reduce_to_zero_then_reduce_again` + - In `subscription_edge_cases_test.rs`, add: + - **`test_reduce_to_zero_then_reduce_again`** (success part, `#[test]`): + 1. Create a group with `initial_usages = 3`. + 2. Call `client.reduce_usage(...)` three times in a loop. + 3. Assert `client.get_remaining_usages(...) == 0`. + - **`test_reduce_below_zero_panics`** (`#[test] #[should_panic]`): + 1. Create a group with `initial_usages = 3`. + 2. Call `client.reduce_usage(...)` four times. The fourth call must panic. + 3. Add comment `// Expects Error::NoUsagesRemaining`. + - _Requirements: 12.1, 12.2, 12.3_ + +- [ ]* 13.1 Write property test for reduce_usage boundary (Property 3) + - **Property 3: reduce_usage terminates at zero** + - Add a `#[test]` named `test_reduce_terminates_at_zero_property` that runs for `initial_usages` in `[1, 2, 5, 10]`: + 1. For each value N, create a fresh group with N usages. + 2. Call `reduce_usage` N times. + 3. Assert `get_remaining_usages == 0`. + - Note: the panic test (over-reduce) remains as `test_reduce_below_zero_panics` from task 13. + - **Validates: Requirements 12.2, 12.3** + +- [ ] 14. Register `subscription_edge_cases_test` module in `lib.rs` + - In `contract/contracts/hello-world/src/lib.rs`, inside the `mod tests { ... }` block, add: + ```rust + #[path = "../tests/subscription_edge_cases_test.rs"] + mod subscription_edge_cases_test; + ``` + - Run `cargo build --all-features` in `contract/` to confirm no compilation errors. + - _Requirements: 16.1_ + +- [ ] 15. Final checkpoint — Task 3 complete + - Run `cargo test --workspace --all-features` in `contract/` and confirm all tests in `subscription_edge_cases_test` pass. + - Confirm the module is registered in `lib.rs`. + - Ensure all tests pass, ask the user if questions arise. + +--- + +## Notes + +- Tasks marked with `*` are optional and can be skipped for a faster MVP; the non-starred tests in the same group still provide solid coverage. +- Each task group (1–4, 5–10, 11–15) maps to a separate branch and PR. +- Property tests in this spec are implemented as parameterised `#[test]` functions with multiple representative inputs rather than a full PBT library, since the Soroban `no_std` environment limits available PBT crates. +- The gas snapshot values in `autoshare.json` must be populated from a real measurement run before the CI regression gate becomes meaningful; the skeleton file with zeros causes the gate to skip (per Requirement 6.4).