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.
This commit is contained in:
Ashwin Phatak 2021-06-01 18:13:41 +05:30 committed by GitHub
parent 7d7cf26f81
commit f6870d88dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 274 additions and 52 deletions

View File

@ -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) {

View File

@ -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!

View File

@ -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) {

View File

@ -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'

View File

@ -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

View File

@ -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);

View File

@ -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) {

View File

@ -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);