// // Copyright 2021 Vulcanize, Inc. // import assert from 'assert'; import debug from 'debug'; import { GraphQLResolveInfo } from 'graphql'; import { ExpressContext } from 'apollo-server-express'; import winston from 'winston'; import { gqlTotalQueryCount, gqlQueryCount, gqlQueryDuration, getResultState, IndexerInterface, GraphQLBigInt, GraphQLBigDecimal, BlockHeight, OrderDirection, jsonBigIntStringReplacer, EventWatcher, // eslint-disable-next-line @typescript-eslint/no-unused-vars setGQLCacheHints } from '@cerc-io/util'; import { Indexer } from './indexer'; import { Transaction } from './entity/Transaction'; import { Order } from './entity/Order'; import { LendingMarket } from './entity/LendingMarket'; import { User } from './entity/User'; import { DailyVolume } from './entity/DailyVolume'; import { Protocol } from './entity/Protocol'; import { Liquidation } from './entity/Liquidation'; import { Transfer } from './entity/Transfer'; import { Deposit } from './entity/Deposit'; import { TransactionCandleStick } from './entity/TransactionCandleStick'; const log = debug('vulcanize:resolver'); const executeAndRecordMetrics = async ( indexer: Indexer, gqlLogger: winston.Logger, opName: string, expressContext: ExpressContext, operation: () => Promise ) => { gqlTotalQueryCount.inc(1); gqlQueryCount.labels(opName).inc(1); const endTimer = gqlQueryDuration.labels(opName).startTimer(); try { const [result, syncStatus] = await Promise.all([ operation(), indexer.getSyncStatus() ]); gqlLogger.info({ opName, query: expressContext.req.body.query, variables: expressContext.req.body.variables, latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber, urlPath: expressContext.req.path, apiKey: expressContext.req.header('x-api-key'), origin: expressContext.req.headers.origin }); return result; } catch (error) { gqlLogger.error({ opName, error, query: expressContext.req.body.query, variables: expressContext.req.body.variables, urlPath: expressContext.req.path, apiKey: expressContext.req.header('x-api-key'), origin: expressContext.req.headers.origin }); throw error; } finally { endTimer(); } }; export const createResolvers = async ( indexerArg: IndexerInterface, eventWatcher: EventWatcher, gqlLogger: winston.Logger ): Promise => { const indexer = indexerArg as Indexer; // eslint-disable-next-line @typescript-eslint/no-unused-vars const gqlCacheConfig = indexer.serverConfig.gql.cache; return { BigInt: GraphQLBigInt, BigDecimal: GraphQLBigDecimal, Event: { __resolveType: (obj: any) => { assert(obj.__typename); return obj.__typename; } }, Subscription: { onEvent: { subscribe: () => eventWatcher.getEventIterator() } }, Mutation: { watchContract: async (_: any, { address, kind, checkpoint, startingBlock = 1 }: { address: string, kind: string, checkpoint: boolean, startingBlock: number }): Promise => { log('watchContract', address, kind, checkpoint, startingBlock); await indexer.watchContract(address, kind, checkpoint, startingBlock); return true; } }, Query: { transaction: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('transaction', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'transaction', expressContext, async () => indexer.getSubgraphEntity(Transaction, id, block, info) ); }, transactions: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('transactions', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'transactions', expressContext, async () => indexer.getSubgraphEntities( Transaction, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, order: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('order', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'order', expressContext, async () => indexer.getSubgraphEntity(Order, id, block, info) ); }, orders: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('orders', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'orders', expressContext, async () => indexer.getSubgraphEntities( Order, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, lendingMarket: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('lendingMarket', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'lendingMarket', expressContext, async () => indexer.getSubgraphEntity(LendingMarket, id, block, info) ); }, lendingMarkets: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('lendingMarkets', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'lendingMarkets', expressContext, async () => indexer.getSubgraphEntities( LendingMarket, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, user: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('user', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'user', expressContext, async () => indexer.getSubgraphEntity(User, id, block, info) ); }, users: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('users', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'users', expressContext, async () => indexer.getSubgraphEntities( User, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, dailyVolume: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('dailyVolume', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'dailyVolume', expressContext, async () => indexer.getSubgraphEntity(DailyVolume, id, block, info) ); }, dailyVolumes: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('dailyVolumes', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'dailyVolumes', expressContext, async () => indexer.getSubgraphEntities( DailyVolume, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, protocol: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('protocol', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'protocol', expressContext, async () => indexer.getSubgraphEntity(Protocol, id, block, info) ); }, protocols: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('protocols', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'protocols', expressContext, async () => indexer.getSubgraphEntities( Protocol, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, liquidation: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('liquidation', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'liquidation', expressContext, async () => indexer.getSubgraphEntity(Liquidation, id, block, info) ); }, liquidations: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('liquidations', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'liquidations', expressContext, async () => indexer.getSubgraphEntities( Liquidation, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, transfer: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('transfer', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'transfer', expressContext, async () => indexer.getSubgraphEntity(Transfer, id, block, info) ); }, transfers: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('transfers', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'transfers', expressContext, async () => indexer.getSubgraphEntities( Transfer, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, deposit: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('deposit', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'deposit', expressContext, async () => indexer.getSubgraphEntity(Deposit, id, block, info) ); }, deposits: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('deposits', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'deposits', expressContext, async () => indexer.getSubgraphEntities( Deposit, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, transactionCandleStick: async ( _: any, { id, block = {} }: { id: string, block: BlockHeight }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('transactionCandleStick', id, JSON.stringify(block, jsonBigIntStringReplacer)); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'transactionCandleStick', expressContext, async () => indexer.getSubgraphEntity(TransactionCandleStick, id, block, info) ); }, transactionCandleSticks: async ( _: any, { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, expressContext: ExpressContext, info: GraphQLResolveInfo ) => { log('transactionCandleSticks', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); // Set cache-control hints // setGQLCacheHints(info, block, gqlCacheConfig); return executeAndRecordMetrics( indexer, gqlLogger, 'transactionCandleSticks', expressContext, async () => indexer.getSubgraphEntities( TransactionCandleStick, block, where, { limit: first, skip, orderBy, orderDirection }, info ) ); }, events: async ( _: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }, expressContext: ExpressContext ) => { log('events', blockHash, contractAddress, name); return executeAndRecordMetrics( indexer, gqlLogger, 'events', expressContext, async () => { const block = await indexer.getBlockProgress(blockHash); if (!block || !block.isComplete) { throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`); } const events = await indexer.getEventsByFilter(blockHash, contractAddress, name); return events.map(event => indexer.getResultEvent(event)); } ); }, eventsInRange: async ( _: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }, expressContext: ExpressContext ) => { log('eventsInRange', fromBlockNumber, toBlockNumber); return executeAndRecordMetrics( indexer, gqlLogger, 'eventsInRange', expressContext, async () => { const syncStatus = await indexer.getSyncStatus(); if (!syncStatus) { throw new Error('No blocks processed yet'); } if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) { throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`); } const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber); return events.map(event => indexer.getResultEvent(event)); } ); }, getStateByCID: async ( _: any, { cid }: { cid: string }, expressContext: ExpressContext ) => { log('getStateByCID', cid); return executeAndRecordMetrics( indexer, gqlLogger, 'getStateByCID', expressContext, async () => { const state = await indexer.getStateByCID(cid); return state && state.block.isComplete ? getResultState(state) : undefined; } ); }, getState: async ( _: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }, expressContext: ExpressContext ) => { log('getState', blockHash, contractAddress, kind); return executeAndRecordMetrics( indexer, gqlLogger, 'getState', expressContext, async () => { const state = await indexer.getPrevState(blockHash, contractAddress, kind); return state && state.block.isComplete ? getResultState(state) : undefined; } ); }, _meta: async ( _: any, { block = {} }: { block: BlockHeight }, expressContext: ExpressContext ) => { log('_meta'); return executeAndRecordMetrics( indexer, gqlLogger, '_meta', expressContext, async () => indexer.getMetaData(block) ); }, getSyncStatus: async ( _: any, __: Record, expressContext: ExpressContext ) => { log('getSyncStatus'); return executeAndRecordMetrics( indexer, gqlLogger, 'getSyncStatus', expressContext, async () => indexer.getSyncStatus() ); } } }; };