Skip to content

Commit

Permalink
add subgraph unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mzywang committed May 20, 2024
1 parent b37b491 commit 92093b5
Show file tree
Hide file tree
Showing 10 changed files with 480 additions and 68 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ build/
node_modules/
src/types/
.DS_STORE
yarn-error.log
yarn-error.log
tests/.bin/
tests/.docker/
tests/.latest.json
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# copied from https://github.com/LimeChain/demo-subgraph/blob/main/Dockerfile

FROM --platform=linux/x86_64 ubuntu:22.04

ARG DEBIAN_FRONTEND=noninteractive

ENV ARGS=""

RUN apt update \
&& apt install -y sudo curl postgresql postgresql-contrib

RUN curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - \
&& sudo apt-get install -y nodejs

RUN curl -OL https://github.com/LimeChain/matchstick/releases/download/0.6.0/binary-linux-22 \
&& chmod a+x binary-linux-22

RUN mkdir matchstick
WORKDIR /matchstick

CMD ../binary-linux-22 ${ARGS}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"scripts": {
"lint": "eslint . --ext .ts --fix",
"build": "run-s codegen && graph build",
"build:docker": "docker build -t matchstick .",
"buildonly": "graph build",
"deploy:alchemy": "graph deploy --node https://subgraphs.alchemy.com/api/subgraphs/deploy --ipfs https://ipfs.satsuma.xyz",
"codegen": "graph codegen --output-dir src/types/",
"test": "graph test -d",
"create-local": "graph create ianlapham/uniswap-v3 --node http://127.0.0.1:8020",
"deploy-local": "graph deploy ianlapham/uniswap-v3 --debug --ipfs http://localhost:5001 --node http://127.0.0.1:8020",
"deploy": "graph deploy ianlapham/uniswap-v3-subgraph --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/ --debug",
Expand All @@ -24,6 +26,7 @@
"@uniswap/eslint-config": "^1.2.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^6.1.0",
"matchstick-as": "^0.6.0",
"npm-run-all": "^4.1.5",
"prettier": "^1.18.2",
"typescript": "^3.5.2"
Expand Down
15 changes: 13 additions & 2 deletions src/mappings/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@ import { fetchTokenDecimals, fetchTokenName, fetchTokenSymbol, fetchTokenTotalSu
import { ADDRESS_ZERO, FACTORY_ADDRESS, ONE_BI, ZERO_BD, ZERO_BI } from './../utils/constants'
import { WHITELIST_TOKENS } from './../utils/pricing'

// The subgraph handler must have this signature to be able to handle events,
// however, we invoke a helper in order to inject dependencies for unit tests.
export function handlePoolCreated(event: PoolCreated): void {
handlePoolCreatedHelper(event)
}

// Exported for unit tests
export function handlePoolCreatedHelper(
event: PoolCreated,
factoryAddress: string = FACTORY_ADDRESS,
whitelistTokens: string[] = WHITELIST_TOKENS
): void {
// temp fix
if (event.params.pool == Address.fromHexString('0x8fe8d9bb8eeba3ed688069c3d6b556c9ca258248')) {
return
Expand Down Expand Up @@ -97,12 +108,12 @@ export function handlePoolCreated(event: PoolCreated): void {
}

// update white listed pools
if (WHITELIST_TOKENS.includes(token0.id)) {
if (whitelistTokens.includes(token0.id)) {
const newPools = token1.whitelistPools
newPools.push(pool.id)
token1.whitelistPools = newPools
}
if (WHITELIST_TOKENS.includes(token1.id)) {
if (whitelistTokens.includes(token1.id)) {
const newPools = token0.whitelistPools
newPools.push(pool.id)
token0.whitelistPools = newPools
Expand Down
4 changes: 3 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ export function equalToZero(value: BigDecimal): boolean {
return false
}

export const NULL_ETH_HEX_STRING = '0x0000000000000000000000000000000000000000000000000000000000000001'

export function isNullEthValue(value: string): boolean {
return value == '0x0000000000000000000000000000000000000000000000000000000000000001'
return value == NULL_ETH_HEX_STRING
}

export function bigDecimalExp18(): BigDecimal {
Expand Down
111 changes: 54 additions & 57 deletions src/utils/staticTokenDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,66 @@
import { Address, BigInt } from '@graphprotocol/graph-ts'
import { Address, BigInt, log } from '@graphprotocol/graph-ts'

// Initialize a Token Definition with the attributes
export class StaticTokenDefinition {
address: Address
symbol: string
name: string
decimals: BigInt
}

// Get all tokens with a static defintion
static getStaticDefinitions(): Array<StaticTokenDefinition> {
const staticDefinitions: Array<StaticTokenDefinition> = [
{
address: Address.fromString('0xe0b7927c4af23765cb51314a0e0521a9645f0e2a'),
symbol: 'DGD',
name: 'DGD',
decimals: BigInt.fromI32(9),
},
{
address: Address.fromString('0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9'),
symbol: 'AAVE',
name: 'Aave Token',
decimals: BigInt.fromI32(18),
},
{
address: Address.fromString('0xeb9951021698b42e4399f9cbb6267aa35f82d59d'),
symbol: 'LIF',
name: 'Lif',
decimals: BigInt.fromI32(18),
},
{
address: Address.fromString('0xbdeb4b83251fb146687fa19d1c660f99411eefe3'),
symbol: 'SVD',
name: 'savedroid',
decimals: BigInt.fromI32(18),
},
{
address: Address.fromString('0xbb9bc244d798123fde783fcc1c72d3bb8c189413'),
symbol: 'TheDAO',
name: 'TheDAO',
decimals: BigInt.fromI32(16),
},
{
address: Address.fromString('0x38c6a68304cdefb9bec48bbfaaba5c5b47818bb2'),
symbol: 'HPB',
name: 'HPBCoin',
decimals: BigInt.fromI32(18),
},
]
return staticDefinitions
}

// Helper for hardcoded tokens
static fromAddress(tokenAddress: Address): StaticTokenDefinition | null {
const staticDefinitions = this.getStaticDefinitions()
const tokenAddressHex = tokenAddress.toHexString()
export const getStaticDefinition = (
tokenAddress: Address,
staticDefinitions: Array<StaticTokenDefinition>
): StaticTokenDefinition | null => {
const tokenAddressHex = tokenAddress.toHexString()

// Search the definition using the address
for (let i = 0; i < staticDefinitions.length; i++) {
const staticDefinition = staticDefinitions[i]
if (staticDefinition.address.toHexString() == tokenAddressHex) {
return staticDefinition
}
// Search the definition using the address
for (let i = 0; i < staticDefinitions.length; i++) {
const staticDefinition = staticDefinitions[i]
if (staticDefinition.address.toHexString() == tokenAddressHex) {
return staticDefinition
}

// If not found, return null
return null
}

// If not found, return null
return null
}

export const STATIC_TOKEN_DEFINITIONS: Array<StaticTokenDefinition> = [
{
address: Address.fromString('0xe0b7927c4af23765cb51314a0e0521a9645f0e2a'),
symbol: 'DGD',
name: 'DGD',
decimals: BigInt.fromI32(9)
},
{
address: Address.fromString('0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9'),
symbol: 'AAVE',
name: 'Aave Token',
decimals: BigInt.fromI32(18)
},
{
address: Address.fromString('0xeb9951021698b42e4399f9cbb6267aa35f82d59d'),
symbol: 'LIF',
name: 'Lif',
decimals: BigInt.fromI32(18)
},
{
address: Address.fromString('0xbdeb4b83251fb146687fa19d1c660f99411eefe3'),
symbol: 'SVD',
name: 'savedroid',
decimals: BigInt.fromI32(18)
},
{
address: Address.fromString('0xbb9bc244d798123fde783fcc1c72d3bb8c189413'),
symbol: 'TheDAO',
name: 'TheDAO',
decimals: BigInt.fromI32(16)
},
{
address: Address.fromString('0x38c6a68304cdefb9bec48bbfaaba5c5b47818bb2'),
symbol: 'HPB',
name: 'HPBCoin',
decimals: BigInt.fromI32(18)
}
]
24 changes: 17 additions & 7 deletions src/utils/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { ERC20 } from '../types/Factory/ERC20'
import { ERC20NameBytes } from '../types/Factory/ERC20NameBytes'
import { ERC20SymbolBytes } from '../types/Factory/ERC20SymbolBytes'
import { isNullEthValue } from '.'
import { StaticTokenDefinition } from './staticTokenDefinition'
import { StaticTokenDefinition, getStaticDefinition, STATIC_TOKEN_DEFINITIONS } from './staticTokenDefinition'
import { log } from 'matchstick-as'

export function fetchTokenSymbol(tokenAddress: Address): string {
export function fetchTokenSymbol(
tokenAddress: Address,
staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS
): string {
const contract = ERC20.bind(tokenAddress)
const contractSymbolBytes = ERC20SymbolBytes.bind(tokenAddress)

Expand All @@ -21,7 +25,7 @@ export function fetchTokenSymbol(tokenAddress: Address): string {
symbolValue = symbolResultBytes.value.toString()
} else {
// try with the static definition
const staticTokenDefinition = StaticTokenDefinition.fromAddress(tokenAddress)
const staticTokenDefinition = getStaticDefinition(tokenAddress, staticTokenDefinitions)
if (staticTokenDefinition != null) {
symbolValue = staticTokenDefinition.symbol
}
Expand All @@ -34,7 +38,10 @@ export function fetchTokenSymbol(tokenAddress: Address): string {
return symbolValue
}

export function fetchTokenName(tokenAddress: Address): string {
export function fetchTokenName(
tokenAddress: Address,
staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS
): string {
const contract = ERC20.bind(tokenAddress)
const contractNameBytes = ERC20NameBytes.bind(tokenAddress)

Expand All @@ -49,7 +56,7 @@ export function fetchTokenName(tokenAddress: Address): string {
nameValue = nameResultBytes.value.toString()
} else {
// try with the static definition
const staticTokenDefinition = StaticTokenDefinition.fromAddress(tokenAddress)
const staticTokenDefinition = getStaticDefinition(tokenAddress, staticTokenDefinitions)
if (staticTokenDefinition != null) {
nameValue = staticTokenDefinition.name
}
Expand All @@ -72,7 +79,10 @@ export function fetchTokenTotalSupply(tokenAddress: Address): BigInt {
return totalSupplyValue
}

export function fetchTokenDecimals(tokenAddress: Address): BigInt | null {
export function fetchTokenDecimals(
tokenAddress: Address,
staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS
): BigInt | null {
const contract = ERC20.bind(tokenAddress)
// try types uint8 for decimals
const decimalResult = contract.try_decimals()
Expand All @@ -83,7 +93,7 @@ export function fetchTokenDecimals(tokenAddress: Address): BigInt | null {
}
} else {
// try with the static definition
const staticTokenDefinition = StaticTokenDefinition.fromAddress(tokenAddress)
const staticTokenDefinition = getStaticDefinition(tokenAddress, staticTokenDefinitions)
if (staticTokenDefinition) {
return staticTokenDefinition.decimals
}
Expand Down
96 changes: 96 additions & 0 deletions tests/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Address, BigInt, ethereum } from '@graphprotocol/graph-ts'
import { assert, createMockedFunction, newMockEvent } from 'matchstick-as'
import { PoolCreated } from '../src/types/Factory/Factory'
import { handlePoolCreatedHelper } from '../src/mappings/factory'
import { Factory } from '../src/types/schema'
import { FACTORY_ADDRESS } from '../src/utils/constants'

const USDC_MAINNET_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
const WETH_MAINNET_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
export const USDC_WETH_03_MAINNET_POOL = '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8'
export const POOL_FEE_TIER_03 = 3000
export const POOL_TICK_SPACING_03 = 60

export class TokenFixture {
address: string
symbol: string
name: string
totalSupply: string
decimals: string
}

export const USDC_MAINNET_FIXTURE: TokenFixture = {
address: USDC_MAINNET_ADDRESS,
symbol: 'USDC',
name: 'USD Coin',
totalSupply: '300',
decimals: '6'
}

export const WETH_MAINNET_FIXTURE: TokenFixture = {
address: WETH_MAINNET_ADDRESS,
symbol: 'WETH',
name: 'Wrapped Ether',
totalSupply: '100',
decimals: '18'
}

export const MOCK_EVENT = newMockEvent()

export const createTestPool = (
mockEvent: ethereum.Event,
factoryAddress: string,
token0: TokenFixture,
token1: TokenFixture,
poolAddressHexString: string,
feeTier: number,
tickSpacing: number
): void => {
const mockEvent = newMockEvent()
const token0Address = Address.fromString(token0.address)
const token1Address = Address.fromString(token1.address)
const poolAddress = Address.fromString(poolAddressHexString)
const parameters = [
new ethereum.EventParam('token0', ethereum.Value.fromAddress(token0Address)),
new ethereum.EventParam('token1', ethereum.Value.fromAddress(token1Address)),
new ethereum.EventParam('fee', ethereum.Value.fromI32(feeTier as i32)),
new ethereum.EventParam('tickSpacing', ethereum.Value.fromI32(tickSpacing as i32)),
new ethereum.EventParam('pool', ethereum.Value.fromAddress(poolAddress))
]
const poolCreatedEvent = new PoolCreated(
mockEvent.address,
mockEvent.logIndex,
mockEvent.transactionLogIndex,
mockEvent.logType,
mockEvent.block,
mockEvent.transaction,
parameters,
mockEvent.receipt
)
// create mock contract calls for token0
createMockedFunction(token0Address, 'symbol', 'symbol():(string)').returns([ethereum.Value.fromString(token0.symbol)])
createMockedFunction(token0Address, 'name', 'name():(string)').returns([ethereum.Value.fromString(token0.name)])
createMockedFunction(token0Address, 'totalSupply', 'totalSupply():(uint256)').returns([
ethereum.Value.fromUnsignedBigInt(BigInt.fromString(token0.totalSupply))
])
createMockedFunction(token0Address, 'decimals', 'decimals():(uint32)').returns([
ethereum.Value.fromUnsignedBigInt(BigInt.fromString(token0.decimals))
])
// create mock contract calls for token1
createMockedFunction(token1Address, 'symbol', 'symbol():(string)').returns([ethereum.Value.fromString(token1.symbol)])
createMockedFunction(token1Address, 'name', 'name():(string)').returns([ethereum.Value.fromString(token1.name)])
createMockedFunction(token1Address, 'totalSupply', 'totalSupply():(uint256)').returns([
ethereum.Value.fromUnsignedBigInt(BigInt.fromString(token1.totalSupply))
])
createMockedFunction(token1Address, 'decimals', 'decimals():(uint32)').returns([
ethereum.Value.fromUnsignedBigInt(BigInt.fromString(token1.decimals))
])
handlePoolCreatedHelper(poolCreatedEvent, factoryAddress, [token0.address, token1.address])
}

// Typescript for Subgraphs do not support Record types so we use a 2D string array to represent the object instead.
export const assertObjectMatches = (entityType: string, id: string, obj: string[][]): void => {
for (let i = 0; i < obj.length; i++) {
assert.fieldEquals(entityType, id, obj[i][0], obj[i][1])
}
}
Loading

0 comments on commit 92093b5

Please sign in to comment.