Skip to content

feat: add payout and split routes for API - #377

Open
BigJohn-dev wants to merge 2 commits into
tributary-protocol:mainfrom
BigJohn-dev:Event-sourced-accounting-layer-Postgres-projections-with-an-on-chain-reconciliation-proof
Open

feat: add payout and split routes for API#377
BigJohn-dev wants to merge 2 commits into
tributary-protocol:mainfrom
BigJohn-dev:Event-sourced-accounting-layer-Postgres-projections-with-an-on-chain-reconciliation-proof

Conversation

@BigJohn-dev

Copy link
Copy Markdown
Contributor

Closes #350

Event-sourced accounting service with Postgres projections and on-chain reconciliation

Summary

Replaces the prototype ndjson indexer with a production event-sourced accounting service. Contract events are ingested into Postgres as an append-only log, projections are built from the log (per-split balances, payout history, recipient earnings), a REST API serves the projections, and a reconciliation job continuously proves projected balances equal on-chain state.

Motivation

The current indexer (indexer/index.mjs) is a prototype that appends decoded events to a flat events.ndjson with a single cursor and no query layer beyond a CSV dump. For a payments protocol, the off-chain view of "who was paid what, and what is currently escrowed" needs to be queryable, correct, and provably consistent with contract storage.

This PR supersedes the ndjson prototype and absorbs/replaces #80 (reorgs), #81 (restart durability), #82 (query API).

What changed

Database (Postgres)

Three migration files create the schema:

events — append-only event log (source of truth for all projections). Each row stores the Stellar event id (unique), ledger, tx hash, event type, split id, and a typed JSONB payload. A reverted flag supports reorg handling.

Projection tables:

  • splits — current state of each split (recipients, shares, controller)
  • split_balances — escrow balance per split+token
  • payout_history — one row per recipient per payout event
  • recipient_earnings — aggregate earnings per address

ingestion_state — cursor position and last reconciliation timestamp.

Ingestion worker (src/ingest.ts)

Replaces index.mjs. Polls Soroban RPC getEvents, decodes events via src/decode.ts, normalizes them into typed payloads, and inserts into the events table (idempotent via event_id UNIQUE constraint). After each batch, routes events to projection builders in a single transaction.

Handles reorg detection: if the RPC cursor rewinds past a previously ingested ledger, affected events are marked reverted = TRUE and their projection effects are rolled back before re-ingestion.

Event decoder (src/decode.ts)

Decodes Soroban ScVal topics and values into typed NormalizedEvent objects. Handles all six event types: SplitCreated, SplitPaid, SplitUpdated, ControlTransferred, Deposited, Distributed.

Known limitation (tracked in #258, #267):

Projection builders (src/projections/)

File Handles Effect
splits.ts SplitCreated, SplitUpdated, ControlTransferred INSERT/UPDATE splits
balances.ts Deposited, Distributed UPSERT split_balances
payouts.ts SplitPaid, Distributed (with legs) INSERT payout_history, UPSERT recipient_earnings
apply.ts All events Routes to builders in a transaction
rebuild.ts Truncates projections, replays from event log

Projections are deterministic: drop and replay the event log produces identical state.

Reconciliation job (src/reconcile.ts)

Periodic job (configurable interval, default 60s) that:

  1. Queries all (split_id, token) pairs from split_balances
  2. Calls balance(split_id, token) and split_count() on-chain via Soroban RPC
  3. Compares projected vs on-chain values
  4. On mismatch, binary-searches the event log to find the first divergent event
  5. POSTs drift details to RECONCILIATION_WEBHOOK_URL
  6. Updates /health endpoint status

Query API (Hono)

Method Path Description
GET /health Health check + last reconciliation status
GET /splits List splits (pagination, optional creator filter)
GET /splits/:id Split details + current balances
GET /splits/:id/balances Escrow balances for a split
GET /splits/:id/payouts Payout history (pagination, optional token filter)
GET /recipients/:address/earnings Earnings + recent payouts for an address
GET /events Raw event log (pagination, optional type/split_id filter)
POST /reconcile Trigger reconciliation manually
POST /admin/rebuild Drop and rebuild all projections

Infrastructure

  • Dockerfile — multi-stage build (TypeScript → Node 20 alpine)
  • docker-compose.yml — Postgres 16 + indexer service with health checks
  • Justfileindexer-up, indexer-down, indexer-rebuild, indexer-reconcile, indexer-health, indexer-test, indexer-typecheck, indexer-migrate
  • vitest — test runner with DB-gated test suites

Tests

  • projections.test.ts — 8 tests: split creation, balance tracking, multi-event batches, rebuild determinism (require Postgres)
  • reconciliation.test.ts — 3 structural tests: drift detection, binary search, webhook
  • reorg.test.ts — 4 tests: revert marking, projection rollback, re-ingestion, idempotency
  • api.test.ts — 5 tests: health, splits, events, earnings, 404 (require Postgres)

Files changed

Modified

File Change
.gitignore Added indexer/dist/, indexer/.env
Justfile Added 9 indexer-* targets
indexer/Dockerfile Rewritten: multi-stage TypeScript build
indexer/package.json Added pg, hono, typescript, vitest, tsx deps; new scripts

Added

File Purpose
docs/indexer-design.md Full design document
indexer/tsconfig.json TypeScript config
indexer/vitest.config.ts Test config
indexer/.dockerignore Docker build exclusions
indexer/docker-compose.yml Local dev stack
indexer/src/config.ts Environment configuration
indexer/src/types.ts TypeScript type definitions
indexer/src/decode.ts Stellar event decoder
indexer/src/ingest.ts Ingestion worker
indexer/src/reconcile.ts Reconciliation job
indexer/src/index.ts Service entrypoint
indexer/src/db/pool.ts Postgres connection pool
indexer/src/db/migrate.ts Migration runner
indexer/src/db/migrations/001_create_events.sql Events table
indexer/src/db/migrations/002_create_projections.sql Projection tables
indexer/src/db/migrations/003_create_ingestion_state.sql Cursor state
indexer/src/projections/apply.ts Event-to-builder router
indexer/src/projections/splits.ts Split projection
indexer/src/projections/balances.ts Balance projection
indexer/src/projections/payouts.ts Payout projection
indexer/src/projections/rebuild.ts Projection rebuild
indexer/src/api/server.ts Hono app setup
indexer/src/api/routes/health.ts Health endpoint
indexer/src/api/routes/splits.ts Split queries
indexer/src/api/routes/payouts.ts Payout queries
indexer/src/api/routes/earnings.ts Earnings queries
indexer/src/api/routes/events.ts Event log queries
indexer/src/api/routes/admin.ts Admin endpoints
indexer/src/__tests__/helpers.ts Test fixtures
indexer/src/__tests__/projections.test.ts Projection tests
indexer/src/__tests__/reconciliation.test.ts Reconciliation tests
indexer/src/__tests__/reorg.test.ts Reorg tests
indexer/src/__tests__/api.test.ts API tests

How to test

# Typecheck
cd indexer && npm run typecheck

# Unit tests (no DB required)
cd indexer && npm test

# Full stack (requires Docker)
cd indexer && docker compose up -d --build

# Run migrations
cd indexer && docker compose exec indexer node dist/db/migrate.js

# Check health
curl http://localhost:3000/health

# Trigger reconciliation
curl -X POST http://localhost:3000/reconcile

# Rebuild projections
curl -X POST http://localhost:3000/admin/rebuild

Dependencies

This PR depends on (and is designed to work with, but not block on):

Architecture

Soroban RPC ──► Ingestion Worker ──► Postgres (events) ──► Projection Builders
                                                  │                   │
                                                  │           ┌───────┘
                                                  │           ▼
                                                  │    Projection Tables
                                                  │    (splits, balances,
                                                  │     payouts, earnings)
                                                  │           │
                                                  │           ▼
                                                  │    Query API (Hono)
                                                  │
                                                  └──► Reconciliation Job
                                                         │
                                                         ▼
                                                    On-chain RPC calls
                                                         │
                                                         ▼
                                                    Webhook alerts

- Implemented payout routes to fetch payout history based on split ID with pagination and optional token filtering.
- Created split routes to retrieve all splits, specific split details, and balances associated with a split.
- Added server setup to integrate new routes into the API.

feat: configure database migration system

- Established a migration system to manage database schema changes.
- Created migration files for events, splits, payout history, and ingestion state tables.
- Implemented migration logic to apply pending migrations and maintain a record of applied migrations.

feat: implement ingestion and reconciliation processes

- Developed ingestion logic to fetch and decode events from the Stellar network, storing them in the database.
- Implemented reconciliation logic to compare on-chain balances with database records, detecting discrepancies and posting webhooks for drifts.

feat: define types for events, splits, payouts, and reconciliation

- Created TypeScript types for various entities including events, splits, payouts, and reconciliation results to ensure type safety across the application.

chore: set up TypeScript configuration and testing framework

- Configured TypeScript compiler options and included necessary settings for module resolution and output.
- Set up Vitest for testing with a configuration file to specify test environment and include patterns.
@BigJohn-dev
BigJohn-dev requested a review from Spagero763 as a code owner July 23, 2026 14:16
…ections-with-an-on-chain-reconciliation-proof
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event-sourced accounting layer: Postgres projections with an on-chain reconciliation proof

1 participant