diff --git a/backend/.env.example b/backend/.env.example index f77ed1c4..ee18ec57 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -91,6 +91,12 @@ HORIZON_URL=https://horizon-testnet.stellar.org # (no scopes required for public profile reads). # GITHUB_TOKEN=ghp_your_token_here +# Drip Scheduler Configuration +# Set to "true" to enable the recurring-support drip scheduler. +# When disabled (default), RecurringSupport subscriptions are accepted +# but no payments are sent automatically. +DRIP_SCHEDULER_ENABLED=false + # Soroban Contract Event Indexer # ──────────────────────────────────────────────────────────────────────────── # SOROBAN_CONTRACT_ID — Required to enable the contract event indexer. diff --git a/backend/src/app.ts b/backend/src/app.ts index 05db9cb1..287e5d69 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -441,7 +441,7 @@ All errors return JSON with an \`error\` field and optional \`code\`: { "error": "Human-readable message", "code": "MACHINE_READABLE_CODE", "requestId": "abc123" } \`\`\``, }, - servers: [{ url: "http://localhost:4000" }], + servers: [{ url: process.env.BACKEND_URL || "http://localhost:4000" }], components: { securitySchemes: { bearerAuth: { @@ -494,7 +494,19 @@ All errors return JSON with an \`error\` field and optional \`code\`: }); app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); - app.get("/docs.json", (req, res) => res.json(swaggerSpec)); + app.get("/docs.json", (req, res) => { + const dynamicSpec = { + ...swaggerSpec, + servers: [ + { + url: process.env.BACKEND_URL + ? process.env.BACKEND_URL.replace(/\/$/, "") + : `${req.protocol}://${req.get("host")}/api/v1` + } + ] + }; + res.json(dynamicSpec); + }); // ── Stellar TOML (#514) ─────────────────────────────────────────────── // Must be registered before any other middleware that might intercept it. diff --git a/contract/SECURITY.md b/contract/SECURITY.md index 0533127e..1a8686ad 100644 --- a/contract/SECURITY.md +++ b/contract/SECURITY.md @@ -27,10 +27,11 @@ changes to the contract. ## What the contract does NOT guarantee -- **No fund custody or transfer.** The contract does not hold, escrow, or move tokens. - Payments happen as a separate Stellar payment operation in the same transaction envelope, - outside the contract's execution. If the payment operation fails, the contract invocation - may still succeed (and vice versa) depending on transaction construction. +- **Fund custody and transfer via Soroban token client.** The contract transfers tokens to itself + on `support()` and from itself on `withdraw()` using the Soroban token client. These transfers + are atomic — both succeed or both fail. `SupportCount` and recipient totals are stored in + persistent storage separately from the actual token balances. The backend is responsible for + reconciling on-chain balances with stored counts. - **No recipient validation.** The contract does not verify that `recipient` is a registered NovaSupport profile, a valid Stellar account, or has any relationship to the platform. @@ -54,8 +55,9 @@ changes to the contract. - **Permissionless.** Anyone can call `support()` for any recipient address. There is no allowlist, role check, or admin gate on who may submit a support action. -- **No admin key.** There is no admin or owner address stored in the contract. No upgrade - authority, pause function, or privileged operation exists. +- **Admin key required for privileged operations.** The contract has an admin address that must + authorize pause/unpause operations via `require_auth()`. An upgrade path does not exist; + contract code is immutable once deployed. - **Immutable once deployed.** The contract has no `upgrade` entry point or admin key. Once deployed to a contract ID, the WASM cannot be altered. This ensures that the logic seen at the time of deployment is what will always execute for that ID. Any "upgrade" requires deploying a new contract instance and updating the platform to use the new ID. @@ -105,3 +107,17 @@ Keep the old contract ID in release notes and monitoring so historical events re - **Global state is shared across all callers.** `SupportCount` is a single contract-wide counter. If you introduce per-user or per-recipient state, use a composite `DataKey` variant (e.g., `DataKey::UserCount(Address)`) to avoid collisions. + +--- + +## Keeping this document in sync + +This document must be updated whenever `lib.rs` changes. Review the guarantees and limitations +sections after any modification to the contract code, especially changes to: +- Authorization checks (`require_auth()` calls) +- Pause/unpause logic +- Fund transfer operations +- Storage model and TTL management + +An out-of-sync SECURITY.md can mislead the backend and frontend teams into incorrect assumptions +about contract behaviour. diff --git a/frontend/src/app/profile/[username]/page.tsx b/frontend/src/app/profile/[username]/page.tsx index 29cf37c0..1a28ba7e 100644 --- a/frontend/src/app/profile/[username]/page.tsx +++ b/frontend/src/app/profile/[username]/page.tsx @@ -216,14 +216,13 @@ export default async function ProfilePage({ params }: PageProps) { const activeMilestone = visibleMilestones.find((m) => m.status === "active"); const milestoneProgress = activeMilestone - ? Math.min( - 100, - Math.round( - (parseFloat(activeMilestone.currentAmount) / - parseFloat(activeMilestone.targetAmount)) * - 100, - ), - ) + ? (() => { + const target = parseFloat(activeMilestone.targetAmount); + const current = parseFloat(activeMilestone.currentAmount); + return target > 0 + ? Math.min(100, Math.round((current / target) * 100)) + : null; + })() : null; const sameAs = [