Smoke test for uni-info-watcher (#184)

* Smoke test for uni-info-watcher and refactor token deployment code.

* Test for Token entity after PoolCreated event.

* Test for Pool entity after PoolCreated event.

* Tests for entities after InitializeEvent.

Co-authored-by: prathamesh0 <prathamesh.musale0@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-08-04 18:57:44 +05:30 committed by GitHub
parent 73e0475dfa
commit a9d411c6df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 395 additions and 63 deletions

View File

@ -23,5 +23,13 @@
"allowArgumentsExplicitlyTypedAsAny": true
}
]
}
},
"overrides": [
{
"files": ["*.test.ts"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}

View File

@ -0,0 +1,4 @@
timeout: '20000'
bail: true
exit: true # TODO: Find out why the program doesn't exit on its own.
require: 'ts-node/register'

View File

@ -22,7 +22,8 @@
"lint": "eslint .",
"build": "tsc",
"generate:schema": "get-graphql-schema https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-alt > docs/analysis/schema/full-schema.graphql",
"lint:schema": "graphql-schema-linter"
"lint:schema": "graphql-schema-linter",
"smoke-test": "mocha src/smoke.test.ts"
},
"devDependencies": {
"@types/chance": "^1.1.2",
@ -41,6 +42,8 @@
"get-graphql-schema": "^2.1.2",
"graphql-schema-linter": "^2.0.1",
"mocha": "^8.4.0",
"nodemon": "^2.0.7"
"nodemon": "^2.0.7",
"ts-node": "^10.0.0",
"typescript": "^4.3.2"
}
}

View File

@ -0,0 +1,186 @@
import { expect } from 'chai';
import { ethers, Contract, Signer } from 'ethers';
import { request } from 'graphql-request';
import 'mocha';
import { Config, getConfig, wait, deployTokens, createPool, initializePool } from '@vulcanize/util';
import { Client as UniClient, watchEvent } from '@vulcanize/uni-watcher';
import {
abi as FACTORY_ABI
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json';
import {
abi as POOL_ABI
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json';
import {
queryFactory,
queryBundle,
queryToken,
queryPoolsByTokens,
queryPoolById,
queryPoolDayData
} from '../test/queries';
const NETWORK_RPC_URL = 'http://localhost:8545';
const TICK_MIN = -887272;
describe('uni-info-watcher', () => {
let factory: Contract;
let pool: Contract;
let token0Address: string;
let token1Address: string;
let signer: Signer;
let config: Config;
let endpoint: string;
let uniClient: UniClient;
before(async () => {
const provider = new ethers.providers.JsonRpcProvider(NETWORK_RPC_URL);
signer = provider.getSigner();
const configFile = './environments/local.toml';
config = await getConfig(configFile);
const { upstream, server: { host, port } } = config;
endpoint = `http://${host}:${port}/graphql`;
const { uniWatcher: { gqlEndpoint, gqlSubscriptionEndpoint } } = upstream;
uniClient = new UniClient({
gqlEndpoint,
gqlSubscriptionEndpoint
});
});
it('should have a Factory entity', async () => {
// Getting the Factory from uni-info-watcher graphQL endpoint.
const data = await request(endpoint, queryFactory);
expect(data.factories).to.not.be.empty;
// Initializing the factory variable.
const factoryAddress = data.factories[0].id;
factory = new ethers.Contract(factoryAddress, FACTORY_ABI, signer);
expect(factory.address).to.not.be.empty;
});
it('should have a Bundle entity', async () => {
// Getting the Bundle from uni-info-watcher graphQL endpoint.
const data = await request(endpoint, queryBundle);
expect(data.bundles).to.not.be.empty;
const bundleId = '1';
expect(data.bundles[0].id).to.equal(bundleId);
});
describe('PoolCreatedEvent', () => {
// NOTE Skipping checking entity updates that cannot be gotten using queries.
const fee = 500;
before(async () => {
// Deploy 2 tokens.
({ token0Address, token1Address } = await deployTokens(signer));
expect(token0Address).to.not.be.empty;
expect(token1Address).to.not.be.empty;
});
it('should not have Token entities', async () => {
// Check that Token entities are absent.
const data0 = await request(endpoint, queryToken, { id: token0Address });
expect(data0.token).to.be.null;
const data1 = await request(endpoint, queryToken, { id: token0Address });
expect(data1.token).to.be.null;
});
it('should create pool', async () => {
// Create Pool.
createPool(factory, token0Address, token1Address, fee);
// Wait for PoolCreatedEvent.
const eventType = 'PoolCreatedEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 5 sec for the entities to be processed.
await wait(5000);
});
it('should create Token entities', async () => {
// Check that Token entities are present.
const data0 = await request(endpoint, queryToken, { id: token0Address });
expect(data0.token).to.not.be.null;
const data1 = await request(endpoint, queryToken, { id: token0Address });
expect(data1.token).to.not.be.null;
});
it('should create a Pool entity', async () => {
const variables = {
tokens: [token0Address, token1Address]
};
// Getting the Pool that has the deployed tokens.
const data = await request(endpoint, queryPoolsByTokens, variables);
expect(data.pools).to.have.lengthOf(1);
// Initializing the pool variable.
const poolAddress = data.pools[0].id;
pool = new Contract(poolAddress, POOL_ABI, signer);
expect(pool.address).to.not.be.empty;
expect(data.pools[0].feeTier).to.be.equal(fee.toString());
});
});
describe('InitializeEvent', () => {
const sqrtPrice = '4295128939';
const tick = TICK_MIN;
it('should not have pool entity initialized', async () => {
const data = await request(endpoint, queryPoolById, { id: pool.address });
expect(data.pool.sqrtPrice).to.not.be.equal(sqrtPrice);
expect(data.pool.tick).to.be.null;
});
it('should initialize pool', async () => {
initializePool(pool, sqrtPrice);
// Wait for InitializeEvent.
const eventType = 'InitializeEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 5 sec for the entities to be processed.
await wait(5000);
const data = await request(endpoint, queryPoolById, { id: pool.address });
expect(data.pool.sqrtPrice).to.be.equal(sqrtPrice);
expect(data.pool.tick).to.be.equal(tick.toString());
});
it('should update PoolDayData entity', async () => {
// Get the latest PoolDayData.
const variables = {
first: 1,
orderBy: 'date',
orderDirection: 'desc',
pool: pool.address
};
const data = await request(endpoint, queryPoolDayData, variables);
expect(data.poolDayDatas).to.not.be.empty;
const dayPoolID: string = data.poolDayDatas[0].id;
const poolID: string = dayPoolID.split('-')[0];
const dayID: number = +dayPoolID.split('-')[1];
const date = data.poolDayDatas[0].date;
const tvlUSD = data.poolDayDatas[0].tvlUSD;
const dayStartTimestamp = dayID * 86400;
const poolData = await request(endpoint, queryPoolById, { id: pool.address });
const totalValueLockedUSD: string = poolData.pool.totalValueLockedUSD;
expect(poolID).to.be.equal(pool.address);
expect(date).to.be.equal(dayStartTimestamp);
expect(tvlUSD).to.be.equal(totalValueLockedUSD);
});
});
});

View File

@ -0,0 +1,54 @@
import { gql } from 'graphql-request';
export const queryToken = gql`
query queryToken($id: ID!) {
token(id: $id) {
id
}
}`;
// Getting the first Factory entity.
export const queryFactory = gql`
{
factories(first: 1) {
id
}
}`;
// Getting the first Bundle entity.
export const queryBundle = gql`
{
bundles(first: 1) {
id
}
}`;
// Getting Pool by id.
export const queryPoolById = gql`
query queryPoolById($id: ID!) {
pool(id: $id) {
id,
sqrtPrice,
tick,
totalValueLockedUSD
}
}`;
// Getting Pool(s) filtered by tokens.
export const queryPoolsByTokens = gql`
query queryPoolsByTokens($tokens: [String!]) {
pools(where: { token0_in: $tokens, token1_in: $tokens }) {
id,
feeTier
}
}`;
// Getting PoolDayData(s) filtered by pool and ordered by date.
export const queryPoolDayData = gql`
query queryPoolDayData($first: Int, $orderBy: PoolDayData_orderBy, $orderDirection: OrderDirection, $pool: String) {
poolDayDatas(first: $first, orderBy: $orderBy, orderDirection: $orderDirection, where: { pool: $pool }) {
id,
date,
tvlUSD
}
}`;

View File

@ -29,7 +29,7 @@
"files": ["*.test.ts", "test/*.ts"],
"rules": {
"no-unused-expressions": "off",
"no-prototype-builtins":"off"
"no-prototype-builtins": "off"
}
}
]

View File

@ -1 +1,2 @@
export * from './src/client';
export * from './src/utils/index';

View File

@ -1,9 +1,8 @@
import { expect, assert } from 'chai';
import { ethers, Contract, ContractTransaction, Signer, constants } from 'ethers';
import 'reflect-metadata';
import 'mocha';
import { Config, getConfig } from '@vulcanize/util';
import { Config, getConfig, deployTokens, TESTERC20_ABI } from '@vulcanize/util';
import { Client as UniClient } from '@vulcanize/uni-watcher';
import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client';
@ -43,10 +42,6 @@ import {
checkDecreaseLiquidityEvent,
checksCollectEvent
} from '../test/utils';
import {
abi as TESTERC20_ABI,
bytecode as TESTERC20_BYTECODE
} from '../artifacts/test/contracts/TestERC20.sol/TestERC20.json';
import {
abi as TESTUNISWAPV3CALLEE_ABI,
bytecode as TESTUNISWAPV3CALLEE_BYTECODE
@ -56,6 +51,8 @@ import {
bytecode as WETH9_BYTECODE
} from '../artifacts/test/contracts/WETH9.sol/WETH9.json';
const NETWORK_RPC_URL = 'http://localhost:8545';
const TICK_MIN = -887272;
const TICK_MAX = 887272;
const getMinTick = (tickSpacing: number) => Math.ceil(TICK_MIN / tickSpacing) * tickSpacing;
@ -119,7 +116,7 @@ describe('uni-watcher', () => {
gqlSubscriptionEndpoint
});
const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545');
const provider = new ethers.providers.JsonRpcProvider(NETWORK_RPC_URL);
signer = provider.getSigner();
recipient = await signer.getAddress();
});
@ -147,15 +144,10 @@ describe('uni-watcher', () => {
it('should deploy 2 tokens', async () => {
// Deploy 2 tokens.
const Token = new ethers.ContractFactory(TESTERC20_ABI, TESTERC20_BYTECODE, signer);
// Not initializing global token contract variables just yet; initialized in `create pool` to maintatin order coherency.
const token0 = await Token.deploy(ethers.BigNumber.from(2).pow(255));
token0Address = token0.address;
({ token0Address, token1Address } = await deployTokens(signer));
expect(token0Address).to.not.be.empty;
const token1 = await Token.deploy(ethers.BigNumber.from(2).pow(255));
token1Address = token1.address;
expect(token1Address).to.not.be.empty;
});

View File

@ -1,6 +1,7 @@
import { ethers } from 'ethers';
import { Database } from '../database';
import { Client as UniClient } from '../client';
export async function watchContract (db: Database, address: string, kind: string, startingBlock: number): Promise<void> {
// Always use the checksum address (https://docs.ethers.io/v5/api/utils/address/#utils-getAddress).
@ -8,3 +9,22 @@ export async function watchContract (db: Database, address: string, kind: string
await db.saveContract(contractAddress, kind, startingBlock);
}
export const watchEvent = async (uniClient: UniClient, eventType: string): Promise<any> => {
return new Promise((resolve, reject) => {
(async () => {
try {
const subscription = await uniClient.watchEvents((value: any) => {
if (value.event.__typename === eventType) {
if (subscription) {
subscription.unsubscribe();
}
resolve(value);
}
});
} catch (error) {
reject(error);
}
})();
});
};

View File

@ -1,11 +1,12 @@
import { ethers, utils, Contract, Signer, BigNumber, ContractTransaction } from 'ethers';
import { ethers, utils, Contract, Signer } from 'ethers';
import { expect } from 'chai';
import 'mocha';
import { Client as UniClient } from '@vulcanize/uni-watcher';
import { createPool, initializePool } from '@vulcanize/util';
// https://github.com/ethers-io/ethers.js/issues/195
export function linkLibraries (
export const linkLibraries = (
{
bytecode,
linkReferences
@ -13,7 +14,7 @@ export function linkLibraries (
bytecode: string
linkReferences: { [fileName: string]: { [contractName: string]: { length: number; start: number }[] } }
},
libraries: { [libraryName: string]: string }): string {
libraries: { [libraryName: string]: string }): string => {
Object.keys(linkReferences).forEach((fileName) => {
Object.keys(linkReferences[fileName]).forEach((contractName) => {
if (!libraries.hasOwnProperty(contractName)) {
@ -31,16 +32,16 @@ export function linkLibraries (
});
});
return bytecode;
}
};
export async function testCreatePool (
export const testCreatePool = async (
uniClient: UniClient,
factory: Contract,
token0Address: string,
token1Address: string,
fee: number,
poolAbi: any,
signer: Signer): Promise<Contract> {
signer: Signer): Promise<Contract> => {
return new Promise((resolve, reject) => {
(async () => {
try {
@ -60,20 +61,19 @@ export async function testCreatePool (
});
// Create pool.
const transaction: ContractTransaction = await factory.createPool(token0Address, token1Address, fee);
await transaction.wait();
await createPool(factory, token0Address, token1Address, fee);
} catch (error) {
reject(error);
}
})();
});
}
};
export function testInitialize (
export const testInitialize = async (
uniClient: UniClient,
pool: Contract,
sqrtPrice: string,
tick: number): Promise<void> {
tick: number): Promise<void> => {
return new Promise((resolve, reject) => {
try {
(async () => {
@ -92,30 +92,29 @@ export function testInitialize (
});
// Pool initialize.
const transaction: ContractTransaction = await pool.initialize(BigNumber.from(sqrtPrice));
await transaction.wait();
await initializePool(pool, sqrtPrice);
})();
} catch (error) {
reject(error);
}
});
}
};
function checkEventCommonValues (value: any, expectedContract: string) {
const checkEventCommonValues = (value: any, expectedContract: string) => {
expect(value.block).to.not.be.empty;
expect(value.tx).to.not.be.empty;
expect(value.contract).to.equal(expectedContract);
expect(value.eventIndex).to.be.a('number');
expect(value.proof).to.not.be.empty;
}
};
export function checkPoolCreatedEvent (
export const checkPoolCreatedEvent = (
value: any,
expectedContract: string,
token0Address: string,
token1Address: string,
fee: number): string {
fee: number): string => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('PoolCreatedEvent');
@ -126,28 +125,28 @@ export function checkPoolCreatedEvent (
expect(value.event.pool).to.not.be.empty;
return value.event.pool;
}
};
export function checkInitializeEvent (
export const checkInitializeEvent = (
value: any,
expectedContract: string,
sqrtPrice: string,
tick: number): void {
tick: number): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('InitializeEvent');
expect(value.event.sqrtPriceX96).to.equal(sqrtPrice);
expect(value.event.tick).to.equal(tick.toString());
}
};
export function checkMintEvent (
export const checkMintEvent = (
value: any,
expectedContract: string,
expectedSender: string,
exptectedOwner: string,
tickLower: number,
tickUpper: number,
amount: number): void {
amount: number): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('MintEvent');
@ -158,15 +157,15 @@ export function checkMintEvent (
expect(value.event.amount).to.equal(amount.toString());
expect(value.event.amount0).to.not.be.empty;
expect(value.event.amount1).to.not.be.empty;
}
};
export function checkBurnEvent (
export const checkBurnEvent = (
value: any,
expectedContract: string,
exptectedOwner: string,
tickLower: number,
tickUpper: number,
amount: number): void {
amount: number): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('BurnEvent');
@ -176,16 +175,16 @@ export function checkBurnEvent (
expect(value.event.amount).to.equal(amount.toString());
expect(value.event.amount0).to.not.be.empty;
expect(value.event.amount1).to.not.be.empty;
}
};
export function checkSwapEvent (
export const checkSwapEvent = (
value: any,
expectedContract: string,
expectedSender: string,
recipient: string,
sqrtPrice: string,
tick: number
): void {
): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('SwapEvent');
@ -196,40 +195,40 @@ export function checkSwapEvent (
expect(value.event.sqrtPriceX96).to.equal(sqrtPrice);
expect(value.event.liquidity).to.not.be.empty;
expect(value.event.tick).to.equal(tick.toString());
}
};
export function checkTransferEvent (
export const checkTransferEvent = (
value: any,
expectedContract: string,
from: string,
recipient: string
): void {
): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('TransferEvent');
expect(value.event.from).to.equal(from);
expect(value.event.to).to.equal(recipient);
expect(value.event.tokenId).to.equal('1');
}
};
export function checkIncreaseLiquidityEvent (
export const checkIncreaseLiquidityEvent = (
value: any,
expectedContract: string,
amount1Desired: number
): void {
): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.tokenId).to.equal('1');
expect(value.event.liquidity).to.equal(amount1Desired.toString());
expect(value.event.amount0).to.equal(amount1Desired.toString());
expect(value.event.amount1).to.equal(amount1Desired.toString());
}
};
export function checkDecreaseLiquidityEvent (
export const checkDecreaseLiquidityEvent = (
value: any,
expectedContract: string,
liquidity: number
): void {
): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('DecreaseLiquidityEvent');
@ -237,13 +236,13 @@ export function checkDecreaseLiquidityEvent (
expect(value.event.liquidity).to.equal(liquidity.toString());
expect(value.event.amount0).to.not.be.empty;
expect(value.event.amount1).to.not.be.empty;
}
};
export function checksCollectEvent (
export const checksCollectEvent = (
value: any,
expectedContract: string,
recipient: string
): void {
): void => {
checkEventCommonValues(value, expectedContract);
expect(value.event.__typename).to.equal('CollectEvent');
@ -251,4 +250,4 @@ export function checksCollectEvent (
expect(value.event.recipient).to.equal(recipient);
expect(value.event.amount0).to.not.be.empty;
expect(value.event.amount1).to.not.be.empty;
}
};

10
packages/util/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.idea/
.vscode/
node_modules/
build/
tmp/
temp/
#Hardhat files
cache
artifacts

View File

@ -0,0 +1,18 @@
import { HardhatUserConfig } from 'hardhat/config';
import '@nomiclabs/hardhat-waffle';
const config: HardhatUserConfig = {
defaultNetwork: 'localhost',
solidity: {
compilers: [
{
version: '0.7.6'
}
]
},
paths: {
sources: './test/contracts'
}
};
export default config;

View File

@ -3,3 +3,4 @@ export * from './src/database';
export * from './src/job-queue';
export * from './src/constants';
export * from './src/index';
export * from './test/actions';

View File

@ -12,10 +12,10 @@
"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",
"@vulcanize/cache": "^0.1.0",
"chai": "^4.3.4",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
@ -24,11 +24,13 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"hardhat": "^2.3.0",
"mocha": "^8.4.0",
"typeorm": "^0.2.32"
},
"scripts": {
"lint": "eslint .",
"build": "tsc"
"build": "tsc",
"test:compile": "hardhat compile"
}
}

View File

@ -0,0 +1,34 @@
import { ethers, Contract, ContractTransaction, Signer, BigNumber } from 'ethers';
import {
abi as TESTERC20_ABI,
bytecode as TESTERC20_BYTECODE
} from '../artifacts/test/contracts/TestERC20.sol/TestERC20.json';
export { abi as TESTERC20_ABI } from '../artifacts/test/contracts/TestERC20.sol/TestERC20.json';
export const deployTokens = async (signer: Signer): Promise<{token0Address: string, token1Address: string}> => {
const Token = new ethers.ContractFactory(TESTERC20_ABI, TESTERC20_BYTECODE, signer);
const token0 = await Token.deploy(ethers.BigNumber.from(2).pow(255));
const token0Address = token0.address;
const token1 = await Token.deploy(ethers.BigNumber.from(2).pow(255));
const token1Address = token1.address;
return { token0Address, token1Address };
};
export const createPool = async (
factory: Contract,
token0Address: string,
token1Address: string,
fee: number): Promise<void> => {
const transaction: ContractTransaction = await factory.createPool(token0Address, token1Address, fee);
await transaction.wait();
};
export const initializePool = async (pool: Contract, sqrtPrice: string): Promise<void> => {
const transaction: ContractTransaction = await pool.initialize(BigNumber.from(sqrtPrice));
await transaction.wait();
};

View File

@ -72,6 +72,6 @@
"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"],
"include": ["src", "test"],
"exclude": ["dist"]
}