From f4101f2aa2bed2e5595b6c5660aa479e536014cd Mon Sep 17 00:00:00 2001 From: Moses <103143573+Defi-Moses@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:59:45 +0100 Subject: [PATCH] Refactoring rfq-indexer API and adding swagger docs [SLT-228] (#3167) * refactoring and adding swagger * remove testing scripts * fix typos and consistency with 404 errors --- packages/rfq-indexer/api/package.json | 26 ++- packages/rfq-indexer/api/src/.babelrc | 3 + packages/rfq-indexer/api/src/app.ts | 32 ++++ .../conflictingProofsController.ts | 50 ++++++ .../controllers/invalidRelaysController.ts | 52 ++++++ .../pendingTransactionsController.ts | 129 +++++++++++++++ .../refundedAndRelayedController.ts | 43 +++++ .../controllers/transactionIdController.ts | 58 +++++++ packages/rfq-indexer/api/src/index.ts | 100 ------------ packages/rfq-indexer/api/src/jest.config.js | 10 ++ .../middleware/showFirstValidationError.ts | 24 +++ .../api/src/queries/claimsQueries.ts | 17 ++ .../api/src/queries/depositsQueries.ts | 26 +++ packages/rfq-indexer/api/src/queries/index.ts | 5 + .../api/src/queries/proofsQueries.ts | 15 ++ .../api/src/queries/refundsQueries.ts | 15 ++ .../api/src/queries/relaysQueries.ts | 16 ++ .../api/src/routes/conflictingProofsRoute.ts | 65 ++++++++ packages/rfq-indexer/api/src/routes/index.ts | 17 ++ .../api/src/routes/invalidRelaysRoute.ts | 65 ++++++++ .../src/routes/pendingTransactionsRoute.ts | 149 ++++++++++++++++++ .../api/src/routes/refundedAndRelayedRoute.ts | 65 ++++++++ .../api/src/routes/transactionIdRoute.ts | 70 ++++++++ packages/rfq-indexer/api/src/swagger.ts | 28 ++++ .../api/src/utils/isTransaction.ts | 0 .../rfq-indexer/api/src/utils/nestResults.ts | 58 +++++++ yarn.lock | 36 ++++- 27 files changed, 1065 insertions(+), 109 deletions(-) create mode 100644 packages/rfq-indexer/api/src/.babelrc create mode 100644 packages/rfq-indexer/api/src/app.ts create mode 100644 packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts create mode 100644 packages/rfq-indexer/api/src/controllers/invalidRelaysController.ts create mode 100644 packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts create mode 100644 packages/rfq-indexer/api/src/controllers/refundedAndRelayedController.ts create mode 100644 packages/rfq-indexer/api/src/controllers/transactionIdController.ts delete mode 100644 packages/rfq-indexer/api/src/index.ts create mode 100644 packages/rfq-indexer/api/src/jest.config.js create mode 100644 packages/rfq-indexer/api/src/middleware/showFirstValidationError.ts create mode 100644 packages/rfq-indexer/api/src/queries/claimsQueries.ts create mode 100644 packages/rfq-indexer/api/src/queries/depositsQueries.ts create mode 100644 packages/rfq-indexer/api/src/queries/index.ts create mode 100644 packages/rfq-indexer/api/src/queries/proofsQueries.ts create mode 100644 packages/rfq-indexer/api/src/queries/refundsQueries.ts create mode 100644 packages/rfq-indexer/api/src/queries/relaysQueries.ts create mode 100644 packages/rfq-indexer/api/src/routes/conflictingProofsRoute.ts create mode 100644 packages/rfq-indexer/api/src/routes/index.ts create mode 100644 packages/rfq-indexer/api/src/routes/invalidRelaysRoute.ts create mode 100644 packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts create mode 100644 packages/rfq-indexer/api/src/routes/refundedAndRelayedRoute.ts create mode 100644 packages/rfq-indexer/api/src/routes/transactionIdRoute.ts create mode 100644 packages/rfq-indexer/api/src/swagger.ts create mode 100644 packages/rfq-indexer/api/src/utils/isTransaction.ts create mode 100644 packages/rfq-indexer/api/src/utils/nestResults.ts diff --git a/packages/rfq-indexer/api/package.json b/packages/rfq-indexer/api/package.json index dda045192c..534967fbf3 100644 --- a/packages/rfq-indexer/api/package.json +++ b/packages/rfq-indexer/api/package.json @@ -6,17 +6,18 @@ "main": "index.js", "scripts": { "check-env": "dotenv -e .env.local -- printenv | grep DATABASE_URL", - "dev:local": "dotenv -e .env.local -- tsx watch src/index.ts", - "dev:prod": "dotenv -e .env.production -- tsx watch src/index.ts", - "start": "tsx src/index.ts", - "start:local": "dotenv -e .env -- tsx src/index.ts", - "dev": "dotenv -e .env -- tsx watch src/index.ts", + "dev:local": "dotenv -e .env.local -- tsx watch src/app.ts", + "dev:prod": "dotenv -e .env.production -- tsx watch src/app.ts", + "start": "tsx src/app.ts", + "start:local": "dotenv -e .env -- tsx src/app.ts", + "dev": "dotenv -e .env -- tsx watch src/app.ts", "lint:check": " ", "ci:lint": " ", "build:go": " ", "build": " ", "build:slither": " ", - "test:coverage": "echo 'No tests defined.'" + "test": "", + "test:coverage": "echo no tests defined" }, "keywords": [], "author": "", @@ -29,10 +30,12 @@ "@types/node": "^22.5.4", "dotenv-cli": "^7.4.2", "express": "^4.21.0", + "express-validator": "^7.2.0", "graphql": "^16.9.0", "graphql-yoga": "^5.7.0", "kysely": "^0.27.4", "pg": "^8.12.0", + "supertest": "^7.0.0", "ts-node": "^10.9.2", "tsx": "^4.19.1", "typescript": "^5.6.2", @@ -42,8 +45,17 @@ "node": ">=18.17" }, "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.4", + "@babel/preset-typescript": "^7.24.7", "@types/pg": "^8.11.9", - "dotenv": "^16.4.5" + "@types/supertest": "^6.0.2", + "@types/swagger-jsdoc": "6.0.4", + "@types/swagger-ui-express": "4.1.6", + "dotenv": "^16.4.5", + "express-validator": "^7.2.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "repository": { "type": "git", diff --git a/packages/rfq-indexer/api/src/.babelrc b/packages/rfq-indexer/api/src/.babelrc new file mode 100644 index 0000000000..3313ff9ef0 --- /dev/null +++ b/packages/rfq-indexer/api/src/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-typescript"] +} diff --git a/packages/rfq-indexer/api/src/app.ts b/packages/rfq-indexer/api/src/app.ts new file mode 100644 index 0000000000..4209656836 --- /dev/null +++ b/packages/rfq-indexer/api/src/app.ts @@ -0,0 +1,32 @@ +import express from 'express' +import swaggerUi from 'swagger-ui-express' +import { createYoga } from 'graphql-yoga' + +import { specs } from './swagger' +import routes from './routes' +import { schema } from './graphql/schema' +import { overrideJsonBigIntSerialization } from './utils/overrideJsonBigIntSerialization' + +const app = express() +const port = process.env.PORT || 3001 + +overrideJsonBigIntSerialization() + +app.use(express.json()) + +// Swagger UI setup +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)) + +// REST API routes +app.use('/api', routes) + +// GraphQL setup +const yoga = createYoga({ schema }) +app.use('/graphql', yoga) + +export const server = app.listen(port, () => { + console.log(`Server listening at ${port}`) + console.info('API server runs on http://localhost:3001') + console.info('REST requests go through http://localhost:3001/api') + console.info('GraphQL requests go through http://localhost:3001/graphql') +}) diff --git a/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts new file mode 100644 index 0000000000..5079bb0d5f --- /dev/null +++ b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts @@ -0,0 +1,50 @@ +import { Request, Response } from 'express' +import { sql } from 'kysely' + +import { db } from '../db' +import { qDeposits, qRelays, qProofs } from '../queries' +import { nest_results } from '../utils/nestResults' + +export const conflictingProofsController = async ( + req: Request, + res: Response +) => { + try { + const query = db + .with('deposits', () => qDeposits()) + .with('relays', () => qRelays()) + .with('proofs', () => qProofs()) + .with('combined', (qb) => + qb + .selectFrom('deposits') + .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay') + .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof') + .selectAll('deposits') + .selectAll('relays') + .selectAll('proofs') + ) + .selectFrom('combined') + .selectAll() + .where('relayer_proof', 'is not', null) + .where('relayer_relay', 'is not', null) + .where( + (eb) => + sql`LOWER(${eb.ref('relayer_relay')}) != LOWER(${eb.ref( + 'relayer_proof' + )})` + ) + .orderBy('blockTimestamp_proof', 'desc') + + const results = await query.execute() + const conflictingProofs = nest_results(results) + + if (conflictingProofs && conflictingProofs.length > 0) { + res.json(conflictingProofs) + } else { + res.status(200).json({ message: 'No conflicting proofs found' }) + } + } catch (error) { + console.error('Error fetching conflicting proofs:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} diff --git a/packages/rfq-indexer/api/src/controllers/invalidRelaysController.ts b/packages/rfq-indexer/api/src/controllers/invalidRelaysController.ts new file mode 100644 index 0000000000..4d17a56711 --- /dev/null +++ b/packages/rfq-indexer/api/src/controllers/invalidRelaysController.ts @@ -0,0 +1,52 @@ +import { Request, Response } from 'express' + +import { db } from '../db' + +export const recentInvalidRelaysController = async ( + req: Request, + res: Response +) => { + try { + const query = db + .selectFrom('BridgeRelayedEvents') + .leftJoin( + 'BridgeRequestEvents', + 'BridgeRelayedEvents.transactionId', + 'BridgeRequestEvents.transactionId' + ) + .select([ + 'BridgeRelayedEvents.transactionId', + 'BridgeRelayedEvents.blockNumber', + 'BridgeRelayedEvents.blockTimestamp', + 'BridgeRelayedEvents.transactionHash', + 'BridgeRelayedEvents.originChain', + 'BridgeRelayedEvents.destChain', + 'BridgeRelayedEvents.originChainId', + 'BridgeRelayedEvents.destChainId', + 'BridgeRelayedEvents.originToken', + 'BridgeRelayedEvents.destToken', + 'BridgeRelayedEvents.originAmountFormatted', + 'BridgeRelayedEvents.destAmountFormatted', + 'BridgeRelayedEvents.to', + 'BridgeRelayedEvents.relayer', + ]) + // lookback approx 2 weeks + .where( + 'BridgeRelayedEvents.blockTimestamp', + '>', + Math.floor(Date.now() / 1000) - 2 * 7 * 24 * 60 * 60 + ) + .where('BridgeRequestEvents.transactionId', 'is', null) + + const results = await query.execute() + + if (results && results.length > 0) { + res.json(results) + } else { + res.status(200).json({ message: 'No recent invalid relays found' }) + } + } catch (error) { + console.error('Error fetching recent invalid relays:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} diff --git a/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts b/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts new file mode 100644 index 0000000000..dfef98c11e --- /dev/null +++ b/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts @@ -0,0 +1,129 @@ +import { Request, Response } from 'express' + +import { db } from '../db' +import { qDeposits, qRelays, qProofs, qClaims, qRefunds } from '../queries' +import { nest_results } from '../utils/nestResults' + +export const pendingTransactionsMissingClaimController = async ( + req: Request, + res: Response +) => { + try { + const query = db + .with('deposits', () => qDeposits()) + .with('relays', () => qRelays()) + .with('proofs', () => qProofs()) + .with('claims', () => qClaims()) + .with('combined', (qb) => + qb + .selectFrom('deposits') + .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay') + .innerJoin('proofs', 'transactionId_deposit', 'transactionId_proof') + .leftJoin('claims', 'transactionId_deposit', 'transactionId_claim') + .selectAll('deposits') + .selectAll('relays') + .selectAll('proofs') + .where('transactionId_claim', 'is', null) + ) + .selectFrom('combined') + .selectAll() + .orderBy('blockTimestamp_proof', 'desc') + + const results = await query.execute() + const nestedResults = nest_results(results) + + if (nestedResults && nestedResults.length > 0) { + res.json(nestedResults) + } else { + res + .status(404) + .json({ message: 'No pending transactions missing claim found' }) + } + } catch (error) { + console.error('Error fetching pending transactions missing claim:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} + + +export const pendingTransactionsMissingProofController = async ( + req: Request, + res: Response +) => { + try { + const query = db + .with('deposits', () => qDeposits()) + .with('relays', () => qRelays()) + .with('proofs', () => qProofs()) + .with('combined', (qb) => + qb + .selectFrom('deposits') + .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay') + .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof') + .selectAll('deposits') + .selectAll('relays') + .where('transactionId_proof', 'is', null) + ) + .selectFrom('combined') + .selectAll() + .orderBy('blockTimestamp_relay', 'desc') + + const results = await query.execute() + const nestedResults = nest_results(results) + + if (nestedResults && nestedResults.length > 0) { + res.json(nestedResults) + } else { + res + .status(404) + .json({ message: 'No pending transactions missing proof found' }) + } + } catch (error) { + console.error('Error fetching pending transactions missing proof:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} + +export const pendingTransactionsMissingRelayController = async ( + req: Request, + res: Response +) => { + try { + const query = db + .with('deposits', () => qDeposits()) + .with('relays', () => qRelays()) + .with('refunds', () => qRefunds()) + .with( + 'combined', + (qb) => + qb + .selectFrom('deposits') + .selectAll('deposits') + .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay') + .leftJoin( + 'refunds', + 'transactionId_deposit', + 'transactionId_refund' + ) + .where('transactionId_relay', 'is', null) // is not relayed + .where('transactionId_refund', 'is', null) // is not refunded + ) + .selectFrom('combined') + .selectAll() + .orderBy('blockTimestamp_deposit', 'desc') + + const results = await query.execute() + const nestedResults = nest_results(results) + + if (nestedResults && nestedResults.length > 0) { + res.json(nestedResults) + } else { + res + .status(404) + .json({ message: 'No pending transactions missing relay found' }) + } + } catch (error) { + console.error('Error fetching pending transactions missing relay:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} diff --git a/packages/rfq-indexer/api/src/controllers/refundedAndRelayedController.ts b/packages/rfq-indexer/api/src/controllers/refundedAndRelayedController.ts new file mode 100644 index 0000000000..dd9fb2f0af --- /dev/null +++ b/packages/rfq-indexer/api/src/controllers/refundedAndRelayedController.ts @@ -0,0 +1,43 @@ +import { Request, Response } from 'express' + +import { db } from '../db' +import { qDeposits, qRelays, qRefunds } from '../queries' +import { nest_results } from '../utils/nestResults' + +export const refundedAndRelayedTransactionsController = async ( + req: Request, + res: Response +) => { + try { + const query = db + .with('deposits', () => qDeposits()) + .with('relays', () => qRelays()) + .with('refunds', () => qRefunds()) + .with('combined', (qb) => + qb + .selectFrom('deposits') + .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay') + .innerJoin('refunds', 'transactionId_deposit', 'transactionId_refund') + .selectAll('deposits') + .selectAll('relays') + .selectAll('refunds') + ) + .selectFrom('combined') + .selectAll() + .orderBy('blockTimestamp_refund', 'desc') + + const results = await query.execute() + const nestedResults = nest_results(results) + + if (nestedResults && nestedResults.length > 0) { + res.json(nestedResults) + } else { + res + .status(200) + .json({ message: 'No refunded and relayed transactions found' }) + } + } catch (error) { + console.error('Error fetching refunded and relayed transactions:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} diff --git a/packages/rfq-indexer/api/src/controllers/transactionIdController.ts b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts new file mode 100644 index 0000000000..73857496fa --- /dev/null +++ b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts @@ -0,0 +1,58 @@ +import { Request, Response } from 'express' + +import { db } from '../db' +import { qDeposits, qRelays, qProofs, qClaims, qRefunds } from '../queries' +import { nest_results } from '../utils/nestResults' + +export const getTransactionById = async (req: Request, res: Response) => { + const { transactionId } = req.params + + try { + const query = db + .with('deposits', () => + qDeposits().where('transactionId', '=', transactionId as string) + ) + .with('relays', () => qRelays()) + .with('proofs', () => qProofs()) + .with('claims', () => qClaims()) + .with('refunds', () => qRefunds()) + .with('combined', (qb) => + qb + .selectFrom('deposits') + .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay') + .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof') + .leftJoin('claims', 'transactionId_deposit', 'transactionId_claim') + .leftJoin('refunds', 'transactionId_deposit', 'transactionId_refund') + .selectAll('deposits') + .selectAll('relays') + .selectAll('proofs') + .selectAll('claims') + .selectAll('refunds') + ) + .selectFrom('combined') + .selectAll() + + const results = await query.execute() + const nestedResult = nest_results(results)[0] || null + + if (nestedResult) { + const filteredResult = Object.fromEntries( + Object.entries(nestedResult).filter(([_, value]) => { + if (value === null) { + return false + } + if (typeof value !== 'object') { + return true + } + return Object.values(value).some((v) => v !== null) + }) + ) + res.json(filteredResult) + } else { + res.status(200).json({ message: 'Transaction not found' }) + } + } catch (error) { + console.error(error) + res.status(500).json({ message: 'Internal server error' }) + } +} diff --git a/packages/rfq-indexer/api/src/index.ts b/packages/rfq-indexer/api/src/index.ts deleted file mode 100644 index f64c412c84..0000000000 --- a/packages/rfq-indexer/api/src/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -import express from 'express' -import { createYoga } from 'graphql-yoga' - -import { schema } from './graphql/schema' -import { overrideJsonBigIntSerialization } from './utils/overrideJsonBigIntSerialization' -import { resolvers } from './graphql/resolvers' - -overrideJsonBigIntSerialization() - -const app = express() - -const yoga = createYoga({ schema }) - -app.use(yoga.graphqlEndpoint, yoga) - -app.get('/api/hello', (req, res) => { - res.json({ message: 'Hello World!' }) -}) - -app.get('/api/pending-transactions-missing-relay', async (req, res) => { - try { - const pendingTransactions = - await resolvers.Query.pendingTransactionsMissingRelay() - res.json(pendingTransactions) - } catch (error) { - console.error('Error fetching pending transactions missing relay:', error) - res.status(500).json({ error: 'Internal server error' }) - } -}) - -app.get('/api/pending-transactions-missing-proof', async (req, res) => { - try { - const pendingTransactionsMissingProof = - await resolvers.Query.pendingTransactionsMissingProof() - res.json(pendingTransactionsMissingProof) - } catch (error) { - console.error('Error fetching pending transactions missing proof:', error) - res.status(500).json({ error: 'Internal server error' }) - } -}) - -app.get('/api/pending-transactions-missing-claim', async (req, res) => { - try { - const pendingTransactionsMissingClaim = - await resolvers.Query.pendingTransactionsMissingClaim() - res.json(pendingTransactionsMissingClaim) - } catch (error) { - console.error('Error fetching pending transactions missing claim:', error) - res.status(500).json({ error: 'Internal server error' }) - } -}) - -app.get('/api/recent-invalid-relays', async (req, res) => { - try { - const queryResult = await resolvers.Query.recentInvalidRelays() - res.json(queryResult) - } catch (error) { - console.error('Error fetching recent invalid relays:', error) - res.status(500).json({ error: 'Internal server error' }) - } -}) - -app.get('/api/conflicting-proofs', async (req, res) => { - try { - const conflictingProofs = await resolvers.Query.conflictingProofs() - res.json(conflictingProofs) - } catch (error) { - console.error('Error fetching conflicting proofs:', error) - res.status(500).json({ error: 'Internal server error' }) - } -}) - -app.get('/api/transaction/:transactionId', async (req, res) => { - try { - const transactionId = req.params.transactionId - const transaction = await resolvers.Query.transactionById(null, { - transactionId, - }) - res.json(transaction) - } catch (error) { - console.error('Error fetching transaction by ID:', error) - res.status(500).json({ error: 'Internal server error' }) - } -}) - -app.get('/api/refunded-and-relayed-transactions', async (req, res) => { - try { - const transactions = await resolvers.Query.refundedAndRelayedTransactions() - res.json(transactions) - } catch (error) { - console.error('Error fetching refunded and relayed transactions:', error) - res.status(500).json({ error: 'Internal server error' }) - } -}) - -app.listen(process.env.PORT, () => { - console.info('API server runs on http://localhost:3001') - console.info('REST requests go through http://localhost:3001/api') - console.info('GraphQL requests go through http://localhost:3001/graphql') -}) diff --git a/packages/rfq-indexer/api/src/jest.config.js b/packages/rfq-indexer/api/src/jest.config.js new file mode 100644 index 0000000000..ba447263ea --- /dev/null +++ b/packages/rfq-indexer/api/src/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + transform: { + '^.+\\.(ts|tsx)$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleDirectories: ['node_modules', ''], +} diff --git a/packages/rfq-indexer/api/src/middleware/showFirstValidationError.ts b/packages/rfq-indexer/api/src/middleware/showFirstValidationError.ts new file mode 100644 index 0000000000..29ccda3e43 --- /dev/null +++ b/packages/rfq-indexer/api/src/middleware/showFirstValidationError.ts @@ -0,0 +1,24 @@ +import { Request, Response, NextFunction } from 'express' +import { validationResult } from 'express-validator' + +export const showFirstValidationError = ( + req: Request, + res: Response, + next: NextFunction +): void => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + const firstError = errors.array({ onlyFirstError: true })[0] + + res.status(400).json({ + error: { + value: (firstError as any).value, + message: firstError?.msg, + field: firstError?.type === 'field' ? firstError?.path : undefined, + location: (firstError as any).location, + }, + }) + return + } + next() +} diff --git a/packages/rfq-indexer/api/src/queries/claimsQueries.ts b/packages/rfq-indexer/api/src/queries/claimsQueries.ts new file mode 100644 index 0000000000..8ce64a5e3e --- /dev/null +++ b/packages/rfq-indexer/api/src/queries/claimsQueries.ts @@ -0,0 +1,17 @@ +import { db } from '../db' + +// typical fields to return for a BridgeDepositClaimed event when it is joined to a BridgeRequest +export const qClaims = () => { + return db + .selectFrom('BridgeDepositClaimedEvents') + .select([ + 'BridgeDepositClaimedEvents.transactionId as transactionId_claim', + 'BridgeDepositClaimedEvents.blockNumber as blockNumber_claim', + 'BridgeDepositClaimedEvents.blockTimestamp as blockTimestamp_claim', + 'BridgeDepositClaimedEvents.transactionHash as transactionHash_claim', + + 'BridgeDepositClaimedEvents.to as to_claim', + 'BridgeDepositClaimedEvents.relayer as relayer_claim', + 'BridgeDepositClaimedEvents.amountFormatted as amountFormatted_claim', + ]) +} diff --git a/packages/rfq-indexer/api/src/queries/depositsQueries.ts b/packages/rfq-indexer/api/src/queries/depositsQueries.ts new file mode 100644 index 0000000000..61e33aa3d1 --- /dev/null +++ b/packages/rfq-indexer/api/src/queries/depositsQueries.ts @@ -0,0 +1,26 @@ +import { db } from '../db' + +export const qDeposits = () => { + return db + .selectFrom('BridgeRequestEvents') + .select([ + 'BridgeRequestEvents.transactionId as transactionId_deposit', + 'BridgeRequestEvents.blockNumber as blockNumber_deposit', + 'BridgeRequestEvents.blockTimestamp as blockTimestamp_deposit', + 'BridgeRequestEvents.transactionHash as transactionHash_deposit', + 'BridgeRequestEvents.originChain', + 'BridgeRequestEvents.destChain', + 'BridgeRequestEvents.originChainId', + 'BridgeRequestEvents.destChainId', + 'BridgeRequestEvents.originToken', + 'BridgeRequestEvents.destToken', + 'BridgeRequestEvents.originAmountFormatted', + 'BridgeRequestEvents.destAmountFormatted', + 'BridgeRequestEvents.sender', + 'BridgeRequestEvents.sendChainGas', + ]) + .where('BridgeRequestEvents.blockTimestamp', '>', 1722729600) + // if index is partially loaded, we must limit lookback or will have various data issues from relays + // that happened to be in flight at the point of the index's start. + // may also improve query performance +} diff --git a/packages/rfq-indexer/api/src/queries/index.ts b/packages/rfq-indexer/api/src/queries/index.ts new file mode 100644 index 0000000000..72bad2522e --- /dev/null +++ b/packages/rfq-indexer/api/src/queries/index.ts @@ -0,0 +1,5 @@ +export { qClaims } from './claimsQueries' +export { qDeposits } from './depositsQueries' +export { qProofs } from './proofsQueries' +export { qRefunds } from './refundsQueries' +export { qRelays } from './relaysQueries' diff --git a/packages/rfq-indexer/api/src/queries/proofsQueries.ts b/packages/rfq-indexer/api/src/queries/proofsQueries.ts new file mode 100644 index 0000000000..e7a1ccc012 --- /dev/null +++ b/packages/rfq-indexer/api/src/queries/proofsQueries.ts @@ -0,0 +1,15 @@ +import { db } from '../db' + +// typical fields to return for a BridgeProofProvided event when it is joined to a BridgeRequest +export const qProofs = () => { + return db + .selectFrom('BridgeProofProvidedEvents') + .select([ + 'BridgeProofProvidedEvents.transactionId as transactionId_proof', + 'BridgeProofProvidedEvents.blockNumber as blockNumber_proof', + 'BridgeProofProvidedEvents.blockTimestamp as blockTimestamp_proof', + 'BridgeProofProvidedEvents.transactionHash as transactionHash_proof', + + 'BridgeProofProvidedEvents.relayer as relayer_proof', + ]) +} diff --git a/packages/rfq-indexer/api/src/queries/refundsQueries.ts b/packages/rfq-indexer/api/src/queries/refundsQueries.ts new file mode 100644 index 0000000000..9de6f72773 --- /dev/null +++ b/packages/rfq-indexer/api/src/queries/refundsQueries.ts @@ -0,0 +1,15 @@ +import { db } from '../db' + +export const qRefunds = () => { + return db + .selectFrom('BridgeDepositRefundedEvents') + .select([ + 'BridgeDepositRefundedEvents.transactionId as transactionId_refund', + 'BridgeDepositRefundedEvents.blockNumber as blockNumber_refund', + 'BridgeDepositRefundedEvents.blockTimestamp as blockTimestamp_refund', + 'BridgeDepositRefundedEvents.transactionHash as transactionHash_refund', + + 'BridgeDepositRefundedEvents.to as to_refund', + 'BridgeDepositRefundedEvents.amountFormatted as amountFormatted_refund', + ]) +} diff --git a/packages/rfq-indexer/api/src/queries/relaysQueries.ts b/packages/rfq-indexer/api/src/queries/relaysQueries.ts new file mode 100644 index 0000000000..bd7b3990a0 --- /dev/null +++ b/packages/rfq-indexer/api/src/queries/relaysQueries.ts @@ -0,0 +1,16 @@ +import { db } from '../db' + +// typical fields to return for a BridgeRelayed event when it is joined to a BridgeRequest +export const qRelays = () => { + return db + .selectFrom('BridgeRelayedEvents') + .select([ + 'BridgeRelayedEvents.transactionId as transactionId_relay', + 'BridgeRelayedEvents.blockNumber as blockNumber_relay', + 'BridgeRelayedEvents.blockTimestamp as blockTimestamp_relay', + 'BridgeRelayedEvents.transactionHash as transactionHash_relay', + + 'BridgeRelayedEvents.relayer as relayer_relay', + 'BridgeRelayedEvents.to as to_relay', + ]) +} diff --git a/packages/rfq-indexer/api/src/routes/conflictingProofsRoute.ts b/packages/rfq-indexer/api/src/routes/conflictingProofsRoute.ts new file mode 100644 index 0000000000..aa48c89e07 --- /dev/null +++ b/packages/rfq-indexer/api/src/routes/conflictingProofsRoute.ts @@ -0,0 +1,65 @@ +import express from 'express' + +import { conflictingProofsController } from '../controllers/conflictingProofsController' + +const router = express.Router() + +/** + * @openapi + * /conflicting-proofs: + * get: + * summary: Get conflicting proofs + * description: Retrieves a list of transactions where the relayer in the proof differs from the relayer in the relay event + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * Bridge: + * type: object + * description: General transaction fields + * BridgeRequest: + * type: object + * description: Deposit information + * BridgeRelay: + * type: object + * description: Relay information + * BridgeRefund: + * type: object + * description: Refund information + * BridgeProof: + * type: object + * description: Proof information (if available) + * BridgeClaim: + * type: object + * description: Claim information (if available) + * BridgeDispute: + * type: object + * description: Dispute information (if available) + * 404: + * description: No conflicting proofs found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +router.get('/', conflictingProofsController) + +export default router diff --git a/packages/rfq-indexer/api/src/routes/index.ts b/packages/rfq-indexer/api/src/routes/index.ts new file mode 100644 index 0000000000..302f878781 --- /dev/null +++ b/packages/rfq-indexer/api/src/routes/index.ts @@ -0,0 +1,17 @@ +import express from 'express' + +import pendingTransactionsRoute from './pendingTransactionsRoute' +import refundedAndRelayedRoute from './refundedAndRelayedRoute' +import invalidRelaysRoute from './invalidRelaysRoute' +import conflictingProofsRoute from './conflictingProofsRoute' +import transactionIdRoute from './transactionIdRoute' + +const router = express.Router() + +router.use('/pending-transactions', pendingTransactionsRoute) +router.use('/refunded-and-relayed', refundedAndRelayedRoute) +router.use('/invalid-relays', invalidRelaysRoute) +router.use('/conflicting-proofs', conflictingProofsRoute) +router.use('/transaction-id', transactionIdRoute) + +export default router diff --git a/packages/rfq-indexer/api/src/routes/invalidRelaysRoute.ts b/packages/rfq-indexer/api/src/routes/invalidRelaysRoute.ts new file mode 100644 index 0000000000..31356156f2 --- /dev/null +++ b/packages/rfq-indexer/api/src/routes/invalidRelaysRoute.ts @@ -0,0 +1,65 @@ +import express from 'express' + +import { recentInvalidRelaysController } from '../controllers/invalidRelaysController' + +const router = express.Router() + +/** + * @openapi + * /invalid-relays: + * get: + * summary: Get recent invalid relays + * description: Retrieves a list of recent invalid relay events from the past 2 weeks + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * Bridge: + * type: object + * description: General transaction fields + * BridgeRequest: + * type: object + * description: Deposit information + * BridgeRelay: + * type: object + * description: Relay information + * BridgeRefund: + * type: object + * description: Refund information + * BridgeProof: + * type: object + * description: Proof information (if available) + * BridgeClaim: + * type: object + * description: Claim information (if available) + * BridgeDispute: + * type: object + * description: Dispute information (if available) + * 404: + * description: No recent invalid relays found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +router.get('/', recentInvalidRelaysController) + +export default router diff --git a/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts b/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts new file mode 100644 index 0000000000..2dbeafb121 --- /dev/null +++ b/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts @@ -0,0 +1,149 @@ +import express from 'express' + +import { + pendingTransactionsMissingClaimController, + pendingTransactionsMissingProofController, + pendingTransactionsMissingRelayController +} from '../controllers/pendingTransactionsController' + +const router = express.Router() + +/** + * @openapi + * /pending-transactions/missing-claim: + * get: + * summary: Get pending transactions missing claim + * description: Retrieves a list of transactions that have been deposited, relayed, and proven, but not yet claimed + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * Bridge: + * type: object + * description: General transaction fields + * BridgeRequest: + * type: object + * description: Deposit information + * BridgeRelay: + * type: object + * description: Relay information + * BridgeRefund: + * type: object + * description: Refund information + * BridgeProof: + * type: object + * description: Proof information (if available) + * BridgeClaim: + * type: object + * description: Claim information (if available) + * BridgeDispute: + * type: object + * description: Dispute information (if available) + * 404: + * description: No pending transactions missing claim found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +router.get('/missing-claim', pendingTransactionsMissingClaimController) + +/** + * @openapi + * /pending-transactions/missing-proof: + * get: + * summary: Get pending transactions missing proof + * description: Retrieves a list of transactions that have been deposited and relayed, but not yet proven + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * deposit: + * type: object + * relay: + * type: object + * 404: + * description: No pending transactions missing proof found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +router.get('/missing-proof', pendingTransactionsMissingProofController) + +/** + * @openapi + * /pending-transactions/missing-relay: + * get: + * summary: Get pending transactions missing relay + * description: Retrieves a list of transactions that have been deposited, but not yet relayed or refunded + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * deposit: + * type: object + * 404: + * description: No pending transactions missing relay found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +router.get('/missing-relay', pendingTransactionsMissingRelayController) + +export default router diff --git a/packages/rfq-indexer/api/src/routes/refundedAndRelayedRoute.ts b/packages/rfq-indexer/api/src/routes/refundedAndRelayedRoute.ts new file mode 100644 index 0000000000..cd38b3b33e --- /dev/null +++ b/packages/rfq-indexer/api/src/routes/refundedAndRelayedRoute.ts @@ -0,0 +1,65 @@ +import express from 'express' + +import { refundedAndRelayedTransactionsController } from '../controllers/refundedAndRelayedController' + +const router = express.Router() + +/** + * @openapi + * /refunded-and-relayed: + * get: + * summary: Get refunded and relayed transactions + * description: Retrieves a list of transactions that have been both refunded and relayed + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * Bridge: + * type: object + * description: General transaction fields + * BridgeRequest: + * type: object + * description: Deposit information + * BridgeRelay: + * type: object + * description: Relay information + * BridgeRefund: + * type: object + * description: Refund information + * BridgeProof: + * type: object + * description: Proof information (if available) + * BridgeClaim: + * type: object + * description: Claim information (if available) + * BridgeDispute: + * type: object + * description: Dispute information (if available) + * 404: + * description: No refunded and relayed transactions found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +router.get('/', refundedAndRelayedTransactionsController) + +export default router diff --git a/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts b/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts new file mode 100644 index 0000000000..ef0b4077a4 --- /dev/null +++ b/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts @@ -0,0 +1,70 @@ +import express from 'express' + +import { getTransactionById } from '../controllers/transactionIdController' + +const router = express.Router() + +/** + * @openapi + * /transaction-id/{transactionId}: + * get: + * summary: Get transaction details by ID + * description: Retrieves detailed information about a transaction, including deposit, relay, proof, claim, and refund data if available + * parameters: + * - in: path + * name: transactionId + * required: true + * schema: + * type: string + * description: The unique identifier of the transaction + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: object + * properties: +* Bridge: +* type: object +* description: General transaction fields +* BridgeRequest: +* type: object +* description: Deposit information +* BridgeRelay: +* type: object +* description: Relay information +* BridgeRefund: +* type: object +* description: Refund information +* BridgeProof: +* type: object +* description: Proof information (if available) +* BridgeClaim: +* type: object +* description: Claim information (if available) +* BridgeDispute: +* type: object +* description: Dispute information (if available) + * 404: + * description: Transaction not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +router.get('/:transactionId', getTransactionById) + +export default router diff --git a/packages/rfq-indexer/api/src/swagger.ts b/packages/rfq-indexer/api/src/swagger.ts new file mode 100644 index 0000000000..aec6d0fb43 --- /dev/null +++ b/packages/rfq-indexer/api/src/swagger.ts @@ -0,0 +1,28 @@ +import swaggerJsdoc from 'swagger-jsdoc' + +const isDevelopment = process.env.NODE_ENV === 'development' + +const devServer = { + url: 'http://localhost:3001/api', + description: 'Local Development Server', +} + +const prodServer = { + url: 'https://triumphant-magic-production.up.railway.app/api', + description: 'Production Server', +} + +const options: swaggerJsdoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'RFQ Indexer API', + version: '1.0.00', + description: 'API documentation for the RFQ Indexer API', + }, + servers: isDevelopment ? [devServer, prodServer] : [prodServer, devServer], + }, + apis: ['./src/routes/*.ts', './src/*.ts'], +} + +export const specs = swaggerJsdoc(options) diff --git a/packages/rfq-indexer/api/src/utils/isTransaction.ts b/packages/rfq-indexer/api/src/utils/isTransaction.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/rfq-indexer/api/src/utils/nestResults.ts b/packages/rfq-indexer/api/src/utils/nestResults.ts new file mode 100644 index 0000000000..6d31050362 --- /dev/null +++ b/packages/rfq-indexer/api/src/utils/nestResults.ts @@ -0,0 +1,58 @@ +export const nest_results = (sqlResults: any[]) => { + return sqlResults.map((transaction: any) => { + const bridgeRequest: { [key: string]: any } = {} + const bridgeRelay: { [key: string]: any } = {} + const bridgeProof: { [key: string]: any } = {} + const bridgeClaim: { [key: string]: any } = {} + const bridgeRefund: { [key: string]: any } = {} + const bridgeDispute: { [key: string]: any } = {} + const transactionFields: { [key: string]: any } = {} + + let transactionIdSet = false + + for (const [key, value] of Object.entries(transaction)) { + if (key.startsWith('transactionId')) { + if (!transactionIdSet) { + transactionFields[key.replace(/_.+$/, '')] = value + transactionIdSet = true + } + // Ignore other transactionId fields + } else if (key.endsWith('_deposit')) { + bridgeRequest[key.replace('_deposit', '')] = value + } else if (key.endsWith('_relay')) { + bridgeRelay[key.replace('_relay', '')] = value + } else if (key.endsWith('_proof')) { + bridgeProof[key.replace('_proof', '')] = value + } else if (key.endsWith('_claim')) { + bridgeClaim[key.replace('_claim', '')] = value + } else if (key.endsWith('_refund')) { + bridgeRefund[key.replace('_refund', '')] = value + } else if (key.endsWith('_dispute')) { + bridgeDispute[key.replace('_dispute', '')] = value + } else { + transactionFields[key] = value + } + } + + const result: { [key: string]: any } = { Bridge: transactionFields } + if (Object.keys(bridgeRequest).length) { + result.BridgeRequest = bridgeRequest + } + if (Object.keys(bridgeRelay).length) { + result.BridgeRelay = bridgeRelay + } + if (Object.keys(bridgeProof).length) { + result.BridgeProof = bridgeProof + } + if (Object.keys(bridgeClaim).length) { + result.BridgeClaim = bridgeClaim + } + if (Object.keys(bridgeRefund).length) { + result.BridgeRefund = bridgeRefund + } + if (Object.keys(bridgeDispute).length) { + result.BridgeDispute = bridgeDispute + } + return result + }) +} diff --git a/yarn.lock b/yarn.lock index 7c64ab096d..56fa13a4de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9992,12 +9992,12 @@ "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" -"@types/swagger-jsdoc@^6.0.4": +"@types/swagger-jsdoc@6.0.4", "@types/swagger-jsdoc@^6.0.4": version "6.0.4" resolved "https://registry.yarnpkg.com/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz#bb4f60f3a5f103818e022f2e29ff8935113fb83d" integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ== -"@types/swagger-ui-express@^4.1.6": +"@types/swagger-ui-express@4.1.6", "@types/swagger-ui-express@^4.1.6": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz#d0929e3fabac1a96a8a9c6c7ee8d42362c5cdf48" integrity sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg== @@ -19383,6 +19383,15 @@ formidable@^2.1.2: once "^1.4.0" qs "^6.11.0" +formidable@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.1.tgz#9360a23a656f261207868b1484624c4c8d06ee1a" + integrity sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -34235,6 +34244,21 @@ superagent@^8.1.2: qs "^6.11.0" semver "^7.3.8" +superagent@^9.0.1: + version "9.0.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-9.0.2.tgz#a18799473fc57557289d6b63960610e358bdebc1" + integrity sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^3.5.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + superstruct@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.4.tgz#0adb99a7578bd2f1c526220da6571b2d485d91ca" @@ -34248,6 +34272,14 @@ supertest@^6.3.3: methods "^1.1.2" superagent "^8.1.2" +supertest@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.0.0.tgz#cac53b3d6872a0b317980b2b0cfa820f09cd7634" + integrity sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA== + dependencies: + methods "^1.1.2" + superagent "^9.0.1" + supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"