mirror of
https://github.com/cerc-io/watcher-ts
synced 2024-11-19 12:26:19 +00:00
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:
parent
7d7cf26f81
commit
f6870d88dc
@ -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) {
|
||||
|
@ -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!
|
||||
|
@ -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({
|
||||
async name(blockHash, token) {
|
||||
const { slot } = getStorageInfo(storageLayout, '_name');
|
||||
|
||||
const vars = {
|
||||
blockHash,
|
||||
account: {
|
||||
address: token,
|
||||
storage: {
|
||||
cid,
|
||||
ipldBlock
|
||||
}
|
||||
}
|
||||
})
|
||||
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) {
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user