diff --git a/.gitignore b/.gitignore index 047c487..08d11ee 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ generated/ node_modules/ .env subgraph.yaml +tests/.bin +tests/.latest.json +tests/*/.bin +tests/*/.latest.json diff --git a/README.md b/README.md index 7103472..0bd7b81 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ There is also a GP v1 subgraph here: https://github.com/gnosis/dex-subgraph ## Model -Further information about the model [here](./model.md) +Further information about the model [here](./docs/model.md) ## Setup of your own test subgraph @@ -55,3 +55,13 @@ yarn deploy If everything went well you'll have a copy of this subgraph running on your hosted service account indexing your desired network. Please notice a subgraph can only index a single network, if you want to index another network you should create a new subgraph and do same steps starting from step 3. + +## Tests + +For running all tests execute: + +```bash +yarn test +``` + +Further information about creating tests, tests organization and running them [here](./docs/tests.md) diff --git a/model.md b/docs/model.md similarity index 100% rename from model.md rename to docs/model.md diff --git a/docs/tests.md b/docs/tests.md new file mode 100644 index 0000000..c4eb904 --- /dev/null +++ b/docs/tests.md @@ -0,0 +1,39 @@ +# Tests explained + +### About test framework + +We are using **matchstick** for testing subgraphs. For being able to run the matchstick tests you need to install *postgresql*. You can find more information [here](https://thegraph.com/docs/en/developing/unit-testing-framework/) + +### About folder organization + +There are 3 different folders for organizing the tests: + +1. **gpv2settlement:** this folder will contain tests related to settlement contract. It doesn't matter where it's deployed which code will be execueted in that case + +2. **gc:** this folder will contain tests related to price calculation that's being done on Gnosis Chain only. At the moment we are using honeyswap (Uniswap v2 pools) to estimate prices. + +3. **mainnet:** this folder will containt tests related to price calculation in mainnet. In mainnet Uniswap v3 is being indexed, it's pools and tokens. + +Inside each folder we will put the tests replicating `src` folder structure of what the test aim to test. +It's important to notice all files should contain .test. string on it's name. +There's also a `utils.js` file that will contain utilities and helpers for creating entities or mocks that are common to different tests. + +### About writting tests + +All files will contain different `describe` functions nested and a `test` function at the end: + +``` Javascript +describe(FileName, () => { // FileName will be replaced by it's file name + describe(FunctionName () => { // FunctionName will be replaced by it's function name + describe(SetupExpectations, () => { // SetupExpectations is the stage we need to build to make possible the test to run + test(WhatAreWeTesting, () => { // Here we will name the test using what's the result we are waiting after test is executed. +``` + +### About test running. + +- `yarn test`: running this command all tests on folder `tests` will be run. +- `yarn test:env`: running this .env variable will be read to run that sepcific set of tests +- `yarn test:gc`: we filter the tests to the gc folder only +- `yarn test:mainnet`: we filter the tests to the mainnet folder only +- `yarn test:cow`: runing this we filter by gpv2settlement contract only. + diff --git a/package.json b/package.json index 6d1d598..39eb371 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,14 @@ "scripts": { "codegen": "graph codegen", "build": "graph build", + "generateConfigs": "yarn generateGcConfigs && yarn generateMainnetConfigs", + "generateGcConfigs": "mustache config/gc.json subgraph.yaml.mustache > subgraph.yaml && yarn codegen", + "generateMainnetConfigs": "mustache config/mainnet.json subgraph.yaml.mustache > subgraph.yaml && yarn codegen", + "test": "yarn generateConfigs && graph test", + "test:env": "node src/scripts/test.js", + "test:gc": "yarn generateConfigs && graph test gc", + "test:mainnet": "yarn generateConfigs && graph test mainnet", + "test:cow": "yarn generateConfigs && graph test gpv2settlement", "deploy": "node src/scripts/deploy.js", "deploy:mainnet": "cross-env NETWORK=mainnet SUBGRAPH=gnosis/cow yarn deploy", "deploy:rinkeby": "cross-env NETWORK=rinkeby SUBGRAPH=gnosis/cow-rinkeby yarn deploy", @@ -17,6 +25,7 @@ "devDependencies": { "chalk": "^2.4.1", "cross-env": "^7.0.3", - "dotenv": "^16.0.0" + "dotenv": "^16.0.0", + "matchstick-as": "^0.5.0" } } diff --git a/schema.graphql b/schema.graphql index 2174168..8929b4b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -405,3 +405,8 @@ type PairHourly @entity { "Total volume in Usd" volumeTradedUsd: BigDecimal } + +type Receiver @entity { + id: ID! + address: Bytes! +} diff --git a/src/mapping.ts b/src/mapping.ts index 33e2e95..04dbd52 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -3,9 +3,10 @@ import { OrderInvalidated, PreSignature, Settlement, - Trade + Trade, + SettleCall } from "../generated/GPV2Settlement/GPV2Settlement" -import { tokens, trades, orders, users } from "./modules" +import { tokens, trades, orders, users, totals } from "./modules" import { getPrices } from "./utils/getPrices" import { MINUS_ONE_BD } from "./utils/constants" import { BigDecimal, BigInt, dataSource } from "@graphprotocol/graph-ts" @@ -138,3 +139,5 @@ export function handleTrade(event: Trade): void { order.save() } + +export function handleSettle(call: SettleCall): void {} \ No newline at end of file diff --git a/src/modules/settlements.ts b/src/modules/settlements.ts index ca19370..99a3fa3 100644 --- a/src/modules/settlements.ts +++ b/src/modules/settlements.ts @@ -7,7 +7,7 @@ import { getEthPriceInUSD } from "../utils/pricing" export namespace settlements { - export function getOrCreateSettlement(txHash: Bytes, tradeTimestamp: i32, solver: Address, txGasPrice: BigInt, feeAmountUsd: BigDecimal): void { + export function getOrCreateSettlement(txHash: Bytes, tradeTimestamp: i32, solver: Address, txGasPrice: BigInt, feeAmountUsd: BigDecimal | null): void { let settlementId = txHash.toHexString() let network = dataSource.network() @@ -36,8 +36,10 @@ export namespace settlements { settlement.profitability = ZERO_BD totals.addSettlementCount(tradeTimestamp) } - let prevFeeAmountUsd = settlement.aggregatedFeeAmountUsd - settlement.aggregatedFeeAmountUsd = prevFeeAmountUsd.plus(feeAmountUsd) + if(feeAmountUsd) { + let prevFeeAmountUsd = settlement.aggregatedFeeAmountUsd + settlement.aggregatedFeeAmountUsd = prevFeeAmountUsd.plus(feeAmountUsd) + } settlement.profitability = settlement.aggregatedFeeAmountUsd.minus(settlement.txCostUsd) settlement.save() } diff --git a/src/modules/trades.ts b/src/modules/trades.ts index 1d066b6..30076ef 100644 --- a/src/modules/trades.ts +++ b/src/modules/trades.ts @@ -53,8 +53,8 @@ export namespace trades { let buyTokenId = buyToken.id let sellTokenId = sellToken.id - let buyTokenPriceUsd = buyToken.priceUsd as BigDecimal - let sellTokenPriceUsd = sellToken.priceUsd as BigDecimal + let buyTokenPriceUsd = _buyTokenPriceUsd ? _buyTokenPriceUsd as BigDecimal : null + let sellTokenPriceUsd = _sellTokenPriceUsd ? _sellTokenPriceUsd as BigDecimal : null tokens.createTokenTradingEvent(timestamp, buyTokenId, tradeId, buyAmount, buyAmountEth, buyAmountUsd, buyTokenPriceUsd) tokens.createTokenTradingEvent(timestamp, sellTokenId, tradeId, sellAmount, sellAmountEth, sellAmountUsd, sellTokenPriceUsd) diff --git a/src/scripts/test.js b/src/scripts/test.js new file mode 100644 index 0000000..c33028f --- /dev/null +++ b/src/scripts/test.js @@ -0,0 +1,11 @@ +const { exec } = require('child_process') +const { series } = require('async') +require('dotenv').config() + +const network = process.env.NETWORK + +series([ + (callback) => exec(`mustache config/${network}.json subgraph.yaml.mustache > subgraph.yaml`, null, callback), + (callback) => exec(`yarn codegen`, null, callback), + () => exec(`yarn test:${network} && yarn test:cow`, null, (_err, stdout, _stderr) => { console.log(stdout) }), +]) diff --git a/src/uniswapMappings/uniswapPools.ts b/src/uniswapMappings/uniswapPools.ts index ad92a40..493777f 100644 --- a/src/uniswapMappings/uniswapPools.ts +++ b/src/uniswapMappings/uniswapPools.ts @@ -15,6 +15,7 @@ export function handleInitialize(event: Initialize): void { let pool = UniswapPool.load(event.address.toHexString()) if (pool) { pool.tick = BigInt.fromI32(event.params.tick) + pool.save() } let token0Id = pool ? pool.token0 : null diff --git a/src/utils/getPrices.ts b/src/utils/getPrices.ts index a3361d0..f43aee9 100644 --- a/src/utils/getPrices.ts +++ b/src/utils/getPrices.ts @@ -44,9 +44,9 @@ function getUniswapPricesForPair(token0: Address, token1: Address, isEthPriceCal let reservesTry = pair.try_getReserves() let reserves = reservesTry.reverted ? EMPTY_RESERVES_RESULT : reservesTry.value let pairToken0Try = pair.try_token0() - let pairToken0 = pairToken0Try.reverted ? ZERO_ADDRESS as Address : pairToken0Try.value + let pairToken0 = pairToken0Try.reverted ? changetype
(ZERO_ADDRESS) : pairToken0Try.value let pairToken1Try = pair.try_token1() - let pairToken1 = pairToken1Try.reverted ? ZERO_ADDRESS as Address : pairToken1Try.value + let pairToken1 = pairToken1Try.reverted ? changetype
(ZERO_ADDRESS) : pairToken1Try.value if (reserves.value0 == ZERO_BI || reserves.value1 == ZERO_BI || diff --git a/subgraph.yaml.mustache b/subgraph.yaml.mustache index 8a97ca0..561ae97 100644 --- a/subgraph.yaml.mustache +++ b/subgraph.yaml.mustache @@ -44,6 +44,9 @@ dataSources: handler: handleSettlement - event: Trade(indexed address,address,address,uint256,uint256,uint256,bytes) handler: handleTrade + callHandlers: + - function: settle(address[],uint256[],(uint256,uint256,address,uint256,uint256,uint32,bytes32,uint256,uint256,uint256,bytes)[],(address,uint256,bytes)[]) + handler: handleSettle file: ./src/mapping.ts {{#uniV3Factory}} - kind: ethereum/contract diff --git a/tests/gc/mapping.test.ts b/tests/gc/mapping.test.ts new file mode 100644 index 0000000..9d8710c --- /dev/null +++ b/tests/gc/mapping.test.ts @@ -0,0 +1,124 @@ +import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { + afterEach, + assert, + clearStore, + describe, + test, + dataSourceMock, +} from "matchstick-as"; +import { Token } from "../../generated/schema"; +import { handleTrade } from "../../src/mapping"; +import { + STABLECOIN_ADDRESS_GC, + UNISWAP_FACTORY, + WETH_ADDRESS_GC, +} from "../../src/utils/constants"; +import { + mockErc20, + mockUniswapFactoryGetPair, + mockUniswapV2Pair, + createTradeEvent, +} from "./utils"; + +describe("Mapping", () => { + afterEach(() => { + clearStore(); + }); + + describe("handleTrade", () => { + describe("when sellToken is a stablecoin", () => { + test("GetPrice should return one dollar price", () => { + let owner = Address.fromString( + "0x0000000000000000000000000000000000000001" + ); + let sellToken = STABLECOIN_ADDRESS_GC + let buyToken = Address.fromString( + "0x0000000000000000000000000000000000000020" + ); + + let sellAmount = BigInt.fromI32(100); + let buyAmount = BigInt.fromI32(50); + let feeAmount = BigInt.fromI32(5); + + let orderUid = Bytes.fromHexString( + "0x0000000000000000000000000000000000000022" + ); + + dataSourceMock.setNetwork("xdai"); + + mockErc20(sellToken, "DAI", "DAI", BigInt.fromI32(6)); + mockErc20(buyToken, "Token 2", "TK2", BigInt.fromI32(18)); + mockErc20(WETH_ADDRESS_GC, "WETH", "WETH", BigInt.fromI32(6)); + mockErc20(STABLECOIN_ADDRESS_GC, "DAI", "DAI", BigInt.fromI32(6)); + + let pair = Address.fromString( + "0x0000000000000000000000000000000000000100" + ); + + let pair2 = Address.fromString( + "0x0000000000000000000000000000000000000101" + ); + + let pair3 = Address.fromString( + "0x0000000000000000000000000000000000000102" + ); + + mockUniswapFactoryGetPair( + UNISWAP_FACTORY, + [sellToken, WETH_ADDRESS_GC], + pair + ); + + mockUniswapV2Pair( + pair, + [BigInt.fromI32(100), BigInt.fromI32(100), BigInt.fromI32(100)], + sellToken, + WETH_ADDRESS_GC + ); + + mockUniswapFactoryGetPair( + UNISWAP_FACTORY, + [WETH_ADDRESS_GC, STABLECOIN_ADDRESS_GC], + pair2 + ); + + mockUniswapV2Pair( + pair2, + [BigInt.fromI32(50), BigInt.fromI32(50), BigInt.fromI32(50)], + WETH_ADDRESS_GC, + STABLECOIN_ADDRESS_GC + ); + + mockUniswapFactoryGetPair( + UNISWAP_FACTORY, + [buyToken, WETH_ADDRESS_GC], + pair3 + ); + + mockUniswapV2Pair( + pair3, + [BigInt.fromI32(10), BigInt.fromI32(10), BigInt.fromI32(10)], + buyToken, + WETH_ADDRESS_GC + ); + + let event = createTradeEvent( + owner, + sellToken, + buyToken, + sellAmount, + buyAmount, + feeAmount, + orderUid + ); + + handleTrade(event); + + let sellTokenId = sellToken.toHexString(); + + assert.fieldEquals("Token", sellTokenId, "priceUsd", "1"); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/gc/utils.ts b/tests/gc/utils.ts new file mode 100644 index 0000000..8bf632a --- /dev/null +++ b/tests/gc/utils.ts @@ -0,0 +1,187 @@ +import { Address, ethereum, BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { createMockedFunction, newMockEvent } from "matchstick-as"; +import { Trade as TradeEvent } from "../../generated/GPV2Settlement/GPV2Settlement"; + +export function createTradeEvent( + owner: Address, + sellToken: Address, + buyToken: Address, + sellAmount: BigInt, + buyAmount: BigInt, + feeAmount: BigInt, + orderUid: Bytes +): TradeEvent { + let mockEvent = newMockEvent(); + + let event = new TradeEvent( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + null + ); + + event.parameters = new Array(); + + let ownerParam = new ethereum.EventParam( + "owner", + ethereum.Value.fromAddress(owner) + ); + let sellTokenParam = new ethereum.EventParam( + "sellToken", + ethereum.Value.fromAddress(sellToken) + ); + let buyTokenParam = new ethereum.EventParam( + "buyToken", + ethereum.Value.fromAddress(buyToken) + ); + let sellAmountParam = new ethereum.EventParam( + "sellAmount", + ethereum.Value.fromSignedBigInt(sellAmount) + ); + let buyAmountParam = new ethereum.EventParam( + "buyAmount", + ethereum.Value.fromSignedBigInt(buyAmount) + ); + let feeAmountParam = new ethereum.EventParam( + "feeAmount", + ethereum.Value.fromSignedBigInt(feeAmount) + ); + let orderUidParam = new ethereum.EventParam( + "orderUid", + ethereum.Value.fromBytes(orderUid) + ); + + event.parameters.push(ownerParam); + event.parameters.push(sellTokenParam); + event.parameters.push(buyTokenParam); + event.parameters.push(sellAmountParam); + event.parameters.push(buyAmountParam); + event.parameters.push(feeAmountParam); + event.parameters.push(orderUidParam); + + return event; +} + +function mockContractFunction( + address: Address, + name: string, + signature: string, + result: ethereum.Value[], + args: ethereum.Value[] +): void { + createMockedFunction(address, name, signature) + .withArgs(args) + .returns(result); +} + +function mockErc20Decimals(address: Address, value: BigInt): void { + mockContractFunction( + address, + "decimals", + "decimals():(uint8)", + [ethereum.Value.fromUnsignedBigInt(value)], + [] + ); +} + +function mockErc20Name(address: Address, value: string): void { + mockContractFunction( + address, + "name", + "name():(string)", + [ethereum.Value.fromString(value)], + [] + ); +} + +function mockErc20Symbol(address: Address, value: string): void { + mockContractFunction( + address, + "symbol", + "symbol():(string)", + [ethereum.Value.fromString(value)], + [] + ); +} + +export function mockErc20( + address: Address, + name: string, + symbol: string, + decimals: BigInt +): void { + mockErc20Name(address, name); + mockErc20Symbol(address, symbol); + mockErc20Decimals(address, decimals); +} + +export function mockUniswapFactoryGetPair( + contractAddress: Address, + functionArgs: Address[], + functionResult: Address +): void { + mockContractFunction( + contractAddress, + "getPair", + "getPair(address,address):(address)", + [ethereum.Value.fromAddress(functionResult)], + [ + ethereum.Value.fromAddress(functionArgs[0]), + ethereum.Value.fromAddress(functionArgs[1]), + ] + ); +} + +function mockUniswapV2PairGetReserves( + address: Address, + one: BigInt, + two: BigInt, + three: BigInt +): void { + mockContractFunction( + address, + "getReserves", + "getReserves():(uint112,uint112,uint32)", + [ + ethereum.Value.fromUnsignedBigInt(one), + ethereum.Value.fromUnsignedBigInt(two), + ethereum.Value.fromUnsignedBigInt(three), + ], + [] + ); +} + +function mockUniswapV2PairToken0(address: Address, value: Address): void { + mockContractFunction( + address, + "token0", + "token0():(address)", + [ethereum.Value.fromAddress(value)], + [] + ); +} + +function mockUniswapV2PairToken1(address: Address, value: Address): void { + mockContractFunction( + address, + "token1", + "token1():(address)", + [ethereum.Value.fromAddress(value)], + [] + ); +} + +export function mockUniswapV2Pair( + address: Address, + reserves: BigInt[], + token0: Address, + token1: Address +): void { + mockUniswapV2PairGetReserves(address, reserves[0], reserves[1], reserves[2]); + mockUniswapV2PairToken0(address, token0); + mockUniswapV2PairToken1(address, token1); +} \ No newline at end of file diff --git a/tests/gpv2settlement/mapping.test.ts b/tests/gpv2settlement/mapping.test.ts new file mode 100644 index 0000000..9f059a3 --- /dev/null +++ b/tests/gpv2settlement/mapping.test.ts @@ -0,0 +1,210 @@ +import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { + afterEach, + assert, + clearStore, + describe, + test, + dataSourceMock, +} from "matchstick-as"; +import { handleTrade } from "../../src/mapping"; +import { + STABLECOIN_ADDRESS_GC, + UNISWAP_FACTORY, + WETH_ADDRESS_GC, +} from "../../src/utils/constants"; +import { + mockErc20, + mockUniswapFactoryGetPair, + mockUniswapV2Pair, + createTradeEvent, +} from "./utils"; + +describe("Mapping", () => { + afterEach(() => { + clearStore(); + }); + + describe("handleTrade", () => { + describe("when buyToken and sellToken do not exist", () => { + test("stores tokens and trades", () => { + let owner = Address.fromString( + "0x0000000000000000000000000000000000000001" + ); + let sellToken = Address.fromString( + "0x0000000000000000000000000000000000000010" + ); + let buyToken = Address.fromString( + "0x0000000000000000000000000000000000000020" + ); + + let sellAmount = BigInt.fromI32(100); + let buyAmount = BigInt.fromI32(50); + let feeAmount = BigInt.fromI32(5); + + let orderUid = Bytes.fromHexString( + "0x0000000000000000000000000000000000000022" + ); + + dataSourceMock.setNetwork("xdai"); + + mockErc20(sellToken, "Token 1", "TK1", BigInt.fromI32(18)); + mockErc20(buyToken, "Token 2", "TK2", BigInt.fromI32(18)); + mockErc20(WETH_ADDRESS_GC, "WETH", "WETH", BigInt.fromI32(6)); + mockErc20(STABLECOIN_ADDRESS_GC, "DAI", "DAI", BigInt.fromI32(6)); + + let pair = Address.fromString( + "0x0000000000000000000000000000000000000100" + ); + + let pair2 = Address.fromString( + "0x0000000000000000000000000000000000000101" + ); + + let pair3 = Address.fromString( + "0x0000000000000000000000000000000000000102" + ); + + mockUniswapFactoryGetPair( + UNISWAP_FACTORY, + [sellToken, WETH_ADDRESS_GC], + pair + ); + + mockUniswapV2Pair( + pair, + [BigInt.fromI32(100), BigInt.fromI32(100), BigInt.fromI32(100)], + sellToken, + WETH_ADDRESS_GC + ); + + mockUniswapFactoryGetPair( + UNISWAP_FACTORY, + [WETH_ADDRESS_GC, STABLECOIN_ADDRESS_GC], + pair2 + ); + + mockUniswapV2Pair( + pair2, + [BigInt.fromI32(50), BigInt.fromI32(50), BigInt.fromI32(50)], + WETH_ADDRESS_GC, + STABLECOIN_ADDRESS_GC + ); + + mockUniswapFactoryGetPair( + UNISWAP_FACTORY, + [buyToken, WETH_ADDRESS_GC], + pair3 + ); + + mockUniswapV2Pair( + pair3, + [BigInt.fromI32(10), BigInt.fromI32(10), BigInt.fromI32(10)], + buyToken, + WETH_ADDRESS_GC + ); + + let event = createTradeEvent( + owner, + sellToken, + buyToken, + sellAmount, + buyAmount, + feeAmount, + orderUid + ); + + handleTrade(event); + + let sellTokenId = sellToken.toHexString(); + let buyTokenId = buyToken.toHexString(); + + assert.fieldEquals("Token", sellTokenId, "name", "Token 1"); + assert.fieldEquals("Token", sellTokenId, "symbol", "TK1"); + assert.fieldEquals("Token", sellTokenId, "decimals", "18"); + assert.fieldEquals("Token", sellTokenId, "totalVolume", "100"); + assert.fieldEquals("Token", sellTokenId, "priceEth", "1000000000000"); + assert.fieldEquals("Token", sellTokenId, "priceUsd", "1000000000000"); + assert.fieldEquals("Token", sellTokenId, "numberOfTrades", "1"); + assert.fieldEquals("Token", sellTokenId, "totalVolumeEth", "0.0001"); + assert.fieldEquals("Token", sellTokenId, "totalVolumeUsd", "0.0001"); + + assert.fieldEquals("Token", buyTokenId, "name", "Token 2"); + assert.fieldEquals("Token", buyTokenId, "symbol", "TK2"); + assert.fieldEquals("Token", buyTokenId, "decimals", "18"); + assert.fieldEquals("Token", buyTokenId, "totalVolume", "50"); + assert.fieldEquals("Token", buyTokenId, "priceEth", "1000000000000"); + assert.fieldEquals("Token", buyTokenId, "priceUsd", "1000000000000"); + assert.fieldEquals("Token", buyTokenId, "numberOfTrades", "1"); + assert.fieldEquals("Token", buyTokenId, "totalVolumeEth", "0.00005"); + assert.fieldEquals("Token", buyTokenId, "totalVolumeUsd", "0.00005"); + + // TODO: Check entity + + // TODO: Check entity + + let tradeId = + orderUid.toHexString() + + "|" + + event.transaction.hash.toHexString() + + "|" + + event.transaction.index.toString(); + + assert.fieldEquals( + "Trade", + tradeId, + "buyToken", + buyToken.toHexString() + ); + assert.fieldEquals("Trade", tradeId, "buyAmount", "50"); + assert.fieldEquals( + "Trade", + tradeId, + "sellToken", + sellToken.toHexString() + ); + assert.fieldEquals("Trade", tradeId, "sellAmount", "100"); + assert.fieldEquals("Trade", tradeId, "order", orderUid.toHexString()); + assert.fieldEquals( + "Trade", + tradeId, + "gasPrice", + event.transaction.gasPrice.toString() + ); + assert.fieldEquals("Trade", tradeId, "feeAmount", feeAmount.toString()); + assert.fieldEquals("Trade", tradeId, "feeAmountUsd", "0.000005"); + assert.fieldEquals("Trade", tradeId, "feeAmountEth", "0.000005"); + assert.fieldEquals("Trade", tradeId, "buyAmountEth", "0.00005"); + assert.fieldEquals("Trade", tradeId, "sellAmountEth", "0.0001"); + assert.fieldEquals("Trade", tradeId, "buyAmountUsd", "0.00005"); + assert.fieldEquals("Trade", tradeId, "sellAmountUsd", "0.0001"); + assert.fieldEquals( + "Trade", + tradeId, + "timestamp", + event.block.timestamp.toString() + ); + assert.fieldEquals( + "Trade", + tradeId, + "txHash", + event.transaction.hash.toHexString() + ); + assert.fieldEquals( + "Trade", + tradeId, + "settlement", + event.transaction.hash.toHexString() + ); + + // TODO: Check entity + + // TODO: Check entity + + // TODO: Check entity + + // TODO: Check entity + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/gpv2settlement/utils.ts b/tests/gpv2settlement/utils.ts new file mode 100644 index 0000000..8bf632a --- /dev/null +++ b/tests/gpv2settlement/utils.ts @@ -0,0 +1,187 @@ +import { Address, ethereum, BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { createMockedFunction, newMockEvent } from "matchstick-as"; +import { Trade as TradeEvent } from "../../generated/GPV2Settlement/GPV2Settlement"; + +export function createTradeEvent( + owner: Address, + sellToken: Address, + buyToken: Address, + sellAmount: BigInt, + buyAmount: BigInt, + feeAmount: BigInt, + orderUid: Bytes +): TradeEvent { + let mockEvent = newMockEvent(); + + let event = new TradeEvent( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + null + ); + + event.parameters = new Array(); + + let ownerParam = new ethereum.EventParam( + "owner", + ethereum.Value.fromAddress(owner) + ); + let sellTokenParam = new ethereum.EventParam( + "sellToken", + ethereum.Value.fromAddress(sellToken) + ); + let buyTokenParam = new ethereum.EventParam( + "buyToken", + ethereum.Value.fromAddress(buyToken) + ); + let sellAmountParam = new ethereum.EventParam( + "sellAmount", + ethereum.Value.fromSignedBigInt(sellAmount) + ); + let buyAmountParam = new ethereum.EventParam( + "buyAmount", + ethereum.Value.fromSignedBigInt(buyAmount) + ); + let feeAmountParam = new ethereum.EventParam( + "feeAmount", + ethereum.Value.fromSignedBigInt(feeAmount) + ); + let orderUidParam = new ethereum.EventParam( + "orderUid", + ethereum.Value.fromBytes(orderUid) + ); + + event.parameters.push(ownerParam); + event.parameters.push(sellTokenParam); + event.parameters.push(buyTokenParam); + event.parameters.push(sellAmountParam); + event.parameters.push(buyAmountParam); + event.parameters.push(feeAmountParam); + event.parameters.push(orderUidParam); + + return event; +} + +function mockContractFunction( + address: Address, + name: string, + signature: string, + result: ethereum.Value[], + args: ethereum.Value[] +): void { + createMockedFunction(address, name, signature) + .withArgs(args) + .returns(result); +} + +function mockErc20Decimals(address: Address, value: BigInt): void { + mockContractFunction( + address, + "decimals", + "decimals():(uint8)", + [ethereum.Value.fromUnsignedBigInt(value)], + [] + ); +} + +function mockErc20Name(address: Address, value: string): void { + mockContractFunction( + address, + "name", + "name():(string)", + [ethereum.Value.fromString(value)], + [] + ); +} + +function mockErc20Symbol(address: Address, value: string): void { + mockContractFunction( + address, + "symbol", + "symbol():(string)", + [ethereum.Value.fromString(value)], + [] + ); +} + +export function mockErc20( + address: Address, + name: string, + symbol: string, + decimals: BigInt +): void { + mockErc20Name(address, name); + mockErc20Symbol(address, symbol); + mockErc20Decimals(address, decimals); +} + +export function mockUniswapFactoryGetPair( + contractAddress: Address, + functionArgs: Address[], + functionResult: Address +): void { + mockContractFunction( + contractAddress, + "getPair", + "getPair(address,address):(address)", + [ethereum.Value.fromAddress(functionResult)], + [ + ethereum.Value.fromAddress(functionArgs[0]), + ethereum.Value.fromAddress(functionArgs[1]), + ] + ); +} + +function mockUniswapV2PairGetReserves( + address: Address, + one: BigInt, + two: BigInt, + three: BigInt +): void { + mockContractFunction( + address, + "getReserves", + "getReserves():(uint112,uint112,uint32)", + [ + ethereum.Value.fromUnsignedBigInt(one), + ethereum.Value.fromUnsignedBigInt(two), + ethereum.Value.fromUnsignedBigInt(three), + ], + [] + ); +} + +function mockUniswapV2PairToken0(address: Address, value: Address): void { + mockContractFunction( + address, + "token0", + "token0():(address)", + [ethereum.Value.fromAddress(value)], + [] + ); +} + +function mockUniswapV2PairToken1(address: Address, value: Address): void { + mockContractFunction( + address, + "token1", + "token1():(address)", + [ethereum.Value.fromAddress(value)], + [] + ); +} + +export function mockUniswapV2Pair( + address: Address, + reserves: BigInt[], + token0: Address, + token1: Address +): void { + mockUniswapV2PairGetReserves(address, reserves[0], reserves[1], reserves[2]); + mockUniswapV2PairToken0(address, token0); + mockUniswapV2PairToken1(address, token1); +} \ No newline at end of file diff --git a/tests/mainnet/uniswapMappings/uniswapPools.test.ts b/tests/mainnet/uniswapMappings/uniswapPools.test.ts new file mode 100644 index 0000000..2435477 --- /dev/null +++ b/tests/mainnet/uniswapMappings/uniswapPools.test.ts @@ -0,0 +1,436 @@ +import { ethereum, BigInt, BigDecimal, Address } from "@graphprotocol/graph-ts"; +import { + describe, + test, + assert, + beforeEach, + afterAll, + clearStore, + newMockEvent, +} from "matchstick-as"; +import { UniswapPool, Bundle } from "../../../generated/schema"; +import { + Initialize as InitializeEvent, + Mint as MintEvent, +} from "../../../generated/templates/Pool/Pool"; +import { + handleInitialize, + handleMint, +} from "../../../src/uniswapMappings/uniswapPools"; +import { buildUniswapPool, buildUniswapToken } from "../utils"; + +let pool: UniswapPool; +let poolId: string = "0xc6a872b86fac61e67c340921f100d366e80244f9"; +let bundle: Bundle; +let tickLower: i32; +let tickUpper: i32; +let amount: BigInt; +let amount0: BigInt; +let amount1: BigInt; + +function createEvent(tick: i32, sqrtPriceX96: BigInt): InitializeEvent { + let mockEvent = newMockEvent(); + + let event = new InitializeEvent( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + null + ); + + event.parameters = new Array(); + + let tickParam = new ethereum.EventParam("tick", ethereum.Value.fromI32(tick)); + let sqrtPriceX96Param = new ethereum.EventParam( + "sqrtPriceX96", + ethereum.Value.fromUnsignedBigInt(sqrtPriceX96) + ); + + event.parameters.push(sqrtPriceX96Param); + event.parameters.push(tickParam); + + return event; +} + +function createMintEvent( + sender: Address, + owner: Address, + tickLower: i32, + tickUpper: i32, + amount: BigInt, + amount0: BigInt, + amount1: BigInt +): MintEvent { + let mockEvent = newMockEvent(); + + let event = new MintEvent( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + null + ); + + event.parameters = new Array(); + + let senderParam = new ethereum.EventParam( + "sender", + ethereum.Value.fromAddress(sender) + ); + let ownerParam = new ethereum.EventParam( + "owner", + ethereum.Value.fromAddress(owner) + ); + let lowerParam = new ethereum.EventParam( + "tickLower", + ethereum.Value.fromI32(tickLower) + ); + let upperParam = new ethereum.EventParam( + "tickUpper", + ethereum.Value.fromI32(tickUpper) + ); + let amountParam = new ethereum.EventParam( + "amount", + ethereum.Value.fromSignedBigInt(amount) + ); + let amount0Param = new ethereum.EventParam( + "amount0", + ethereum.Value.fromSignedBigInt(amount0) + ); + let amount1Param = new ethereum.EventParam( + "amount1", + ethereum.Value.fromSignedBigInt(amount1) + ); + + event.parameters.push(senderParam); + event.parameters.push(ownerParam); + event.parameters.push(lowerParam); + event.parameters.push(upperParam); + event.parameters.push(amountParam); + event.parameters.push(amount0Param); + event.parameters.push(amount1Param); + + return event; +} + +describe("uniswapPools", () => { + describe("handleInitialize", () => { + afterAll(() => { + clearStore(); + }); + + describe("when pool exist", () => { + beforeEach(() => { + let token0 = buildUniswapToken( + "token0", + Address.fromString("0x0000000000000000000000000000000000000001"), + "token0", + "tk0", + 8, + ["p1"] + ); + token0.save(); + + let token1 = buildUniswapToken( + "token1", + Address.fromString("0x0000000000000000000000000000000000000002"), + "token1", + "tk1", + 8, + ["p1"] + ); + token1.save(); + + pool = buildUniswapPool( + poolId, + BigInt.fromI32(1), + token0.id, + token1.id, + 100.5, + 50.5, + 100, + 50.5, + 44.5 + ); + pool.save(); + }); + + describe("and Bundle exist", () => { + beforeEach(() => { + bundle = new Bundle("1"); + bundle.ethPriceUSD = BigDecimal.fromString("10"); + bundle.save(); + }); + + test("updates pool.tick, bundle.ethPriceUSD, token.priceEth and token.priceUsd", () => { + let event = createEvent(10, BigInt.fromI32(100)); + event.address = Address.fromString(poolId); + + handleInitialize(event); + + assert.fieldEquals("UniswapPool", poolId, "tick", "10"); + assert.fieldEquals("Bundle", "1", "ethPriceUSD", "0"); + assert.fieldEquals("UniswapToken", "token0", "priceEth", "0"); + assert.fieldEquals("UniswapToken", "token1", "priceEth", "0"); + assert.fieldEquals("UniswapToken", "token0", "priceUsd", "0"); + assert.fieldEquals("UniswapToken", "token1", "priceUsd", "0"); + }); + }); + }); + + describe("when pool does not exist", () => { + describe("and bundle exist", () => { + beforeEach(() => { + bundle = new Bundle("1"); + bundle.ethPriceUSD = BigDecimal.fromString("10"); + bundle.save(); + }); + + test("updats bundle.ethPriceUSD", () => { + let event = createEvent(10, BigInt.fromI32(100)); + + handleInitialize(event); + + assert.fieldEquals("Bundle", "1", "ethPriceUSD", "0"); + }); + }); + }); + }); + + describe("handleMint", () => { + afterAll(() => { + clearStore(); + }); + + describe("when pool exist", () => { + beforeEach(() => { + let token0 = buildUniswapToken( + "token0", + Address.fromString("0x0000000000000000000000000000000000000001"), + "token0", + "tk0", + 8, + ["p1"] + ); + token0.save(); + + let token1 = buildUniswapToken( + "token1", + Address.fromString("0x0000000000000000000000000000000000000002"), + "token1", + "tk1", + 8, + ["p1"] + ); + token1.save(); + + pool = buildUniswapPool( + poolId, + null, + token0.id, + token1.id, + 100.5, + 50.5, + 100, + 50.5, + 44.5 + ); + pool.save(); + }); + + test("updates totalValueLockedToken0 and totalValueLockedToken1", () => { + amount = BigInt.fromI32(100); + amount0 = BigInt.fromString("5000000000"); + amount1 = BigInt.fromString("4000000000"); + + let mintEvent = createMintEvent( + Address.fromString("0x1000000000000000000000000000000000000000"), + Address.fromString("0x2000000000000000000000000000000000000000"), + null, + null, + amount, + amount0, + amount1 + ); + + mintEvent.address = Address.fromString(poolId); + + handleMint(mintEvent); + + assert.fieldEquals( + "UniswapPool", + poolId, + "totalValueLockedToken0", + pool.totalValueLockedToken0 + .plus(BigDecimal.fromString("50")) + .toString() + ); + assert.fieldEquals( + "UniswapPool", + poolId, + "totalValueLockedToken1", + pool.totalValueLockedToken1 + .plus(BigDecimal.fromString("40")) + .toString() + ); + }); + + describe("and pool tick is not null", () => { + beforeEach(() => { + pool.tick = BigInt.fromI32(5); + pool.save(); + }); + + describe("and tickLower is lower than pool tick", () => { + beforeEach(() => { + tickLower = 4; + }); + + describe("and tickUpper is greater than pool tick", () => { + beforeEach(() => { + tickUpper = 15; + }); + + test("updates pool liquidity", () => { + amount = BigInt.fromI32(100); + amount0 = BigInt.fromI32(50); + amount1 = BigInt.fromI32(40); + + let mintEvent = createMintEvent( + Address.fromString( + "0x1000000000000000000000000000000000000000" + ), + Address.fromString( + "0x2000000000000000000000000000000000000000" + ), + tickLower, + tickUpper, + amount, + amount0, + amount1 + ); + mintEvent.address = Address.fromString(poolId); + + handleMint(mintEvent); + + assert.fieldEquals( + "UniswapPool", + poolId, + "liquidity", + pool.liquidity.plus(amount).toString() + ); + }); + }); + + describe("and tickUpper is lower than pool tick", () => { + beforeEach(() => { + tickUpper = 4; + }); + + test("does not update pool liquidity", () => { + amount = BigInt.fromI32(100); + amount0 = BigInt.fromI32(50); + amount1 = BigInt.fromI32(40); + + let mintEvent = createMintEvent( + Address.fromString( + "0x1000000000000000000000000000000000000000" + ), + Address.fromString( + "0x2000000000000000000000000000000000000000" + ), + tickLower, + tickUpper, + amount, + amount0, + amount1 + ); + mintEvent.address = Address.fromString(poolId); + + handleMint(mintEvent); + + assert.fieldEquals( + "UniswapPool", + poolId, + "liquidity", + pool.liquidity.toString() + ); + }); + }); + }); + + describe("and tickLower is greater than pool tick", () => { + beforeEach(() => { + tickLower = 20; + }); + + test("does not update liquidity", () => { + amount = BigInt.fromI32(100); + amount0 = BigInt.fromI32(50); + amount1 = BigInt.fromI32(40); + + let mintEvent = createMintEvent( + Address.fromString("0x1000000000000000000000000000000000000000"), + Address.fromString("0x2000000000000000000000000000000000000000"), + tickLower, + tickUpper, + amount, + amount0, + amount1 + ); + mintEvent.address = Address.fromString(poolId); + + handleMint(mintEvent); + + assert.fieldEquals( + "UniswapPool", + poolId, + "liquidity", + pool.liquidity.toString() + ); + }); + }); + }); + + describe("and pool tick is null", () => { + beforeEach(() => { + pool.tick = null; + pool.save(); + }); + + test("does not update pool liquidity", () => { + amount = BigInt.fromI32(100); + amount0 = BigInt.fromString("5000000000"); + amount1 = BigInt.fromString("4000000000"); + + let mintEvent = createMintEvent( + Address.fromString("0x1000000000000000000000000000000000000000"), + Address.fromString("0x2000000000000000000000000000000000000000"), + null, + null, + amount, + amount0, + amount1 + ); + mintEvent.address = Address.fromString(poolId); + + handleMint(mintEvent); + + assert.fieldEquals( + "UniswapPool", + poolId, + "liquidity", + pool.liquidity.toString() + ); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/mainnet/utils.ts b/tests/mainnet/utils.ts new file mode 100644 index 0000000..f91f30f --- /dev/null +++ b/tests/mainnet/utils.ts @@ -0,0 +1,47 @@ +import { Address, BigInt, BigDecimal } from "@graphprotocol/graph-ts"; +import { UniswapPool, UniswapToken } from "../../generated/schema"; + +export function buildUniswapToken( + id: string, + address: Address, + name: string, + symbol: string, + decimals: i32, + allowedPools: string[] +): UniswapToken { + let token = new UniswapToken(id); + token.address = address; + token.name = name; + token.symbol = symbol; + token.decimals = decimals; + token.allowedPools = allowedPools; + + return token; +} + +export function buildUniswapPool( + id: string, + tick: BigInt | null, + token0: string, + token1: string, + token0Price: number, + token1Price: number, + liquidity: i32, + totalValueLockedToken0: number, + totalValueLockedToken1: number +): UniswapPool { + let pool = new UniswapPool(id); + pool.tick = tick; + pool.token0 = token0; + pool.token1 = token1; + pool.token0Price = BigDecimal.fromString(token0Price.toString()); + pool.token1Price = BigDecimal.fromString(token1Price.toString()); + pool.liquidity = BigInt.fromI32(liquidity); + pool.totalValueLockedToken0 = BigDecimal.fromString( + totalValueLockedToken0.toString() + ); + pool.totalValueLockedToken1 = BigDecimal.fromString( + totalValueLockedToken1.toString() + ); + return pool; +} diff --git a/yarn.lock b/yarn.lock index 7442607..7768626 100644 --- a/yarn.lock +++ b/yarn.lock @@ -467,6 +467,15 @@ assemblyscript@0.19.10: binaryen "101.0.0-nightly.20210723" long "^4.0.0" +assemblyscript@^0.19.20: + version "0.19.23" + resolved "https://registry.yarnpkg.com/assemblyscript/-/assemblyscript-0.19.23.tgz#16ece69f7f302161e2e736a0f6a474e6db72134c" + integrity sha512-fwOQNZVTMga5KRsfY80g7cpOl4PsFQczMwHzdtgoqLXaYhkhavufKb0sB0l3T1DUxpAufA0KNhlbpuuhZUwxMA== + dependencies: + binaryen "102.0.0-nightly.20211028" + long "^5.2.0" + source-map-support "^0.5.20" + assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" @@ -554,6 +563,11 @@ binaryen@101.0.0-nightly.20210723: resolved "https://registry.yarnpkg.com/binaryen/-/binaryen-101.0.0-nightly.20210723.tgz#b6bb7f3501341727681a03866c0856500eec3740" integrity sha512-eioJNqhHlkguVSbblHOtLqlhtC882SOEPKmNFZaDuz1hzQjolxZ+eu3/kaS10n3sGPONsIZsO7R9fR00UyhEUA== +binaryen@102.0.0-nightly.20211028: + version "102.0.0-nightly.20211028" + resolved "https://registry.yarnpkg.com/binaryen/-/binaryen-102.0.0-nightly.20211028.tgz#8f1efb0920afd34509e342e37f84313ec936afb2" + integrity sha512-GCJBVB5exbxzzvyt8MGDv/MeUjs6gkXDvf4xOIItRBptYl0Tz5sm1o/uG95YK0L0VeG5ajDu3hRtkBP2kzqC5w== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -2141,6 +2155,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.0.tgz#2696dadf4b4da2ce3f6f6b89186085d94d52fd61" + integrity sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w== + looper@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/looper/-/looper-3.0.0.tgz#2efa54c3b1cbaba9b94aee2e5914b0be57fbb749" @@ -2174,6 +2193,15 @@ mafmt@^7.0.0: dependencies: multiaddr "^7.3.0" +matchstick-as@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/matchstick-as/-/matchstick-as-0.5.0.tgz#cdafc1ef49d670b9cbe98e933bc2a5cb7c450aeb" + integrity sha512-4K619YDH+so129qt4RB4JCNxaFwJJYLXPc7drpG+/mIj86Cfzg6FKs/bA91cnajmS1CLHdhHl9vt6Kd6Oqvfkg== + dependencies: + "@graphprotocol/graph-ts" "^0.27.0" + assemblyscript "^0.19.20" + wabt "1.0.24" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -2993,6 +3021,19 @@ signed-varint@^2.0.1: dependencies: varint "~5.0.0" +source-map-support@^0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + split-ca@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" @@ -3322,6 +3363,11 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +wabt@1.0.24: + version "1.0.24" + resolved "https://registry.yarnpkg.com/wabt/-/wabt-1.0.24.tgz#c02e0b5b4503b94feaf4a30a426ef01c1bea7c6c" + integrity sha512-8l7sIOd3i5GWfTWciPL0+ff/FK/deVK2Q6FN+MPz4vfUcD78i2M/49XJTwF6aml91uIiuXJEsLKWMB2cw/mtKg== + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"