From 2b520a9b01d160b86f1d8738f97c54bb58c22021 Mon Sep 17 00:00:00 2001 From: Michael Wang <44713145+mzywang@users.noreply.github.com> Date: Mon, 20 May 2024 13:08:47 -0400 Subject: [PATCH] add subgraph unit tests --- .eslintrc.js | 15 +- .gitignore | 5 +- Dockerfile | 21 +++ README.md | 9 +- package.json | 3 + src/mappings/factory.ts | 19 ++- src/utils/index.ts | 4 +- src/utils/staticTokenDefinition.ts | 109 ++++++------ src/utils/token.ts | 23 ++- tests/constants.ts | 95 +++++++++++ tests/handlePoolCreated.test.ts | 256 +++++++++++++++++++++++++++++ yarn.lock | 12 ++ 12 files changed, 500 insertions(+), 71 deletions(-) create mode 100644 Dockerfile create mode 100644 tests/constants.ts create mode 100644 tests/handlePoolCreated.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 77bcca52..3d6e4080 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,5 +29,18 @@ module.exports = { } } ] - } + }, + overrides: [ + { + files: ['tests/**/*.ts'], + settings: { + jest: { + version: 26 + }, + // jest is added as a plugin in our org's eslint config, but we use + // matchstick, and this would crash when linting matchstick files. + 'disable/plugins': ['jest'] + } + } + ] } diff --git a/.gitignore b/.gitignore index 0da354ab..2d305763 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ build/ node_modules/ src/types/ .DS_STORE -yarn-error.log \ No newline at end of file +yarn-error.log +tests/.bin/ +tests/.docker/ +tests/.latest.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..824749cb --- /dev/null +++ b/Dockerfile @@ -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} diff --git a/README.md b/README.md index 3cde004d..e43a8fda 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ # Uniswap V3 Subgraph -### Subgraph Endpoint +### Subgraph Endpoint Synced at: https://thegraph.com/hosted-service/subgraph/ianlapham/uniswap-v3-subgraph?selected=playground Pending Changes at same URL + +### Running Unit Tests + +1. Install [Docker](https://docs.docker.com/get-docker/) if you don't have it already +2. Install postgres: `brew install postgresql` +3. `yarn run build:docker` +4. `yarn run test` diff --git a/package.json b/package.json index 14867ded..17e6b4d8 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" diff --git a/src/mappings/factory.ts b/src/mappings/factory.ts index 13cfb4ff..a26652d5 100644 --- a/src/mappings/factory.ts +++ b/src/mappings/factory.ts @@ -8,16 +8,27 @@ 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 } // load factory - let factory = Factory.load(FACTORY_ADDRESS) + let factory = Factory.load(factoryAddress) if (factory === null) { - factory = new Factory(FACTORY_ADDRESS) + factory = new Factory(factoryAddress) factory.poolCount = ZERO_BI factory.totalVolumeETH = ZERO_BD factory.totalVolumeUSD = ZERO_BD @@ -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 diff --git a/src/utils/index.ts b/src/utils/index.ts index 69017d27..c9ba3372 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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 { diff --git a/src/utils/staticTokenDefinition.ts b/src/utils/staticTokenDefinition.ts index e2662764..35bd3aca 100644 --- a/src/utils/staticTokenDefinition.ts +++ b/src/utils/staticTokenDefinition.ts @@ -6,64 +6,61 @@ export class StaticTokenDefinition { symbol: string name: string decimals: BigInt +} - // Get all tokens with a static defintion - static getStaticDefinitions(): Array { - const staticDefinitions: Array = [ - { - 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 | 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 = [ + { + 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), + }, +] diff --git a/src/utils/token.ts b/src/utils/token.ts index bacfdac8..15857b9f 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -4,9 +4,12 @@ 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 { getStaticDefinition, STATIC_TOKEN_DEFINITIONS, StaticTokenDefinition } from './staticTokenDefinition' -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) @@ -21,7 +24,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 } @@ -34,7 +37,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) @@ -49,7 +55,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 } @@ -72,7 +78,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() @@ -83,7 +92,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 } diff --git a/tests/constants.ts b/tests/constants.ts new file mode 100644 index 00000000..36b8c446 --- /dev/null +++ b/tests/constants.ts @@ -0,0 +1,95 @@ +import { Address, BigInt, ethereum } from '@graphprotocol/graph-ts' +import { assert, createMockedFunction, newMockEvent } from 'matchstick-as' + +import { handlePoolCreatedHelper } from '../src/mappings/factory' +import { PoolCreated } from '../src/types/Factory/Factory' + +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]) + } +} diff --git a/tests/handlePoolCreated.test.ts b/tests/handlePoolCreated.test.ts new file mode 100644 index 00000000..8c88f406 --- /dev/null +++ b/tests/handlePoolCreated.test.ts @@ -0,0 +1,256 @@ +import { Address, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' +import { assert, createMockedFunction, test } from 'matchstick-as/assembly/index' +import { describe, test } from 'matchstick-as/assembly/index' + +import { NULL_ETH_HEX_STRING } from '../src/utils' +import { FACTORY_ADDRESS } from '../src/utils/constants' +import { StaticTokenDefinition } from '../src/utils/staticTokenDefinition' +import { fetchTokenDecimals, fetchTokenName, fetchTokenSymbol, fetchTokenTotalSupply } from '../src/utils/token' +import { + assertObjectMatches, + createTestPool, + MOCK_EVENT, + POOL_FEE_TIER_03, + POOL_TICK_SPACING_03, + USDC_MAINNET_FIXTURE, + USDC_WETH_03_MAINNET_POOL, + WETH_MAINNET_FIXTURE, +} from './constants' + +describe('handlePoolCreated', () => { + test('success - create a pool', () => { + assert.notInStore('Factory', FACTORY_ADDRESS) + assert.notInStore('Pool', USDC_WETH_03_MAINNET_POOL) + assert.notInStore('Token', USDC_MAINNET_FIXTURE.address) + assert.notInStore('Token', USDC_MAINNET_FIXTURE.address) + + createTestPool( + MOCK_EVENT, + FACTORY_ADDRESS, + USDC_MAINNET_FIXTURE, + WETH_MAINNET_FIXTURE, + USDC_WETH_03_MAINNET_POOL, + POOL_FEE_TIER_03, + POOL_TICK_SPACING_03 + ) + + assertObjectMatches('Factory', FACTORY_ADDRESS, [ + ['poolCount', '1'], + ['totalVolumeETH', '0'], + ['totalVolumeUSD', '0'], + ['untrackedVolumeUSD', '0'], + ['totalFeesUSD', '0'], + ['totalFeesETH', '0'], + ['totalValueLockedETH', '0'], + ['totalValueLockedUSD', '0'], + ['totalValueLockedETHUntracked', '0'], + ['totalValueLockedUSDUntracked', '0'], + ]) + + assertObjectMatches('Bundle', '1', [['ethPriceUSD', '0']]) + + assertObjectMatches('Token', USDC_MAINNET_FIXTURE.address, [ + ['symbol', USDC_MAINNET_FIXTURE.symbol], + ['name', USDC_MAINNET_FIXTURE.name], + ['totalSupply', USDC_MAINNET_FIXTURE.totalSupply], + ['decimals', USDC_MAINNET_FIXTURE.decimals], + ['derivedETH', '0'], + ['volume', '0'], + ['volumeUSD', '0'], + ['feesUSD', '0'], + ['untrackedVolumeUSD', '0'], + ['totalValueLocked', '0'], + ['totalValueLockedUSD', '0'], + ['totalValueLockedUSDUntracked', '0'], + ['txCount', '0'], + ['poolCount', '0'], + ['whitelistPools', `[${USDC_WETH_03_MAINNET_POOL}]`], + ]) + + assertObjectMatches('Token', WETH_MAINNET_FIXTURE.address, [ + ['symbol', WETH_MAINNET_FIXTURE.symbol], + ['name', WETH_MAINNET_FIXTURE.name], + ['totalSupply', WETH_MAINNET_FIXTURE.totalSupply], + ['decimals', WETH_MAINNET_FIXTURE.decimals], + ['derivedETH', '0'], + ['volume', '0'], + ['volumeUSD', '0'], + ['feesUSD', '0'], + ['untrackedVolumeUSD', '0'], + ['totalValueLocked', '0'], + ['totalValueLockedUSD', '0'], + ['totalValueLockedUSDUntracked', '0'], + ['txCount', '0'], + ['poolCount', '0'], + ['whitelistPools', `[${USDC_WETH_03_MAINNET_POOL}]`], + ]) + + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [ + ['token0', USDC_MAINNET_FIXTURE.address], + ['token1', WETH_MAINNET_FIXTURE.address], + ['feeTier', POOL_FEE_TIER_03.toString()], + ['createdAtTimestamp', MOCK_EVENT.block.timestamp.toString()], + ['createdAtBlockNumber', MOCK_EVENT.block.number.toString()], + ['liquidityProviderCount', '0'], + ['txCount', '0'], + ['sqrtPrice', '0'], + ['token0Price', '0'], + ['token1Price', '0'], + ['observationIndex', '0'], + ['totalValueLockedToken0', '0'], + ['totalValueLockedToken1', '0'], + ['totalValueLockedUSD', '0'], + ['totalValueLockedETH', '0'], + ['totalValueLockedUSDUntracked', '0'], + ['volumeToken0', '0'], + ['volumeToken1', '0'], + ['volumeUSD', '0'], + ['feesUSD', '0'], + ['untrackedVolumeUSD', '0'], + ['collectedFeesToken0', '0'], + ['collectedFeesToken1', '0'], + ['collectedFeesUSD', '0'], + ]) + }) + + describe('fetchTokenSymbol', () => { + test('success - fetch token symbol', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'symbol', 'symbol():(string)').returns([ethereum.Value.fromString('USDC')]) + const symbol = fetchTokenSymbol(usdcAddress) + assert.stringEquals(symbol, 'USDC') + }) + + test('success - fetch token symbol falls back to bytes call', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'symbol', 'symbol():(string)').reverts() + createMockedFunction(usdcAddress, 'symbol', 'symbol():(bytes32)').returns([ + ethereum.Value.fromBytes(Bytes.fromUTF8('USDC')), + ]) + const symbol = fetchTokenSymbol(usdcAddress) + assert.stringEquals(symbol, 'USDC') + }) + + test('success - fetch token symbol falls back to static definition', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'symbol', 'symbol():(string)').reverts() + createMockedFunction(usdcAddress, 'symbol', 'symbol():(bytes32)').returns([ + ethereum.Value.fromBytes(Bytes.fromHexString(NULL_ETH_HEX_STRING)), + ]) + const staticDefinitions: Array = [ + { + address: Address.fromString(USDC_MAINNET_FIXTURE.address), + symbol: 'USDC', + name: 'USD Coin', + decimals: BigInt.fromI32(6), + }, + ] + const symbol = fetchTokenSymbol(usdcAddress, staticDefinitions) + assert.stringEquals(symbol, 'USDC') + }) + + test('failure - fetch token symbol reverts', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'symbol', 'symbol():(string)').reverts() + createMockedFunction(usdcAddress, 'symbol', 'symbol():(bytes32)').reverts() + const symbol = fetchTokenSymbol(usdcAddress) + assert.stringEquals(symbol, 'unknown') + }) + }) + + describe('fetchTokenName', () => { + test('success - fetch token name', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'name', 'name():(string)').returns([ethereum.Value.fromString('USD Coin')]) + const name = fetchTokenName(usdcAddress) + assert.stringEquals(name, 'USD Coin') + }) + + test('success - fetch token name falls back to bytes call', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'name', 'name():(string)').reverts() + createMockedFunction(usdcAddress, 'name', 'name():(bytes32)').returns([ + ethereum.Value.fromBytes(Bytes.fromUTF8('USD Coin')), + ]) + const name = fetchTokenName(usdcAddress) + assert.stringEquals(name, 'USD Coin') + }) + + test('success - fetch token name falls back to static definition', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'name', 'name():(string)').reverts() + createMockedFunction(usdcAddress, 'name', 'name():(bytes32)').returns([ + ethereum.Value.fromBytes(Bytes.fromHexString(NULL_ETH_HEX_STRING)), + ]) + const staticDefinitions: Array = [ + { + address: Address.fromString(USDC_MAINNET_FIXTURE.address), + symbol: 'USDC', + name: 'USD Coin', + decimals: BigInt.fromI32(6), + }, + ] + const name = fetchTokenName(usdcAddress, staticDefinitions) + assert.stringEquals(name, 'USD Coin') + }) + + test('failure - fetch token name reverts', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'name', 'name():(string)').reverts() + createMockedFunction(usdcAddress, 'name', 'name():(bytes32)').reverts() + const name = fetchTokenName(usdcAddress) + assert.stringEquals(name, 'unknown') + }) + }) + + describe('fetchTokenTotalSupply', () => { + test('success - fetch token total supply', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'totalSupply', 'totalSupply():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('300')), + ]) + const totalSupply = fetchTokenTotalSupply(usdcAddress) + assert.bigIntEquals(totalSupply, BigInt.fromString('300')) + }) + + test('failure - fetch token total supply reverts', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'totalSupply', 'totalSupply():(uint256)').reverts() + const totalSupply = fetchTokenTotalSupply(usdcAddress) + assert.bigIntEquals(totalSupply, BigInt.zero()) + }) + }) + + describe('fetchTokenDecimals', () => { + test('success - fetch token decimals', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'decimals', 'decimals():(uint32)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(6)), + ]) + const decimals = fetchTokenDecimals(usdcAddress) + assert.assertTrue(decimals == BigInt.fromI32(6)) + }) + + test('success - fetch token decimals falls back to static definition', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'decimals', 'decimals():(uint32)').reverts() + const staticDefinitions: Array = [ + { + address: Address.fromString(USDC_MAINNET_FIXTURE.address), + symbol: 'USDC', + name: 'USD Coin', + decimals: BigInt.fromI32(6), + }, + ] + const decimals = fetchTokenDecimals(usdcAddress, staticDefinitions) + assert.assertTrue(decimals == BigInt.fromI32(6)) + }) + + test('failure - fetch token decimals reverts', () => { + const usdcAddress = Address.fromString(USDC_MAINNET_FIXTURE.address) + createMockedFunction(usdcAddress, 'decimals', 'decimals():(uint32)').reverts() + const decimals: BigInt | null = fetchTokenDecimals(usdcAddress) + assert.assertTrue(decimals === null) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index b4833d2d..2e146812 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3768,6 +3768,13 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +matchstick-as@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/matchstick-as/-/matchstick-as-0.6.0.tgz#c65296b1f51b1014d605c52067d9b5321ea630e8" + integrity sha512-E36fWsC1AbCkBFt05VsDDRoFvGSdcZg6oZJrtIe/YDBbuFh8SKbR5FcoqDhNWqSN+F7bN/iS2u8Md0SM+4pUpw== + dependencies: + 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" @@ -5431,6 +5438,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"