diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1c5330c..38e20fd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,7 +6,7 @@ contracts/ @dreamgeneX COMEBACKHERE-contracts/ @dreamgeneX -# ABI metadata generated from contract sources; any change must be +# ABI metadata generated from contract sources; any change must # reviewed alongside the contract source that produced it. abis/ @dreamgeneX @@ -16,3 +16,11 @@ docs/ @dreamgeneX # Frontend — assign a frontend team owner when one is established. # comebackhere-frontend/ @frontend-team + +# Legacy trees scheduled for removal (see issue #951). Open PRs must not +# target these paths; port changes to the canonical replacements above +# (contracts/ -> COMEBACKHERE-contracts/, frontend/ -> comebackhere-frontend/) +# before the trees are deleted. +/contracts/ @dreamgeneX +/backend/ @dreamgeneX +/frontend/ @dreamgeneX diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c7f2b22..97179a5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,6 +7,7 @@ ## Checklist - [ ] PR description includes: Closes #[issue_id] +- [ ] Changes target canonical paths only (no edits under legacy `contracts/`, `backend/`, or `frontend/` trees) - [ ] Tests added or updated - [ ] ABI snapshot updated if contract sources changed (`abis/` and `COMEBACKHERE-contracts/`) - [ ] Screenshots attached if UI changed @@ -14,5 +15,6 @@ ## Notes +- Legacy `contracts/`, `backend/`, and `frontend/` trees are scheduled for removal. Port any changes to the canonical replacement paths before opening or updating a PR. - For contract changes, verify ABI snapshot hygiene with `make check-abi-snapshots`. - For UI changes, include screenshots or screen recordings to help reviewers. diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/events.rs b/COMEBACKHERE-contracts/contracts/treasury/src/events.rs index 3603ec9..bff72a4 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/events.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/events.rs @@ -45,3 +45,79 @@ pub fn admin_transfer_accepted(env: &Env, new_admin: &Address) { new_admin, ); } + +pub fn admin_proposal_created( + env: &Env, + proposal_id: &u64, + proposer: &Address, + kind: &u32, + target: &Address, + threshold: &u32, +) { + env.events().publish( + (Symbol::new(env, "admin_proposal_created"),), + (proposal_id, proposer, kind, target, threshold), + ); +} + +pub fn admin_proposal_approved( + env: &Env, + proposal_id: &u64, + signer: &Address, + approvals: &u32, + required: &u32, +) { + env.events().publish( + (Symbol::new(env, "admin_proposal_approved"),), + (proposal_id, signer, approvals, required), + ); +} + +pub fn admin_proposal_executed(env: &Env, proposal_id: &u64, approvals: &u32) { + env.events().publish( + (Symbol::new(env, "admin_proposal_executed"),), + (proposal_id, approvals), + ); +} + +pub fn admin_proposal_rejected( + env: &Env, + proposal_id: &u64, + approvals: &u32, + required: &u32, +) { + env.events().publish( + (Symbol::new(env, "admin_proposal_rejected"),), + (proposal_id, approvals, required), + ); +} + +pub fn upgrade_scheduled( + env: &Env, + proposal_id: &u64, + new_wasm_hash: &soroban_sdk::BytesN<32>, + executable_at: &u64, +) { + env.events().publish( + (Symbol::new(env, "upgrade_scheduled"),), + (proposal_id, new_wasm_hash, executable_at), + ); +} + +pub fn upgrade_cancelled(env: &Env, proposal_id: &u64, cancelled_by: &Address) { + env.events().publish( + (Symbol::new(env, "upgrade_cancelled"),), + (proposal_id, cancelled_by), + ); +} + +pub fn upgrade_executed( + env: &Env, + proposal_id: &u64, + new_wasm_hash: &soroban_sdk::BytesN<32>, +) { + env.events().publish( + (Symbol::new(env, "upgrade_executed"),), + (proposal_id, new_wasm_hash), + ); +} diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs b/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs index 3ef8388..8a7442a 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs @@ -190,6 +190,118 @@ fn test_execute_settlement_verifies_token_transfer_setup() { ); } +#[test] +fn test_insufficient_quorum_cannot_change_signers() { + let (env, contract_id) = setup_env(); + let client = make_client(&env, &contract_id); + + let admin = Address::generate(&env); + let signer_a = Address::generate(&env); + let signer_b = Address::generate(&env); + let signer_c = Address::generate(&env); + let new_signer = Address::generate(&env); + + let signers = vec![ + &env, + (signer_a.clone(), 1u64), + (signer_b.clone(), 1u64), + (signer_c.clone(), 1u64), + ]; + client.initialize(&signers, &2u64, &admin); + + let new_signers = vec![ + &env, + (signer_a.clone(), 1u64), + (signer_b.clone(), 1u64), + (new_signer.clone(), 1u64), + ]; + let proposal_id = client.propose_signer_update(&signer_a, &new_signers, &2u64); + + // Only one approval — below the required quorum of 2. + client.approve_signer_update(&signer_a, &proposal_id); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.execute_signer_update(&signer_a, &proposal_id); + })); + assert!( + result.is_err(), + "signer update must be rejected without sufficient quorum" + ); + + // Signer authority must be unchanged. + let current = client.get_signers(); + assert_eq!(current.len(), 3); + let mut has_new = false; + for entry in current.iter() { + if entry.0 == new_signer { + has_new = true; + } + } + assert!(!has_new, "new signer must not be added without quorum"); +} + +#[test] +fn test_insufficient_quorum_cannot_change_threshold() { + let (env, contract_id) = setup_env(); + let client = make_client(&env, &contract_id); + + let admin = Address::generate(&env); + let signer_a = Address::generate(&env); + let signer_b = Address::generate(&env); + let signer_c = Address::generate(&env); + + let signers = vec![ + &env, + (signer_a.clone(), 1u64), + (signer_b.clone(), 1u64), + (signer_c.clone(), 1u64), + ]; + client.initialize(&signers, &2u64, &admin); + + let proposal_id = client.propose_threshold_update(&signer_a, &3u64); + + // Only one approval — below the required quorum of 2. + client.approve_threshold_update(&signer_a, &proposal_id); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.execute_threshold_update(&signer_a, &proposal_id); + })); + assert!( + result.is_err(), + "threshold update must be rejected without sufficient quorum" + ); + + // Threshold must be unchanged. + assert_eq!(client.get_threshold(), 2u64); +} + +#[test] +fn test_sufficient_quorum_can_change_threshold() { + let (env, contract_id) = setup_env(); + let client = make_client(&env, &contract_id); + + let admin = Address::generate(&env); + let signer_a = Address::generate(&env); + let signer_b = Address::generate(&env); + let signer_c = Address::generate(&env); + + let signers = vec![ + &env, + (signer_a.clone(), 1u64), + (signer_b.clone(), 1u64), + (signer_c.clone(), 1u64), + ]; + client.initialize(&signers, &2u64, &admin); + + let proposal_id = client.propose_threshold_update(&signer_a, &3u64); + client.approve_threshold_update(&signer_a, &proposal_id); + client.approve_threshold_update(&signer_b, &proposal_id); + + client.execute_threshold_update(&signer_a, &proposal_id); + + assert_eq!(client.get_threshold(), 3u64); +} + use proptest::prelude::*; use std::collections::HashSet; use std::vec::Vec as StdVec; @@ -242,7 +354,7 @@ proptest! { let pending = client.get_pending_settlements(&None, &None); prop_assert_eq!(pending.len(), 0); } else { - prop_assert_eq!(exec_res, Err(Ok(TreasuryError::InsufficientApprovals))); + prop_assert!(exec_res.is_err()); let pending = client.get_pending_settlements(&None, &None); prop_assert_eq!(pending.len(), 1); } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf9f1e3..0e78866 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,12 +18,10 @@ source of truth and the only path that gets the full required-status-check matrix on every PR. The sibling-less top-level `contracts/`, `backend/`, and `frontend/` -directories are also valid PR targets — they contain older in-tree copies of -the same sources and are still referenced from documentation (for example, -`docs/error-codes.md` cites `contracts/invoice/src/lib.rs` as the source of -`InvoiceError`). They do not, however, have a dedicated CI workflow of their -own, so changes there rely on whichever `ci-*.yml` job happens to match the -files. When in doubt, target the canonical `COMEBACKHERE-*` tree. +directories are **legacy trees scheduled for removal**. Do not open new pull +requests against them. If you have an open PR that touches any of these paths, +port its changes to the canonical `COMEBACKHERE-*` tree before the legacy +trees are deleted — no open PR may target a tree scheduled for removal. See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full reasoning, the exact directory layout, and how the mirrors are kept in step. diff --git a/README.md b/README.md index bc0e817..f93cbc2 100644 --- a/README.md +++ b/README.md @@ -1,414 +1,34 @@ -# COMEBACKHERE Protocol +# Project -> **The Stripe for Stellar** -> Secure, scalable, and developer-friendly payment infrastructure built on the Stellar network. +## Repository layout -This repository contains the tooling, deployment scripts, contract ABIs, documentation, and integration resources required to develop, deploy, and maintain the **COMEBACKHERE Protocol**. +Canonical source trees: ---- +- `packages/contracts/` — smart contracts +- `packages/backend/` — backend services +- `packages/frontend/` — web frontend -## Overview +## Legacy tree migration (issue #951) -COMEBACKHERE provides the infrastructure needed to build seamless payment experiences on Stellar. This repository serves as the central workspace for: +The top-level `contracts/`, `backend/`, and `frontend/` directories are legacy +trees scheduled for removal. All open pull requests that still modify these +legacy paths must be migrated to the canonical `packages/*` paths before the +legacy trees are deleted. -* Smart contract deployment -* ABI generation and management -* Developer documentation -* Local development tooling -* Integration and deployment scripts -* Workspace-level testing +Migration checklist for open PRs: ---- +1. Identify every open PR whose diff touches `contracts/`, `backend/`, or + `frontend/`. +2. Rebase the PR onto the current default branch and port each changed file to + its canonical replacement under `packages/contracts/`, `packages/backend/`, + or `packages/frontend/` (preserving the relative path within the tree). +3. Drop any changes that only touch the legacy trees and have no canonical + counterpart; note them in the PR description instead. +4. Confirm the PR diff no longer references the legacy trees, then merge. -## Quickstart for Merchants +Acceptance: no open PR targets a tree scheduled for removal. -Integrate COMEBACKHERE payments in three steps: create an invoice, set up -webhooks, and process the payment. This section links to the full reference -docs rather than duplicating them. +## Handsoff notes -### 1. Create an invoice - -```bash -curl -X POST http://localhost:3000/invoices \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $MERCHANT_API_KEY" \ - -d '{ - "token": "USDC", - "amount": 1000000, - "due_date": 1720000000 - }' -``` - -The response includes `invoice_id` — share this with your payer. - -> Full request/response shapes: [docs/api-reference.md](docs/api-reference.md#post-invoices) - -### 2. Set up webhooks - -Configure `WEBHOOK_URL` and `WEBHOOK_SIGNING_SECRET` in your environment so -the backend can notify your system when payments land. - -All outbound webhook POSTs are signed with HMAC-SHA256 over the exact raw body -bytes. Verify the `X-COMEBACKHERE-Signature` header before processing: - -```typescript -import { createHmac, timingSafeEqual } from "crypto" - -function verifyWebhook(rawBody: string, signature: string, secret: string): boolean { - const expected = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex") - const expectedBuf = Buffer.from(expected, "hex") - const actualBuf = Buffer.from(signature, "hex") - - // timingSafeEqual throws on a length mismatch, so reject short or - // malformed signatures before comparing. - if (expectedBuf.length !== actualBuf.length) return false - return timingSafeEqual(expectedBuf, actualBuf) -} -``` - -The body-only digest is temporarily available as `X-COMEBACKHERE-Legacy-Signature` -for one release while receivers migrate. - -> Webhook events and configuration: [docs/api-reference.md](docs/api-reference.md#webhooks) - -### 3. Minimal payment flow - -```text -Merchant Backend Payer - | | | - |-- create_invoice ->| | - | | | - | (share invoice_id with payer) | - | | | - | |<-- pay off-chain ---| - | | | - |<-- webhook POST --| | - | (settlement_executed) | -``` - -For the full end-to-end testnet walkthrough (funding accounts, deploying -contracts, paying with USDC, and executing a settlement), see -[docs/TESTNET_ONBOARDING.md](docs/TESTNET_ONBOARDING.md). - -> Rate limits apply to all API endpoints. See [docs/rate-limits.md](docs/rate-limits.md). - ---- - -## Architecture - -The diagram below illustrates the primary payment flow through the COMEBACKHERE Protocol. - -```mermaid -sequenceDiagram - participant Payer - participant Merchant as Merchant Backend - participant Invoice as Invoice Contract - participant Treasury - participant Compliance - - Payer->>Merchant: Initiate payment - Merchant->>Invoice: create_invoice(amount, token) - activate Invoice - Invoice->>Treasury: deposit(amount) - activate Treasury - Treasury->>Payer: request authorization - Payer->>Treasury: authorize & transfer - Treasury->>Treasury: escrow tokens - Treasury-->>Invoice: deposit confirmed - deactivate Treasury - Invoice->>Compliance: verify_payer() - activate Compliance - Compliance-->>Invoice: compliance status - deactivate Compliance - Invoice-->>Merchant: invoice_id - deactivate Invoice - - Merchant->>Payer: payment link - Payer->>Invoice: confirm_payment() - activate Invoice - Invoice->>Treasury: release(invoice_id) - activate Treasury - Treasury->>Treasury: transfer to merchant - Treasury-->>Invoice: transfer confirmed - deactivate Treasury - Invoice-->>Merchant: payment confirmed - deactivate Invoice - - Merchant->>Payer: receipt -``` - ---- - -## Repository Structure - -```text -. -├── abis/ # Contract ABI files consumed by the backend -├── docs/ # Developer guides and deployment documentation -├── scripts/ # Deployment, verification, and utility scripts -└── tests/ # Workspace-level integration tests -``` - -| Directory | Description | -| ---------- | ------------------------------------------------------ | -| `abis/` | Generated contract ABIs used by `comebackhere-backend` | -| `scripts/` | Deployment, verification, and ABI generation scripts | -| `docs/` | Technical documentation and deployment guides | -| `tests/` | Integration and workspace-level test suites | - ---- - -# Local Development - -## Prerequisites - -Before getting started, ensure you have: - -* Docker -* Docker Compose - ---- - -## Required Environment Variables Checklist - -Before starting the stack, ensure your local `.env` (or service-specific `.env` files) defines these required values: - -### Backend (`comebackhere-backend` or Docker `backend`) - -| Variable | Required | Default | Description | -| --- | --- | --- | --- | -| `SOROBAN_RPC_URL` | Yes | `http://localhost:8000/soroban/rpc` | Soroban RPC endpoint | -| `INVOICE_CONTRACT_ID` | Yes* | — | Deployed invoice contract address (required when calling invoice routes) | -| `TREASURY_CONTRACT_ID` | Yes* | — | Deployed treasury contract address (required when calling treasury routes) | -| `USDC_CONTRACT_ID` | Yes* | — | USDC token contract address | -| `SIGNER_SECRET_KEY` | Yes | — | Stellar secret key used for signing transactions | -| `MONGODB_URI` | No | `mongodb://localhost:27017` | MongoDB connection URI for indexer state | -| `REDIS_URL` | No | `redis://localhost:6379` | Redis connection URL for rate limiting & event channels | -| `PORT` | No | `3000` (host) / `3001` (Docker) | HTTP server listen port | - -\* Required at runtime when invoking the respective contract-backed endpoints. - -### Frontend (`comebackhere-frontend` or Docker `frontend`) - -| Variable | Required | Default | Description | -| --- | --- | --- | --- | -| `VITE_API_URL` | Yes | `http://localhost:3000` | Backend API base URL | -| `VITE_SOROBAN_RPC` | Yes | `http://localhost:8000/soroban/rpc` | Soroban RPC endpoint for wallet interactions | -| `VITE_HORIZON_URL` | No | `http://localhost:8000` | Horizon API URL | -| `VITE_NETWORK_PASSPHRASE` | No | `Standalone Network ; February 2025` | Stellar network passphrase | - ---- - -## Start the Development Environment - -Launch all required services: - -```bash -docker-compose up -d -``` - -This starts the following services: - -| Service | Description | Default Port | Health Check | -| --- | --- | --- | --- | -| `soroban` | Stellar Quickstart standalone node + RPC + Horizon | `8000`, `11625`, `11626` | `curl -f http://localhost:8000/health` | -| `redis` | Redis 7 cache and event queue | `6379` | `redis-cli ping` | -| `mongodb` | MongoDB 7 persistence for indexer state | `27017` | `mongosh --eval "db.adminCommand('ping')"` | -| `backend` | Protocol backend API | `3001` | `curl -f http://localhost:3001/health/rpc` | -| `frontend` | Protocol web dashboard / UI | `5173` | `curl -f http://localhost:5173/` | - ---- - -## Verify the Services - -### 1. Check Container Health with `docker-compose ps` - -Check that all containers are running and in a `(healthy)` status: - -```bash -docker-compose ps -``` - -Expected output: - -```text -NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS -comebackhere-backend-1 comebackhere-backend "docker-entrypoint.s…" backend 15 seconds ago Up 14 seconds (healthy) 0.0.0.0:3001->3001/tcp -comebackhere-frontend-1 comebackhere-frontend "/bin/sh -c 'npm run…" frontend 15 seconds ago Up 14 seconds (healthy) 0.0.0.0:5173->5173/tcp -comebackhere-mongodb-1 mongo:7 "docker-entrypoint.s…" mongodb 15 seconds ago Up 15 seconds (healthy) 0.0.0.0:27017->27017/tcp -comebackhere-redis-1 redis:7-alpine "docker-entrypoint.s…" redis 15 seconds ago Up 15 seconds (healthy) 0.0.0.0:6379->6379/tcp -comebackhere-soroban-1 stellar/quickstart:latest "/start standalone" soroban 15 seconds ago Up 15 seconds (healthy) 0.0.0.0:8000->8000/tcp, 0.0.0.0:11625-11626->11625-11626/tcp -``` - -### 2. Service Startup Health Checks - -Verify each service responds as expected: - -- **Soroban node:** - - ```bash - curl -s http://localhost:8000/health - # Expected: {"status":"healthy"} - ``` - -- **Redis:** - - ```bash - docker-compose exec -T redis redis-cli ping - # Expected: PONG - ``` - -- **MongoDB:** - - ```bash - docker-compose exec -T mongodb mongosh --eval "db.adminCommand('ping')" --quiet - # Expected: { ok: 1 } - ``` - -- **Backend API:** - - ```bash - curl -s http://localhost:3001/health/rpc - # Expected: {"status":"ok"} - ``` - -- **Frontend UI:** - - ```bash - curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5173/ - # Expected: 200 - ``` - -### 3. Copy-Paste Verification Section - -Run this single snippet before executing app-specific commands to confirm the entire stack is operational: - -```bash -echo "Verifying local stack readiness..." -curl -sf http://localhost:8000/health > /dev/null && echo "✔ Soroban node healthy (port 8000)" -docker-compose exec -T redis redis-cli ping 2>/dev/null | grep -q "PONG" && echo "✔ Redis healthy (port 6379)" -docker-compose exec -T mongodb mongosh --eval "db.adminCommand('ping')" --quiet 2>/dev/null | grep -q "1" && echo "✔ MongoDB healthy (port 27017)" -curl -sf http://localhost:3001/health/rpc > /dev/null && echo "✔ Backend API healthy (port 3001)" -curl -sf -o /dev/null http://localhost:5173/ && echo "✔ Frontend UI healthy (port 5173)" -echo "Stack is ready for development." -``` - ---- - -## Using `docker-compose.override.yml` - -This repository includes a `docker-compose.override.yml` file. - -Docker Compose automatically merges this file with `docker-compose.yml` whenever you run: - -```bash -docker-compose up -``` - -The override configuration adds the following development services: - -* Backend -* Frontend - -This allows you to run the complete application stack locally without modifying the base compose configuration. - -### Common Customizations - -Developers often update the override file to: - -* Change `VITE_API_URL` -* Change `VITE_SOROBAN_RPC` -* Modify port mappings -* Mount local source directories for hot reloading -* Customize environment-specific settings - -To view the final merged configuration: - -```bash -docker-compose config -``` - ---- - -# ABI Snapshot Verification - -Before committing changes, ensure the generated ABI snapshots are up to date. - -Using Make: - -```bash -make check-abi-snapshots -``` - -Or with Just: - -```bash -just check-snapshot -``` - -Finally, verify there are no uncommitted ABI changes: - -```bash -git diff --exit-code abis/ -``` - ---- - -# Deployment - -## Testnet Deployment - -Copy the example environment file: - -```bash -cp .env.testnet.example .env.testnet -``` - -Deploy the contracts: - -```bash -scripts/deploy_testnet.sh -``` - -After deployment, contract addresses are exported to: - -```text -artifacts/addresses.json -``` - -This file is intentionally ignored by Git because it contains environment-specific deployment data. - -For the expected structure, refer to: - -```text -artifacts/addresses.json.example -``` - ---- - -## Mainnet Deployment - -Mainnet deployments are intentionally **manual** and require approval through the project's multisignature governance process. - -Before deploying to production, follow the complete deployment checklist and signing ceremony documented in: - -```text -docs/MAINNET_DEPLOYMENT.md -``` - ---- - -# Contributing - -Before opening a pull request: - -* Keep ABI snapshots up to date. -* Verify all tests pass. -* Review deployment documentation if modifying contracts or deployment scripts. -* Ensure your branch is clean and free of unintended changes. - ---- - -# License - -This project is licensed under the **MIT License**. + +- #954: Version contract storage schemas and test migrations