Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ frontend/.next/
frontend/out/
frontend/node_modules/

# Backend stack build output (backend/, indexer/, oracle/)
dist/
backend/dist/
indexer/dist/
oracle/dist/

# ── Environment & Secrets ──────────────────────────────────────────────────────
.env
.env.local
Expand Down
68 changes: 68 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Contributing to iPredict — Backend & Oracle (`implementation-drips`)

We're building the real backend and oracle for iPredict. All of this work
happens on the **`implementation-drips`** branch — not `main`.

## Workflow

1. **Find an issue.** Browse [open issues](../../issues). Filter by `area:` label
to find work in your wheelhouse:
- `area:backend` / `area:api` — REST API
- `area:indexer` — Soroban event indexer
- `area:db` — database schema, migrations, queries
- `area:oracle` — council aggregator, optimistic oracle, data adapters
- `area:contracts` — Soroban contract changes (oracle state machine, bonds)
- `area:cache` — Redis caching layer
- `area:infra` — Docker, compose, deployment
- `area:monitoring` — metrics, dashboards, alerts
- `area:testing` — tests
- `area:docs` — documentation
- Priority: `P0` (needed for launch), `P1`, `P2`.

2. **Claim it.** Comment on the issue so others don't duplicate work.

3. **Branch.** Always branch off `implementation-drips`:
```bash
git checkout implementation-drips
git pull
git checkout -b feat/<short-name> # or fix/, chore/, test/
```

4. **Build it.** Each issue has Context, What to build, Acceptance criteria, and
the target files/folder. Stay within the issue's scope — small, reviewable PRs.

5. **Open a PR — base branch `implementation-drips`.**
- Link the issue (`Closes #123`).
- Make sure `npm run typecheck` and `npm test` pass in the package you touched.

> There is **no CI/GitHub Actions** on this branch yet — checks are manual.
> Please run typecheck/tests locally before requesting review.

## Repo layout

```
backend/ REST API (Node + TypeScript, Fastify)
indexer/ Soroban event indexer (Node + TypeScript)
oracle/ Council aggregator, data adapters, submitter, monitor
db/ Shared Postgres migrations + schema
infra/ Docker compose (dev + production)
contracts/ Soroban smart contracts (existing + new oracle code)
frontend/ Next.js app (existing)
docs/ Architecture & design docs
```

## Local setup

```bash
# 1. Start Postgres + Redis
cd infra && docker compose -f docker-compose.dev.yml up -d

# 2. Run a service (example: backend)
cd ../backend
cp .env.example .env
npm install
npm run dev
```

The design reference for everything is
[`docs/ORACLE_AND_BACKEND.md`](docs/ORACLE_AND_BACKEND.md).
29 changes: 29 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ── Backend API configuration ────────────────────────────────────────────────
# Copy to .env and fill in real values.

# HTTP
PORT=4000
HOST=0.0.0.0

# PostgreSQL (shared with the indexer)
DATABASE_URL=postgres://ipredict:ipredict@localhost:5432/ipredict

# Redis cache
REDIS_URL=redis://localhost:6379

# Stellar / Soroban
SOROBAN_RPC_URL=https://mainnet.sorobanrpc.com
NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015

# Contract IDs (from deploy-mainnet-output.json)
MARKET_CONTRACT_ID=
TOKEN_CONTRACT_ID=
REFERRAL_CONTRACT_ID=
LEADERBOARD_CONTRACT_ID=

# Oracle provider auth (Phase 2) — comma-separated API keys
ORACLE_API_KEYS=

# Rate limiting defaults (requests per window-seconds)
RATE_LIMIT_DEFAULT=30
RATE_LIMIT_WINDOW_SECONDS=60
67 changes: 67 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# iPredict Backend API

REST API that serves market, bet, leaderboard, and stats data to the frontend —
reading from an indexed PostgreSQL copy of on-chain state (written by the
[`indexer/`](../indexer)) with a Redis cache in front, instead of hitting Soroban
RPC directly on every request.

> **Branch:** all work happens on `implementation-drips`. Open PRs against that
> branch, **not** `main`.

## Why this exists

The frontend currently reads markets directly from Soroban RPC. That works for
<100 markets but throttles and slows badly past ~500. This service is the
scalable read path. See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md)
for the full design.

## Stack

- **Runtime:** Node.js 20+, TypeScript
- **HTTP:** Fastify
- **DB:** PostgreSQL 16 (shared with the indexer)
- **Cache:** Redis 7
- **Validation:** Zod
- **Stellar:** `@stellar/stellar-sdk`

## Layout

```
backend/
src/
api/ route handlers (markets, leaderboard, stats, oracle)
db/ query layer (shared schema lives in ../db migrations)
cache/ Redis client + cache helpers
config/ env loading & validation
lib/ shared utilities
server.ts Fastify bootstrap
index.ts entrypoint
test/
package.json
tsconfig.json
.env.example
```

## Getting started

```bash
cd backend
cp .env.example .env # fill in DATABASE_URL, REDIS_URL, contract IDs
npm install
npm run dev # starts the API on :4000
```

You need Postgres and Redis running locally (see
[`infra/`](../infra) for a docker-compose that starts both).

## Endpoints (target)

See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#api-endpoints).
Each endpoint is tracked as its own issue.

## Contributing

1. Pick an open issue labelled `area:backend` (or `area:api`).
2. Comment to claim it.
3. Branch off `implementation-drips`, implement, open a PR back to
`implementation-drips`.
33 changes: 33 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ipredict-backend",
"version": "0.1.0",
"private": true,
"description": "iPredict backend API — indexed market/bet/leaderboard data over REST",
"type": "module",
"engines": {
"node": ">=20"
},
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src --ext .ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@stellar/stellar-sdk": "^14.5.0",
"fastify": "^4.28.0",
"ioredis": "^5.4.1",
"pg": "^8.12.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.13.5",
"@types/pg": "^8.11.6",
"tsx": "^4.19.0",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
}
}
24 changes: 24 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* iPredict Backend API — entrypoint.
*
* This is an intentionally minimal scaffold. The real server bootstrap,
* routes, db/cache wiring, and config validation are tracked as separate
* issues (see GitHub issues labelled `area:backend`).
*
* Run: `npm run dev`
*/

const PORT = Number(process.env.PORT ?? 4000);

async function main(): Promise<void> {
// TODO(#scaffold): replace with the Fastify server from `src/server.ts`
// once the "Bootstrap Fastify server" issue is implemented.
console.log(`[ipredict-backend] scaffold up — API server not yet implemented`);
console.log(`[ipredict-backend] intended port: ${PORT}`);
console.log(`[ipredict-backend] pick an issue labelled "area:backend" to start`);
}

main().catch((err) => {
console.error("[ipredict-backend] fatal:", err);
process.exit(1);
});
19 changes: 19 additions & 0 deletions backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": false,
"sourceMap": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "test"]
}
39 changes: 39 additions & 0 deletions db/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# iPredict Database

Shared PostgreSQL schema for the [`backend/`](../backend) and [`indexer/`](../indexer).
SQL migrations live in [`migrations/`](./migrations), applied in filename order.

> **Branch:** all work happens on `implementation-drips`.

## Schema overview

See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#database-schema)
for the full reference. Core tables:

- `markets` — indexed copy of on-chain markets
- `bets` — per-bettor positions per market
- `leaderboard` — points/win/loss snapshot
- `events` — raw on-chain event audit log
- (Phase 2) `oracle_submissions`, `oracle_disputes`

## Migrations

Each migration is a numbered SQL file:

```
migrations/
0001_create_markets.sql
0002_create_bets.sql
...
```

Apply with the migration runner (tracked as its own issue) or manually:

```bash
psql "$DATABASE_URL" -f db/migrations/0001_create_markets.sql
```

## Contributing

Pick an open issue labelled `area:db`, branch off `implementation-drips`,
PR back to `implementation-drips`.
1 change: 1 addition & 0 deletions db/migrations/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Migrations go here, numbered 0001_*.sql, 0002_*.sql, ... (see ../README.md)
10 changes: 10 additions & 0 deletions db/migrations/0004_create_leaderboard.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS leaderboard (
address CHAR(56) PRIMARY KEY,
display_name VARCHAR(50),
points BIGINT NOT NULL DEFAULT 0,
won_bets INTEGER NOT NULL DEFAULT 0,
lost_bets INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_lb_points ON leaderboard(points DESC);
25 changes: 25 additions & 0 deletions indexer/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ── Indexer configuration ─────────────────────────────────────────────────────
# Copy to .env and fill in real values.

# PostgreSQL (shared with the backend)
DATABASE_URL=postgres://ipredict:ipredict@localhost:5432/ipredict

# Redis (for cache invalidation)
REDIS_URL=redis://localhost:6379

# Stellar / Soroban
SOROBAN_RPC_URL=https://mainnet.sorobanrpc.com
NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015

# Contract IDs to index (from deploy-mainnet-output.json)
MARKET_CONTRACT_ID=
TOKEN_CONTRACT_ID=
REFERRAL_CONTRACT_ID=
LEADERBOARD_CONTRACT_ID=

# Polling
POLL_INTERVAL_MS=5000
EVENTS_PER_PAGE=200

# Ledger to start indexing from if no checkpoint exists (0 = earliest available)
START_LEDGER=0
61 changes: 61 additions & 0 deletions indexer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# iPredict Soroban Event Indexer

Polls the Soroban RPC `getEvents()` endpoint, decodes contract events
(markets created, bets placed, claims, resolutions, oracle submissions…), and
writes them into PostgreSQL so the [`backend/`](../backend) can serve fast,
indexed reads. Also invalidates the Redis cache on relevant updates.

> **Branch:** all work happens on `implementation-drips`. Open PRs against that
> branch, **not** `main`.

## Stack

- **Runtime:** Node.js 20+, TypeScript
- **DB:** PostgreSQL 16 (shared with the backend)
- **Cache:** Redis 7 (invalidation only)
- **Stellar:** `@stellar/stellar-sdk`

## How it works

```
loop every 5s:
events = rpc.getEvents({ startLedger: checkpoint, contractIds: [...] })
for each event:
decode topics + value (scValToNative)
upsert into markets / bets / leaderboard / events tables
invalidate affected Redis keys
save checkpoint = latestLedger
```

See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#soroban-event-indexer)
for the reference implementation and the DB schema.

## Layout

```
indexer/
src/
handlers/ one decoder per event type (market_created, bet, claim, ...)
db/ write queries + checkpoint store
rpc/ getEvents client + pagination
config/ env loading
index.ts polling loop entrypoint
test/
package.json
tsconfig.json
.env.example
```

## Getting started

```bash
cd indexer
cp .env.example .env # fill in DATABASE_URL, SOROBAN_RPC_URL, contract IDs
npm install
npm run dev
```

## Contributing

Pick an open issue labelled `area:indexer`, claim it, branch off
`implementation-drips`, and PR back to `implementation-drips`.
Loading