Handle pool initialize event (#127)

* Move getConfig to util package.

* Handle Pool initialize event.

* Update Bundle entity ethPriceUSD.

* Update Pool day and hour data.

* Update token derivedETH and complete handleInitialize.

Co-authored-by: nabarun <nabarun@deepstacksoft.com>
This commit is contained in:
Ashwin Phatak 2021-07-09 12:38:25 +05:30 committed by GitHub
parent 7f5229bf2f
commit 61f211f2d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 794 additions and 178 deletions

View File

@ -28,6 +28,7 @@
"@vulcanize/ipld-eth-client": "^0.1.0",
"@vulcanize/solidity-mapper": "^0.1.0",
"@vulcanize/tracing-client": "^0.1.0",
"@vulcanize/util": "^0.1.0",
"apollo-server-express": "^2.25.0",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",

View File

@ -1,40 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
import toml from 'toml';
import debug from 'debug';
import { ConnectionOptions } from 'typeorm';
import { Config as CacheConfig } from '@vulcanize/cache';
const log = debug('vulcanize:config');
export interface Config {
server: {
host: string;
port: number;
};
database: ConnectionOptions;
upstream: {
gqlEndpoint: string;
gqlSubscriptionEndpoint: string;
traceProviderEndpoint: string;
cache: CacheConfig
}
jobQueue: {
dbConnectionString: string;
maxCompletionLag: number;
}
}
export const getConfig = async (configFile: string): Promise<Config> => {
const configFilePath = path.resolve(configFile);
const fileExists = await fs.pathExists(configFilePath);
if (!fileExists) {
throw new Error(`Config file not found: ${configFilePath}`);
}
const config = toml.parse(await fs.readFile(configFilePath, 'utf8'));
log('config', JSON.stringify(config, null, 2));
return config;
};

View File

@ -10,13 +10,13 @@ import { createServer } from 'http';
import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { TracingClient } from '@vulcanize/tracing-client';
import { getConfig } from '@vulcanize/util';
import typeDefs from './schema';
import { createResolvers } from './resolvers';
import { Indexer } from './indexer';
import { Database } from './database';
import { getConfig } from './config';
import { TxWatcher } from './tx-watcher';
import { JobQueue } from './job-queue';

View File

@ -27,6 +27,7 @@
"@vulcanize/cache": "^0.1.0",
"@vulcanize/ipld-eth-client": "^0.1.0",
"@vulcanize/solidity-mapper": "^0.1.0",
"@vulcanize/util": "^0.1.0",
"apollo-server-express": "^2.25.0",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",

View File

@ -1,35 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
import toml from 'toml';
import debug from 'debug';
import { ConnectionOptions } from 'typeorm';
import { Config as CacheConfig } from '@vulcanize/cache';
const log = debug('vulcanize:config');
export interface Config {
server: {
host: string;
port: number;
};
database: ConnectionOptions;
upstream: {
gqlEndpoint: string;
gqlSubscriptionEndpoint: string;
cache: CacheConfig
}
}
export const getConfig = async (configFile: string): Promise<Config> => {
const configFilePath = path.resolve(configFile);
const fileExists = await fs.pathExists(configFilePath);
if (!fileExists) {
throw new Error(`Config file not found: ${configFilePath}`);
}
const config = toml.parse(await fs.readFile(configFilePath, 'utf8'));
log('config', JSON.stringify(config, null, 2));
return config;
};

View File

@ -10,6 +10,7 @@ import { createServer } from 'http';
import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { getConfig } from '@vulcanize/util';
import artifacts from './artifacts/ERC20.json';
import typeDefs from './schema';
@ -19,7 +20,6 @@ import { createResolvers } from './resolvers';
import { Indexer } from './indexer';
import { Database } from './database';
import { EventWatcher } from './events';
import { getConfig } from './config';
const log = debug('vulcanize:server');

View File

@ -8,6 +8,7 @@
"@vulcanize/cache": "^0.1.0",
"@vulcanize/erc20-watcher": "^0.1.0",
"@vulcanize/ipld-eth-client": "^0.1.0",
"@vulcanize/util": "^0.1.0",
"apollo-server-express": "^2.25.0",
"apollo-type-bigint": "^0.1.3",
"typeorm": "^0.2.32"

View File

@ -1,12 +1,15 @@
import assert from 'assert';
import { Connection, ConnectionOptions, createConnection, DeepPartial } from 'typeorm';
import { Connection, ConnectionOptions, createConnection, DeepPartial, FindConditions, FindOneOptions, LessThanOrEqual } from 'typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { EventSyncProgress } from './entity/EventProgress';
import { Factory } from './entity/Factory';
import { Pool } from './entity/Pool';
import { Event } from './entity/Event';
import { Token } from './entity/Token';
import { EventSyncProgress } from './entity/EventProgress';
import { Bundle } from './entity/Bundle';
import { PoolDayData } from './entity/PoolDayData';
import { PoolHourData } from './entity/PoolHourData';
export class Database {
_config: ConnectionOptions
@ -33,25 +36,54 @@ export class Database {
async getToken ({ id, blockNumber }: DeepPartial<Token>): Promise<Token | undefined> {
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();
const whereOptions: FindConditions<Token> = { id };
if (blockNumber) {
whereOptions.blockNumber = LessThanOrEqual(blockNumber);
}
const findOptions: FindOneOptions<Token> = {
where: whereOptions,
relations: ['whitelistPools', 'whitelistPools.token0', 'whitelistPools.token1'],
order: {
blockNumber: 'DESC'
}
};
return repo.findOne(findOptions);
}
async getPool ({ id, blockNumber }: DeepPartial<Pool>): Promise<Pool | undefined> {
const repo = this._conn.getRepository(Pool);
const whereOptions: FindConditions<Pool> = { id };
if (blockNumber) {
whereOptions.blockNumber = LessThanOrEqual(blockNumber);
}
const findOptions: FindOneOptions<Pool> = {
where: whereOptions,
relations: ['token0', 'token1'],
order: {
blockNumber: 'DESC'
}
};
return repo.findOne(findOptions);
}
async loadFactory ({ id, blockNumber, ...values }: DeepPartial<Factory>): Promise<Factory> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Factory);
let entity = await repo.createQueryBuilder('factory')
.where('id = :id AND block_number <= :blockNumber', {
id,
blockNumber
})
.orderBy('factory.block_number', 'DESC')
let selectQueryBuilder = repo.createQueryBuilder('factory')
.where('id = :id', { id });
if (blockNumber) {
selectQueryBuilder = selectQueryBuilder.andWhere('block_number <= :blockNumber', { blockNumber });
}
let entity = await selectQueryBuilder.orderBy('factory.block_number', 'DESC')
.getOne();
if (!entity) {
@ -67,13 +99,21 @@ export class Database {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Pool);
let entity = await repo.createQueryBuilder('pool')
.where('id = :id AND block_number <= :blockNumber', {
id,
blockNumber
})
.orderBy('pool.block_number', 'DESC')
.getOne();
const whereOptions: FindConditions<Pool> = { id };
if (blockNumber) {
whereOptions.blockNumber = LessThanOrEqual(blockNumber);
}
const findOptions: FindOneOptions<Pool> = {
where: whereOptions,
relations: ['token0', 'token1'],
order: {
blockNumber: 'DESC'
}
};
let entity = await repo.findOne(findOptions);
if (!entity) {
entity = repo.create({ blockNumber, id, ...values });
@ -88,12 +128,92 @@ export class Database {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Token);
let entity = await repo.createQueryBuilder('token')
.where('id = :id AND block_number <= :blockNumber', {
id,
blockNumber
})
.orderBy('token.block_number', 'DESC')
const whereOptions: FindConditions<Token> = { id };
if (blockNumber) {
whereOptions.blockNumber = LessThanOrEqual(blockNumber);
}
const findOptions: FindOneOptions<Token> = {
where: whereOptions,
relations: ['whitelistPools', 'whitelistPools.token0', 'whitelistPools.token1'],
order: {
blockNumber: 'DESC'
}
};
let entity = await repo.findOne(findOptions);
if (!entity) {
entity = repo.create({ blockNumber, id, ...values });
entity = await repo.save(entity);
// TODO: Find way to preload relations during create.
entity.whitelistPools = [];
}
return entity;
});
}
async loadBundle ({ id, blockNumber, ...values }: DeepPartial<Bundle>): Promise<Bundle> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Bundle);
let selectQueryBuilder = repo.createQueryBuilder('bundle')
.where('id = :id', { id });
if (blockNumber) {
selectQueryBuilder = selectQueryBuilder.andWhere('block_number <= :blockNumber', { blockNumber });
}
let entity = await selectQueryBuilder.orderBy('bundle.block_number', 'DESC')
.getOne();
if (!entity) {
entity = repo.create({ blockNumber, id, ...values });
entity = await repo.save(entity);
}
return entity;
});
}
async loadPoolDayData ({ id, blockNumber, ...values }: DeepPartial<PoolDayData>): Promise<PoolDayData> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(PoolDayData);
let selectQueryBuilder = repo.createQueryBuilder('pool_day_data')
.where('id = :id', { id });
if (blockNumber) {
selectQueryBuilder = selectQueryBuilder.andWhere('block_number <= :blockNumber', { blockNumber });
}
let entity = await selectQueryBuilder.orderBy('pool_day_data.block_number', 'DESC')
.getOne();
if (!entity) {
entity = repo.create({ blockNumber, id, ...values });
entity = await repo.save(entity);
}
return entity;
});
}
async loadPoolHourData ({ id, blockNumber, ...values }: DeepPartial<PoolHourData>): Promise<PoolHourData> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(PoolHourData);
let selectQueryBuilder = repo.createQueryBuilder('pool_hour_data')
.where('id = :id', { id });
if (blockNumber) {
selectQueryBuilder = selectQueryBuilder.andWhere('block_number <= :blockNumber', { blockNumber });
}
let entity = await selectQueryBuilder.orderBy('pool_hour_data.block_number', 'DESC')
.getOne();
if (!entity) {
@ -113,6 +233,46 @@ export class Database {
});
}
async saveBundle (bundle: Bundle, blockNumber: number): Promise<Bundle> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Bundle);
bundle.blockNumber = blockNumber;
return repo.save(bundle);
});
}
async savePool (pool: Pool, blockNumber: number): Promise<Pool> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Pool);
pool.blockNumber = blockNumber;
return repo.save(pool);
});
}
async savePoolDayData (poolDayData: PoolDayData, blockNumber: number): Promise<PoolDayData> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(PoolDayData);
poolDayData.blockNumber = blockNumber;
return repo.save(poolDayData);
});
}
async savePoolHourData (poolHourData: PoolHourData, blockNumber: number): Promise<PoolHourData> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(PoolHourData);
poolHourData.blockNumber = blockNumber;
return repo.save(poolHourData);
});
}
async saveToken (token: Token, blockNumber: number): Promise<Token> {
return this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Token);
token.blockNumber = blockNumber;
return repo.save(token);
});
}
// Returns true if events have already been synced for the (block, token) combination.
async didSyncEvents ({ blockHash, token }: { blockHash: string, token: string }): Promise<boolean> {
const numRows = await this._conn.getRepository(EventSyncProgress)

View File

@ -0,0 +1,13 @@
import { Entity, PrimaryColumn, Column } from 'typeorm';
@Entity()
export class Bundle {
@PrimaryColumn('varchar', { length: 1 })
id!: string;
@PrimaryColumn('integer')
blockNumber!: number;
@Column('numeric', { default: 0 })
ethPriceUSD!: number
}

View File

@ -1,12 +1,11 @@
import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity()
@Index(['blockNumber', 'id'], { unique: true })
export class Factory {
@PrimaryColumn('varchar', { length: 42 })
id!: string;
@PrimaryColumn('numeric')
@PrimaryColumn('integer')
blockNumber!: number;
@Column('numeric', { default: BigInt(0) })

View File

@ -1,14 +1,13 @@
import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm';
import { Token } from './Token';
@Entity()
@Index(['blockNumber', 'id'])
export class Pool {
@PrimaryColumn('varchar', { length: 42 })
id!: string;
@PrimaryColumn('numeric')
@PrimaryColumn('integer')
blockNumber!: number;
@ManyToOne(() => Token)
@ -17,8 +16,38 @@ export class Pool {
@ManyToOne(() => Token)
token1!: Token;
@Column('numeric', { default: 0 })
token0Price!: number
@Column('numeric', { default: 0 })
token1Price!: number
@Column('numeric')
feeTier!: bigint
@Column('numeric', { default: BigInt(0) })
sqrtPrice!: bigint
@Column('numeric', { default: BigInt(0) })
tick!: bigint
@Column('numeric', { default: BigInt(0) })
liquidity!: bigint
@Column('numeric', { default: BigInt(0) })
feeGrowthGlobal0X128!: bigint
@Column('numeric', { default: BigInt(0) })
feeGrowthGlobal1X128!: bigint
@Column('numeric', { default: 0 })
totalValueLockedUSD!: number
@Column('numeric', { default: 0 })
totalValueLockedToken0!: number
@Column('numeric', { default: 0 })
totalValueLockedToken1!: number
// TODO: Add remaining fields when they are used.
}

View File

@ -0,0 +1,58 @@
import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm';
import { Pool } from './Pool';
@Entity()
export class PoolDayData {
@PrimaryColumn('varchar')
id!: string;
@PrimaryColumn('integer')
blockNumber!: number;
@Column('integer')
date!: number;
@ManyToOne(() => Pool)
pool!: Pool;
@Column('numeric')
high!: number;
@Column('numeric')
low!: number;
@Column('numeric')
open!: number;
@Column('numeric')
close!: number;
@Column('numeric', { default: BigInt(0) })
sqrtPrice!: bigint
@Column('numeric', { default: BigInt(0) })
tick!: bigint
@Column('numeric', { default: BigInt(0) })
liquidity!: bigint
@Column('numeric', { default: BigInt(0) })
feeGrowthGlobal0X128!: bigint
@Column('numeric', { default: BigInt(0) })
feeGrowthGlobal1X128!: bigint
@Column('numeric', { default: 0 })
token0Price!: number
@Column('numeric', { default: 0 })
token1Price!: number
@Column('numeric', { default: 0 })
tvlUSD!: number
@Column('numeric', { default: BigInt(0) })
txCount!: bigint
// TODO: Add remaining fields when they are used.
}

View File

@ -0,0 +1,58 @@
import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm';
import { Pool } from './Pool';
@Entity()
export class PoolHourData {
@PrimaryColumn('varchar')
id!: string;
@PrimaryColumn('integer')
blockNumber!: number;
@Column('integer')
periodStartUnix!: number;
@ManyToOne(() => Pool)
pool!: Pool;
@Column('numeric')
high!: number;
@Column('numeric')
low!: number;
@Column('numeric')
open!: number;
@Column('numeric')
close!: number;
@Column('numeric', { default: BigInt(0) })
sqrtPrice!: bigint
@Column('numeric', { default: BigInt(0) })
tick!: bigint
@Column('numeric', { default: BigInt(0) })
liquidity!: bigint
@Column('numeric', { default: BigInt(0) })
feeGrowthGlobal0X128!: bigint
@Column('numeric', { default: BigInt(0) })
feeGrowthGlobal1X128!: bigint
@Column('numeric', { default: 0 })
token0Price!: number
@Column('numeric', { default: 0 })
token1Price!: number
@Column('numeric', { default: 0 })
tvlUSD!: number
@Column('numeric', { default: BigInt(0) })
txCount!: bigint
// TODO: Add remaining fields when they are used.
}

View File

@ -1,12 +1,12 @@
import { Entity, PrimaryColumn, Column, Index } from 'typeorm';
import { Entity, PrimaryColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Pool } from './Pool';
@Entity()
@Index(['blockNumber', 'id'])
export class Token {
@PrimaryColumn('varchar', { length: 42 })
id!: string;
@PrimaryColumn('numeric')
@PrimaryColumn('integer')
blockNumber!: number;
@Column('varchar')
@ -18,5 +18,12 @@ export class Token {
@Column('numeric')
totalSupply!: number;
@Column('numeric', { default: 0 })
derivedETH!: number;
@ManyToMany(() => Pool)
@JoinTable()
whitelistPools!: Pool[];
// TODO: Add remaining fields when they are used.
}

View File

@ -5,6 +5,9 @@ import { Client as ERC20Client } from '@vulcanize/erc20-watcher';
import { BigNumber } from 'ethers';
import { Database } from './database';
import { findEthPerToken, getEthPriceInUSD, WHITELIST_TOKENS } from './utils/pricing';
import { updatePoolDayData, updatePoolHourData } from './utils/intervalUpdates';
import { Token } from './entity/Token';
const log = debug('vulcanize:events');
@ -16,6 +19,11 @@ interface PoolCreatedEvent {
pool: string;
}
interface InitializeEvent {
sqrtPriceX96: bigint;
tick: bigint;
}
interface ResultEvent {
proof: {
data: string
@ -59,10 +67,15 @@ export class EventWatcher {
switch (eventType) {
case 'PoolCreatedEvent':
log('PoolCreated event', contract);
log('Factory PoolCreated event', contract);
this._handlePoolCreated(blockHash, blockNumber, contract, eventValues as PoolCreatedEvent);
break;
case 'InitializeEvent':
log('Pool Initialize event', contract);
this._handleInitialize(blockHash, blockNumber, contract, eventValues as InitializeEvent);
break;
default:
break;
}
@ -85,36 +98,18 @@ export class EventWatcher {
this._db.getToken({ blockNumber, id: token1Address })
]);
// Create Token.
const createToken = async (tokenAddress: string) => {
const { value: symbol } = await this._erc20Client.getSymbol(blockHash, tokenAddress);
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
});
};
// Create Tokens if not present.
if (!token0) {
token0 = await createToken(token0Address);
token0 = await this._createToken(blockHash, blockNumber, token0Address);
}
if (!token1) {
token1 = await createToken(token1Address);
token1 = await this._createToken(blockHash, blockNumber, token1Address);
}
// Create new Pool entity.
// Skipping adding createdAtTimestamp field as it is not queried in frontend subgraph.
await this._db.loadPool({
const pool = await this._db.loadPool({
blockNumber,
id: poolAddress,
token0: token0,
@ -122,9 +117,74 @@ export class EventWatcher {
feeTier: BigInt(fee)
});
// Skipping updating token whitelistPools field as it is not queried in frontend subgraph.
// Update white listed pools.
if (WHITELIST_TOKENS.includes(token0.id)) {
token1.whitelistPools.push(pool);
await this._db.saveToken(token1, blockNumber);
}
if (WHITELIST_TOKENS.includes(token1.id)) {
token0.whitelistPools.push(pool);
await this._db.saveToken(token0, blockNumber);
}
// Save entities to DB.
await this._db.saveFactory(factory, blockNumber);
}
/**
* Create new Token.
* @param tokenAddress
*/
async _createToken (blockHash: string, blockNumber: number, tokenAddress: string): Promise<Token> {
const { value: symbol } = await this._erc20Client.getSymbol(blockHash, tokenAddress);
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: tokenAddress,
symbol,
name,
totalSupply
});
}
async _handleInitialize (blockHash: string, blockNumber: number, contractAddress: string, initializeEvent: InitializeEvent): Promise<void> {
const { sqrtPriceX96, tick } = initializeEvent;
const pool = await this._db.getPool({ id: contractAddress, blockNumber });
assert(pool, `Pool ${contractAddress} not found.`);
// Update Pool.
pool.sqrtPrice = BigInt(sqrtPriceX96);
pool.tick = BigInt(tick);
this._db.savePool(pool, blockNumber);
// Update ETH price now that prices could have changed.
const bundle = await this._db.loadBundle({ id: '1', blockNumber });
bundle.ethPriceUSD = await getEthPriceInUSD(this._db);
this._db.saveBundle(bundle, blockNumber);
await updatePoolDayData(this._db, { contractAddress, blockNumber });
await updatePoolHourData(this._db, { contractAddress, blockNumber });
const [token0, token1] = await Promise.all([
this._db.getToken({ id: pool.token0.id, blockNumber }),
this._db.getToken({ id: pool.token1.id, blockNumber })
]);
assert(token0 && token1, 'Pool tokens not found.');
// Update token prices.
token0.derivedETH = await findEthPerToken(token0);
token1.derivedETH = await findEthPerToken(token1);
await Promise.all([
this._db.saveToken(token0, blockNumber),
this._db.saveToken(token1, blockNumber)
]);
}
}

View File

@ -12,6 +12,7 @@ import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { Client as ERC20Client } from '@vulcanize/erc20-watcher';
import { Client as UniClient } from '@vulcanize/uni-watcher';
import { getConfig } from '@vulcanize/util';
import typeDefs from './schema';
@ -20,7 +21,6 @@ import { createResolvers } from './resolvers';
import { Indexer } from './indexer';
import { Database } from './database';
import { EventWatcher } from './events';
import { getConfig } from './config';
const log = debug('vulcanize:server');

View File

@ -0,0 +1,103 @@
import { BigNumber } from 'ethers';
import { Database } from '../database';
import { PoolDayData } from '../entity/PoolDayData';
import { PoolHourData } from '../entity/PoolHourData';
export const updatePoolDayData = async (db: Database, event: { contractAddress: string, blockNumber: number }): Promise<PoolDayData> => {
const { contractAddress, blockNumber } = event;
// TODO: Get block timestamp from event.
// let timestamp = event.block.timestamp.toI32()
const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp.
const dayID = Math.floor(timestamp / 86400);
const dayStartTimestamp = dayID * 86400;
const dayPoolID = contractAddress
.concat('-')
.concat(dayID.toString());
const pool = await db.loadPool({ id: contractAddress, blockNumber });
let poolDayData = await db.loadPoolDayData({
id: dayPoolID,
blockNumber,
date: dayStartTimestamp,
pool: pool,
open: pool.token0Price,
high: pool.token0Price,
low: pool.token0Price,
close: pool.token0Price
});
if (Number(pool.token0Price) > Number(poolDayData.high)) {
poolDayData.high = pool.token0Price;
}
if (Number(pool.token0Price) < Number(poolDayData.low)) {
poolDayData.low = pool.token0Price;
}
poolDayData.liquidity = pool.liquidity;
poolDayData.sqrtPrice = pool.sqrtPrice;
poolDayData.feeGrowthGlobal0X128 = pool.feeGrowthGlobal0X128;
poolDayData.feeGrowthGlobal1X128 = pool.feeGrowthGlobal1X128;
poolDayData.token0Price = pool.token0Price;
poolDayData.token1Price = pool.token1Price;
poolDayData.tick = pool.tick;
poolDayData.tvlUSD = pool.totalValueLockedUSD;
poolDayData.txCount = BigInt(BigNumber.from(poolDayData.txCount).add(1).toHexString());
poolDayData = await db.savePoolDayData(poolDayData, blockNumber);
return poolDayData;
};
export const updatePoolHourData = async (db: Database, event: { contractAddress: string, blockNumber: number }): Promise<PoolHourData> => {
const { contractAddress, blockNumber } = event;
// TODO: Get block timestamp from event.
// let timestamp = event.block.timestamp.toI32()
const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp.
const hourIndex = Math.floor(timestamp / 3600); // Get unique hour within unix history.
const hourStartUnix = hourIndex * 3600; // Want the rounded effect.
const hourPoolID = contractAddress
.concat('-')
.concat(hourIndex.toString());
const pool = await db.loadPool({ id: contractAddress, blockNumber });
let poolHourData = await db.loadPoolHourData({
id: hourPoolID,
blockNumber,
periodStartUnix: hourStartUnix,
pool: pool,
open: pool.token0Price,
high: pool.token0Price,
low: pool.token0Price,
close: pool.token0Price
});
if (Number(pool.token0Price) > Number(poolHourData.high)) {
poolHourData.high = pool.token0Price;
}
if (Number(pool.token0Price) < Number(poolHourData.low)) {
poolHourData.low = pool.token0Price;
}
poolHourData.liquidity = pool.liquidity;
poolHourData.sqrtPrice = pool.sqrtPrice;
poolHourData.token0Price = pool.token0Price;
poolHourData.token1Price = pool.token1Price;
poolHourData.feeGrowthGlobal0X128 = pool.feeGrowthGlobal0X128;
poolHourData.feeGrowthGlobal1X128 = pool.feeGrowthGlobal1X128;
poolHourData.close = pool.token0Price;
poolHourData.tick = pool.tick;
poolHourData.tvlUSD = pool.totalValueLockedUSD;
poolHourData.txCount = BigInt(BigNumber.from(poolHourData.txCount).add(1).toHexString());
poolHourData = await db.savePoolHourData(poolHourData, blockNumber);
return poolHourData;
};

View File

@ -0,0 +1,96 @@
import { BigNumber } from 'ethers';
import { Database } from '../database';
import { Token } from '../entity/Token';
// TODO: Move constants to config.
const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const USDC_WETH_03_POOL = '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8';
// Token where amounts should contribute to tracked volume and liquidity.
// Usually tokens that many tokens are paired with.
// TODO: Load whitelisted tokens from config.
export const WHITELIST_TOKENS: string[] = [
WETH_ADDRESS, // WETH
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
'0x0000000000085d4780b73119b644ae5ecd22b376', // TUSD
'0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', // WBTC
'0x5d3a536e4d6dbd6114cc1ead35777bab948e3643', // cDAI
'0x39aa39c021dfbae8fac545936693ac917d5e7563', // cUSDC
'0x86fadb80d8d2cff3c3680819e4da99c10232ba0f', // EBASE
'0x57ab1ec28d129707052df4df418d58a2d46d5f51', // sUSD
'0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', // MKR
'0xc00e94cb662c3520282e6f5717214004a7f26888', // COMP
'0x514910771af9ca656af840dff83e8264ecf986ca', // LINK
'0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', // SNX
'0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e', // YFI
'0x111111111117dc0aa78b770fa6a738034120c302', // 1INCH
'0xdf5e0e81dff6faf3a7e52ba697820c5e32d806a8', // yCurv
'0x956f47f50a910163d8bf957cf5846d573e7f87ca', // FEI
'0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0', // MATIC
'0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9' // AAVE
];
const MINIMUM_ETH_LOCKED = 52;
export const getEthPriceInUSD = async (db: Database): Promise<number> => {
// Fetch eth prices for each stablecoin.
const usdcPool = await db.getPool({ id: USDC_WETH_03_POOL }); // DAI is token0.
if (usdcPool) {
return usdcPool.token0Price;
} else {
return 0;
}
};
/**
* Search through graph to find derived Eth per token.
* @todo update to be derived ETH (add stablecoin estimates)
**/
export const findEthPerToken = async (token: Token): Promise<number> => {
if (token.id === WETH_ADDRESS) {
return 1;
}
const whiteList = token.whitelistPools;
// For now just take USD from pool with greatest TVL.
// Need to update this to actually detect best rate based on liquidity distribution.
let largestLiquidityETH = 0;
let priceSoFar = 0;
for (let i = 0; i < whiteList.length; ++i) {
const pool = whiteList[i];
if (BigNumber.from(pool.liquidity).gt(0)) {
if (pool.token0.id === token.id) {
// Whitelist token is token1.
const token1 = pool.token1;
// Get the derived ETH in pool.
const ethLocked = Number(pool.totalValueLockedToken1) * Number(token1.derivedETH);
if (ethLocked > largestLiquidityETH && ethLocked > MINIMUM_ETH_LOCKED) {
largestLiquidityETH = ethLocked;
// token1 per our token * Eth per token1
priceSoFar = Number(pool.token1Price) * Number(token1.derivedETH);
}
}
if (pool.token1.id === token.id) {
const token0 = pool.token0;
// Get the derived ETH in pool.
const ethLocked = Number(pool.totalValueLockedToken0) * Number(token0.derivedETH);
if (ethLocked > largestLiquidityETH && ethLocked > MINIMUM_ETH_LOCKED) {
largestLiquidityETH = ethLocked;
// token0 per our token * ETH per token0
priceSoFar = Number(pool.token0Price) * Number(token0.derivedETH);
}
}
}
}
return priceSoFar; // If nothing was found return 0.
};

View File

@ -28,6 +28,7 @@
"@vulcanize/cache": "^0.1.0",
"@vulcanize/ipld-eth-client": "^0.1.0",
"@vulcanize/solidity-mapper": "^0.1.0",
"@vulcanize/util": "^0.1.0",
"apollo-server-express": "^2.25.0",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",

View File

@ -3,7 +3,7 @@ import yargs from 'yargs';
import 'reflect-metadata';
import { ethers } from 'ethers';
import { Config, getConfig } from '../config';
import { Config, getConfig } from '@vulcanize/util';
import { Database } from '../database';
(async () => {

View File

@ -25,6 +25,7 @@ export class Client {
}
event {
__typename
... on PoolCreatedEvent {
token0
token1
@ -32,6 +33,11 @@ export class Client {
tickSpacing
pool
}
... on InitializeEvent {
sqrtPriceX96
tick
}
}
}
}

View File

@ -1,35 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
import toml from 'toml';
import debug from 'debug';
import { ConnectionOptions } from 'typeorm';
import { Config as CacheConfig } from '@vulcanize/cache';
const log = debug('vulcanize:config');
export interface Config {
server: {
host: string;
port: number;
};
database: ConnectionOptions;
upstream: {
gqlEndpoint: string;
gqlSubscriptionEndpoint: string;
cache: CacheConfig
}
}
export const getConfig = async (configFile: string): Promise<Config> => {
const configFilePath = path.resolve(configFile);
const fileExists = await fs.pathExists(configFilePath);
if (!fileExists) {
throw new Error(`Config file not found: ${configFilePath}`);
}
const config = toml.parse(await fs.readFile(configFilePath, 'utf8'));
log('config', JSON.stringify(config, null, 2));
return config;
};

View File

@ -8,11 +8,11 @@ import { PubSub } from 'apollo-server-express';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { GetStorageAt } from '@vulcanize/solidity-mapper';
import { Config } from '@vulcanize/util';
import { Database } from './database';
import { Event } from './entity/Event';
import { Contract, KIND_FACTORY, KIND_POOL } from './entity/Contract';
import { Config } from './config';
import factoryABI from './artifacts/factory.json';
import poolABI from './artifacts/pool.json';

View File

@ -10,6 +10,7 @@ import { createServer } from 'http';
import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { getConfig } from '@vulcanize/util';
import typeDefs from './schema';
@ -18,7 +19,6 @@ import { createResolvers } from './resolvers';
import { Indexer } from './indexer';
import { Database } from './database';
import { EventWatcher } from './events';
import { getConfig } from './config';
const log = debug('vulcanize:server');

View File

@ -0,0 +1,5 @@
# Don't lint node_modules.
node_modules
# Don't lint build output.
dist

View File

@ -0,0 +1,18 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"semistandard",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
]
}

1
packages/util/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './src/config';

View File

@ -0,0 +1,32 @@
{
"name": "@vulcanize/util",
"version": "0.1.0",
"main": "index.js",
"license": "UNLICENSED",
"dependencies": {
"debug": "^4.3.1",
"ethers": "^5.2.0",
"fs-extra": "^10.0.0",
"toml": "^3.0.0"
},
"devDependencies": {
"@vulcanize/cache": "^0.1.0",
"@types/fs-extra": "^9.0.11",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"chai": "^4.3.4",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"mocha": "^8.4.0",
"typeorm": "^0.2.32"
},
"scripts": {
"lint": "eslint .",
"build": "tsc"
}
}

View File

@ -18,7 +18,7 @@ export interface Config {
gqlEndpoint: string;
gqlSubscriptionEndpoint: string;
traceProviderEndpoint: string;
cache: CacheConfig;
cache: CacheConfig,
uniWatcher: {
gqlEndpoint: string;
gqlSubscriptionEndpoint: string;
@ -27,7 +27,7 @@ export interface Config {
gqlEndpoint: string;
gqlSubscriptionEndpoint: string;
}
}
},
jobQueue: {
dbConnectionString: string;
maxCompletionLag: number;

View File

@ -0,0 +1,77 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [ "ES5", "ES6", "ES2020" ], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"./src/types"
], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"resolveJsonModule": true /* Enabling the option allows importing JSON, and validating the types in that JSON file. */
},
"include": ["src"],
"exclude": ["dist"]
}