diff --git a/contract/README.md b/contract/README.md index c0a7486..e251e8d 100644 --- a/contract/README.md +++ b/contract/README.md @@ -1,79 +1,264 @@ -# Soroban Project +# Agora Soroban Contracts Overview -## Project Structure +This directory contains the Soroban smart contracts for Agora's on-chain event and ticketing flow. -This repository uses the recommended structure for a Soroban project: +## Tech Stack and Layout -```text -. -├── contracts -│   └── hello_world -│   ├── src -│   │   ├── lib.rs -│   │   └── test.rs -│   └── Cargo.toml -├── Cargo.toml -└── README.md +This project uses a Rust `cargo` workspace, not Scarb. Build, test, and package management all run through the files in this directory: + +- `Cargo.toml`: workspace definition for the Soroban contracts +- `contracts/event_registry`: event lifecycle, organizer controls, inventory, loyalty, staking, and governance +- `contracts/ticket_payment`: ticket purchases, escrow, refunds, settlement, transfers, auctions, and payment-side governance +- `scripts/deploy_devnet.sh`: deploys or upgrades both contracts on Stellar testnet/devnet-style environments +- `scripts/generate_coverage.sh`: generates coverage artifacts for the `ticket-payment` crate + +## Contract Catalog + +### `event_registry` + +`event_registry` is the source of truth for event state. It stores event metadata, tier configuration, organizer ownership, inventory counters, promo settings, scanner permissions, loyalty profiles, staking records, and multi-admin governance proposals. + +Key storage keys in [`contracts/event_registry/src/types.rs`](./contracts/event_registry/src/types.rs): + +- `Event(event_id)`: full `EventInfo` record for an event +- `OrganizerEvent`, `OrganizerEventShard`, `OrganizerEventCount`: organizer-to-event indexes using sharded storage +- `EventReceipt`, `OrganizerReceipt*`: lightweight archived-event history +- `PlatformWallet`, `PlatformFee`, `TicketPaymentContract`, `Initialized`: core contract configuration +- `MultiSigConfig`, `ProposalCounter`, `Proposal`, `ActiveProposals`: admin governance state +- `Series`, `SeriesPass`, `HolderSeriesPass`, `SeriesEvent`: series and season-pass support +- `BlacklistedOrganizer`, `BlacklistLog`: organizer moderation and audit trail +- `AuthorizedScanner`: per-event scanner authorization +- `GuestProfile`: loyalty tracking for attendees +- `OrganizerStake`, `MinStakeAmount`, `StakingToken`, `TotalStaked`, `StakersList`: organizer staking and verification +- `TokenWhitelist`, `GlobalPromoBps`, `PromoExpiry`, `GlobalEventCount`, `GlobalActiveEventCount`, `GlobalTicketsSold`: platform-wide policy and aggregate counters + +Main public functions in [`contracts/event_registry/src/lib.rs`](./contracts/event_registry/src/lib.rs): + +- `initialize(admin, platform_wallet, platform_fee_percent, usdc_token)`: one-time setup; stores admin config and whitelists the initial payment token +- `get_version()` / `version()`: contract version helpers +- `register_event(args)`: creates a new event with metadata, tier map, supply limits, refund settings, and optional sales-goal configuration +- `get_event(event_id)`: returns the current `EventInfo` +- `get_event_payment_info(event_id)`: returns payment-facing config such as payment wallet, fee settings, and tiers +- `update_event_status(event_id, is_active)`: toggles whether an event is accepting payments +- `cancel_event(event_id)`: permanently cancels an event +- `archive_event(event_id)`: removes full event state and retains a minimal receipt for historical lookup +- `update_metadata(event_id, new_metadata_cid)`: updates event metadata CID +- `store_event(event_info)`: internal-style public entrypoint used to persist event state +- `get_organizer_address(event_id)`: returns the organizer for an event +- `get_total_tickets_sold(event_id)`: returns sold inventory for an event +- `get_managed_events_count()`: total events ever registered +- `get_active_events_count()`: total currently active events +- `get_global_tickets_sold()`: aggregate platform ticket sales +- `event_exists(event_id)`: quick existence check +- `get_organizer_events(organizer)`: returns event IDs owned by an organizer +- `get_organizer_receipts(organizer)`: returns archived event receipts for an organizer +- `set_platform_fee(new_fee_percent)` / `get_platform_fee()`: manage the default platform fee +- `set_custom_event_fee(event_id, custom_fee_bps)`: set or clear a per-event fee override +- `get_admin()` / `set_admin(new_admin)`: legacy single-admin getter/setter retained alongside multisig support +- `get_platform_wallet()`: returns the fee-collection wallet +- `set_ticket_payment_contract(ticket_payment_address)` / `get_ticket_payment_contract()`: links the payment contract allowed to mutate inventory +- `increment_inventory(event_id, tier_id, quantity)`: increases ticket counters after successful purchases +- `decrement_inventory(event_id, tier_id)`: decreases counters after refunds or reversals +- `register_series(series_id, name, event_ids, organizer_address, metadata_cid)`: groups multiple events into a series +- `get_series(series_id)`: fetches series metadata +- `issue_series_pass(pass_id, series_id, holder, usage_limit, expires_at)`: mints a reusable series pass +- `get_series_pass(pass_id)` / `get_holder_series_pass(holder, series_id)`: retrieves series-pass records +- `blacklist_organizer(organizer_address, reason)` / `remove_from_blacklist(organizer_address, reason)`: moderation controls for organizers +- `is_organizer_blacklisted(organizer_address)` / `get_blacklist_audit_log()`: moderation queries +- `set_global_promo(global_promo_bps, promo_expiry)` / `get_global_promo_bps()` / `get_promo_expiry()`: global promo configuration +- `postpone_event(event_id, grace_period_end)`: marks an event as postponed and opens a refund grace period +- `authorize_scanner(event_id, scanner)` / `is_scanner_authorized(event_id, scanner)`: scanner authorization used by ticket check-in +- `set_staking_config(token, min_stake_amount)`: configures organizer staking +- `stake_collateral(organizer, amount)` / `unstake_collateral(organizer)`: manages organizer collateral +- `distribute_staker_rewards(caller, total_reward)` / `claim_staker_rewards(organizer)`: reward distribution and claims for stakers +- `get_organizer_stake(organizer)` / `is_organizer_verified(organizer)`: staking status lookups +- `update_loyalty_score(caller, guest, tickets_purchased, amount_spent)`: updates attendee loyalty after ticket activity +- `get_guest_profile(guest)` / `get_loyalty_discount_bps(guest)`: loyalty reads used by the payment contract +- `get_multisig_config()` / `is_admin(address)` / `set_multisig_config(caller, admins, threshold)`: multisig configuration +- `propose_parameter_change(proposer, change, expiry_ledgers)`: generic governance proposal creation +- `propose_add_admin(proposer, admin, expiry_ledgers)` / `propose_remove_admin(proposer, admin, expiry_ledgers)` / `propose_set_threshold(proposer, threshold, expiry_ledgers)` / `propose_set_platform_wallet(proposer, wallet, expiry_ledgers)`: convenience governance proposal helpers +- `approve_proposal(approver, proposal_id)` / `execute_proposal(executor, proposal_id)`: multisig approval and execution +- `get_proposal(proposal_id)` / `get_active_proposals()`: governance queries +- `upgrade(new_wasm_hash)`: upgrades the contract code + +### `ticket_payment` + +`ticket_payment` handles the monetary side of the platform. It validates event payment settings against `event_registry`, processes purchases, keeps escrow balances, settles platform fees, supports refunds and transfers, and emits payment-centric events for indexers and off-chain services. + +Key storage keys in [`contracts/ticket_payment/src/types.rs`](./contracts/ticket_payment/src/types.rs): + +- `Payment(payment_id)`: full payment record +- `EventPayment*`, `BuyerPayment*`, `EventPaymentStatus*`: sharded indexes for event, buyer, and status-based lookups +- `Balances(event_id)`: escrow and organizer/platform amounts for an event +- `Admin`, `UsdcToken`, `PlatformWallet`, `EventRegistry`, `Initialized`: base contract configuration +- `TokenWhitelist`, `OracleAddress`, `SlippageBps`: accepted assets and pricing controls +- `TransferFee(event_id)`: secondary transfer fee per event +- `BulkRefundIndex`, `PartialRefundIndex`, `PartialRefundPercentage`, `DisputeStatus(event_id)`, `IsPaused`: operational safety and refund state +- `TotalVolumeProcessed`, `TotalFeesCollected(token)`, `ActiveEscrowTotal`, `ActiveEscrowByToken(token)`: protocol-wide accounting +- `DiscountCodeHash`, `DiscountCodeUsed`: discount-code registration and redemption tracking +- `WithdrawalCap`, `DailyWithdrawalAmount`: withdrawal throttling +- `HighestBid`, `AuctionClosed`: auction state +- `Governor`, `TotalGovernors`, `Proposal`, `ProposalCount`: payment-side governance + +Main responsibilities in [`contracts/ticket_payment/src/contract.rs`](./contracts/ticket_payment/src/contract.rs): + +- Initializes with admin, payment token, platform wallet, and linked `event_registry` contract +- Processes ticket purchases and updates event inventory through `event_registry` +- Confirms payments and records transaction hashes +- Supports guest refunds, admin refunds, automatic refunds, bulk refunds, and partial refunds +- Tracks event escrow balances and organizer/platform settlement amounts +- Handles organizer withdrawals, platform fee settlement, revenue claims, and withdrawal caps +- Supports ticket check-in, transfers, resale fee controls, and event disputes +- Integrates optional price-oracle-based asset pricing and token whitelisting +- Supports tier auctions, bid placement, auction closeout, and governance proposals for contract parameters + +## Build and Test + +Run commands from [`contract/`](./). + +### Build all contracts + +```bash +cargo build --target wasm32-unknown-unknown --release ``` -- New Soroban contracts can be put in `contracts`, each in their own directory. There is already a `hello_world` contract in there to get you started. -- If you initialized this project with any other example contracts via `--with-example`, those contracts will be in the `contracts` directory as well. -- Contracts should have their own `Cargo.toml` files that rely on the top-level `Cargo.toml` workspace for their dependencies. -- Frontend libraries can be added to the top-level directory as well. If you initialized this project with a frontend template via `--frontend-template` you will have those files already included. +### Run all tests -## Integration + Coverage +```bash +cargo test +``` -Run the end-to-end integration suite (ticket-payment crate): +### Run a single contract's tests ```bash +cargo test -p event-registry cargo test -p ticket-payment ``` -Generate coverage artifacts for the integration suite: +### Generate coverage for `ticket-payment` + +`scripts/generate_coverage.sh` expects `cargo-llvm-cov` to be installed and writes outputs under `coverage/`. ```bash ./scripts/generate_coverage.sh ``` -## Devnet Deployment +## Deployment -The project includes a script to deploy the smart contracts to the Soroban devnet (or testnet). This is useful for E2E testing and development. +The provided deployment flow is in [`scripts/deploy_devnet.sh`](./scripts/deploy_devnet.sh). It is a Bash script that: -### 1. Configure Environment +1. Loads environment variables from `.env.devnet` +2. Builds the WASM artifacts with Cargo +3. Deploys or upgrades `event_registry` +4. Initializes `event_registry` +5. Deploys or upgrades `ticket_payment` +6. Initializes `ticket_payment` +7. Links `ticket_payment` back into `event_registry` via `set_ticket_payment_contract` -Create a `.env.devnet` file in the `contract/` directory from the template: +Required environment variables include: -```bash -cp .env.devnet.example .env.devnet # Or just edit the existing .env.devnet -``` +- `SOROBAN_NETWORK_PASSPHRASE` +- `SOROBAN_RPC_URL` +- `SOROBAN_ACCOUNT_SECRET` +- `ADMIN_ADDRESS` +- `PLATFORM_WALLET` -Update the following variables in `.env.devnet`: -- `SOROBAN_ACCOUNT_SECRET`: Your secret key for deployment. -- `ADMIN_ADDRESS`: The admin address for the contracts. -- `PLATFORM_WALLET`: The address that receives platform fees. -- `USDC_TOKEN_ADDRESS`: The address of the USDC token on devnet (or leave it to deploy a mock token). +Optional deployment inputs: -### 2. Deploy Contracts +- `USDC_TOKEN_ADDRESS`: existing Stellar asset contract to use for payments; otherwise the script expects you to provide or deploy a mock token separately +- `EVENT_REGISTRY_ID` +- `TICKET_PAYMENT_ID` -Run the deployment script from the `contract/` directory: +Typical usage: ```bash -chmod +x ./scripts/deploy_devnet.sh ./scripts/deploy_devnet.sh ``` -The script will: -1. Build all contracts. -2. Deploy the `EventRegistry` and `TicketPayment` contracts. -3. Initialize the contracts with the provided configuration. -4. Output the contract IDs and addresses. - -### 3. Upgrade Contracts - -To upgrade existing contracts, use the `--upgrade` flag: +Upgrade existing deployments: ```bash ./scripts/deploy_devnet.sh --upgrade ``` -Ensure `EVENT_REGISTRY_ID` and `TICKET_PAYMENT_ID` are set in your `.env.devnet` before running an upgrade. +## Storage Model + +Both contracts keep their actual `env.storage()` calls in `storage.rs` instead of scattering them across entrypoints. + +- `storage.rs` acts as a thin storage access layer around Soroban persistent storage +- key creation is centralized through the `DataKey` enums in each contract's `types.rs` +- large append-only lists are sharded to avoid oversized storage entries +- counters and indexes are updated together so read paths such as "get organizer events" or "get buyer payments" stay efficient +- the payment contract stores accounting state separately from registry state, while the registry remains authoritative for event metadata and inventory + +## Events + +Events are defined in each contract's `events.rs` and are emitted with Soroban topics so off-chain indexers can react to contract activity. + +### `event_registry` events + +Defined in [`contracts/event_registry/src/events.rs`](./contracts/event_registry/src/events.rs): + +- `ContractInitialized` +- `ContractUpgraded` +- `EventRegistered` +- `EventStatusUpdated` +- `EventCancelled` +- `EventArchived` +- `MetadataUpdated` +- `FeeUpdated` +- `InventoryIncremented` +- `InventoryDecremented` +- `OrganizerBlacklisted` +- `OrganizerRemovedFromBlacklist` +- `EventsSuspended` +- `GlobalPromoUpdated` +- `EventPostponed` +- `ScannerAuthorized` +- `GoalMet` +- `CollateralStaked` +- `CollateralUnstaked` +- `StakerRewardsDistributed` +- `StakerRewardsClaimed` +- `LoyaltyScoreUpdated` +- `CustomFeeSet` +- admin/governance events including proposal creation, approval/execution, and admin updates + +### `ticket_payment` events + +Defined in [`contracts/ticket_payment/src/events.rs`](./contracts/ticket_payment/src/events.rs): + +- `ContractInitialized` +- `PaymentProcessed` +- `PaymentStatusChanged` +- `TicketTransferred` +- `PriceSwitched` +- `BulkRefundProcessed` +- `PartialRefundProcessed` +- `DiscountCodeApplied` +- `GlobalPromoApplied` +- `RevenueClaimed` +- `FeeSettled` +- `ContractPaused` +- `DisputeStatusChanged` +- `TicketCheckedIn` +- `BidPlaced` +- `AuctionClosed` +- governance events for proposal creation, voting, and execution +- `ContractVerificationFailed` + +## Contract Interaction Summary + +The contracts are intentionally split by responsibility: + +- `event_registry` owns event metadata, organizer policy, inventory truth, loyalty, and staking +- `ticket_payment` owns funds movement, escrow accounting, refunds, fee settlement, and purchase lifecycle +- `ticket_payment` calls into `event_registry` to read event payment settings and to increment or decrement inventory after payment state changes + +## PR Note + +When you open the PR for this documentation task, link the issue and include: + +```text +Closes #issue_number +``` diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..6e6632d --- /dev/null +++ b/server/README.md @@ -0,0 +1,205 @@ +# Agora Server + +This directory contains the Rust backend for Agora. The API is built with Axum, uses SQLx for database access and migrations, and stores data in PostgreSQL. + +## Tech Stack + +### Axum + +Axum is the HTTP framework used to define routes, attach middleware layers, manage shared application state, and return typed JSON responses. + +### SQLx + +SQLx is used for PostgreSQL connectivity, running migrations, and mapping rows into Rust structs. In this codebase, the server creates a `PgPool` at startup and shares it with route handlers through Axum state. + +### PostgreSQL + +PostgreSQL is the backing database for Agora. The local development database is provided by [`docker-compose.yml`](/c:/Users/User/Desktop/agora/server/docker-compose.yml), and the schema is created from the SQL files in [`migrations/`](/c:/Users/User/Desktop/agora/server/migrations). + +## Prerequisites + +- Rust stable toolchain +- Cargo +- Docker Desktop or a local PostgreSQL instance +- `sqlx-cli` + +Install `sqlx-cli` with PostgreSQL support: + +```bash +cargo install sqlx-cli --no-default-features --features postgres +``` + +## Environment Variables + +These variables are read from `.env` at startup: + +| Variable | Required | Default | Purpose | +|---|---|---|---| +| `DATABASE_URL` | Yes | None | PostgreSQL connection string used by the server and SQLx migrations | +| `PORT` | No | `3001` | Port the Axum server binds to | +| `RUST_ENV` | No | `development` | Enables production-specific behavior such as HSTS when set to `production` | +| `RUST_LOG` | No | `info` | Log level for `tracing` output | +| `CORS_ALLOWED_ORIGINS` | No | `http://localhost:3000,http://localhost:5173` | Comma-separated allowlist for browser clients | + +Example values are already provided in [`server/.env.example`](/c:/Users/User/Desktop/agora/server/.env.example). + +## Local Setup + +Run all commands from the [`server/`](/c:/Users/User/Desktop/agora/server) directory. + +### 1. Create your local env file + +```bash +cp .env.example .env +``` + +If you are on PowerShell, use: + +```powershell +Copy-Item .env.example .env +``` + +The default local database URL is: + +```text +postgres://user:password@localhost:5432/agora +``` + +### 2. Start PostgreSQL + +```bash +docker compose up -d +``` + +This starts a local PostgreSQL container with: + +- Host: `localhost` +- Port: `5432` +- Database: `agora` +- Username: `user` +- Password: `password` + +### 3. Run migrations + +```bash +sqlx migrate run +``` + +The migration files live in [`server/migrations/`](/c:/Users/User/Desktop/agora/server/migrations) and create the initial schema for users, organizers, events, ticket tiers, tickets, and transactions. + +### 4. Start the server + +```bash +cargo run +``` + +On success, the API will start on `http://localhost:3001` unless you override `PORT`. + +### 5. Verify the server + +Try the health endpoint: + +```bash +curl http://localhost:3001/api/v1/health +``` + +## Architecture Overview + +The backend follows a straightforward layered Axum structure: + +```text +Request + -> Layer + -> Route + -> Handler + -> Model / Database + -> Response +``` + +### Directory Structure + +```text +src/ +|- main.rs # Startup: load env, init logging, connect DB, run migrations, serve app +|- lib.rs # Module exports +|- config/ # Middleware and environment configuration +|- routes/ # Router assembly and endpoint registration +|- handlers/ # HTTP handlers and request/response orchestration +|- models/ # SQLx-backed Rust structs representing database entities +`- utils/ # Shared response, error, logging, and test helpers +``` + +### Request Lifecycle + +1. `main.rs` loads `.env`, initializes logging, reads config, connects to PostgreSQL, and runs embedded SQLx migrations. +2. [`src/routes/mod.rs`](/c:/Users/User/Desktop/agora/server/src/routes/mod.rs) builds the Axum router and applies shared middleware layers. +3. Incoming requests pass through middleware from [`src/config/`](/c:/Users/User/Desktop/agora/server/src/config): + - request ID generation and propagation + - CORS configuration + - security headers +4. The matched route forwards control to a handler in [`src/handlers/`](/c:/Users/User/Desktop/agora/server/src/handlers). +5. The handler uses shared state such as the `PgPool`, performs validation or queries, and builds a response. +6. Model structs in [`src/models/`](/c:/Users/User/Desktop/agora/server/src/models) define the Rust shape of database records and are the right place for table-backed types. +7. Shared helpers in [`src/utils/response.rs`](/c:/Users/User/Desktop/agora/server/src/utils/response.rs) and [`src/utils/error.rs`](/c:/Users/User/Desktop/agora/server/src/utils/error.rs) keep API responses consistent. + +### Where To Add New Endpoints + +When adding a new API feature, use this flow: + +1. Add or update the database schema in [`migrations/`](/c:/Users/User/Desktop/agora/server/migrations) if the feature needs persistence. +2. Add or update a model in [`src/models/`](/c:/Users/User/Desktop/agora/server/src/models) if the endpoint returns or stores a table-backed entity. +3. Create a handler in [`src/handlers/`](/c:/Users/User/Desktop/agora/server/src/handlers) for the request logic. +4. Register the route in [`src/routes/mod.rs`](/c:/Users/User/Desktop/agora/server/src/routes/mod.rs). +5. Reuse helpers in [`src/utils/`](/c:/Users/User/Desktop/agora/server/src/utils) for standard success and error responses. + +For example, a new organizer endpoint would typically mean: + +- adding `handlers/organizers.rs` +- exporting that module from `handlers/mod.rs` +- wiring routes in `routes/mod.rs` +- using models from `models/organizer.rs` + +## Testing + +### Health endpoint smoke test + +Start the server first, then run: + +```bash +bash ./test_health_endpoints.sh +``` + +If you are using Git Bash on Windows, run the same command there. The script checks: + +- `GET /api/v1/health` +- `GET /api/v1/health/db` +- `GET /api/v1/health/ready` + +### Rust tests + +Run the full test suite with: + +```bash +cargo test +``` + +On Windows, if your default Rust toolchain is GNU and you hit a `dlltool.exe` error, use the installed MSVC toolchain instead: + +```powershell +cargo +stable-x86_64-pc-windows-msvc test +``` + +Useful additional checks: + +```bash +cargo fmt --all -- --check +cargo clippy --all-targets --all-features -- -D warnings +``` + +## Notes For Pull Requests + +When you open your PR for this task, include: + +```text +Closes #issue_number +``` diff --git a/server/test_health_endpoints.sh b/server/test_health_endpoints.sh index 1c41ee5..fba2c34 100755 --- a/server/test_health_endpoints.sh +++ b/server/test_health_endpoints.sh @@ -3,7 +3,7 @@ # Health Check Endpoints Test Script # This script tests all health check endpoints with various scenarios -BASE_URL="http://localhost:3001" +BASE_URL="http://localhost:3001/api/v1" echo "==========================================" echo "Testing Health Check Endpoints" @@ -11,21 +11,21 @@ echo "==========================================" echo "" # Test 1: Basic health check -echo "1. Testing GET /health (Basic API health check)" +echo "1. Testing GET /api/v1/health (Basic API health check)" echo "Expected: 200 OK" curl -i -X GET "$BASE_URL/health" echo "" echo "" # Test 2: Database health check (with database running) -echo "2. Testing GET /health/db (Database health check - DB running)" +echo "2. Testing GET /api/v1/health/db (Database health check - DB running)" echo "Expected: 200 OK with database: connected" curl -i -X GET "$BASE_URL/health/db" echo "" echo "" # Test 3: Readiness check (with both healthy) -echo "3. Testing GET /health/ready (Readiness check - all healthy)" +echo "3. Testing GET /api/v1/health/ready (Readiness check - all healthy)" echo "Expected: 200 OK with api: ok, database: ok" curl -i -X GET "$BASE_URL/health/ready" echo "" @@ -40,13 +40,13 @@ echo "" echo "1. Stop the database:" echo " docker-compose down (or stop your PostgreSQL service)" echo "" -echo "2. Test /health/db endpoint (should return 503):" +echo "2. Test /api/v1/health/db endpoint (should return 503):" echo " curl -i -X GET $BASE_URL/health/db" echo "" -echo "3. Test /health/ready endpoint (should return 503):" +echo "3. Test /api/v1/health/ready endpoint (should return 503):" echo " curl -i -X GET $BASE_URL/health/ready" echo "" -echo "4. Test /health endpoint (should still return 200):" +echo "4. Test /api/v1/health endpoint (should still return 503 in the current implementation because it also checks database connectivity):" echo " curl -i -X GET $BASE_URL/health" echo "" echo "=========================================="