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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Database Configuration
# Docker (recommended): docker compose up -d db
DATABASE_URL="postgresql://postgres:password@localhost:5432/tradeflow?schema=public"
# Alternative Database Configuration (for TypeORM)
DB_HOST=localhost
Expand Down
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,98 @@ Get historical Total Value Locked (TVL) data for the analytics dashboard.
curl http://localhost:3000/api/v1/stats/tvl/history
```

### Admin Metrics

Aggregated protocol-wide statistics for administrative dashboards. Metrics are recomputed every **5 minutes** by a background scheduler and served from Redis cache (with PostgreSQL snapshot fallback).

All endpoints require a valid admin JWT obtained via `POST /api/v1/admin/login`.

#### `POST /api/v1/admin/login`

Authenticate with the configured `ADMIN_PASSWORD` to obtain a Bearer token.

```bash
curl -X POST http://localhost:3000/api/v1/admin/login \
-H "Content-Type: application/json" \
-d '{"password":"your-admin-password"}'
```

**Response:**
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

#### `GET /api/v1/admin/metrics/tvl`

Returns global Total Value Locked (sum of USD value across all active pools).

**Response Format:**
```json
{
"success": true,
"data": {
"tvlUSD": 14500000.50,
"poolCount": 12,
"lastUpdated": "2026-06-24T12:00:00.000Z"
},
"timestamp": "2026-06-24T12:00:00.000Z"
}
```

**Example:**
```bash
curl http://localhost:3000/api/v1/admin/metrics/tvl \
-H "Authorization: Bearer <admin-token>"
```

#### `GET /api/v1/admin/metrics/revenue`

Returns total cumulative protocol fees routed to the Treasury (derived from indexed swaps × pool fee tiers).

**Response Format:**
```json
{
"success": true,
"data": {
"totalRevenueUSD": 125000.75,
"lastUpdated": "2026-06-24T12:00:00.000Z"
},
"timestamp": "2026-06-24T12:00:00.000Z"
}
```

**Example:**
```bash
curl http://localhost:3000/api/v1/admin/metrics/revenue \
-H "Authorization: Bearer <admin-token>"
```

#### `GET /api/v1/admin/metrics/active-users`

Returns distinct active trader counts for rolling 24-hour, 7-day, and 30-day windows.

**Response Format:**
```json
{
"success": true,
"data": {
"activeUsers24h": 142,
"activeUsers7d": 890,
"activeUsers30d": 3200,
"lastUpdated": "2026-06-24T12:00:00.000Z"
},
"timestamp": "2026-06-24T12:00:00.000Z"
}
```

**Example:**
```bash
curl http://localhost:3000/api/v1/admin/metrics/active-users \
-H "Authorization: Bearer <admin-token>"
```

### Analytics Endpoints

#### `GET /api/v1/analytics/leaderboard`
Expand Down
15 changes: 13 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
version: '3.8'
services:
api:
build: .
ports: ["3000:3000"]
environment:
- DATABASE_HOST=db
- DATABASE_URL=postgresql://postgres:password@db:5432/tradeflow?schema=public
depends_on:
- db
- redis
db:
image: postgres:15-alpine
ports: ["5432:5432"]
environment:
- POSTGRES_PASSWORD=password
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: tradeflow
volumes:
- tradeflow_pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports: ["6379:6379"]

volumes:
tradeflow_pgdata:
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.4",
"compression": "^1.7.4",
"express-rate-limit": "^8.5.2",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.3",
"lru-cache": "^11.2.7",
Expand Down
74 changes: 74 additions & 0 deletions prisma/migrations/20260624000000_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'EXECUTED', 'CANCELLED');

-- CreateTable
CREATE TABLE "orders" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"assetPair" TEXT NOT NULL,
"side" TEXT NOT NULL,
"price" TEXT NOT NULL,
"quantity" TEXT NOT NULL,
"status" "OrderStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"executedAt" TIMESTAMP(3),

CONSTRAINT "orders_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "pools" (
"id" TEXT NOT NULL,
"address" TEXT NOT NULL,
"tokenA" TEXT NOT NULL,
"tokenB" TEXT NOT NULL,
"fee" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "pools_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "trades" (
"id" TEXT NOT NULL,
"poolId" TEXT NOT NULL,
"userAddress" TEXT NOT NULL,
"amountIn" TEXT NOT NULL,
"amountOut" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "trades_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "tokens" (
"id" TEXT NOT NULL,
"address" TEXT NOT NULL,
"symbol" TEXT NOT NULL,
"name" TEXT NOT NULL,
"decimals" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "tokens_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "orders_userId_idx" ON "orders"("userId");

-- CreateIndex
CREATE INDEX "orders_status_idx" ON "orders"("status");

-- CreateIndex
CREATE INDEX "orders_userId_status_idx" ON "orders"("userId", "status");

-- CreateIndex
CREATE UNIQUE INDEX "pools_address_key" ON "pools"("address");

-- CreateIndex
CREATE UNIQUE INDEX "tokens_address_key" ON "tokens"("address");

-- AddForeignKey
ALTER TABLE "trades" ADD CONSTRAINT "trades_poolId_fkey" FOREIGN KEY ("poolId") REFERENCES "pools"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- AlterTable
ALTER TABLE "pools" ADD COLUMN "reserveA" TEXT,
ADD COLUMN "reserveB" TEXT,
ADD COLUMN "tvlUsd" DECIMAL(20,8),
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "lastSyncedAt" TIMESTAMP(3);

-- CreateTable
CREATE TABLE "protocol_metrics_snapshots" (
"id" TEXT NOT NULL,
"globalTvlUsd" DECIMAL(20,8) NOT NULL,
"totalRevenueUsd" DECIMAL(20,8) NOT NULL,
"activeUsers24h" INTEGER NOT NULL,
"activeUsers7d" INTEGER NOT NULL,
"activeUsers30d" INTEGER NOT NULL,
"computedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "protocol_metrics_snapshots_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "protocol_metrics_snapshots_computedAt_idx" ON "protocol_metrics_snapshots"("computedAt");

-- CreateIndex
CREATE INDEX "trades_userAddress_timestamp_idx" ON "trades"("userAddress", "timestamp");
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
41 changes: 30 additions & 11 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This is your Prisma schema file,
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
Expand Down Expand Up @@ -44,23 +44,42 @@ model Trade {

pool Pool @relation(fields: [poolId], references: [id])

@@index([userAddress, timestamp])
@@map("trades")
}

model Pool {
id String @id @default(cuid())
address String @unique
tokenA String
tokenB String
fee String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

id String @id @default(cuid())
address String @unique
tokenA String
tokenB String
fee String
reserveA String?
reserveB String?
tvlUsd Decimal? @db.Decimal(20, 8)
isActive Boolean @default(true)
lastSyncedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

trades Trade[]

@@map("pools")
}

model ProtocolMetricsSnapshot {
id String @id @default(cuid())
globalTvlUsd Decimal @db.Decimal(20, 8)
totalRevenueUsd Decimal @db.Decimal(20, 8)
activeUsers24h Int
activeUsers7d Int
activeUsers30d Int
computedAt DateTime @default(now())

@@index([computedAt])
@@map("protocol_metrics_snapshots")
}

model Token {
id String @id @default(cuid())
address String @unique
Expand All @@ -69,6 +88,6 @@ model Token {
decimals Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("tokens")
}
16 changes: 14 additions & 2 deletions services/eventIndexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,22 @@ async function handleContractEvent(event) {

console.log('Decoded Payload:', JSON.stringify(payload, null, 2));

// Ensure pool exists and resolve FK to Pool.id (not contract address)
const pool = await prisma.pool.upsert({
where: { address: event.contractId },
update: {},
create: {
address: event.contractId,
tokenA: 'unknown',
tokenB: 'unknown',
fee: '30',
},
});

// Map Soroban event data to our Prisma Trade model
// Expected structure from SwapEvent: { user, amount_in, amount_out }
const tradeData = {
poolId: event.contractId,
poolId: pool.id,
userAddress: payload.user || payload.address || 'Unknown',
amountIn: (payload.amount_in || payload.amountIn || '0').toString(),
amountOut: (payload.amount_out || payload.amountOut || '0').toString(),
Expand All @@ -122,7 +134,7 @@ async function handleContractEvent(event) {

// Save to Database via Prisma
const savedTrade = await prisma.trade.create({
data: tradeData
data: tradeData,
});

console.log(`💾 Indexed Trade saved. DB ID: ${savedTrade.id}`);
Expand Down
Loading