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
14 changes: 14 additions & 0 deletions stellar/examples/indexer/.env.example
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions stellar/examples/indexer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

CMD ["npm", "start"]
94 changes: 94 additions & 0 deletions stellar/examples/indexer/README.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 38 additions & 0 deletions stellar/examples/indexer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
28 changes: 28 additions & 0 deletions stellar/examples/indexer/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
57 changes: 57 additions & 0 deletions stellar/examples/indexer/schema.sql
Original file line number Diff line number Diff line change
@@ -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);
123 changes: 123 additions & 0 deletions stellar/examples/indexer/src/db.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
await client.query(
`DELETE FROM names WHERE contract_address = $1 AND name = $2 AND owner = $3`,
[contractAddress, name, owner]
);
}
13 changes: 13 additions & 0 deletions stellar/examples/indexer/src/index.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading