diff --git a/packages/erc20-watcher/environments/local.toml b/packages/erc20-watcher/environments/local.toml index 64137e8c..601d1aac 100644 --- a/packages/erc20-watcher/environments/local.toml +++ b/packages/erc20-watcher/environments/local.toml @@ -22,7 +22,7 @@ subscribersDir = "src/subscriber" [upstream] - gqlEndpoint = "http://127.0.0.1:8083/graphql" + gqlEndpoint = "http://127.0.0.1:8082/graphql" gqlSubscriptionEndpoint = "http://127.0.0.1:5000/graphql" [upstream.cache] diff --git a/packages/erc20-watcher/src/client.ts b/packages/erc20-watcher/src/client.ts index 2c0a38b8..50c14ca1 100644 --- a/packages/erc20-watcher/src/client.ts +++ b/packages/erc20-watcher/src/client.ts @@ -1,24 +1,19 @@ -import { gql } from 'apollo-server-express'; -import { GraphQLClient } from '@vulcanize/ipld-eth-client'; +import { gql } from '@apollo/client/core'; +import { GraphQLClient, GraphQLConfig } from '@vulcanize/ipld-eth-client'; -import { querySymbol } from './queries'; - -interface Config { - gqlEndpoint: string; - gqlSubscriptionEndpoint: string; -} +import { queryName, queryDecimals, queryTotalSupply, querySymbol } from './queries'; export class Client { - _config: Config; + _config: GraphQLConfig; _client: GraphQLClient; - constructor (config: Config) { + constructor (config: GraphQLConfig) { this._config = config; this._client = new GraphQLClient(config); } - async getSymbol (blockHash: string | undefined, token: string): Promise { + async getSymbol (blockHash: string, token: string): Promise { const { symbol } = await this._client.query( gql(querySymbol), { blockHash, token } @@ -26,4 +21,31 @@ export class Client { return symbol; } + + async getName (blockHash: string, token: string): Promise { + const { name } = await this._client.query( + gql(queryName), + { blockHash, token } + ); + + return name; + } + + async getTotalSupply (blockHash: string, token: string): Promise { + const { totalSupply } = await this._client.query( + gql(queryTotalSupply), + { blockHash, token } + ); + + return totalSupply; + } + + async getDecimals (blockHash: string, token: string): Promise { + const { decimals } = await this._client.query( + gql(queryDecimals), + { blockHash, token } + ); + + return decimals; + } } diff --git a/packages/ipld-eth-client/src/eth-client.ts b/packages/ipld-eth-client/src/eth-client.ts index 61c693c1..852ac69a 100644 --- a/packages/ipld-eth-client/src/eth-client.ts +++ b/packages/ipld-eth-client/src/eth-client.ts @@ -4,11 +4,9 @@ import { Cache } from '@vulcanize/cache'; import ethQueries from './eth-queries'; import { padKey } from './utils'; -import { GraphQLClient } from './graphql-client'; +import { GraphQLClient, GraphQLConfig } from './graphql-client'; -interface Config { - gqlEndpoint: string; - gqlSubscriptionEndpoint: string; +interface Config extends GraphQLConfig { cache: Cache | undefined; } diff --git a/packages/ipld-eth-client/src/graphql-client.ts b/packages/ipld-eth-client/src/graphql-client.ts index 38269342..6dc8b837 100644 --- a/packages/ipld-eth-client/src/graphql-client.ts +++ b/packages/ipld-eth-client/src/graphql-client.ts @@ -10,16 +10,16 @@ import { WebSocketLink } from '@apollo/client/link/ws'; const log = debug('vulcanize:client'); -interface Config { +export interface GraphQLConfig { gqlEndpoint: string; gqlSubscriptionEndpoint: string; } export class GraphQLClient { - _config: Config; + _config: GraphQLConfig; _client: ApolloClient; - constructor (config: Config) { + constructor (config: GraphQLConfig) { this._config = config; const { gqlEndpoint, gqlSubscriptionEndpoint } = config; diff --git a/packages/uni-info-watcher/src/database.ts b/packages/uni-info-watcher/src/database.ts index 1bbf34bc..d274d770 100644 --- a/packages/uni-info-watcher/src/database.ts +++ b/packages/uni-info-watcher/src/database.ts @@ -30,7 +30,19 @@ export class Database { return this._conn.close(); } - async loadFactory ({ id, blockNumber }: DeepPartial): Promise { + async getToken ({ id, blockNumber }: DeepPartial): Promise { + const repo = this._conn.getRepository(Token); + + return repo.createQueryBuilder('token') + .where('id = :id AND block_number <= :blockNumber', { + id, + blockNumber + }) + .orderBy('token.block_number', 'DESC') + .getOne(); + } + + async loadFactory ({ id, blockNumber, ...values }: DeepPartial): Promise { return this._conn.transaction(async (tx) => { const repo = tx.getRepository(Factory); @@ -39,10 +51,11 @@ export class Database { id, blockNumber }) + .orderBy('factory.block_number', 'DESC') .getOne(); if (!entity) { - entity = repo.create({ blockNumber, id }); + entity = repo.create({ blockNumber, id, ...values }); entity = await repo.save(entity); } @@ -50,7 +63,7 @@ export class Database { }); } - async loadPool ({ id, blockNumber }: DeepPartial): Promise { + async loadPool ({ id, blockNumber, ...values }: DeepPartial): Promise { return this._conn.transaction(async (tx) => { const repo = tx.getRepository(Pool); @@ -59,10 +72,11 @@ export class Database { id, blockNumber }) + .orderBy('pool.block_number', 'DESC') .getOne(); if (!entity) { - entity = repo.create({ blockNumber, id }); + entity = repo.create({ blockNumber, id, ...values }); entity = await repo.save(entity); } @@ -70,7 +84,7 @@ export class Database { }); } - async loadToken ({ id, blockNumber }: DeepPartial, getValues: () => Promise>): Promise { + async loadToken ({ id, blockNumber, ...values }: DeepPartial): Promise { return this._conn.transaction(async (tx) => { const repo = tx.getRepository(Token); @@ -79,11 +93,11 @@ export class Database { id, blockNumber }) + .orderBy('token.block_number', 'DESC') .getOne(); if (!entity) { - const tokenValues = await getValues(); - entity = repo.create({ blockNumber, id, ...tokenValues }); + entity = repo.create({ blockNumber, id, ...values }); entity = await repo.save(entity); } @@ -91,6 +105,14 @@ export class Database { }); } + async saveFactory (factory: Factory, blockNumber: number): Promise { + return this._conn.transaction(async (tx) => { + const repo = tx.getRepository(Factory); + factory.blockNumber = blockNumber; + return repo.save(factory); + }); + } + // Returns true if events have already been synced for the (block, token) combination. async didSyncEvents ({ blockHash, token }: { blockHash: string, token: string }): Promise { const numRows = await this._conn.getRepository(EventSyncProgress) diff --git a/packages/uni-info-watcher/src/entity/Factory.ts b/packages/uni-info-watcher/src/entity/Factory.ts index 54fec9cd..d84cb991 100644 --- a/packages/uni-info-watcher/src/entity/Factory.ts +++ b/packages/uni-info-watcher/src/entity/Factory.ts @@ -6,9 +6,11 @@ export class Factory { @PrimaryColumn('varchar', { length: 42 }) id!: string; - @Column('numeric') + @PrimaryColumn('numeric') blockNumber!: number; - @Column('numeric', { default: 0 }) - poolCount!: number; + @Column('numeric', { default: BigInt(0) }) + poolCount!: bigint; + + // TODO: Add remaining fields when they are used. } diff --git a/packages/uni-info-watcher/src/entity/Pool.ts b/packages/uni-info-watcher/src/entity/Pool.ts index 3786a3e2..64175646 100644 --- a/packages/uni-info-watcher/src/entity/Pool.ts +++ b/packages/uni-info-watcher/src/entity/Pool.ts @@ -1,4 +1,6 @@ -import { Entity, PrimaryColumn, Column, Index } from 'typeorm'; +import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; + +import { Token } from './Token'; @Entity() @Index(['blockNumber', 'id']) @@ -6,6 +8,17 @@ export class Pool { @PrimaryColumn('varchar', { length: 42 }) id!: string; - @Column('numeric') + @PrimaryColumn('numeric') blockNumber!: number; + + @ManyToOne(() => Token) + token0!: Token; + + @ManyToOne(() => Token) + token1!: Token; + + @Column('numeric') + feeTier!: bigint + + // TODO: Add remaining fields when they are used. } diff --git a/packages/uni-info-watcher/src/entity/Token.ts b/packages/uni-info-watcher/src/entity/Token.ts index 524ef018..d9cb9328 100644 --- a/packages/uni-info-watcher/src/entity/Token.ts +++ b/packages/uni-info-watcher/src/entity/Token.ts @@ -6,9 +6,17 @@ export class Token { @PrimaryColumn('varchar', { length: 42 }) id!: string; - @Column('numeric') + @PrimaryColumn('numeric') blockNumber!: number; @Column('varchar') symbol!: string; + + @Column('varchar') + name!: string; + + @Column('numeric') + totalSupply!: number; + + // TODO: Add remaining fields when they are used. } diff --git a/packages/uni-info-watcher/src/events.ts b/packages/uni-info-watcher/src/events.ts index fab60719..61021c3c 100644 --- a/packages/uni-info-watcher/src/events.ts +++ b/packages/uni-info-watcher/src/events.ts @@ -2,6 +2,7 @@ import assert from 'assert'; import debug from 'debug'; import { Client as UniClient } from '@vulcanize/uni-watcher'; import { Client as ERC20Client } from '@vulcanize/erc20-watcher'; +import { BigNumber } from 'ethers'; import { Database } from './database'; @@ -58,6 +59,7 @@ export class EventWatcher { switch (eventType) { case 'PoolCreatedEvent': + log('PoolCreated event', contract); this._handlePoolCreated(blockHash, blockNumber, contract, eventValues as PoolCreatedEvent); break; @@ -67,28 +69,62 @@ export class EventWatcher { } async _handlePoolCreated (blockHash: string, blockNumber: number, contractAddress: string, poolCreatedEvent: PoolCreatedEvent): Promise { - const { token0: token0Address, token1: token1Address, fee, tickSpacing, pool: poolAddress } = poolCreatedEvent; + const { token0: token0Address, token1: token1Address, fee, pool: poolAddress } = poolCreatedEvent; // Load factory. const factory = await this._db.loadFactory({ blockNumber, id: contractAddress }); - factory.poolCount = factory.poolCount + 1; - // Create new Pool entity. - const pool = this._db.loadPool({ blockNumber, id: poolAddress }); + // Update Factory. + let factoryPoolCount = BigNumber.from(factory.poolCount); + factoryPoolCount = factoryPoolCount.add(1); + factory.poolCount = BigInt(factoryPoolCount.toHexString()); - // TODO: Load Token entities. - const getTokenValues = async (tokenAddress: string) => { + // Get Tokens. + let [token0, token1] = await Promise.all([ + this._db.getToken({ blockNumber, id: token0Address }), + this._db.getToken({ blockNumber, id: token1Address }) + ]); + + // Create Token. + const createToken = async (tokenAddress: string) => { const { value: symbol } = await this._erc20Client.getSymbol(blockHash, tokenAddress); - return { symbol }; + const { value: name } = await this._erc20Client.getName(blockHash, tokenAddress); + const { value: totalSupply } = await this._erc20Client.getTotalSupply(blockHash, tokenAddress); + + // TODO: decimals not implemented by erc20-watcher. + // const { value: decimals } = await this._erc20Client.getDecimals(blockHash, tokenAddress); + + return this._db.loadToken({ + blockNumber, + id: token1Address, + symbol, + name, + totalSupply + }); }; - const token0 = this._db.loadToken({ blockNumber, id: token0Address }, () => getTokenValues(token0Address)); - const token1 = this._db.loadToken({ blockNumber, id: token1Address }, () => getTokenValues(token1Address)); + // Create Tokens if not present. + if (!token0) { + token0 = await createToken(token0Address); + } - // TODO: Update Token entities. + if (!token1) { + token1 = await createToken(token1Address); + } - // TODO: Update Pool entity. + // Create new Pool entity. + // Skipping adding createdAtTimestamp field as it is not queried in frontend subgraph. + await this._db.loadPool({ + blockNumber, + id: poolAddress, + token0: token0, + token1: token1, + feeTier: BigInt(fee) + }); - // TODO: Save entities to DB. + // Skipping updating token whitelistPools field as it is not queried in frontend subgraph. + + // Save entities to DB. + await this._db.saveFactory(factory, blockNumber); } } diff --git a/packages/uni-watcher/README.md b/packages/uni-watcher/README.md index 33d6b28e..9e916ab2 100644 --- a/packages/uni-watcher/README.md +++ b/packages/uni-watcher/README.md @@ -11,6 +11,24 @@ Start watching the factory contract: Example: ```bash -$ npx ts-node src/cli/watch-contract.ts --configFile environments/local.toml --address 0xfE0034a874c2707c23F91D7409E9036F5e08ac34 --kind factory --startingBlock 100 +$ yarn watch:contract --address 0xfE0034a874c2707c23F91D7409E9036F5e08ac34 --kind factory --startingBlock 100 ``` +## Scripts + +* `yarn server` + + Start the GraphQL server. + +* `yarn watch:contract` + + Add contract to watch. + +* `yarn lint` + + Lint files. + + ```bash + # Lint fix. + $ yarn lint --fix + ``` diff --git a/packages/uni-watcher/package.json b/packages/uni-watcher/package.json index 84a93959..b8d78e84 100644 --- a/packages/uni-watcher/package.json +++ b/packages/uni-watcher/package.json @@ -9,7 +9,8 @@ "server:mock": "MOCK=1 nodemon src/server.ts -f environments/local.toml", "test": "mocha -r ts-node/register src/**/*.spec.ts", "lint": "eslint .", - "build": "tsc" + "build": "tsc", + "watch:contract": "ts-node src/cli/watch-contract.ts --configFile environments/local.toml" }, "repository": { "type": "git", diff --git a/packages/uni-watcher/src/client.ts b/packages/uni-watcher/src/client.ts index 94309ab6..f1978cc8 100644 --- a/packages/uni-watcher/src/client.ts +++ b/packages/uni-watcher/src/client.ts @@ -1,16 +1,11 @@ -import { gql } from 'apollo-server-express'; -import { GraphQLClient } from '@vulcanize/ipld-eth-client'; - -interface Config { - gqlEndpoint: string; - gqlSubscriptionEndpoint: string; -} +import { gql } from '@apollo/client/core'; +import { GraphQLClient, GraphQLConfig } from '@vulcanize/ipld-eth-client'; export class Client { - _config: Config; + _config: GraphQLConfig; _client: GraphQLClient; - constructor (config: Config) { + constructor (config: GraphQLConfig) { this._config = config; this._client = new GraphQLClient(config);