From e20bde9dfaa42f7b5f584fd09a34419a705196a1 Mon Sep 17 00:00:00 2001 From: heymide Date: Fri, 24 Jul 2026 08:34:54 +0100 Subject: [PATCH] Subgraph-style JS/Postgres indexer example --- stellar/examples/indexer/.env.example | 14 ++ stellar/examples/indexer/Dockerfile | 11 + stellar/examples/indexer/README.md | 94 +++++++++ stellar/examples/indexer/docker-compose.yml | 38 ++++ stellar/examples/indexer/package.json | 28 +++ stellar/examples/indexer/schema.sql | 57 ++++++ stellar/examples/indexer/src/db.ts | 123 +++++++++++ stellar/examples/indexer/src/index.ts | 13 ++ .../examples/indexer/src/processor.test.ts | 119 +++++++++++ stellar/examples/indexer/src/processor.ts | 193 ++++++++++++++++++ stellar/examples/indexer/src/server.ts | 57 ++++++ stellar/examples/indexer/tsconfig.json | 16 ++ 12 files changed, 763 insertions(+) create mode 100644 stellar/examples/indexer/.env.example create mode 100644 stellar/examples/indexer/Dockerfile create mode 100644 stellar/examples/indexer/README.md create mode 100644 stellar/examples/indexer/docker-compose.yml create mode 100644 stellar/examples/indexer/package.json create mode 100644 stellar/examples/indexer/schema.sql create mode 100644 stellar/examples/indexer/src/db.ts create mode 100644 stellar/examples/indexer/src/index.ts create mode 100644 stellar/examples/indexer/src/processor.test.ts create mode 100644 stellar/examples/indexer/src/processor.ts create mode 100644 stellar/examples/indexer/src/server.ts create mode 100644 stellar/examples/indexer/tsconfig.json diff --git a/stellar/examples/indexer/.env.example b/stellar/examples/indexer/.env.example new file mode 100644 index 0000000..b67010c --- /dev/null +++ b/stellar/examples/indexer/.env.example @@ -0,0 +1,14 @@ +# Database +DATABASE_URL=postgres://wraith:wraith@localhost:5432/wraith_indexer + +# Stellar +RPC_URL=https://futurenet.sorobanrpc.com +NETWORK_PASSPHRASE=Test SDF Future Network ; October 2022 +STEALTH_ANNOUNCER_ADDRESS=CBXYZANN0UNCERXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0001 +STEALTH_REGISTRY_ADDRESS=CBREG1STRYXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0002 +STEALTH_SENDER_ADDRESS=CBSEND3RXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0003 +WRAITH_NAMES_ADDRESS=CBNAM3SWRA1THXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX004 + +# Indexer +POLL_INTERVAL_MS=5000 +PORT=3000 diff --git a/stellar/examples/indexer/Dockerfile b/stellar/examples/indexer/Dockerfile new file mode 100644 index 0000000..9f5c5c6 --- /dev/null +++ b/stellar/examples/indexer/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +CMD ["npm", "start"] diff --git a/stellar/examples/indexer/README.md b/stellar/examples/indexer/README.md new file mode 100644 index 0000000..ee6a5d4 --- /dev/null +++ b/stellar/examples/indexer/README.md @@ -0,0 +1,94 @@ +# Wraith Protocol Indexer Example + +A Subgraph-style TypeScript indexer for Wraith Protocol on Stellar that ingests contract events, stores them in Postgres, and exposes a GraphQL API. + +## Features + +- Ingests events from all 4 Wraith contracts (Stealth Announcer, Registry, Sender, Wraith Names) +- Reorg-safe with watermark tracking and rollback +- GraphQL API powered by PostGraphile +- Docker Compose setup for easy deployment + +## Quick Start + +```bash +# Start with Docker Compose +docker-compose up -d +``` + +The GraphQL API will be available at http://localhost:3000/graphiql. + +## Manual Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Copy .env.example to .env and configure: + ```bash + cp .env.example .env + ``` + +3. Start Postgres: + ```bash + docker-compose up -d postgres + ``` + +4. Run the indexer: + ```bash + npm run dev + ``` + +## GraphQL Endpoints + +### Name Resolution +```graphql +query ResolveName($name: String!) { + allNames(condition: { name: $name }, orderBy: CREATED_AT_DESC, first: 1) { + nodes { + name + owner + stealthMetaAddress + } + } +} +``` + +### Announcement Lookup +```graphql +query GetAnnouncements($stealthAddress: String!) { + allAnnouncements(condition: { stealthAddress: $stealthAddress }) { + nodes { + ledger + transactionHash + schemeId + stealthAddress + ephemeralPubKey + metadata + } + } +} +``` + +### Registry Lookup +```graphql +query GetRegistry($registrant: String!, $schemeId: Int!) { + allRegistries(condition: { registrant: $registrant, schemeId: $schemeId }, orderBy: CREATED_AT_DESC, first: 1) { + nodes { + registrant + schemeId + stealthMetaAddress + } + } +} +``` + +## Testing + +Run tests with: +```bash +npm test +``` + +The tests verify reorg handling logic. diff --git a/stellar/examples/indexer/docker-compose.yml b/stellar/examples/indexer/docker-compose.yml new file mode 100644 index 0000000..4b20c02 --- /dev/null +++ b/stellar/examples/indexer/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' +services: + postgres: + image: postgres:16 + environment: + POSTGRES_USER: wraith + POSTGRES_PASSWORD: wraith + POSTGRES_DB: wraith_indexer + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U wraith -d wraith_indexer"] + interval: 5s + timeout: 5s + retries: 5 + + indexer: + build: . + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgres://wraith:wraith@postgres:5432/wraith_indexer + RPC_URL: https://futurenet.sorobanrpc.com + NETWORK_PASSPHRASE: Test SDF Future Network ; October 2022 + STEALTH_ANNOUNCER_ADDRESS: CBXYZANN0UNCERXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0001 + STEALTH_REGISTRY_ADDRESS: CBREG1STRYXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0002 + STEALTH_SENDER_ADDRESS: CBSEND3RXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0003 + WRAITH_NAMES_ADDRESS: CBNAM3SWRA1THXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX004 + POLL_INTERVAL_MS: 5000 + ports: + - "3000:3000" + +volumes: + postgres_data: diff --git a/stellar/examples/indexer/package.json b/stellar/examples/indexer/package.json new file mode 100644 index 0000000..2f4b202 --- /dev/null +++ b/stellar/examples/indexer/package.json @@ -0,0 +1,28 @@ +{ + "name": "wraith-indexer-example", + "version": "1.0.0", + "description": "Reference Subgraph-style indexer for Wraith Protocol on Stellar", + "type": "module", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@stellar/stellar-sdk": "^12.0.0", + "fastify": "^5.0.0", + "pg": "^8.11.0", + "postgraphile": "^4.13.0", + "dotenv": "^16.4.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/pg": "^8.11.0", + "tsx": "^4.0.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" + } +} diff --git a/stellar/examples/indexer/schema.sql b/stellar/examples/indexer/schema.sql new file mode 100644 index 0000000..f9ed6dd --- /dev/null +++ b/stellar/examples/indexer/schema.sql @@ -0,0 +1,57 @@ +-- Watermarks for reorg handling +CREATE TABLE IF NOT EXISTS watermark ( + id SERIAL PRIMARY KEY, + contract_address VARCHAR NOT NULL, + latest_ledger INTEGER NOT NULL, + cursor TEXT NOT NULL, + updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + UNIQUE(contract_address) +); + +-- Stealth Announcer events +CREATE TABLE IF NOT EXISTS announcements ( + id SERIAL PRIMARY KEY, + ledger INTEGER NOT NULL, + transaction_hash VARCHAR NOT NULL, + contract_address VARCHAR NOT NULL, + scheme_id INTEGER NOT NULL, + stealth_address VARCHAR NOT NULL, + ephemeral_pub_key BYTEA NOT NULL, + metadata BYTEA, + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_announcements_ledger ON announcements(ledger); +CREATE INDEX IF NOT EXISTS idx_announcements_stealth_address ON announcements(stealth_address); + +-- Stealth Registry events +CREATE TABLE IF NOT EXISTS registries ( + id SERIAL PRIMARY KEY, + ledger INTEGER NOT NULL, + transaction_hash VARCHAR NOT NULL, + contract_address VARCHAR NOT NULL, + registrant VARCHAR NOT NULL, + scheme_id INTEGER NOT NULL, + stealth_meta_address BYTEA NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_registries_ledger ON registries(ledger); +CREATE INDEX IF NOT EXISTS idx_registries_registrant ON registries(registrant); +CREATE INDEX IF NOT EXISTS idx_registries_scheme_id ON registries(scheme_id); + +-- Wraith Names events +CREATE TABLE IF NOT EXISTS names ( + id SERIAL PRIMARY KEY, + ledger INTEGER NOT NULL, + transaction_hash VARCHAR NOT NULL, + contract_address VARCHAR NOT NULL, + name VARCHAR NOT NULL, + owner VARCHAR NOT NULL, + stealth_meta_address BYTEA NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_names_ledger ON names(ledger); +CREATE INDEX IF NOT EXISTS idx_names_name ON names(name); +CREATE INDEX IF NOT EXISTS idx_names_owner ON names(owner); diff --git a/stellar/examples/indexer/src/db.ts b/stellar/examples/indexer/src/db.ts new file mode 100644 index 0000000..a3c6950 --- /dev/null +++ b/stellar/examples/indexer/src/db.ts @@ -0,0 +1,123 @@ +import pg from 'pg'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const pool = new pg.Pool({ + connectionString: process.env.DATABASE_URL, +}); + +export async function getClient() { + return pool.connect(); +} + +export async function getWatermark(contractAddress: string, client: pg.PoolClient): Promise<{ latestLedger: number; cursor: string }> { + const result = await client.query( + 'SELECT latest_ledger, cursor FROM watermark WHERE contract_address = $1', + [contractAddress] + ); + if (result.rows.length === 0) { + return { latestLedger: 0, cursor: '0' }; + } + return { + latestLedger: result.rows[0].latest_ledger, + cursor: result.rows[0].cursor, + }; +} + +export async function setWatermark( + contractAddress: string, + latestLedger: number, + cursor: string, + client: pg.PoolClient +): Promise { + await client.query( + `INSERT INTO watermark (contract_address, latest_ledger, cursor, updated_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (contract_address) + DO UPDATE SET latest_ledger = $2, cursor = $3, updated_at = NOW()`, + [contractAddress, latestLedger, cursor] + ); +} + +export async function rollbackToLedger(contractAddress: string, ledger: number, client: pg.PoolClient): Promise { + await client.query( + 'DELETE FROM announcements WHERE contract_address = $1 AND ledger > $2', + [contractAddress, ledger] + ); + await client.query( + 'DELETE FROM registries WHERE contract_address = $1 AND ledger > $2', + [contractAddress, ledger] + ); + await client.query( + 'DELETE FROM names WHERE contract_address = $1 AND ledger > $2', + [contractAddress, ledger] + ); + await client.query( + `UPDATE watermark SET latest_ledger = $2, updated_at = NOW() WHERE contract_address = $1`, + [contractAddress, ledger] + ); +} + +export async function insertAnnouncement( + ledger: number, + txHash: string, + contractAddress: string, + schemeId: number, + stealthAddress: string, + ephemeralPubKey: Buffer, + metadata: Buffer | null, + client: pg.PoolClient +): Promise { + await client.query( + `INSERT INTO announcements (ledger, transaction_hash, contract_address, scheme_id, stealth_address, ephemeral_pub_key, metadata) + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ledger, txHash, contractAddress, schemeId, stealthAddress, ephemeralPubKey, metadata] + ); +} + +export async function insertRegistry( + ledger: number, + txHash: string, + contractAddress: string, + registrant: string, + schemeId: number, + stealthMetaAddress: Buffer, + client: pg.PoolClient +): Promise { + await client.query( + `INSERT INTO registries (ledger, transaction_hash, contract_address, registrant, scheme_id, stealth_meta_address) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ledger, txHash, contractAddress, registrant, schemeId, stealthMetaAddress] + ); +} + +export async function insertName( + ledger: number, + txHash: string, + contractAddress: string, + name: string, + owner: string, + stealthMetaAddress: Buffer, + client: pg.PoolClient +): Promise { + await client.query( + `INSERT INTO names (ledger, transaction_hash, contract_address, name, owner, stealth_meta_address) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ledger, txHash, contractAddress, name, owner, stealthMetaAddress] + ); +} + +export async function deleteName( + ledger: number, + txHash: string, + contractAddress: string, + name: string, + owner: string, + client: pg.PoolClient +): Promise { + await client.query( + `DELETE FROM names WHERE contract_address = $1 AND name = $2 AND owner = $3`, + [contractAddress, name, owner] + ); +} diff --git a/stellar/examples/indexer/src/index.ts b/stellar/examples/indexer/src/index.ts new file mode 100644 index 0000000..8dce81b --- /dev/null +++ b/stellar/examples/indexer/src/index.ts @@ -0,0 +1,13 @@ +import { startProcessing } from './processor.js'; +import { startServer } from './server.js'; + +async function main() { + console.log('Starting Wraith Indexer...'); + await startProcessing(); + await startServer(); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/stellar/examples/indexer/src/processor.test.ts b/stellar/examples/indexer/src/processor.test.ts new file mode 100644 index 0000000..cc26593 --- /dev/null +++ b/stellar/examples/indexer/src/processor.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; +import pg from 'pg'; +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { + getClient, + setWatermark, + rollbackToLedger, + insertAnnouncement, + getWatermark, +} from './db.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const TEST_DB_URL = 'postgres://wraith:wraith@localhost:5432/wraith_indexer_test'; +const pool = new pg.Pool({ connectionString: TEST_DB_URL }); + +async function setupTestDatabase() { + const client = await pool.connect(); + try { + const schemaPath = path.join(__dirname, '../../schema.sql'); + const schema = await fs.readFile(schemaPath, 'utf-8'); + await client.query(schema); + } finally { + client.release(); + } +} + +async function teardownTestDatabase() { + const client = await pool.connect(); + try { + await client.query('DROP TABLE IF EXISTS watermark, announcements, registries, names CASCADE'); + } finally { + client.release(); + } + await pool.end(); +} + +describe('Reorg Handling', () => { + beforeAll(async () => { + await teardownTestDatabase(); + await setupTestDatabase(); + }); + + afterAll(async () => { + await teardownTestDatabase(); + }); + + beforeEach(async () => { + const client = await pool.connect(); + try { + await client.query('TRUNCATE TABLE watermark, announcements, registries, names RESTART IDENTITY'); + } finally { + client.release(); + } + }); + + it('should rollback events beyond the given ledger', async () => { + const contractAddress = 'TEST_CONTRACT'; + const client = await pool.connect(); + + try { + // Insert some events + await insertAnnouncement( + 100, + 'tx1', + contractAddress, + 1, + 'STEALTH1', + Buffer.from('ephem1'), + null, + client + ); + await insertAnnouncement( + 101, + 'tx2', + contractAddress, + 1, + 'STEALTH2', + Buffer.from('ephem2'), + null, + client + ); + await insertAnnouncement( + 102, + 'tx3', + contractAddress, + 1, + 'STEALTH3', + Buffer.from('ephem3'), + null, + client + ); + await setWatermark(contractAddress, 102, 'cursor3', client); + + // Verify initial state + const initialResult = await client.query('SELECT * FROM announcements'); + expect(initialResult.rows.length).toBe(3); + + const initialWatermark = await getWatermark(contractAddress, client); + expect(initialWatermark.latestLedger).toBe(102); + + // Rollback to ledger 100 + await rollbackToLedger(contractAddress, 100, client); + + // Verify rollback + const afterRollbackResult = await client.query('SELECT * FROM announcements'); + expect(afterRollbackResult.rows.length).toBe(1); + expect(afterRollbackResult.rows[0].ledger).toBe(100); + + const afterRollbackWatermark = await getWatermark(contractAddress, client); + expect(afterRollbackWatermark.latestLedger).toBe(100); + } finally { + client.release(); + } + }); +}); diff --git a/stellar/examples/indexer/src/processor.ts b/stellar/examples/indexer/src/processor.ts new file mode 100644 index 0000000..244b4d3 --- /dev/null +++ b/stellar/examples/indexer/src/processor.ts @@ -0,0 +1,193 @@ +import { Server } from '@stellar/stellar-sdk'; +import { + getClient, + getWatermark, + setWatermark, + rollbackToLedger, + insertAnnouncement, + insertRegistry, + insertName, + deleteName, +} from './db.js'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const RPC_URL = process.env.RPC_URL || 'https://futurenet.sorobanrpc.com'; +const server = new Server(RPC_URL); + +interface ContractConfig { + address: string; + type: 'announcer' | 'registry' | 'sender' | 'names'; +} + +const CONTRACTS: Record = { + stealthAnnouncer: { + address: process.env.STEALTH_ANNOUNCER_ADDRESS || '', + type: 'announcer', + }, + stealthRegistry: { + address: process.env.STEALTH_REGISTRY_ADDRESS || '', + type: 'registry', + }, + stealthSender: { + address: process.env.STEALTH_SENDER_ADDRESS || '', + type: 'sender', + }, + wraithNames: { + address: process.env.WRAITH_NAMES_ADDRESS || '', + type: 'names', + }, +}; + +export async function processContract(config: ContractConfig) { + const client = await getClient(); + try { + await client.query('BEGIN'); + + const { latestLedger, cursor } = await getWatermark(config.address, client); + + // Check for reorg: fetch ledger info + let latestKnownLedger = latestLedger; + try { + const latestLedgerInfo = await server.getLatestLedger(); + if (latestLedger > latestLedgerInfo.sequence) { + console.warn(`Reorg detected for ${config.address}! Rolling back to ledger ${latestLedgerInfo.sequence}`); + await rollbackToLedger(config.address, latestLedgerInfo.sequence, client); + latestKnownLedger = latestLedgerInfo.sequence; + } + } catch (e) { + console.error('Error checking latest ledger:', e); + } + + // Fetch events + const response = await server.getEvents({ + filters: [ + { + type: 'contract', + contractIds: [config.address], + }, + ], + cursor, + limit: 100, + }); + + let newCursor = cursor; + let newLatestLedger = latestKnownLedger; + + for (const event of response.events) { + newCursor = event.pagingToken; + newLatestLedger = event.ledger; + + try { + await processEvent(config, event, client); + } catch (err) { + console.error('Error processing event:', err); + } + } + + // Update watermark + await setWatermark(config.address, newLatestLedger, newCursor, client); + + await client.query('COMMIT'); + } catch (err) { + await client.query('ROLLBACK'); + console.error('Error processing contract:', err); + throw err; + } finally { + client.release(); + } +} + +async function processEvent(config: ContractConfig, event: any, client: any) { + const txHash = event.transactionHash || ''; + const ledger = event.ledger; + + if (!event.value || !Array.isArray(event.value)) { + console.warn('Invalid event value:', event); + return; + } + + switch (config.type) { + case 'announcer': + case 'sender': + // Both emit the same announcement event + if (Array.isArray(event.topic) && event.topic[0] === 'announce') { + const [schemeId, stealthAddress, ephemeralPubKey, metadata] = event.value; + await insertAnnouncement( + ledger, + txHash, + config.address, + schemeId, + stealthAddress, + Buffer.from(ephemeralPubKey, 'base64'), + metadata ? Buffer.from(metadata, 'base64') : null, + client + ); + } + break; + case 'registry': + if (Array.isArray(event.topic) && event.topic[0] === 'register_keys') { + const [registrant, schemeId, stealthMetaAddress] = event.value; + await insertRegistry( + ledger, + txHash, + config.address, + registrant, + schemeId, + Buffer.from(stealthMetaAddress, 'base64'), + client + ); + } + break; + case 'names': + if (Array.isArray(event.topic)) { + if (event.topic[0] === 'register') { + const [owner, name, stealthMetaAddress] = event.value; + await insertName( + ledger, + txHash, + config.address, + name, + owner, + Buffer.from(stealthMetaAddress, 'base64'), + client + ); + } else if (event.topic[0] === 'release') { + const [owner, name] = event.value; + await deleteName(ledger, txHash, config.address, name, owner, client); + } else if (event.topic[0] === 'update') { + const [owner, name, newMetaAddress] = event.value; + await insertName( + ledger, + txHash, + config.address, + name, + owner, + Buffer.from(newMetaAddress, 'base64'), + client + ); + } + } + break; + } +} + +export async function startProcessing() { + const POLL_INTERVAL_MS = parseInt(process.env.POLL_INTERVAL_MS || '5000', 10); + + for (const [name, config] of Object.entries(CONTRACTS)) { + if (!config.address) { + console.log(`Skipping ${name}: no address configured`); + continue; + } + + // Initial process + await processContract(config); + + // Poll + setInterval(async () => { + await processContract(config); + }, POLL_INTERVAL_MS); + } +} diff --git a/stellar/examples/indexer/src/server.ts b/stellar/examples/indexer/src/server.ts new file mode 100644 index 0000000..b03b688 --- /dev/null +++ b/stellar/examples/indexer/src/server.ts @@ -0,0 +1,57 @@ +import fastify from 'fastify'; +import { postgraphile } from 'postgraphile'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const PORT = parseInt(process.env.PORT || '3000', 10); +const DATABASE_URL = process.env.DATABASE_URL || ''; + +const app = fastify(); + +// PostGraphile middleware +app.register(async (instance) => { + instance.addHook('onRequest', (req, res, done) => { + done(); + }); + + const pgMiddleware = postgraphile(DATABASE_URL, 'public', { + graphiql: true, + enhanceGraphiql: true, + enableCors: true, + }); + + instance.route({ + method: ['GET', 'POST'], + url: '/graphql', + handler: async (req, res) => { + // @ts-ignore + await pgMiddleware(req.raw, res.raw); + }, + }); + + instance.route({ + method: 'GET', + url: '/graphiql', + handler: async (req, res) => { + // @ts-ignore + await pgMiddleware(req.raw, res.raw); + }, + }); +}); + +// Health check +app.get('/health', async () => { + return { status: 'healthy' }; +}); + +export async function startServer() { + try { + await app.listen({ port: PORT, host: '0.0.0.0' }); + console.log(`Server running at http://localhost:${PORT}`); + console.log(`GraphiQL available at http://localhost:${PORT}/graphiql`); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} diff --git a/stellar/examples/indexer/tsconfig.json b/stellar/examples/indexer/tsconfig.json new file mode 100644 index 0000000..ce2ed2b --- /dev/null +++ b/stellar/examples/indexer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +}