Test for entities after TransferEvent (#196)

* Add resolver for Position entity in uni-info-watcher.

* Test for entities after TransferEvent.

Co-authored-by: prathamesh0 <prathamesh.musale0@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-08-10 12:09:00 +05:30 committed by GitHub
parent abd3175c09
commit 0c2efde028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 342 additions and 102 deletions

View File

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

View File

@ -1139,7 +1139,7 @@ export class Indexer {
const [token0, token1] = await Promise.all([
this._db.getTokenNoTx({ id: token0Address, blockHash }),
this._db.getTokenNoTx({ id: token0Address, blockHash })
this._db.getTokenNoTx({ id: token1Address, blockHash })
]);
assert(token0 && token1);
position.token0 = token0;

View File

@ -16,6 +16,7 @@ import { TokenDayData } from './entity/TokenDayData';
import { TokenHourData } from './entity/TokenHourData';
import { Transaction } from './entity/Transaction';
import { UniswapDayData } from './entity/UniswapDayData';
import { Position } from './entity/Position';
const log = debug('vulcanize:resolver');
@ -122,6 +123,12 @@ export const createResolvers = async (indexer: Indexer): Promise<any> => {
log('uniswapDayDatas', first, skip, orderBy, orderDirection, where);
return indexer.getEntities(UniswapDayData, {}, where, { limit: first, skip, orderBy, orderDirection });
},
positions: async (_: any, { first, where }: { first: number, where: { [key: string]: any } }) => {
log('uniswapDayDatas', first, where);
return indexer.getEntities(Position, {}, where, { limit: first }, ['pool', 'token0', 'token1', 'tickLower', 'tickUpper', 'transaction']);
}
}
};

View File

@ -138,6 +138,24 @@ type TokenHourData {
periodStartUnix: Int!
}
type Position {
id: ID!
pool: Pool!
token0: Token!
token1: Token!
tickLower: Tick!
tickUpper: Tick!
transaction: Transaction!
liquidity: BigInt!
depositedToken0: BigDecimal!
depositedToken1: BigDecimal!
collectedFeesToken0: BigDecimal!
collectedFeesToken1: BigDecimal!
owner: Bytes!
feeGrowthInside0LastX128: BigInt!
feeGrowthInside1LastX128: BigInt!
}
enum OrderDirection {
asc
desc
@ -242,6 +260,10 @@ enum TokenHourData_orderBy {
periodStartUnix
}
input Position_filter {
id: ID
}
type Query {
bundle(
id: ID!
@ -382,5 +404,10 @@ type Query {
orderDirection: OrderDirection
where: UniswapDayData_filter
): [UniswapDayData!]!
positions(
first: Int = 100
where: Position_filter
): [Position!]!
}
`;

View File

@ -1,5 +1,5 @@
import { expect } from 'chai';
import { ethers, Contract, Signer } from 'ethers';
import { ethers, Contract, Signer, constants } from 'ethers';
import { request } from 'graphql-request';
import 'mocha';
import _ from 'lodash';
@ -18,7 +18,8 @@ import {
initializePool,
getMinTick,
getMaxTick,
approveToken
approveToken,
NFPM_ABI
} from '@vulcanize/util/test';
import { Client as UniClient, watchEvent } from '@vulcanize/uni-watcher';
import {
@ -37,7 +38,8 @@ import {
queryMints,
queryTicks,
queryBurns,
querySwaps
querySwaps,
queryPositions
} from '../test/queries';
import {
checkUniswapDayData,
@ -57,6 +59,7 @@ describe('uni-info-watcher', () => {
let token1: Contract;
let token0Address: string;
let token1Address: string;
let nfpm: Contract;
let tickLower: number;
let tickUpper: number;
@ -105,7 +108,7 @@ describe('uni-info-watcher', () => {
});
describe('PoolCreatedEvent', () => {
// NOTE Skipping checking entity updates that cannot be gotten/derived using queries.
// NOTE: Skipping checking entity updates that cannot be gotten/derived using queries.
// Checked entities: Token, Pool.
const fee = 500;
@ -135,7 +138,7 @@ describe('uni-info-watcher', () => {
const eventType = 'PoolCreatedEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 10 sec for the entities to be processed.
// Sleeping for 10 sec for the event to be processed.
await wait(10000);
});
@ -187,13 +190,14 @@ describe('uni-info-watcher', () => {
});
it('should trigger InitializeEvent', async () => {
// Initialize Pool.
initializePool(pool, sqrtPrice);
// Wait for InitializeEvent.
const eventType = 'InitializeEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 5 sec for the entities to be processed.
// Sleeping for 5 sec for the event to be processed.
await wait(5000);
});
@ -260,7 +264,7 @@ describe('uni-info-watcher', () => {
const eventType = 'MintEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 20 sec for the entities to be processed.
// Sleeping for 20 sec for the event to be processed.
await wait(20000);
});
@ -432,7 +436,7 @@ describe('uni-info-watcher', () => {
const eventType = 'BurnEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 15 sec for the entities to be processed.
// Sleeping for 15 sec for the event to be processed.
await wait(15000);
});
@ -602,7 +606,7 @@ describe('uni-info-watcher', () => {
const eventType = 'SwapEvent';
eventValue = await watchEvent(uniClient, eventType);
// Sleeping for 5 sec for the entities to be processed.
// Sleeping for 5 sec for the event to be processed.
await wait(5000);
});
@ -703,4 +707,122 @@ describe('uni-info-watcher', () => {
checkTokenHourData(endpoint, token1.address);
});
});
describe('TransferEvent', () => {
// NOTE: The test cases for TransferEvent are written such that IncreaseLiquidityEvent has also been processed right after.
// Checked entities: Transaction, Position.
const fee = 3000;
const sqrtPrice = '79228162514264337593543950336';
let eventType: string;
let eventValue: any;
let expectedTxID: string;
const amount0Desired = 15;
const amount1Desired = 15;
const amount0Min = 0;
const amount1Min = 0;
const deadline = 1634367993;
before(async () => {
// Get the NFPM contract.
const latestContract = await uniClient.getLatestContract('nfpm');
expect(latestContract.address).to.not.be.empty;
nfpm = new Contract(latestContract.address, NFPM_ABI, signer);
// Create Pool.
createPool(factory, token0Address, token1Address, fee);
// Wait for PoolCreatedEvent.
eventType = 'PoolCreatedEvent';
eventValue = await watchEvent(uniClient, eventType);
// Sleeping for 10 sec for the event to be processed.
await wait(10000); // not needed
// Reinitializing the pool variable.
const poolAddress = eventValue.event.pool;
pool = new Contract(poolAddress, POOL_ABI, signer);
expect(pool.address).to.not.be.empty;
// Reinitializing the ticks
const tickSpacing = await pool.tickSpacing();
tickLower = getMinTick(tickSpacing);
tickUpper = getMaxTick(tickSpacing);
// Initialize Pool.
initializePool(pool, sqrtPrice);
// Wait for InitializeEvent.
eventType = 'InitializeEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 5 sec for the event to be processed.
await wait(5000);
// Approving tokens for NonfungiblePositionManager contract.
await approveToken(token0, nfpm.address, BigInt(constants.MaxUint256.toString()));
await approveToken(token1, nfpm.address, BigInt(constants.MaxUint256.toString()));
});
it('should trigger TransferEvent', async () => {
nfpm.mint({
token0: token0Address,
token1: token1Address,
tickLower,
tickUpper,
amount0Desired,
amount1Desired,
amount0Min,
amount1Min,
recipient,
deadline,
fee
});
// Wait for MintEvent.
eventType = 'MintEvent';
await watchEvent(uniClient, eventType);
// Wait for TransferEvent.
eventType = 'TransferEvent';
eventValue = await watchEvent(uniClient, eventType);
// Wait for IncreaseLiquidityEvent.
eventType = 'IncreaseLiquidityEvent';
await watchEvent(uniClient, eventType);
// Sleeping for 15 sec for the events to be processed.
await wait(15000);
});
it('should create a Transaction entity', async () => {
const eventType = 'MintEvent';
({ expectedTxID } = await checkTransaction(endpoint, eventType));
});
it('should createa a Position entity', async () => {
// Checked values: pool, token0, token1, tickLower, tickUpper, transaction, owner.
// Unchecked values: feeGrowthInside0LastX128, feeGrowthInside0LastX128.
// Get the Position using tokenId.
const data = await request(endpoint, queryPositions, { id: Number(eventValue.event.tokenId) });
expect(data.positions).to.not.be.empty;
const position = data.positions[0];
const positionTickLower = position.tickLower.id.split('#')[1];
const positionTickUpper = position.tickUpper.id.split('#')[1];
const expectedOwner = eventValue.event.to;
expect(position.pool.id).to.be.equal(pool.address);
expect(position.token0.id).to.be.equal(token0.address);
expect(position.token1.id).to.be.equal(token1.address);
expect(positionTickLower).to.be.equal(tickLower.toString());
expect(positionTickUpper).to.be.equal(tickUpper.toString());
expect(position.transaction.id).to.be.equal(expectedTxID);
expect(position.owner).to.be.equal(expectedOwner);
});
});
});

View File

@ -252,3 +252,37 @@ query queryTransactions(
timestamp
}
}`;
// Getting position filtered by id.
export const queryPositions = gql`
query queryPositions($first: Int, $id: ID) {
positions(first: $first, where: { id: $id }) {
id,
pool {
id
},
token0 {
id
},
token1 {
id
},
tickLower {
id
},
tickUpper {
id
},
transaction {
id
},
liquidity,
depositedToken0,
depositedToken1,
collectedFeesToken0,
collectedFeesToken1,
owner,
feeGrowthInside0LastX128,
feeGrowthInside1LastX128
}
}`;

View File

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

View File

@ -7,9 +7,6 @@ const config: HardhatUserConfig = {
compilers: [
{
version: '0.7.6'
},
{
version: '0.5.0'
}
]
},

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client/core';
import { GraphQLClient, GraphQLConfig } from '@vulcanize/ipld-eth-client';
import { queryGetPool, queryPoolIdToPoolKey, queryPosition, queryEvents, subscribeEvents } from './queries';
import { queryGetPool, queryPoolIdToPoolKey, queryPosition, queryEvents, subscribeEvents, queryLatestContract } from './queries';
export class Client {
_config: GraphQLConfig;
@ -71,4 +71,15 @@ export class Client {
return getPool;
}
async getLatestContract (type: string): Promise<any> {
const { latestContract } = await this._client.query(
gql(queryLatestContract),
{
type
}
);
return latestContract;
}
}

View File

@ -604,4 +604,11 @@ export class Indexer {
...mappingKeys
);
}
async getLatestContract (type: string): Promise<any> {
const contract = await this._db.getLatestContract(type);
assert(contract, `No ${type} contract watched.`);
return contract;
}
}

View File

@ -155,3 +155,11 @@ query getPool($blockHash: String!, $token0: String!, $token1: String!, $fee: Str
}
}
`;
export const queryLatestContract = gql`
query queryLatestContract($type: String!) {
latestContract(type: $type) {
address
}
}
`;

View File

@ -89,6 +89,11 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch
getPool: (_: any, { blockHash, token0, token1, fee }: { blockHash: string, token0: string, token1: string, fee: string }) => {
log('getPool', blockHash, token0, token1, fee);
return indexer.getPool(blockHash, token0, token1, fee);
},
latestContract: (_: any, { type }: { type: string }) => {
log('latestContract', type);
return indexer.getLatestContract(type);
}
}
};

View File

@ -193,6 +193,10 @@ type BlockProgressEvent {
isComplete: Boolean!
}
type Contract {
address: String!
}
#
# Queries
#
@ -201,6 +205,10 @@ type Query {
# NonfungiblePositionManager
latestContract(
type: String!
): Contract
position(
blockHash: String!
tokenId: String!

View File

@ -13,7 +13,9 @@ import {
TICK_MIN,
getMinTick,
getMaxTick,
approveToken
approveToken,
deployWETH9Token,
deployNFPM
} from '@vulcanize/util/test';
import { Client as UniClient } from '@vulcanize/uni-watcher';
import { getCache } from '@vulcanize/cache';
@ -25,25 +27,11 @@ import {
import {
abi as POOL_ABI
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json';
import {
abi as NFTD_ABI,
bytecode as NFTD_BYTECODE
} from '@uniswap/v3-periphery/artifacts/contracts/libraries/NFTDescriptor.sol/NFTDescriptor.json';
import {
abi as NFTPD_ABI,
bytecode as NFTPD_BYTECODE,
linkReferences as NFTPD_LINKREFS
} from '@uniswap/v3-periphery/artifacts/contracts/NonfungibleTokenPositionDescriptor.sol/NonfungibleTokenPositionDescriptor.json';
import {
abi as NFPM_ABI,
bytecode as NFPM_BYTECODE
} from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json';
import { Indexer } from './indexer';
import { Database } from './database';
import { watchContract } from './utils/index';
import {
linkLibraries,
testCreatePool,
testInitialize,
checkMintEvent,
@ -54,10 +42,6 @@ import {
checkDecreaseLiquidityEvent,
checksCollectEvent
} from '../test/utils';
import {
abi as WETH9_ABI,
bytecode as WETH9_BYTECODE
} from '../artifacts/test/contracts/WETH9.sol/WETH9.json';
const NETWORK_RPC_URL = 'http://localhost:8545';
@ -270,47 +254,18 @@ describe('uni-watcher', () => {
it('should deploy a WETH9 token', async () => {
// Deploy weth9 token.
const WETH9 = new ethers.ContractFactory(WETH9_ABI, WETH9_BYTECODE, signer);
const weth9 = await WETH9.deploy();
weth9Address = weth9.address;
expect(weth9.address).to.not.be.empty;
weth9Address = await deployWETH9Token(signer);
expect(weth9Address).to.not.be.empty;
});
it('should deploy NonfungiblePositionManager', async () => {
// Deploy NonfungiblePositionManager.
// https://github.com/Uniswap/uniswap-v3-periphery/blob/main/test/shared/completeFixture.ts#L31
const nftDescriptorLibraryFactory = new ethers.ContractFactory(NFTD_ABI, NFTD_BYTECODE, signer);
const nftDescriptorLibrary = await nftDescriptorLibraryFactory.deploy();
expect(nftDescriptorLibrary.address).to.not.be.empty;
// Linking NFTDescriptor library to NFTPD before deploying.
const linkedNFTPDBytecode = linkLibraries({
bytecode: NFTPD_BYTECODE,
linkReferences: NFTPD_LINKREFS
}, {
NFTDescriptor: nftDescriptorLibrary.address
}
);
const positionDescriptorFactory = new ethers.ContractFactory(
NFTPD_ABI,
linkedNFTPDBytecode,
signer);
const nftDescriptor = await positionDescriptorFactory.deploy(weth9Address);
expect(nftDescriptor.address).to.not.be.empty;
const positionManagerFactory = new ethers.ContractFactory(
NFPM_ABI,
NFPM_BYTECODE,
signer);
nfpm = await positionManagerFactory.deploy(factory.address, weth9Address, nftDescriptor.address);
nfpm = await deployNFPM(signer, factory, weth9Address);
expect(nfpm.address).to.not.be.empty;
});
it('should watch NonfungiblePositionManager contract', async () => {
// Watch factory contract.
// Watch NFPM contract.
await watchContract(db, nfpm.address, 'nfpm', 100);
// Verifying with the db.

View File

@ -1,39 +1,10 @@
import { ethers, utils, Contract, Signer } from 'ethers';
import { ethers, Contract, Signer } from 'ethers';
import { expect } from 'chai';
import 'mocha';
import { Client as UniClient } from '@vulcanize/uni-watcher';
import { createPool, initializePool } from '@vulcanize/util/test';
// https://github.com/ethers-io/ethers.js/issues/195
export const linkLibraries = (
{
bytecode,
linkReferences
}: {
bytecode: string
linkReferences: { [fileName: string]: { [contractName: string]: { length: number; start: number }[] } }
},
libraries: { [libraryName: string]: string }): string => {
Object.keys(linkReferences).forEach((fileName) => {
Object.keys(linkReferences[fileName]).forEach((contractName) => {
if (!libraries.hasOwnProperty(contractName)) {
throw new Error(`Missing link library name ${contractName}`);
}
const address = utils.getAddress(libraries[contractName]).toLowerCase().slice(2);
linkReferences[fileName][contractName].forEach(({ start: byteStart, length: byteLength }) => {
const start = 2 + byteStart * 2;
const length = byteLength * 2;
bytecode = bytecode
.slice(0, start)
.concat(address)
.concat(bytecode.slice(start + length, bytecode.length));
});
});
});
return bytecode;
};
export const testCreatePool = async (
uniClient: UniClient,
factory: Contract,
@ -201,13 +172,13 @@ export const checkTransferEvent = (
value: any,
expectedContract: string,
from: string,
recipient: string
to: string
): 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.to).to.equal(to);
expect(value.event.tokenId).to.equal('1');
};

View File

@ -7,6 +7,9 @@ const config: HardhatUserConfig = {
compilers: [
{
version: '0.7.6'
},
{
version: '0.5.0'
}
]
},

View File

@ -1,4 +1,19 @@
import { ethers, Contract, ContractTransaction, Signer, BigNumber } from 'ethers';
import { ethers, Contract, ContractTransaction, Signer, BigNumber, utils } from 'ethers';
import assert from 'assert';
import {
abi as NFTD_ABI,
bytecode as NFTD_BYTECODE
} from '@uniswap/v3-periphery/artifacts/contracts/libraries/NFTDescriptor.sol/NFTDescriptor.json';
import {
abi as NFTPD_ABI,
bytecode as NFTPD_BYTECODE,
linkReferences as NFTPD_LINKREFS
} from '@uniswap/v3-periphery/artifacts/contracts/NonfungibleTokenPositionDescriptor.sol/NonfungibleTokenPositionDescriptor.json';
import {
abi as NFPM_ABI,
bytecode as NFPM_BYTECODE
} from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json';
import {
abi as TESTERC20_ABI,
@ -8,7 +23,12 @@ import {
abi as TESTUNISWAPV3CALLEE_ABI,
bytecode as TESTUNISWAPV3CALLEE_BYTECODE
} from '../artifacts/test/contracts/TestUniswapV3Callee.sol/TestUniswapV3Callee.json';
import {
abi as WETH9_ABI,
bytecode as WETH9_BYTECODE
} from '../artifacts/test/contracts/WETH9.sol/WETH9.json';
export { abi as NFPM_ABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json';
export { abi as TESTERC20_ABI } from '../artifacts/test/contracts/TestERC20.sol/TestERC20.json';
export const TICK_MIN = -887272;
@ -57,3 +77,69 @@ export const initializePool = async (pool: Contract, sqrtPrice: string): Promise
const transaction: ContractTransaction = await pool.initialize(BigNumber.from(sqrtPrice));
await transaction.wait();
};
export const deployWETH9Token = async (signer: Signer): Promise<string> => {
const WETH9 = new ethers.ContractFactory(WETH9_ABI, WETH9_BYTECODE, signer);
const weth9 = await WETH9.deploy();
return weth9.address;
};
// https://github.com/ethers-io/ethers.js/issues/195
const linkLibraries = (
{
bytecode,
linkReferences
}: {
bytecode: string
linkReferences: { [fileName: string]: { [contractName: string]: { length: number; start: number }[] } }
},
libraries: { [libraryName: string]: string }): string => {
Object.keys(linkReferences).forEach((fileName) => {
Object.keys(linkReferences[fileName]).forEach((contractName) => {
if (!Object.prototype.hasOwnProperty.call(libraries, contractName)) {
throw new Error(`Missing link library name ${contractName}`);
}
const address = utils.getAddress(libraries[contractName]).toLowerCase().slice(2);
linkReferences[fileName][contractName].forEach(({ start: byteStart, length: byteLength }) => {
const start = 2 + byteStart * 2;
const length = byteLength * 2;
bytecode = bytecode
.slice(0, start)
.concat(address)
.concat(bytecode.slice(start + length, bytecode.length));
});
});
});
return bytecode;
};
export const deployNFPM = async (signer: Signer, factory: Contract, weth9Address: string): Promise<Contract> => {
// Deploy NonfungiblePositionManager.
// https://github.com/Uniswap/uniswap-v3-periphery/blob/main/test/shared/completeFixture.ts#L31
const nftDescriptorLibraryFactory = new ethers.ContractFactory(NFTD_ABI, NFTD_BYTECODE, signer);
const nftDescriptorLibrary = await nftDescriptorLibraryFactory.deploy();
assert(nftDescriptorLibrary.address, 'NFTDescriptorLibrary not deployed.');
// Linking NFTDescriptor library to NFTPD before deploying.
const linkedNFTPDBytecode = linkLibraries({
bytecode: NFTPD_BYTECODE,
linkReferences: NFTPD_LINKREFS
}, {
NFTDescriptor: nftDescriptorLibrary.address
}
);
const positionDescriptorFactory = new ethers.ContractFactory(
NFTPD_ABI,
linkedNFTPDBytecode,
signer);
const nftDescriptor = await positionDescriptorFactory.deploy(weth9Address);
assert(nftDescriptor.address, 'NFTDescriptor not deployed.');
const positionManagerFactory = new ethers.ContractFactory(
NFPM_ABI,
NFPM_BYTECODE,
signer);
return await positionManagerFactory.deploy(factory.address, weth9Address, nftDescriptor.address);
};