diff --git a/packages/uni-info-watcher/docs/analysis/schema/frontend.graphql b/packages/uni-info-watcher/docs/analysis/schema/frontend.graphql index 0463f144..22fff958 100644 --- a/packages/uni-info-watcher/docs/analysis/schema/frontend.graphql +++ b/packages/uni-info-watcher/docs/analysis/schema/frontend.graphql @@ -103,6 +103,7 @@ type Transaction { } type Token { + decimals: BigInt! derivedETH: BigDecimal! feesUSD: BigDecimal! id: ID! @@ -362,9 +363,11 @@ type Query { ): [TokenHourData!]! tokens( + first: Int = 100 orderBy: Token_orderBy orderDirection: OrderDirection where: Token_filter + block: Block_height ): [Token!]! transactions( diff --git a/packages/uni-info-watcher/environments/local.toml b/packages/uni-info-watcher/environments/local.toml index 885a22bb..d2e81ae5 100644 --- a/packages/uni-info-watcher/environments/local.toml +++ b/packages/uni-info-watcher/environments/local.toml @@ -1,6 +1,9 @@ [server] host = "127.0.0.1" port = 3004 + # Use mode demo when running watcher locally. + # Mode demo whitelists all tokens so that entity values get updated. + mode = "demo" [database] type = "postgres" diff --git a/packages/uni-info-watcher/src/database.ts b/packages/uni-info-watcher/src/database.ts index 8e35629d..3e7e5f94 100644 --- a/packages/uni-info-watcher/src/database.ts +++ b/packages/uni-info-watcher/src/database.ts @@ -18,7 +18,8 @@ import { DatabaseInterface, BlockHeight, QueryOptions, - Where + Where, + Relation } from '@vulcanize/util'; import { Factory } from './entity/Factory'; @@ -412,7 +413,7 @@ export class Database implements DatabaseInterface { return entity; } - async getModelEntities (queryRunner: QueryRunner, entity: new () => Entity, block: BlockHeight, where: Where = {}, queryOptions: QueryOptions = {}, relations: string[] = []): Promise { + async getModelEntities (queryRunner: QueryRunner, entity: new () => Entity, block: BlockHeight, where: Where = {}, queryOptions: QueryOptions = {}, relations: Relation[] = []): Promise { return this._baseDatabase.getModelEntities(queryRunner, entity, block, where, queryOptions, relations); } diff --git a/packages/uni-info-watcher/src/fill.ts b/packages/uni-info-watcher/src/fill.ts index d1325a22..3d1076b3 100644 --- a/packages/uni-info-watcher/src/fill.ts +++ b/packages/uni-info-watcher/src/fill.ts @@ -50,7 +50,7 @@ export const main = async (): Promise => { assert(config.server, 'Missing server config'); - const { upstream, database: dbConfig, jobQueue: jobQueueConfig } = config; + const { upstream, database: dbConfig, jobQueue: jobQueueConfig, server: { mode } } = config; assert(dbConfig, 'Missing database config'); @@ -74,7 +74,7 @@ export const main = async (): Promise => { // Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries const pubsub = new PubSub(); - const indexer = new Indexer(db, uniClient, erc20Client, ethClient); + const indexer = new Indexer(db, uniClient, erc20Client, ethClient, mode); assert(jobQueueConfig, 'Missing job queue config'); const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig; diff --git a/packages/uni-info-watcher/src/indexer.ts b/packages/uni-info-watcher/src/indexer.ts index 96282b73..9d390f4f 100644 --- a/packages/uni-info-watcher/src/indexer.ts +++ b/packages/uni-info-watcher/src/indexer.ts @@ -11,7 +11,7 @@ import { utils } from 'ethers'; import { Client as UniClient } from '@vulcanize/uni-watcher'; import { Client as ERC20Client } from '@vulcanize/erc20-watcher'; import { EthClient } from '@vulcanize/ipld-eth-client'; -import { IndexerInterface, Indexer as BaseIndexer, QueryOptions, OrderDirection, BlockHeight } from '@vulcanize/util'; +import { IndexerInterface, Indexer as BaseIndexer, QueryOptions, OrderDirection, BlockHeight, Relation } from '@vulcanize/util'; import { findEthPerToken, getEthPriceInUSD, getTrackedAmountUSD, sqrtPriceX96ToTokenPrices, WHITELIST_TOKENS } from './utils/pricing'; import { updatePoolDayData, updatePoolHourData, updateTokenDayData, updateTokenHourData, updateUniswapDayData } from './utils/interval-updates'; @@ -52,8 +52,9 @@ export class Indexer implements IndexerInterface { _erc20Client: ERC20Client _ethClient: EthClient _baseIndexer: BaseIndexer + _isDemo: boolean - constructor (db: Database, uniClient: UniClient, erc20Client: ERC20Client, ethClient: EthClient) { + constructor (db: Database, uniClient: UniClient, erc20Client: ERC20Client, ethClient: EthClient, mode: string) { assert(db); assert(uniClient); assert(erc20Client); @@ -64,6 +65,7 @@ export class Indexer implements IndexerInterface { this._erc20Client = erc20Client; this._ethClient = ethClient; this._baseIndexer = new BaseIndexer(this._db, this._ethClient); + this._isDemo = mode === 'demo'; } getResultEvent (event: Event): ResultEvent { @@ -255,7 +257,7 @@ export class Indexer implements IndexerInterface { return res; } - async getEntities (entity: new () => Entity, block: BlockHeight, where: { [key: string]: any } = {}, queryOptions: QueryOptions, relations?: string[]): Promise { + async getEntities (entity: new () => Entity, block: BlockHeight, where: { [key: string]: any } = {}, queryOptions: QueryOptions, relations?: Relation[]): Promise { const dbTx = await this._db.createTransactionRunner(); let res; @@ -439,6 +441,11 @@ export class Indexer implements IndexerInterface { token0 = await this._db.saveToken(dbTx, token0, block); token1 = await this._db.saveToken(dbTx, token1, block); + token0 = await this._db.getToken(dbTx, token0); + token1 = await this._db.getToken(dbTx, token1); + assert(token0); + assert(token1); + pool.token0 = token0; pool.token1 = token1; pool.feeTier = BigInt(fee); @@ -448,11 +455,11 @@ export class Indexer implements IndexerInterface { pool = await this._db.savePool(dbTx, pool, block); // Update white listed pools. - if (WHITELIST_TOKENS.includes(token0.id)) { + if (WHITELIST_TOKENS.includes(token0.id) || this._isDemo) { token1.whitelistPools.push(pool); } - if (WHITELIST_TOKENS.includes(token1.id)) { + if (WHITELIST_TOKENS.includes(token1.id) || this._isDemo) { token0.whitelistPools.push(pool); } @@ -860,7 +867,7 @@ export class Indexer implements IndexerInterface { const amount1USD = amount1ETH.times(bundle.ethPriceUSD); // Get amount that should be tracked only - div 2 because cant count both input and output as volume. - const trackedAmountUSD = await getTrackedAmountUSD(this._db, dbTx, amount0Abs, token0, amount1Abs, token1); + const trackedAmountUSD = await getTrackedAmountUSD(this._db, dbTx, amount0Abs, token0, amount1Abs, token1, this._isDemo); const amountTotalUSDTracked = trackedAmountUSD.div(new Decimal('2')); const amountTotalETHTracked = safeDiv(amountTotalUSDTracked, bundle.ethPriceUSD); const amountTotalUSDUntracked = amount0USD.plus(amount1USD).div(new Decimal('2')); diff --git a/packages/uni-info-watcher/src/job-runner.ts b/packages/uni-info-watcher/src/job-runner.ts index f76fe6f4..51e4a1dd 100644 --- a/packages/uni-info-watcher/src/job-runner.ts +++ b/packages/uni-info-watcher/src/job-runner.ts @@ -90,7 +90,7 @@ export const main = async (): Promise => { assert(config.server, 'Missing server config'); - const { upstream, database: dbConfig, jobQueue: jobQueueConfig } = config; + const { upstream, database: dbConfig, jobQueue: jobQueueConfig, server: { mode } } = config; assert(dbConfig, 'Missing database config'); @@ -116,7 +116,7 @@ export const main = async (): Promise => { const erc20Client = new ERC20Client(tokenWatcher); - const indexer = new Indexer(db, uniClient, erc20Client, ethClient); + const indexer = new Indexer(db, uniClient, erc20Client, ethClient, mode); assert(jobQueueConfig, 'Missing job queue config'); diff --git a/packages/uni-info-watcher/src/resolvers.ts b/packages/uni-info-watcher/src/resolvers.ts index 9a0b4981..cc138fcb 100644 --- a/packages/uni-info-watcher/src/resolvers.ts +++ b/packages/uni-info-watcher/src/resolvers.ts @@ -63,7 +63,7 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch burns: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('burns', first, orderBy, orderDirection, where); - return indexer.getEntities(Burn, {}, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); + return indexer.getEntities(Burn, {}, where, { limit: first, orderBy, orderDirection }, ['burn.pool', 'burn.transaction']); }, factories: async (_: any, { block = {}, first }: { first: number, block: BlockHeight }) => { @@ -75,7 +75,7 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch mints: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('mints', first, orderBy, orderDirection, where); - return indexer.getEntities(Mint, {}, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); + return indexer.getEntities(Mint, {}, where, { limit: first, orderBy, orderDirection }, ['mint.pool', 'mint.transaction']); }, pool: async (_: any, { id, block = {} }: { id: string, block: BlockHeight }) => { @@ -93,13 +93,13 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch pools: async (_: any, { block = {}, first, orderBy, orderDirection, where = {} }: { block: BlockHeight, first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('pools', block, first, orderBy, orderDirection, where); - return indexer.getEntities(Pool, block, where, { limit: first, orderBy, orderDirection }, ['token0', 'token1']); + return indexer.getEntities(Pool, block, where, { limit: first, orderBy, orderDirection }, ['pool.token0', 'pool.token1']); }, swaps: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('swaps', first, orderBy, orderDirection, where); - return indexer.getEntities(Swap, {}, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); + return indexer.getEntities(Swap, {}, where, { limit: first, orderBy, orderDirection }, ['swap.pool', 'swap.transaction']); }, ticks: async (_: any, { block = {}, first, skip, where = {} }: { block: BlockHeight, first: number, skip: number, where: { [key: string]: any } }) => { @@ -114,10 +114,10 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch return indexer.getToken(id, block); }, - tokens: async (_: any, { orderBy, orderDirection, where }: { orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { + tokens: async (_: any, { block = {}, first, orderBy, orderDirection, where }: { block: BlockHeight, first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('tokens', orderBy, orderDirection, where); - return indexer.getEntities(Token, {}, where, { orderBy, orderDirection }); + return indexer.getEntities(Token, block, where, { limit: first, orderBy, orderDirection }); }, tokenDayDatas: async (_: any, { first, skip, orderBy, orderDirection, where }: { first: number, skip: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { @@ -135,7 +135,65 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch transactions: async (_: any, { first, orderBy, orderDirection }: { first: number, orderBy: string, orderDirection: OrderDirection}) => { log('transactions', first, orderBy, orderDirection); - return indexer.getEntities(Transaction, {}, {}, { limit: first, orderBy, orderDirection }, ['burns', 'mints', 'swaps']); + return indexer.getEntities( + Transaction, + {}, + {}, + { limit: first, orderBy, orderDirection }, + [ + 'transaction.mints', + 'transaction.burns', + 'transaction.swaps', + { + property: 'mints.transaction', + alias: 'mintsTransaction' + }, + { + property: 'burns.transaction', + alias: 'burnsTransaction' + }, + { + property: 'swaps.transaction', + alias: 'swapsTransaction' + }, + { + property: 'mints.pool', + alias: 'mintsPool' + }, + { + property: 'burns.pool', + alias: 'burnsPool' + }, + { + property: 'swaps.pool', + alias: 'swapsPool' + }, + { + property: 'mintsPool.token0', + alias: 'mintsPoolToken0' + }, + { + property: 'mintsPool.token1', + alias: 'mintsPoolToken1' + }, + { + property: 'burnsPool.token0', + alias: 'burnsPoolToken0' + }, + { + property: 'burnsPool.token1', + alias: 'burnsPoolToken1' + }, + { + property: 'swapsPool.token0', + alias: 'swapsPoolToken0' + }, + { + property: 'swapsPool.token1', + alias: 'swapsPoolToken1' + } + ] + ); }, uniswapDayDatas: async (_: any, { first, skip, orderBy, orderDirection, where }: { first: number, skip: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { @@ -147,7 +205,20 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch positions: async (_: any, { first, where }: { first: number, where: { [key: string]: any } }) => { log('positions', first, where); - return indexer.getEntities(Position, {}, where, { limit: first }, ['pool', 'token0', 'token1', 'tickLower', 'tickUpper', 'transaction']); + return indexer.getEntities( + Position, + {}, + where, + { limit: first }, + [ + 'position.pool', + 'position.token0', + 'position.token1', + 'position.tickLower', + 'position.tickUpper', + 'position.transaction' + ] + ); }, blocks: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { diff --git a/packages/uni-info-watcher/src/schema.ts b/packages/uni-info-watcher/src/schema.ts index ea21654b..c8cce5d8 100644 --- a/packages/uni-info-watcher/src/schema.ts +++ b/packages/uni-info-watcher/src/schema.ts @@ -109,6 +109,7 @@ type Transaction { } type Token { + decimals: BigInt! derivedETH: BigDecimal! feesUSD: BigDecimal! id: ID! @@ -440,9 +441,11 @@ type Query { ): [TokenHourData!]! tokens( + first: Int = 100 orderBy: Token_orderBy orderDirection: OrderDirection where: Token_filter + block: Block_height ): [Token!]! transactions( diff --git a/packages/uni-info-watcher/src/server.ts b/packages/uni-info-watcher/src/server.ts index 5bb403c3..09825224 100644 --- a/packages/uni-info-watcher/src/server.ts +++ b/packages/uni-info-watcher/src/server.ts @@ -42,7 +42,7 @@ export const main = async (): Promise => { assert(config.server, 'Missing server config'); - const { host, port } = config.server; + const { host, port, mode } = config.server; const { upstream, database: dbConfig, jobQueue: jobQueueConfig } = config; @@ -74,7 +74,7 @@ export const main = async (): Promise => { const uniClient = new UniClient(uniWatcher); const erc20Client = new ERC20Client(tokenWatcher); - const indexer = new Indexer(db, uniClient, erc20Client, ethClient); + const indexer = new Indexer(db, uniClient, erc20Client, ethClient, mode); assert(jobQueueConfig, 'Missing job queue config'); diff --git a/packages/uni-info-watcher/src/utils/pricing.ts b/packages/uni-info-watcher/src/utils/pricing.ts index b44c5c5e..055ddbf1 100644 --- a/packages/uni-info-watcher/src/utils/pricing.ts +++ b/packages/uni-info-watcher/src/utils/pricing.ts @@ -131,7 +131,8 @@ export const getTrackedAmountUSD = async ( tokenAmount0: Decimal, token0: Token, tokenAmount1: Decimal, - token1: Token + token1: Token, + isDemo: boolean ): Promise => { const bundle = await db.getBundle(dbTx, { id: '1' }); assert(bundle); @@ -139,7 +140,8 @@ export const getTrackedAmountUSD = async ( const price1USD = token1.derivedETH.times(bundle.ethPriceUSD); // Both are whitelist tokens, return sum of both amounts. - if (WHITELIST_TOKENS.includes(token0.id) && WHITELIST_TOKENS.includes(token1.id)) { + // Use demo mode + if ((WHITELIST_TOKENS.includes(token0.id) && WHITELIST_TOKENS.includes(token1.id)) || isDemo) { return tokenAmount0.times(price0USD).plus(tokenAmount1.times(price1USD)); } diff --git a/packages/util/src/database.ts b/packages/util/src/database.ts index 33249d61..13e9e5e9 100644 --- a/packages/util/src/database.ts +++ b/packages/util/src/database.ts @@ -61,6 +61,8 @@ export interface Where { }] } +export type Relation = string | { property: string, alias: string } + export class Database { _config: ConnectionOptions _conn!: Connection @@ -330,7 +332,7 @@ export class Database { return await repo.save(entity); } - async getModelEntities (queryRunner: QueryRunner, entity: new () => Entity, block: BlockHeight, where: Where = {}, queryOptions: QueryOptions = {}, relations: string[] = []): Promise { + async getModelEntities (queryRunner: QueryRunner, entity: new () => Entity, block: BlockHeight, where: Where = {}, queryOptions: QueryOptions = {}, relations: Relation[] = []): Promise { const repo = queryRunner.manager.getRepository(entity); const { tableName } = repo.metadata; @@ -357,7 +359,17 @@ export class Database { .setParameters(subQuery.getParameters()); relations.forEach(relation => { - selectQueryBuilder = selectQueryBuilder.leftJoinAndSelect(`${repo.metadata.tableName}.${relation}`, relation); + let alias, property; + + if (typeof relation === 'string') { + [, alias] = relation.split('.'); + property = relation; + } else { + alias = relation.alias; + property = relation.property; + } + + selectQueryBuilder = selectQueryBuilder.leftJoinAndSelect(property, alias); }); Object.entries(where).forEach(([field, filters]) => { @@ -393,6 +405,10 @@ export class Database { whereClause += '%'; } else if (operator === 'in') { whereClause += ')'; + + if (!value.length) { + whereClause = 'FALSE'; + } } selectQueryBuilder = selectQueryBuilder.andWhere(whereClause, { [variableName]: value });