Skip to content
This repository was archived by the owner on Mar 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ad43f89
relayer stats for transaction handling
dharjeezy Jan 23, 2026
c401205
indexed collator rewards
dharjeezy Jan 24, 2026
61464d2
implement signature decoding for the relayer field
dharjeezy Jan 29, 2026
0653b7e
use scale-ts
dharjeezy Jan 29, 2026
718871e
remove token list indexing and services
dharjeezy Jan 29, 2026
6a2b9d9
relayer stats migration
dharjeezy Jan 29, 2026
ee38e25
remove migration and token List
dharjeezy Jan 30, 2026
801a3a2
remove migration
dharjeezy Jan 30, 2026
b8931b4
some improvements
Wizdave97 Feb 2, 2026
08ba9d3
add retries for fetch
Wizdave97 Feb 2, 2026
f2d2e8f
merge main
Wizdave97 Mar 5, 2026
a06e5c9
fix build
Wizdave97 Mar 5, 2026
b55ff4b
version some entities
Wizdave97 Mar 5, 2026
e2c6833
add migration script
Wizdave97 Mar 6, 2026
c6a19fc
nit
Wizdave97 Mar 6, 2026
c4fd3be
Merge branch 'main' into dami/index-properly
Wizdave97 Mar 6, 2026
7efe801
update migration and test,
dharjeezy Mar 6, 2026
c908e8e
migrate command update
dharjeezy Mar 6, 2026
f3f356b
make createdAt optional
dharjeezy Mar 6, 2026
5dad735
fix
Wizdave97 Mar 6, 2026
bfd8437
index relayer withdraw event
dharjeezy Mar 6, 2026
0bd6b8c
Make cumulative withdrawn optional
Wizdave97 Mar 7, 2026
0a5a7c8
Enhance data migration script to order by _block_range DESC for lates…
Wizdave97 Mar 7, 2026
4ee48ee
Rename OrderV2 to IOrderV2 in schema.graphql and update related impor…
Wizdave97 Mar 7, 2026
7bd6eb0
Update schema.graphql to use IOrderV2 types and adjust intentGatewayV…
Wizdave97 Mar 7, 2026
e4d2877
use alternative deduplication
Wizdave97 Mar 9, 2026
6931817
fail if any row insertion fails
Wizdave97 Mar 9, 2026
d16b5f0
Enhance data migration script to stringify JSON objects for jsonb col…
Wizdave97 Mar 9, 2026
71a576a
update intent gateway v2 config
dharjeezy Mar 10, 2026
996aea7
bump sdk
Wizdave97 Mar 10, 2026
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
4 changes: 3 additions & 1 deletion packages/indexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"down": "./scripts/down.sh ./docker/$ENV",
"start:local": "ENV=local npm run build && docker-compose -f docker/docker-compose.local.yml --env-file ../../.env.local up --force-recreate --remove-orphans && docker-compose -f docker/docker-compose.local.yml --env-file ../../.env.local rm -fsv",
"start:nexus-ci": "ENV=nexus-ci npm run build && docker-compose -f docker/docker-compose.nexus-ci.yml --env-file ../../.env.nexus-ci up --force-recreate --remove-orphans && docker-compose -f docker/docker-compose.nexus-ci.yml --env-file ../../.env.nexus-ci rm -fsv",
"build": "npm run codegen:yamls && npm run codegen:l2-chains && npm run codegen:subql && ./node_modules/.bin/subql build"
"build": "npm run codegen:yamls && npm run codegen:l2-chains && npm run codegen:subql && ./node_modules/.bin/subql build",
"migrate": "tsx scripts/migrate-entity-data.ts RequestV2 ResponseV2 GetRequestV2 AssetTeleportedV2 TokenGatewayAssetTeleportedV2 RelayerV2 RelayerStatsPerChainV2 --drop-source"
},
"dependencies": {
"@ethersproject/abi": "^5.7.0",
Expand All @@ -67,6 +68,7 @@
"fast-text-encoding": "^1.0.6",
"handlebars": "^4.7.8",
"node-fetch": "2.7.0",
"pg": "^8.20.0",
"safe-stable-stringify": "^2.5.0",
"scale-ts": "^1.6.1",
"viem": "^2.23.5",
Expand Down
152 changes: 80 additions & 72 deletions packages/indexer/pnpm-lock.yaml

Large diffs are not rendered by default.

375 changes: 375 additions & 0 deletions packages/indexer/scripts/__tests__/migrate-real-data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
import { Pool } from 'pg';
import * as dotenv from 'dotenv';
import * as path from 'path';
import * as fs from 'fs';
import { migrateEntities, MigrationResult } from '../migrate-entity-data';

// Load environment variables
const root = process.cwd();
const env = process.env.ENV || 'local';
dotenv.config({ path: path.resolve(root, `../../.env.${env}`) });

// Database configuration
const DB_CONFIG = {
user: process.env.DB_USER || 'postgres',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_DATABASE || 'postgres',
password: process.env.DB_PASS || 'postgres',
port: parseInt(process.env.DB_PORT || '5432'),
};

const DB_SCHEMA = process.env.DB_SCHEMA || 'app';

// Entities to migrate (from schema.graphql)
const ENTITIES_TO_MIGRATE = [
'RequestV2',
'ResponseV2',
'GetRequestV2',
'AssetTeleportedV2',
'TokenGatewayAssetTeleportedV2',
'RelayerV2',
'RelayerStatsPerChainV2',
];

// GraphQL to PostgreSQL type mapping
const TYPE_MAPPING: Record<string, string> = {
'ID': 'TEXT PRIMARY KEY',
'String': 'TEXT',
'BigInt': 'NUMERIC',
'Int': 'INTEGER',
'Boolean': 'BOOLEAN',
'Float': 'DOUBLE PRECISION',
'Date': 'TIMESTAMP WITH TIME ZONE',
'Bytes': 'BYTEA',
};

interface GraphQLField {
name: string;
type: string;
required: boolean;
isArray: boolean;
isDerived: boolean;
}

interface GraphQLEntity {
name: string;
fields: GraphQLField[];
}

/**
* Parse GraphQL schema file and extract V2 entity definitions
*/
function parseGraphQLSchema(schemaPath: string): Map<string, GraphQLEntity> {
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
const entities = new Map<string, GraphQLEntity>();

// Split into type definitions
// Updated regex to allow directives between @entity and {
const typeRegex = /type\s+(\w+)\s*@entity[^{]*\{([^}]+)\}/g;
let match;

while ((match = typeRegex.exec(schemaContent)) !== null) {
const typeName = match[1];
const fieldsBlock = match[2];

// Parse fields
const fields: GraphQLField[] = [];
const fieldLines = fieldsBlock.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('#'));

for (const line of fieldLines) {
// Skip comments and directives
if (line.startsWith('#') || line.startsWith('@') || !line.includes(':')) {
continue;
}

// Parse field: name: Type! @directives
const fieldMatch = line.match(/^(\w+):\s*(\w+)(!)?(\[\])?\s*(.*)/);
if (fieldMatch) {
const fieldName = fieldMatch[1];
const fieldType = fieldMatch[2];
const isRequired = fieldMatch[3] === '!';
const isArray = line.includes('[');
const isDerived = line.includes('@derivedFrom');

// Skip derived fields (they're virtual)
if (isDerived) {
continue;
}

fields.push({
name: fieldName,
type: fieldType,
required: isRequired,
isArray: isArray,
isDerived: isDerived,
});
}
}

if (fields.length > 0) {
entities.set(typeName, { name: typeName, fields });
}
}

return entities;
}

/**
* Convert GraphQL type to PostgreSQL type
*/
function graphqlToPostgresType(graphqlType: string, isArray: boolean): string {
if (isArray) {
return 'JSONB';
}

const mapped = TYPE_MAPPING[graphqlType];
return mapped || 'TEXT';
}

/**
* Convert a PascalCase/camelCase name to snake_case,
* matching how SubQuery maps entity names to table names
* and field names to column names.
*/
function toSnakeCase(name: string): string {
return name
.replace(/([A-Z])/g, '_$1')
.toLowerCase()
.replace(/^_/, '');
}

/**
* Get SubQuery table name (snake_case with plural suffix)
* SubQuery creates tables with plural names (e.g., requests, responses)
*/
function getSubqueryTableName(entityName: string): string {
return toSnakeCase(entityName) + 's';
}

/**
* Generate CREATE TABLE statement from GraphQL entity
*/
function generateCreateTable(entity: GraphQLEntity, schema: string): string {
const columnDefs: string[] = [];

for (const field of entity.fields) {
const pgType = graphqlToPostgresType(field.type, field.isArray);
const columnName = toSnakeCase(field.name);
let columnDef = `"${columnName}" ${pgType}`;

columnDefs.push(columnDef);
}

const tableName = getSubqueryTableName(entity.name);
return `
CREATE TABLE IF NOT EXISTS ${schema}.${tableName} (
${columnDefs.join(',\n ')}
);
`;
}

/**
* Create V2 tables from GraphQL schema
*/
async function createV2TablesFromSchema(
pool: Pool,
schema: string,
schemaPath: string
): Promise<void> {
console.log('📖 Parsing GraphQL schema...');
const entities = parseGraphQLSchema(schemaPath);

const client = await pool.connect();

try {
for (const entityName of ENTITIES_TO_MIGRATE) {
const entity = entities.get(entityName);

if (!entity) {
console.log(`⚠️ Entity ${entityName} not found in schema, skipping`);
continue;
}

// Check if table already exists
const tableName = getSubqueryTableName(entityName);
const exists = await tableExists(pool, schema, tableName);
if (exists) {
console.log(`✓ Table ${schema}.${tableName} already exists`);
continue;
}

// Generate and execute CREATE TABLE
const createSQL = generateCreateTable(entity, schema);
await client.query(createSQL);
console.log(`✓ Created table ${schema}.${tableName} from GraphQL schema`);
}
} finally {
client.release();
}
}

/**
* Check if a table exists
*/
async function tableExists(
pool: Pool,
schema: string,
tableName: string
): Promise<boolean> {
const client = await pool.connect();
try {
const result = await client.query(
`SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = $1 AND table_name = $2
)`,
[schema, tableName]
);
return result.rows[0].exists;
} finally {
client.release();
}
}

/**
* Get row count for a table
*/
async function getRowCount(
pool: Pool,
schema: string,
tableName: string
): Promise<number> {
const client = await pool.connect();
try {
const result = await client.query(
`SELECT COUNT(*) as count FROM ${schema}.${tableName}`
);
return parseInt(result.rows[0].count);
} finally {
client.release();
}
}

/**
* Get column information for a table
*/
async function getTableColumns(
pool: Pool,
schema: string,
tableName: string
): Promise<{ column_name: string; data_type: string }[]> {
const client = await pool.connect();
try {
const result = await client.query(
`SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2
ORDER BY ordinal_position`,
[schema, tableName]
);
return result.rows;
} finally {
client.release();
}
}

describe('Real Data Migration from GraphQL Schema', () => {
let pool: Pool;

beforeAll(async () => {
pool = new Pool(DB_CONFIG);

// Test database connection
const client = await pool.connect();
const result = await client.query('SELECT NOW()');
console.log(`\n✓ Connected to database at ${result.rows[0].now}`);
console.log(` Database: ${DB_CONFIG.database}`);
console.log(` Schema: ${DB_SCHEMA}`);
console.log(` Environment: ${env}\n`);
client.release();
});

afterAll(async () => {
await pool.end();
});

test('should migrate real data from local database', async () => {
// Path to GraphQL schema
const schemaPath = path.join(__dirname, '../../src/configs/schema.graphql');
console.log(`📄 GraphQL schema path: ${schemaPath}`);

// Create V2 tables from GraphQL schema
await createV2TablesFromSchema(pool, DB_SCHEMA, schemaPath);

console.log('\n🔄 Running migration for', ENTITIES_TO_MIGRATE.length, 'entities...\n');

// Run migration
const results = await migrateEntities({
entities: ENTITIES_TO_MIGRATE,
pool,
schema: DB_SCHEMA,
logger: console,
limit: 3000,
dropSourceTables: true,
});

console.log('\n📊 Migration Results:');
console.table(results.map(r => ({
Entity: r.entity,
'Source Table': r.sourceTable,
'Dest Table': r.destTable,
'Rows Copied': r.copiedRows,
'Skipped Columns': r.skippedColumns.join(', '),
Success: r.success ? '✓' : '✗',
Error: r.error || '-',
})));

// Verify results
console.log('\n🔍 Verifying migration results...\n');

const successCount = results.filter(r => r.success).length;
const failCount = results.filter(r => !r.success).length;

console.log(`✅ Successful migrations: ${successCount}`);
if (failCount > 0) {
console.log(`❌ Failed migrations: ${failCount}`);
}

// Show row counts for successful migrations
console.log('\n📈 Row Counts:');
for (const result of results) {
if (result.success) {
const destCount = await getRowCount(pool, DB_SCHEMA, result.destTable);

console.log(` ${result.entity}:`);
console.log(` Destination (${result.destTable}): ${destCount} rows`);

}
}

// Assertions
expect(results.length).toBe(ENTITIES_TO_MIGRATE.length);
expect(successCount).toBeGreaterThan(0);

// Verify successful migrations have data in destination tables
// Note: Destination count may be less than copiedRows due to duplicate IDs with ON CONFLICT DO NOTHING
for (const result of results) {
if (result.success && result.copiedRows > 0) {
const destCount = await getRowCount(pool, DB_SCHEMA, result.destTable);
expect(destCount).toBeGreaterThan(0);
expect(destCount).toBeLessThanOrEqual(result.copiedRows);
}
}

// Verify source tables were dropped after successful migration
console.log('\n🔍 Verifying source tables were dropped...\n');
for (const result of results) {
if (result.success && result.sourceTable) {
const sourceExists = await tableExists(pool, DB_SCHEMA, result.sourceTable);
console.log(` ${result.sourceTable}: ${sourceExists ? '❌ Still exists' : '✓ Dropped'}`);
expect(sourceExists).toBe(false);
}
}
});
});
Loading
Loading