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