diff --git a/packages/address-watcher/environments/local.toml b/packages/address-watcher/environments/local.toml index 1e27463f..8f64ab18 100644 --- a/packages/address-watcher/environments/local.toml +++ b/packages/address-watcher/environments/local.toml @@ -34,5 +34,5 @@ deleteOnStart = false [jobQueue] - dbConnectionString = "postgres://postgres:postgres@localhost/job-queue" + dbConnectionString = "postgres://postgres:postgres@localhost/address-watcher-job-queue" maxCompletionLag = 300 diff --git a/packages/uni-watcher/.eslintrc.json b/packages/uni-watcher/.eslintrc.json index 476d529d..9ab662bc 100644 --- a/packages/uni-watcher/.eslintrc.json +++ b/packages/uni-watcher/.eslintrc.json @@ -23,5 +23,14 @@ "allowArgumentsExplicitlyTypedAsAny": true } ] - } + }, + "overrides": [ + { + "files": ["*.test.ts", "test/*.ts"], + "rules": { + "no-unused-expressions": "off", + "no-prototype-builtins":"off" + } + } + ] } diff --git a/packages/uni-watcher/.gitignore b/packages/uni-watcher/.gitignore index b3ab1ae4..3ac0fb0a 100644 --- a/packages/uni-watcher/.gitignore +++ b/packages/uni-watcher/.gitignore @@ -3,4 +3,8 @@ node_modules/ build/ tmp/ -temp/ \ No newline at end of file +temp/ + +#Hardhat files +cache +artifacts diff --git a/packages/uni-watcher/.mocharc.json b/packages/uni-watcher/.mocharc.json new file mode 100644 index 00000000..2d6086d2 --- /dev/null +++ b/packages/uni-watcher/.mocharc.json @@ -0,0 +1,6 @@ +{ + "timeout": "30000", + "bail": true, + "exit": true, + "require": "ts-node/register" +} diff --git a/packages/uni-watcher/README.md b/packages/uni-watcher/README.md index 98e2d896..81c9a1ab 100644 --- a/packages/uni-watcher/README.md +++ b/packages/uni-watcher/README.md @@ -86,3 +86,14 @@ To test the watchers locally: * Send transactions to trigger events See https://github.com/vulcanize/uniswap-v3-periphery/blob/watcher-ts/demo.md for instructions. + +### Smoke test + +To run a smoke test: + +* Start the server and the job-runner. +* Run: + + ```bash + $ yarn smoke-test + ``` diff --git a/packages/uni-watcher/hardhat.config.ts b/packages/uni-watcher/hardhat.config.ts new file mode 100644 index 00000000..35dc8be8 --- /dev/null +++ b/packages/uni-watcher/hardhat.config.ts @@ -0,0 +1,21 @@ +import { HardhatUserConfig } from 'hardhat/config'; +import '@nomiclabs/hardhat-waffle'; + +const config: HardhatUserConfig = { + defaultNetwork: 'localhost', + solidity: { + compilers: [ + { + version: '0.7.6' + }, + { + version: '0.5.0' + } + ] + }, + paths: { + sources: './test/contracts' + } +}; + +export default config; diff --git a/packages/uni-watcher/package.json b/packages/uni-watcher/package.json index ad14bad4..b877d751 100644 --- a/packages/uni-watcher/package.json +++ b/packages/uni-watcher/package.json @@ -9,10 +9,12 @@ "server:mock": "MOCK=1 nodemon src/server.ts -f environments/local.toml", "job-runner": "DEBUG=vulcanize:* nodemon src/job-runner.ts -f environments/local.toml", "fill": "DEBUG=vulcanize:* ts-node src/fill.ts -f environments/local.toml", - "test": "mocha -r ts-node/register src/**/*.spec.ts", + "test": "mocha src/**/*.spec.ts", "lint": "eslint .", "build": "tsc", - "watch:contract": "ts-node src/cli/watch-contract.ts --configFile environments/local.toml" + "watch:contract": "ts-node src/cli/watch-contract.ts --configFile environments/local.toml", + "test:compile": "hardhat compile", + "smoke-test": "yarn test:compile && mocha src/smoke.test.ts" }, "repository": { "type": "git", @@ -52,12 +54,18 @@ }, "devDependencies": { "@ethersproject/abi": "^5.3.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-waffle": "^2.0.1", + "@types/chai": "^4.2.18", "@types/express": "^4.17.11", "@types/fs-extra": "^9.0.11", "@types/json-bigint": "^1.0.0", + "@types/mocha": "^8.2.2", "@types/yargs": "^17.0.0", "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", + "@uniswap/v3-core": "1.0.0", + "@uniswap/v3-periphery": "^1.1.1", "chai": "^4.3.4", "eslint": "^7.27.0", "eslint-config-semistandard": "^15.0.1", @@ -66,7 +74,10 @@ "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", - "nodemon": "^2.0.7" + "nodemon": "^2.0.7", + "ts-node": "^10.0.0", + "typescript": "^4.3.2" } } diff --git a/packages/uni-watcher/src/cli/watch-contract.ts b/packages/uni-watcher/src/cli/watch-contract.ts index 5c981ee2..9b8164ee 100644 --- a/packages/uni-watcher/src/cli/watch-contract.ts +++ b/packages/uni-watcher/src/cli/watch-contract.ts @@ -1,10 +1,11 @@ import assert from 'assert'; import yargs from 'yargs'; import 'reflect-metadata'; -import { ethers } from 'ethers'; import { Config, getConfig } from '@vulcanize/util'; + import { Database } from '../database'; +import { watchContract } from '../utils/index'; (async () => { const argv = await yargs.parserConfiguration({ @@ -43,9 +44,7 @@ import { Database } from '../database'; const db = new Database(dbConfig); await db.init(); - // Always use the checksum address (https://docs.ethers.io/v5/api/utils/address/#utils-getAddress). - const address = ethers.utils.getAddress(argv.address); + await watchContract(db, argv.address, argv.kind, argv.startingBlock); - await db.saveContract(address, argv.kind, argv.startingBlock); await db.close(); })(); diff --git a/packages/uni-watcher/src/smoke.test.ts b/packages/uni-watcher/src/smoke.test.ts new file mode 100644 index 00000000..63592fbb --- /dev/null +++ b/packages/uni-watcher/src/smoke.test.ts @@ -0,0 +1,268 @@ +import { expect, assert } from 'chai'; +import { ethers, Contract, Signer } from 'ethers'; +import 'reflect-metadata'; +import 'mocha'; + +import { Config, getConfig } from '@vulcanize/util'; +import { Client as UniClient } from '@vulcanize/uni-watcher'; +import { getCache } from '@vulcanize/cache'; +import { EthClient } from '@vulcanize/ipld-eth-client'; +import { + abi as FACTORY_ABI, + bytecode as FACTORY_BYTECODE +} 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 { Indexer } from './indexer'; +import { Database } from './database'; +import { watchContract } from './utils/index'; +import { testCreatePool, testInitialize } 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 +} from '../artifacts/test/contracts/TestUniswapV3Callee.sol/TestUniswapV3Callee.json'; + +const TICK_MIN = -887272; +const TICK_MAX = 887272; +const getMinTick = (tickSpacing: number) => Math.ceil(TICK_MIN / tickSpacing) * tickSpacing; +const getMaxTick = (tickSpacing: number) => Math.floor(TICK_MAX / tickSpacing) * tickSpacing; + +describe('uni-watcher', () => { + let factory: Contract; + let token0: Contract; + let token1: Contract; + let pool: Contract; + + let poolAddress: string; + let tickLower: number; + let tickUpper: number; + let config: Config; + let db: Database; + let uniClient: UniClient; + let ethClient: EthClient; + let signer: Signer; + let recipient: string; + + before(async () => { + const configFile = './environments/local.toml'; + config = await getConfig(configFile); + + const { database: dbConfig, upstream, server: { host, port } } = config; + assert(dbConfig, 'Missing dbConfig.'); + assert(upstream, 'Missing upstream.'); + assert(host, 'Missing host.'); + assert(port, 'Missing port.'); + + const { ethServer: { gqlApiEndpoint, gqlPostgraphileEndpoint }, cache: cacheConfig } = upstream; + assert(gqlApiEndpoint, 'Missing upstream ethServer.gqlApiEndpoint.'); + assert(gqlPostgraphileEndpoint, 'Missing upstream ethServer.gqlPostgraphileEndpoint.'); + assert(cacheConfig, 'Missing dbConfig.'); + + db = new Database(dbConfig); + await db.init(); + + const cache = await getCache(cacheConfig); + ethClient = new EthClient({ + gqlEndpoint: gqlApiEndpoint, + gqlSubscriptionEndpoint: gqlPostgraphileEndpoint, + cache + }); + + const endpoint = `http://${host}:${port}/graphql`; + const gqlEndpoint = endpoint; + const gqlSubscriptionEndpoint = endpoint; + uniClient = new UniClient({ + gqlEndpoint, + gqlSubscriptionEndpoint + }); + + const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + signer = provider.getSigner(); + recipient = await signer.getAddress(); + }); + + after(async () => { + await db.close(); + }); + + it('should deploy contract factory', async () => { + // Deploy factory from uniswap package. + const Factory = new ethers.ContractFactory(FACTORY_ABI, FACTORY_BYTECODE, signer); + factory = await Factory.deploy(); + + expect(factory.address).to.not.be.empty; + }); + + it('should watch factory contract', async () => { + // Watch factory contract. + await watchContract(db, factory.address, 'factory', 100); + + // Verifying with the db. + const indexer = new Indexer(config, db, ethClient); + assert(await indexer.isUniswapContract(factory.address), 'Factory contract not added to database.'); + }); + + it('should deploy 2 tokens', async () => { + // Deploy 2 tokens. + const Token = new ethers.ContractFactory(TESTERC20_ABI, TESTERC20_BYTECODE, signer); + + token0 = await Token.deploy(ethers.BigNumber.from(2).pow(255)); + expect(token0.address).to.not.be.empty; + + token1 = await Token.deploy(ethers.BigNumber.from(2).pow(255)); + expect(token1.address).to.not.be.empty; + }); + + it('should create pool', async () => { + const fee = 500; + + pool = await testCreatePool(uniClient, factory, token0, token1, POOL_ABI, signer, fee); + poolAddress = pool.address; + }); + + it('should initialize pool', async () => { + const sqrtPrice = '4295128939'; + + await testInitialize(uniClient, pool, TICK_MIN, sqrtPrice); + }); + + it('should mint specified amount', done => { + (async () => { + const amount = '10'; + const approveAmount = BigInt(1000000000000000000000000); + + const TestUniswapV3Callee = new ethers.ContractFactory(TESTUNISWAPV3CALLEE_ABI, TESTUNISWAPV3CALLEE_BYTECODE, signer); + const poolCallee = await TestUniswapV3Callee.deploy(); + + const tickSpacing = await pool.tickSpacing(); + // https://github.com/Uniswap/uniswap-v3-core/blob/main/test/UniswapV3Pool.spec.ts#L196 + tickLower = getMinTick(tickSpacing); + tickUpper = getMaxTick(tickSpacing); + + // Approving tokens for TestUniswapV3Callee contract. + // https://github.com/Uniswap/uniswap-v3-core/blob/main/test/shared/utilities.ts#L187 + const t0 = await token0.approve(poolCallee.address, approveAmount); + await t0.wait(); + + const t1 = await token1.approve(poolCallee.address, approveAmount); + await t1.wait(); + + // Subscribe using UniClient. + const subscription = await uniClient.watchEvents((value: any) => { + if (value.event.__typename === 'MintEvent') { + expect(value.block).to.not.be.empty; + expect(value.tx).to.not.be.empty; + expect(value.contract).to.equal(pool.address); + expect(value.eventIndex).to.be.a('number'); + + expect(value.event.__typename).to.equal('MintEvent'); + expect(value.event.sender).to.equal(poolCallee.address); + expect(value.event.owner).to.equal(recipient); + expect(value.event.tickLower).to.equal(tickLower.toString()); + expect(value.event.tickUpper).to.equal(tickUpper.toString()); + expect(value.event.amount).to.equal(amount); + expect(value.event.amount0).to.not.be.empty; + expect(value.event.amount1).to.not.be.empty; + + expect(value.proof).to.not.be.empty; + + if (subscription) { + subscription.unsubscribe(); + } + done(); + } + }); + + // Pool mint. + await poolCallee.mint(pool.address, recipient, BigInt(tickLower), BigInt(tickUpper), BigInt(amount)); + })().catch((error) => { + console.error(error); + }); + }); + + it('should burn specified amount', done => { + (async () => { + const amount = '10'; + + const tickSpacing = await pool.tickSpacing(); + // https://github.com/Uniswap/uniswap-v3-core/blob/main/test/UniswapV3Pool.spec.ts#L196 + const tickLower = getMinTick(tickSpacing); + const tickUpper = getMaxTick(tickSpacing); + + // Subscribe using UniClient. + const subscription = await uniClient.watchEvents((value: any) => { + if (value.event.__typename === 'BurnEvent') { + expect(value.block).to.not.be.empty; + expect(value.tx).to.not.be.empty; + expect(value.contract).to.equal(pool.address); + expect(value.eventIndex).to.be.a('number'); + + expect(value.event.__typename).to.equal('BurnEvent'); + expect(value.event.owner).to.equal(recipient); + expect(value.event.tickLower).to.equal(tickLower.toString()); + expect(value.event.tickUpper).to.equal(tickUpper.toString()); + expect(value.event.amount).to.equal(amount); + expect(value.event.amount0).to.not.be.empty; + expect(value.event.amount1).to.not.be.empty; + + expect(value.proof).to.not.be.empty; + + if (subscription) { + subscription.unsubscribe(); + } + done(); + } + }); + + // Pool burn. + await pool.burn(BigInt(tickLower), BigInt(tickUpper), BigInt(amount)); + })().catch((error) => { + console.error(error); + }); + }); + + it('should swap pool tokens', done => { + (async () => { + const sqrtPrice = '4295128938'; + + const TestUniswapV3Callee = new ethers.ContractFactory(TESTUNISWAPV3CALLEE_ABI, TESTUNISWAPV3CALLEE_BYTECODE, signer); + const poolCallee = await TestUniswapV3Callee.deploy(); + + // Subscribe using UniClient. + const subscription = await uniClient.watchEvents((value: any) => { + if (value.event.__typename === 'SwapEvent') { + expect(value.block).to.not.be.empty; + expect(value.tx).to.not.be.empty; + expect(value.contract).to.equal(poolAddress); + expect(value.eventIndex).to.be.a('number'); + + expect(value.event.__typename).to.equal('SwapEvent'); + expect(value.event.sender).to.equal(poolCallee.address); + expect(value.event.recipient).to.equal(recipient); + expect(value.event.amount0).to.not.be.empty; + expect(value.event.amount1).to.not.be.empty; + expect(value.event.sqrtPriceX96).to.equal(sqrtPrice); + expect(value.event.liquidity).to.not.be.empty; + expect(value.event.tick).to.equal(TICK_MIN.toString()); + + expect(value.proof).to.not.be.empty; + + if (subscription) { + subscription.unsubscribe(); + } + done(); + } + }); + + await poolCallee.swapToLowerSqrtPrice(poolAddress, BigInt(sqrtPrice), recipient); + })().catch((error) => { + console.error(error); + }); + }); +}); diff --git a/packages/uni-watcher/src/utils/index.ts b/packages/uni-watcher/src/utils/index.ts new file mode 100644 index 00000000..fc739c68 --- /dev/null +++ b/packages/uni-watcher/src/utils/index.ts @@ -0,0 +1,10 @@ +import { ethers } from 'ethers'; + +import { Database } from '../database'; + +export async function watchContract (db: Database, address: string, kind: string, startingBlock: number): Promise { + // Always use the checksum address (https://docs.ethers.io/v5/api/utils/address/#utils-getAddress). + const contractAddress = ethers.utils.getAddress(address); + + await db.saveContract(contractAddress, kind, startingBlock); +} diff --git a/packages/uni-watcher/test/contracts/TestERC20.sol b/packages/uni-watcher/test/contracts/TestERC20.sol new file mode 100644 index 00000000..8eaf7e73 --- /dev/null +++ b/packages/uni-watcher/test/contracts/TestERC20.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.7.6; + +import '@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol'; + +contract TestERC20 is IERC20Minimal { + mapping(address => uint256) public override balanceOf; + mapping(address => mapping(address => uint256)) public override allowance; + + constructor(uint256 amountToMint) { + mint(msg.sender, amountToMint); + } + + function mint(address to, uint256 amount) public { + uint256 balanceNext = balanceOf[to] + amount; + require(balanceNext >= amount, 'overflow balance'); + balanceOf[to] = balanceNext; + } + + function transfer(address recipient, uint256 amount) external override returns (bool) { + uint256 balanceBefore = balanceOf[msg.sender]; + require(balanceBefore >= amount, 'insufficient balance'); + balanceOf[msg.sender] = balanceBefore - amount; + + uint256 balanceRecipient = balanceOf[recipient]; + require(balanceRecipient + amount >= balanceRecipient, 'recipient balance overflow'); + balanceOf[recipient] = balanceRecipient + amount; + + emit Transfer(msg.sender, recipient, amount); + return true; + } + + function approve(address spender, uint256 amount) external override returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external override returns (bool) { + uint256 allowanceBefore = allowance[sender][msg.sender]; + + require(allowanceBefore >= amount, 'allowance insufficient'); + + allowance[sender][msg.sender] = allowanceBefore - amount; + + uint256 balanceRecipient = balanceOf[recipient]; + require(balanceRecipient + amount >= balanceRecipient, 'overflow balance recipient'); + balanceOf[recipient] = balanceRecipient + amount; + uint256 balanceSender = balanceOf[sender]; + require(balanceSender >= amount, 'underflow balance sender'); + balanceOf[sender] = balanceSender - amount; + + emit Transfer(sender, recipient, amount); + return true; + } +} diff --git a/packages/uni-watcher/test/contracts/TestUniswapV3Callee.sol b/packages/uni-watcher/test/contracts/TestUniswapV3Callee.sol new file mode 100644 index 00000000..14f50c1e --- /dev/null +++ b/packages/uni-watcher/test/contracts/TestUniswapV3Callee.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.7.6; + +import '@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol'; + +import '@uniswap/v3-core/contracts/libraries/SafeCast.sol'; +import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; + +import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol'; +import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; +import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3FlashCallback.sol'; + +import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; + +contract TestUniswapV3Callee is IUniswapV3MintCallback, IUniswapV3SwapCallback, IUniswapV3FlashCallback { + using SafeCast for uint256; + + function swapExact0For1( + address pool, + uint256 amount0In, + address recipient, + uint160 sqrtPriceLimitX96 + ) external { + IUniswapV3Pool(pool).swap(recipient, true, amount0In.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); + } + + function swap0ForExact1( + address pool, + uint256 amount1Out, + address recipient, + uint160 sqrtPriceLimitX96 + ) external { + IUniswapV3Pool(pool).swap(recipient, true, -amount1Out.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); + } + + function swapExact1For0( + address pool, + uint256 amount1In, + address recipient, + uint160 sqrtPriceLimitX96 + ) external { + IUniswapV3Pool(pool).swap(recipient, false, amount1In.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); + } + + function swap1ForExact0( + address pool, + uint256 amount0Out, + address recipient, + uint160 sqrtPriceLimitX96 + ) external { + IUniswapV3Pool(pool).swap(recipient, false, -amount0Out.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); + } + + function swapToLowerSqrtPrice( + address pool, + uint160 sqrtPriceX96, + address recipient + ) external { + IUniswapV3Pool(pool).swap(recipient, true, type(int256).max, sqrtPriceX96, abi.encode(msg.sender)); + } + + function swapToHigherSqrtPrice( + address pool, + uint160 sqrtPriceX96, + address recipient + ) external { + IUniswapV3Pool(pool).swap(recipient, false, type(int256).max, sqrtPriceX96, abi.encode(msg.sender)); + } + + event SwapCallback(int256 amount0Delta, int256 amount1Delta); + + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external override { + address sender = abi.decode(data, (address)); + + emit SwapCallback(amount0Delta, amount1Delta); + + if (amount0Delta > 0) { + IERC20Minimal(IUniswapV3Pool(msg.sender).token0()).transferFrom(sender, msg.sender, uint256(amount0Delta)); + } else if (amount1Delta > 0) { + IERC20Minimal(IUniswapV3Pool(msg.sender).token1()).transferFrom(sender, msg.sender, uint256(amount1Delta)); + } else { + // if both are not gt 0, both must be 0. + assert(amount0Delta == 0 && amount1Delta == 0); + } + } + + function mint( + address pool, + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount + ) external { + IUniswapV3Pool(pool).mint(recipient, tickLower, tickUpper, amount, abi.encode(msg.sender)); + } + + event MintCallback(uint256 amount0Owed, uint256 amount1Owed); + + function uniswapV3MintCallback( + uint256 amount0Owed, + uint256 amount1Owed, + bytes calldata data + ) external override { + address sender = abi.decode(data, (address)); + + emit MintCallback(amount0Owed, amount1Owed); + if (amount0Owed > 0) + IERC20Minimal(IUniswapV3Pool(msg.sender).token0()).transferFrom(sender, msg.sender, amount0Owed); + if (amount1Owed > 0) + IERC20Minimal(IUniswapV3Pool(msg.sender).token1()).transferFrom(sender, msg.sender, amount1Owed); + } + + event FlashCallback(uint256 fee0, uint256 fee1); + + function flash( + address pool, + address recipient, + uint256 amount0, + uint256 amount1, + uint256 pay0, + uint256 pay1 + ) external { + IUniswapV3Pool(pool).flash(recipient, amount0, amount1, abi.encode(msg.sender, pay0, pay1)); + } + + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external override { + emit FlashCallback(fee0, fee1); + + (address sender, uint256 pay0, uint256 pay1) = abi.decode(data, (address, uint256, uint256)); + + if (pay0 > 0) IERC20Minimal(IUniswapV3Pool(msg.sender).token0()).transferFrom(sender, msg.sender, pay0); + if (pay1 > 0) IERC20Minimal(IUniswapV3Pool(msg.sender).token1()).transferFrom(sender, msg.sender, pay1); + } +} diff --git a/packages/uni-watcher/test/utils.ts b/packages/uni-watcher/test/utils.ts new file mode 100644 index 00000000..6ce237a0 --- /dev/null +++ b/packages/uni-watcher/test/utils.ts @@ -0,0 +1,119 @@ +import { ethers, utils, Contract, Signer, BigNumber } from 'ethers'; +import { expect } from 'chai'; +import 'mocha'; + +import { Client as UniClient } from '@vulcanize/uni-watcher'; + +// https://github.com/ethers-io/ethers.js/issues/195 +export function 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 async function testCreatePool ( + uniClient: UniClient, + factory: Contract, + token0: Contract, + token1: Contract, + poolAbi: any, + signer: Signer, + fee: number): Promise { + return new Promise((resolve, reject) => { + (async () => { + try { + const subscription = await uniClient.watchEvents((value: any) => { + // Function gets called with previous events. Check for PoolCreatedEvent. + if (value.event.__typename === 'PoolCreatedEvent') { + expect(value.block).to.not.be.empty; + expect(value.tx).to.not.be.empty; + expect(value.contract).to.equal(factory.address); + expect(value.eventIndex).to.be.a('number'); + expect(value.event.__typename).to.equal('PoolCreatedEvent'); + + const tokens = new Set([token0.address, token1.address]); + expect(new Set([value.event.token0, value.event.token1])).to.eql(tokens); + expect(value.event.fee).to.equal(fee.toString()); + expect(value.event.tickSpacing).to.not.be.empty; + expect(value.event.pool).to.not.be.empty; + + expect(value.proof).to.not.be.empty; + const poolAddress = value.event.pool; + const pool = new ethers.Contract(poolAddress, poolAbi, signer); + + if (subscription) { + subscription.unsubscribe(); + } + resolve(pool); + } + }); + + // Create pool. + await factory.createPool(token0.address, token1.address, fee); + } catch (error) { + reject(error); + } + })(); + }); +} + +export function testInitialize ( + uniClient: UniClient, + pool: Contract, + expectedTick: number, + sqrtPrice: string): Promise { + return new Promise((resolve, reject) => { + try { + (async () => { + // Subscribe using UniClient. + const subscription = await uniClient.watchEvents((value: any) => { + // Function gets called with previous events. Check for InitializeEvent. + if (value.event.__typename === 'InitializeEvent') { + expect(value.block).to.not.be.empty; + expect(value.tx).to.not.be.empty; + expect(value.contract).to.equal(pool.address); + expect(value.eventIndex).to.be.a('number'); + + expect(value.event.__typename).to.equal('InitializeEvent'); + expect(value.event.sqrtPriceX96).to.equal(sqrtPrice); + expect(value.event.tick).to.equal(expectedTick.toString()); + + expect(value.proof).to.not.be.empty; + + if (subscription) { + subscription.unsubscribe(); + } + resolve(); + } + }); + + // Pool initialize. + await pool.initialize(BigNumber.from(sqrtPrice)); + })(); + } catch (error) { + reject(error); + } + }); +} diff --git a/yarn.lock b/yarn.lock index 8e5f37df..1f7db796 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1890,6 +1890,11 @@ dependencies: "@octokit/openapi-types" "^7.2.3" +"@openzeppelin/contracts@3.4.1-solc-0.7-2": + version "3.4.1-solc-0.7-2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92" + integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -2576,6 +2581,33 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== +"@uniswap/lib@^4.0.1-alpha": + version "4.0.1-alpha" + resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" + integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== + +"@uniswap/v2-core@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" + integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== + +"@uniswap/v3-core@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d" + integrity sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA== + +"@uniswap/v3-periphery@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.1.1.tgz#be6dfca7b29318ea0d76a7baf15d3b33c3c5e90a" + integrity sha512-orqD2Xy4lxVPF6pxd7ECSJY0gzEuqyeVSDHjzM86uWxOXlA4Nlh5pvI959KaS32pSOFBOVVA4XbbZywbJj+CZg== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/lib" "^4.0.1-alpha" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + base64-sol "1.0.1" + hardhat-watcher "^2.1.1" + "@wry/context@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.6.0.tgz#f903eceb89d238ef7e8168ed30f4511f92d83e06" @@ -2840,7 +2872,7 @@ any-promise@^1.0.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= -anymatch@~3.1.1: +anymatch@~3.1.1, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== @@ -3785,6 +3817,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64-sol@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/base64-sol/-/base64-sol-1.0.1.tgz#91317aa341f0bc763811783c5729f1c2574600f6" + integrity sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -4358,6 +4395,21 @@ chokidar@3.5.1, chokidar@^3.2.2, chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.1" +chokidar@^3.4.3: + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -6755,7 +6807,7 @@ fsevents@~2.1.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fsevents@~2.3.1: +fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -6961,7 +7013,7 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -7204,6 +7256,13 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +hardhat-watcher@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/hardhat-watcher/-/hardhat-watcher-2.1.1.tgz#8b05fec429ed45da11808bbf6054a90f3e34c51a" + integrity sha512-zilmvxAYD34IofBrwOliQn4z92UiDmt2c949DW4Gokf0vS0qk4YTfVCi/LmUBICThGygNANE3WfnRTpjCJGtDA== + dependencies: + chokidar "^3.4.3" + hardhat@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.3.0.tgz#5c29f8b4d08155c3dc8c908af9713fd5079522d5" @@ -11131,6 +11190,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"