Skip to content

Commit 6d7ff3c

Browse files
Merge branch 'main' into feature/countdown-timer
2 parents 50b2381 + 5389a59 commit 6d7ff3c

138 files changed

Lines changed: 14579 additions & 13650 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.vscode/settings.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
{
2-
}
1+
{}

MULTI_TOKEN_DESIGN_DECISION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ Each campaign is currently tied to a single token at creation time. There is no
7474
- **Claim Flow**: The `claim` function iterates over all `accepted_tokens` and transfers the full balance of each token to the creator.
7575
- **Refund Flow**: The `refund` function iterates over all `accepted_tokens` and returns the specific tokens contributed by the user.
7676

77+
**Token identity conventions**: The API examples use `code:issuer` notation (e.g., `USDC:GA...`) to disambiguate classic Stellar assets that share the same asset code but have different issuers. On-chain, this maps to the Stellar Asset Contract (SAC) address for the corresponding issuer. Soroban-native tokens are identified by their contract address directly. The backend currently normalizes token codes to uppercase strings, which does not include issuer information — see `adr/0006-multi-token-design.md` for the canonical token identity gap.
78+
7779
### API for Integrators
7880

7981
#### Campaign Creation

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Architecture decision records
5151
- See `adr/0001-sqlite-off-chain-mvp.md` for the SQLite off-chain MVP decision.
5252
- See `adr/0002-react-express-mvp.md` for the React + Express + Soroban MVP architecture decision.
5353
- See `adr/0003-freighter-wallet-integration.md` for the Freighter wallet signing approach.
54+
- See `adr/0004-sqlite-mvp-postgresql-migration.md` for the SQLite / PostgreSQL database decision.
55+
- See `adr/0005-soroban-smart-contract-platform.md` for the Soroban smart contract platform decision.
56+
- See `adr/0006-multi-token-design.md` for the multi-token campaign design decision.
5457

5558
## TypeScript Bindings
5659

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 0004 - SQLite for MVP with PostgreSQL Migration Path
2+
3+
## Context
4+
5+
The MVP needs a persistence layer for campaigns, pledges, and event history. Several database options were evaluated:
6+
7+
1. **SQLite** — Embedded, zero-configuration relational database. Data is stored in a single file on disk. No separate server process required.
8+
9+
2. **PostgreSQL** — Full-featured production relational database with connection pooling, role-based access control, and strong consistency guarantees. Requires a dedicated server or container.
10+
11+
3. **MongoDB** — Document-oriented NoSQL database. Schema-less design allows rapid iteration but trades away relational integrity and transactional guarantees.
12+
13+
4. **In-memory storage** — Data held in process memory only. Fastest option but entirely ephemeral; no durability across restarts.
14+
15+
The project is a community MVP built by contributors who may not have infrastructure experience. The database needs to be:
16+
17+
- Trivial to set up — clone, install, and run with zero configuration
18+
- Reliable enough for local development and demos
19+
- Able to hold campaign, pledge, and event data with referential integrity
20+
- Backward-compatible with a future production database migration
21+
22+
## Decision
23+
24+
Use **SQLite** for the MVP persistence layer, with an abstracted data access layer designed to ease a future migration to PostgreSQL.
25+
26+
The backend (`backend/src/services/campaignStore.ts` and `backend/src/services/eventHistory.ts`) uses `better-sqlite3` — a synchronous, native Node.js binding. The data access layer is encapsulated behind a `Database` class so that swapping the underlying engine does not require rewriting every consumer.
27+
28+
The migration path to PostgreSQL is preserved by:
29+
30+
- Using standard SQL (CREATE TABLE, INSERT, SELECT, JOIN) without SQLite-specific extensions
31+
- Keeping transaction logic in the service layer rather than in database-specific triggers or stored procedures
32+
- Storing dates as UNIX epoch integers (which both SQLite and PostgreSQL handle natively)
33+
- Limiting PRAGMA usage to migration-safe schema introspection (column existence checks via `PRAGMA table_info`) rather than core application logic
34+
35+
## Consequences
36+
37+
- **Zero setup for contributors** — running `npm run dev:backend` automatically creates the database file at `backend/data/campaigns.db`. No server install, no user creation, no connection string configuration.
38+
- **Fast CI and test runs** — SQLite's in-memory mode (`:memory:`) is used for test suites, making tests self-contained and fast without a separate test database.
39+
- **Portability** — the entire database is a single file. Contributors can share snapshots for debugging.
40+
- **Migration effort** — switching to PostgreSQL later will require: swapping `better-sqlite3` for `pg` or `node-postgres`, adding connection pooling configuration, updating any deployment scripts, and running a schema migration. The data access abstraction reduces this to a contained change.
41+
- **Concurrency limits** — SQLite serializes writes at the file level. This is acceptable for an MVP with few concurrent users but will become a bottleneck under load, which is the primary motivator for the PostgreSQL migration path.
42+
43+
## References
44+
45+
- `backend/src/database.ts` — database connection and schema initialization
46+
- `backend/src/services/campaignStore.ts` — campaign CRUD operations
47+
- `backend/src/services/eventHistory.ts` — event log persistence
48+
- `adr/0001-sqlite-off-chain-mvp.md` — original SQLite off-chain decision
49+
- `adr/0002-react-express-mvp.md` — overall architecture context
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# 0005 - Soroban over Other Smart Contract Platforms
2+
3+
## Context
4+
5+
The project needs a smart contract platform to manage on-chain campaign state — creation, pledges, claims, and refunds. Several platforms were evaluated:
6+
7+
1. **Soroban (Stellar)** — Rust-based smart contract platform native to the Stellar network. Uses the Stellar asset model (trustlines, classic assets) and settles via Stellar consensus. WASM-based execution with a capacity-bound fee model.
8+
9+
2. **Solidity (EVM — Ethereum, Polygon, Arbitrum)** — The dominant smart contract platform with the largest ecosystem of tools, libraries, and developers. Well-understood security patterns (OpenZeppelin, etc.) but high complexity for simple state-machine contracts.
10+
11+
3. **Solana (BPF)** — High-throughput, low-latency platform using Rust and a parallel execution model. Account-based architecture with rent and minimal fee market. Attractive for scale but steeper learning curve for account model.
12+
13+
4. **Tezos (Michelson / LIGO)** — Self-amending ledger with on-chain governance. Formal verification tooling is mature. Smaller developer ecosystem and fewer wallet integrations.
14+
15+
5. **Algorand (TEAL / AVM)** — Pure proof-of-stake with transaction-finality guarantees. Pythonic contract development via PyTEAL but limited debugging tooling.
16+
17+
The project is a Stellar ecosystem crowdfunding app. The platform decision must consider:
18+
19+
- **Ecosystem fit** — the app exists to serve Stellar users and assets
20+
- **Asset model** — Stellar's native asset model (trustlines, asset codes, distribution accounts) should be directly usable without wrapping or bridging
21+
- **Developer onboarding** — contributors come from the Stellar community and should not need to learn a foreign chain
22+
- **MVP scope** — the contract surface is small: `create_campaign`, `contribute`, `claim`, `refund` with a few administrative functions
23+
24+
## Decision
25+
26+
Use **Soroban** as the smart contract platform.
27+
28+
Soroban is Stellar's native smart contract platform. Contracts are written in Rust, compiled to WASM, and deployed to the Stellar network. The Stellar ecosystem provides tooling (`stellar CLI`, `@stellar/stellar-sdk`, Freighter wallet) that integrates directly with Soroban without adapters or bridges.
29+
30+
The contract lives in `contracts/` and is compiled, deployed, and available for invocation. The **planned architecture** is for the frontend to invoke it via `@stellar/stellar-sdk` and `@stellar/freighter-api`. The backend stores the deployed `CONTRACT_ID` and `SOROBAN_RPC_URL` in environment variables and exposes them to the frontend through `/api/config`. As of this writing, the live wallet-signing flow is not yet fully wired into the frontend (see the README for current integration status).
31+
32+
## Consequences
33+
34+
- **Direct Stellar asset access** — campaigns can accept any Stellar asset (USDC, XLM, PYUSD) without wrapping. The `token` parameter in `contribute` is a Soroban `Address` that identifies the token's on-chain contract. For classic Stellar assets, this is the Stellar Asset Contract (SAC) address, which must be deployed on the network before the asset can be accepted by a campaign. SAC deployment is a prerequisite for accepting classic Stellar assets in pledge flows.
35+
- **Rust-based development** — contract developers need Rust and `wasm32-unknown-unknown` target. The Rust toolchain is well-supported but adds a dependency for contributors who only work on frontend or backend.
36+
- **Small platform ecosystem** — Soroban has fewer third-party libraries, audited patterns, and tooling compared to EVM chains. Custom implementations are more common.
37+
- **Ecosystem alignment** — Freighter, Stellar RPC, and the Stellar testnet faucet all target Soroban first. Users and contributors from the Stellar community will be familiar with the stack.
38+
- **Migration cost to switch** — moving to EVM or Solana later would require a complete contract rewrite. The backend and frontend abstractions isolate some of this (the reconcile pattern is chain-agnostic), but the contract logic would not port directly.
39+
40+
## References
41+
42+
- `contracts/` — Soroban contract source
43+
- `frontend/src/services/soroban.ts` — contract interaction from the frontend
44+
- `frontend/src/services/freighter.ts` — wallet signing for Soroban transactions
45+
- [Soroban documentation](https://soroban.stellar.org/docs)
46+
- [Stellar smart contracts overview](https://developers.stellar.org/docs/smart-contracts)
47+
- `adr/0003-freighter-wallet-integration.md` — wallet integration for Soroban signing

adr/0006-multi-token-design.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# 0006 - Multi-Token Campaign Design
2+
3+
## Context
4+
5+
Campaigns in the MVP initially supported a single accepted token (`assetCode`). Contributors could only pledge one type of asset per campaign, limiting flexibility for campaigns that wanted to accept multiple Stellar assets (e.g., USDC and XLM).
6+
7+
Three approaches were considered:
8+
9+
1. **Multi-token support (extend contract model)** — Modify the `Campaign` struct to hold `accepted_tokens: Vec<Address>`. Track pledges per token. Claim iterates over all accepted tokens and transfers each balance. Refund returns the specific token contributed.
10+
11+
2. **Single token per campaign (status quo)** — Each campaign accepts exactly one token. Simple contract logic and clear valuation, but contributors must hold the specific token, which may reduce participation.
12+
13+
3. **Token conversion at contribution time** — Campaign specifies a primary token and secondary tokens. Secondary contributions are automatically swapped to the primary token via an oracle integration at contribution time. All accounting is in the primary token.
14+
15+
## Decision
16+
17+
Adopt **Option 1: Multi-token support**.
18+
19+
The Soroban contract stores `accepted_tokens: Vec<Address>` on each campaign (Soroban-native address type identifying each token's on-chain contract). The `contribute` function validates that the pledged asset address is in the accepted list before recording the pledge. Pledged amounts are tracked per token using `Contribution(u64, Address, Address)` and `CampaignTokenBalance(u64, Address)` storage keys.
20+
21+
**Canonical token identity**: The project defines a single canonical token identifier format to prevent balances from being incorrectly merged or split across different issuers or contract addresses:
22+
23+
- **Classic Stellar assets**: `CODE:ISSUER` (e.g., `USDC:GA5ZSE...`). The asset code of up to 12 characters and the issuing account's public key are combined with a colon separator. This resolves ambiguity when multiple issuers use the same asset code. **Important**: `CODE:ISSUER` is an off-chain canonical identifier only. Before use in any on-chain contribution flow, it must be resolved to a Soroban contract address via the existing `config.assetAddresses[assetCode]` lookup. The `CODE:ISSUER` format is used for pledge validation, token balance grouping, and analytics; it never appears in Soroban contract calls directly.
24+
- **Native XLM**: `XLM` (no issuer component). XLM is the native Stellar asset and is represented by the string `"XLM"` as its canonical token ID. Its Soroban contract address is resolved via the same `config.assetAddresses` lookup.
25+
- **Soroban-native tokens**: The token's Soroban contract address directly (e.g., `C...`). These addresses double as both off-chain canonical IDs and on-chain contract identifiers.
26+
27+
On-chain, all token identities map to a Soroban `Address` type via the asset-address lookup. Off-chain, the backend stores canonical IDs in `accepted_tokens_json` and groups pledges by `token_id` (the canonical identifier stored alongside the legacy `asset_code` column). The `token_id` column in the `pledges` table holds the canonical identifier; `asset_code` is retained as a denormalized shorthand for backward compatibility. When `token_id` is not provided (legacy data), the system falls back to `asset_code`.
28+
29+
Valuation uses a simple 1:1 unit sum — `pledged_amount` is the raw sum of all token amounts. Creators should only accept tokens of similar value (e.g., stablecoins) or understand that the target is a sum of units. This avoids oracle complexity for the MVP while leaving room for price-feed integration later.
30+
31+
The full design rationale, including storage schema, API contracts, and trade-offs, is documented in `MULTI_TOKEN_DESIGN_DECISION.md`.
32+
33+
## Consequences
34+
35+
- **Flexibility for creators** — campaigns can accept any combination of Stellar assets, increasing the likelihood of reaching funding targets.
36+
- **Consistent contract behavior** — multi-token support is enforced at the Soroban contract level, so all frontends behave the same way.
37+
- **UI complexity** — the frontend renders a token selector when `acceptedTokens.length > 1` and displays per-token progress bars (`CampaignCard` shows individual `<div class="progress-bar">` elements).
38+
- **Valuation caveat** — the 1:1 unit sum means a campaign accepting both USDC and XLM would count 1 USDC == 1 XLM toward the target. Integrators must understand this limitation.
39+
- **Decimal-scale normalization** — contributions across tokens with different decimal scales (e.g., a 7-decimal asset vs. an 18-decimal asset) must be normalized to a common unit before being summed into `pledged_amount`. Raw unit-sum comparison against `target_amount` is only valid when all accepted tokens share the same decimal scale — otherwise the arithmetic is meaningless. Campaign creators are responsible for accepting only tokens of compatible decimal scales, or the system must reject mixed-scale configurations at campaign creation time. Full decimal-aware normalization is tracked as a future refinement.
40+
- **Campaign-level token identity gap**`accepted_tokens_json` currently stores uppercase asset codes without issuer info (e.g., `["USDC"]`), so campaign acceptance validation cannot distinguish between the same asset code from different issuers. The canonical `token_id` (with issuer) is enforced at the pledge level only. Full issuer-aware campaign token acceptance is tracked as a future refinement.
41+
- **Backend tracking**`getCampaignTokenBalances(campaignId)` queries the `pledges` table grouped by `token_id` (the canonical identifier). When `token_id` is `NULL` (legacy records), the query falls back to `asset_code`. **Legacy caveat**: legacy-backfilled `token_id` values (e.g., bare asset codes like `"USDC"` without an issuer, set by the migration `UPDATE pledges SET token_id = asset_code WHERE token_id IS NULL`) are NOT canonical and must not be treated as issuer-aware. These entries are explicitly bucketed as "legacy/unknown" — the `COALESCE(token_id, asset_code)` grouping can merge balances across different issuers of the same asset code. Integrators relying on token-level granularity must ensure all data uses canonical `CODE:ISSUER` or contract-address identifiers. The `tokenBalances` map is keyed by the canonical token ID and returned on every campaign read. Legacy data without `token_id` is automatically backfilled by the database migration.
42+
43+
## References
44+
45+
- `MULTI_TOKEN_DESIGN_DECISION.md` — full design document with alternatives, storage schema, and API payloads
46+
- `contracts/` — Soroban contract with multi-token campaign creation and contribution validation
47+
- `frontend/src/components/CampaignCard.tsx` — per-token progress bars
48+
- `frontend/src/components/CampaignDetailPanel.tsx` — token selector in pledge form
49+
- `backend/src/services/campaignStore.ts``getCampaignTokenBalances` implementation
50+
- `adr/0001-sqlite-off-chain-mvp.md` — off-chain state tracking for pledges
51+
- `adr/0005-soroban-smart-contract-platform.md` — platform context for contract decisions

backend/.eslintrc.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"ignorePatterns": ["src/services/campaignController.ts"],
44
"parser": "@typescript-eslint/parser",
55
"parserOptions": {
6-
76
"ecmaVersion": 2022,
87
"sourceType": "module"
98
},

backend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"express": "^4.21.2",
1616
"helmet": "^7.1.0",
1717
"lru-cache": "^11.5.1",
18+
"pino": "^10.3.1",
19+
"pino-http": "^11.0.0",
1820
"redis": "^4.6.13",
1921
"swagger-ui-express": "^5.0.1",
2022
"zod": "^4.3.6"
@@ -36,12 +38,14 @@
3638
"@types/cors": "^2.8.17",
3739
"@types/express": "^5.0.3",
3840
"@types/node": "^22.14.1",
41+
"@types/pino-http": "^5.8.4",
3942
"@types/supertest": "^6.0.2",
4043
"@typescript-eslint/eslint-plugin": "^7.0.0",
4144
"@typescript-eslint/parser": "^7.0.0",
4245
"@vitest/coverage-v8": "^1.6.1",
4346
"autocannon": "^7.15.0",
4447
"eslint": "^8.57.0",
48+
"pino-pretty": "^13.1.3",
4549
"supertest": "^7.0.0",
4650
"@apidevtools/swagger-cli": "4.0.4",
4751
"ts-node": "^10.9.2",

0 commit comments

Comments
 (0)