From f6870d88dc0e81ddb3a7a598f31143a3c4b326d9 Mon Sep 17 00:00:00 2001 From: Ashwin Phatak Date: Tue, 1 Jun 2021 18:13:41 +0530 Subject: [PATCH] Support GQL query for name, symbol, totalSupply (#24) * Update mock server and tests for additional ERC20 state vars. * Refactor eth-client getStorageAt, impl totalSupply. * Impl ERC20 API for name, symbol. --- packages/ipld-eth-client/src/eth-client.ts | 24 +++++- packages/watcher/src/erc20.graphql | 55 ++++++++++--- packages/watcher/src/indexer.ts | 90 +++++++++++++--------- packages/watcher/src/mock/data.ts | 4 +- packages/watcher/src/mock/resolvers.ts | 36 +++++++++ packages/watcher/src/mock/server.spec.ts | 53 ++++++++++++- packages/watcher/src/queries.ts | 44 +++++++++++ packages/watcher/src/resolvers.ts | 20 +++++ 8 files changed, 274 insertions(+), 52 deletions(-) diff --git a/packages/ipld-eth-client/src/eth-client.ts b/packages/ipld-eth-client/src/eth-client.ts index b0e9ad79..e4b88c4a 100644 --- a/packages/ipld-eth-client/src/eth-client.ts +++ b/packages/ipld-eth-client/src/eth-client.ts @@ -4,6 +4,7 @@ import { GraphQLClient } from 'graphql-request'; import { Cache } from '@vulcanize/cache'; import ethQueries from './eth-queries'; +import { padKey } from './utils'; export class EthClient { @@ -21,11 +22,28 @@ export class EthClient { this._cache = cache; } - async getStorageAt(vars) { - const result = await this._getCachedOrFetch('getStorageAt', vars); + async getStorageAt({ blockHash, contract, slot }) { + slot = `0x${padKey(slot)}`; + + const result = await this._getCachedOrFetch('getStorageAt', { blockHash, contract, slot }); const { getStorageAt: { value, cid, ipldBlock } } = result; - return { value, cid, ipldBlock }; + return { + value, + proof: { + // TODO: Return proof only if requested. + data: JSON.stringify({ + blockHash, + account: { + address: contract, + storage: { + cid, + ipldBlock + } + } + }) + } + }; } async getLogs(vars) { diff --git a/packages/watcher/src/erc20.graphql b/packages/watcher/src/erc20.graphql index 690647b0..8dc958a2 100644 --- a/packages/watcher/src/erc20.graphql +++ b/packages/watcher/src/erc20.graphql @@ -1,6 +1,9 @@ # # ERC20 GQL schema # +# See: https://eips.ethereum.org/EIPS/eip-20 +# ABI: https://ethereumdev.io/abi-for-erc20-contract-on-ethereum/ +# # Types @@ -13,6 +16,14 @@ type Proof { data: String! } +# Result type, with proof, for string method return values. +type ResultString { + value: String + + # Proof from state/storage trie. + proof: Proof +} + # Result type, with proof, for uint256 method return values. type ResultUInt256 { value: BigInt! @@ -21,15 +32,6 @@ type ResultUInt256 { proof: Proof } -# ERC20 Token https://eips.ethereum.org/EIPS/eip-20 -# ABI: https://ethereumdev.io/abi-for-erc20-contract-on-ethereum/ -type Token { - name: String! - symbol: String! - decimals: Int! - totalSupply: BigInt! -} - # Transfer Event # Emitted by: `function transfer(address _to, uint256 _value) public returns (bool success)` # Emitted by: `function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)` @@ -65,6 +67,17 @@ type ResultEvent { type Query { + # + # Interface of the ERC20 standard as defined in the EIP. + # https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#IERC20 + # + + # `function totalSupply() public view returns (uint256)` + totalSupply( + blockHash: String! + token: String! + ): ResultUInt256! + # `function balanceOf(address _owner) public view returns (uint256 balance)` balanceOf( blockHash: String! @@ -82,6 +95,30 @@ type Query { spender: String! ): ResultUInt256! + # + # Optional functions from the ERC20 standard. + # https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#ERC20Detailed + # + + name( + blockHash: String! + token: String! + ): ResultString! + + symbol( + blockHash: String! + token: String! + ): ResultString! + + decimals( + blockHash: String! + token: String! + ): ResultUInt256! + + # + # Additional watcher queries. + # + # Get token events at a certain block, optionally filter by event name. events( blockHash: String! diff --git a/packages/watcher/src/indexer.ts b/packages/watcher/src/indexer.ts index e2b4fbbb..bdaac03d 100644 --- a/packages/watcher/src/indexer.ts +++ b/packages/watcher/src/indexer.ts @@ -22,6 +22,21 @@ export class Indexer { this._ethClient = ethClient; } + async totalSupply(blockHash, token) { + const { slot } = getStorageInfo(storageLayout, '_totalSupply'); + + const vars = { + blockHash, + contract: token, + slot + }; + + const result = await this._ethClient.getStorageAt(vars); + log(JSON.stringify(result, null, 2)); + + return result; + } + async getBalanceOf(blockHash, token, owner) { const { slot: balancesSlot } = getStorageInfo(storageLayout, '_balances'); const slot = getMappingSlot(balancesSlot, owner); @@ -35,24 +50,7 @@ export class Indexer { const result = await this._ethClient.getStorageAt(vars); log(JSON.stringify(result, null, 2)); - const { value, cid, ipldBlock } = result; - - return { - value, - proof: { - // TODO: Return proof only if requested. - data: JSON.stringify({ - blockHash, - account: { - address: token, - storage: { - cid, - ipldBlock - } - } - }) - } - } + return result; } async getAllowance(blockHash, token, owner, spender) { @@ -68,24 +66,46 @@ export class Indexer { const result = await this._ethClient.getStorageAt(vars); log(JSON.stringify(result, null, 2)); - const { value, cid, ipldBlock } = result; + return result; + } - return { - value, - proof: { - // TODO: Return proof only if requested. - data: JSON.stringify({ - blockHash, - account: { - address: token, - storage: { - cid, - ipldBlock - } - } - }) - } - } + async name(blockHash, token) { + const { slot } = getStorageInfo(storageLayout, '_name'); + + const vars = { + blockHash, + contract: token, + slot + }; + + // TODO: Integrate with storage-mapper to get string value (currently hex encoded). + const result = await this._ethClient.getStorageAt(vars); + log(JSON.stringify(result, null, 2)); + + return result; + } + + async symbol(blockHash, token) { + const { slot } = getStorageInfo(storageLayout, '_symbol'); + + const vars = { + blockHash, + contract: token, + slot + }; + + // TODO: Integrate with storage-mapper to get string value (currently hex encoded). + const result = await this._ethClient.getStorageAt(vars); + log(JSON.stringify(result, null, 2)); + + return result; + } + + async decimals(blockHash, token) { + // Not a state variable, uses hardcoded return value in contract function. + // See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L86 + + throw new Error('Not implemented.'); } async getEvents(blockHash, token, name) { diff --git a/packages/watcher/src/mock/data.ts b/packages/watcher/src/mock/data.ts index 26f5541e..977fbe14 100644 --- a/packages/watcher/src/mock/data.ts +++ b/packages/watcher/src/mock/data.ts @@ -4,7 +4,7 @@ export const tokens = { '0xd87fea54f506972e3267239ec8e159548892074a': { name: 'ChainLink Token', symbol: 'LINK', - decimals: 18, + decimals: '18', totalSupply: '1000000' } }; @@ -15,6 +15,8 @@ export const blocks = { // ERC20 token address. '0xd87fea54f506972e3267239ec8e159548892074a': { + ...tokens['0xd87fea54f506972e3267239ec8e159548892074a'], + balanceOf: { '0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc': '10000', '0xCA6D29232D1435D8198E3E5302495417dD073d61': '500' diff --git a/packages/watcher/src/mock/resolvers.ts b/packages/watcher/src/mock/resolvers.ts index ef80e635..2f66e0d1 100644 --- a/packages/watcher/src/mock/resolvers.ts +++ b/packages/watcher/src/mock/resolvers.ts @@ -22,6 +22,15 @@ export const createResolvers = async (config) => { Query: { + totalSupply: (_, { blockHash, token }) => { + log('totalSupply', blockHash, token); + + return { + value: blocks[blockHash][token].totalSupply, + proof: { data: '' } + } + }, + balanceOf: (_, { blockHash, token, owner }) => { log('balanceOf', blockHash, token, owner); @@ -40,6 +49,33 @@ export const createResolvers = async (config) => { } }, + name: (_, { blockHash, token }) => { + log('name', blockHash, token); + + return { + value: blocks[blockHash][token].name, + proof: { data: '' } + } + }, + + symbol: (_, { blockHash, token }) => { + log('symbol', blockHash, token); + + return { + value: blocks[blockHash][token].symbol, + proof: { data: '' } + } + }, + + decimals: (_, { blockHash, token }) => { + log('decimals', blockHash, token); + + return { + value: blocks[blockHash][token].decimals, + proof: { data: '' } + } + }, + events: (_, { blockHash, token, name }) => { log('events', blockHash, token, name); return blocks[blockHash][token].events diff --git a/packages/watcher/src/mock/server.spec.ts b/packages/watcher/src/mock/server.spec.ts index a3c6dff9..e51d6080 100644 --- a/packages/watcher/src/mock/server.spec.ts +++ b/packages/watcher/src/mock/server.spec.ts @@ -4,14 +4,23 @@ import _ from 'lodash'; import { GraphQLClient } from 'graphql-request'; -import { queryBalanceOf, queryAllowance, queryEvents } from '../queries'; +import { + queryName, + querySymbol, + queryDecimals, + queryTotalSupply, + queryBalanceOf, + queryAllowance, + queryEvents +} from '../queries'; -import { blocks } from './data'; +import { blocks, tokens as tokenInfo } from './data'; const testCases = { 'balanceOf': [], 'allowance': [], - 'events': [] + 'events': [], + 'tokens': [] }; const blockHashes = _.keys(blocks); @@ -21,7 +30,14 @@ blockHashes.forEach(blockHash => { tokens.forEach(token => { const tokenObj = block[token]; - // Event tests cases. + // Token info test cases. + testCases.tokens.push({ + blockHash, + token, + info: tokenInfo[token] + }); + + // Event test cases. testCases.events.push({ blockHash, token, @@ -61,6 +77,35 @@ describe('server', () => { const client = new GraphQLClient("http://localhost:3001/graphql"); + it('query token info', async () => { + const tests = testCases.tokens; + expect(tests.length).to.be.greaterThan(0); + + for (let i = 0; i < tests.length; i++) { + const testCase = tests[i]; + + // Token totalSupply. + let result = await client.request(queryTotalSupply, testCase); + expect(result.totalSupply.value).to.equal(testCase.info.totalSupply); + expect(result.totalSupply.proof.data).to.equal(''); + + // Token name. + result = await client.request(queryName, testCase); + expect(result.name.value).to.equal(testCase.info.name); + expect(result.name.proof.data).to.equal(''); + + // Token symbol. + result = await client.request(querySymbol, testCase); + expect(result.symbol.value).to.equal(testCase.info.symbol); + expect(result.symbol.proof.data).to.equal(''); + + // Token decimals. + result = await client.request(queryDecimals, testCase); + expect(result.decimals.value).to.equal(testCase.info.decimals); + expect(result.decimals.proof.data).to.equal(''); + } + }); + it('query balanceOf', async () => { const tests = testCases.balanceOf; expect(tests.length).to.be.greaterThan(0); diff --git a/packages/watcher/src/queries.ts b/packages/watcher/src/queries.ts index 52cc6b60..52ed6308 100644 --- a/packages/watcher/src/queries.ts +++ b/packages/watcher/src/queries.ts @@ -1,5 +1,16 @@ import { gql } from 'graphql-request'; +export const queryTotalSupply = gql` +query getTotalSupply($blockHash: String!, $token: String!) { + totalSupply(blockHash: $blockHash, token: $token) { + value + proof { + data + } + } +} +`; + export const queryBalanceOf = gql` query getBalance($blockHash: String!, $token: String!, $owner: String!) { balanceOf(blockHash: $blockHash, token: $token, owner: $owner) { @@ -22,6 +33,39 @@ query getAllowance($blockHash: String!, $token: String!, $owner: String!, $spend } `; +export const queryName = gql` +query getName($blockHash: String!, $token: String!) { + name(blockHash: $blockHash, token: $token) { + value + proof { + data + } + } +} +`; + +export const querySymbol = gql` +query getSymbol($blockHash: String!, $token: String!) { + symbol(blockHash: $blockHash, token: $token) { + value + proof { + data + } + } +} +`; + +export const queryDecimals = gql` +query getDecimals($blockHash: String!, $token: String!) { + decimals(blockHash: $blockHash, token: $token) { + value + proof { + data + } + } +} +`; + export const queryEvents = gql` query getEvents($blockHash: String!, $token: String!) { events(blockHash: $blockHash, token: $token) { diff --git a/packages/watcher/src/resolvers.ts b/packages/watcher/src/resolvers.ts index 9c62b716..6e519d9d 100644 --- a/packages/watcher/src/resolvers.ts +++ b/packages/watcher/src/resolvers.ts @@ -44,6 +44,11 @@ export const createResolvers = async (config) => { Query: { + totalSupply: (_, { blockHash, token }) => { + log('totalSupply', blockHash, token); + return indexer.totalSupply(blockHash, token); + }, + balanceOf: async (_, { blockHash, token, owner }) => { log('balanceOf', blockHash, token, owner); return indexer.getBalanceOf(blockHash, token, owner); @@ -54,6 +59,21 @@ export const createResolvers = async (config) => { return indexer.getAllowance(blockHash, token, owner, spender); }, + name: (_, { blockHash, token }) => { + log('name', blockHash, token); + return indexer.name(blockHash, token); + }, + + symbol: (_, { blockHash, token }) => { + log('symbol', blockHash, token); + return indexer.symbol(blockHash, token); + }, + + decimals: (_, { blockHash, token }) => { + log('decimals', blockHash, token); + return indexer.decimals(blockHash, token); + }, + events: async (_, { blockHash, token, name }) => { log('events', blockHash, token, name); return indexer.getEvents(blockHash, token, name);