diff --git a/package.json b/package.json index c530ebc9..0253c574 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "homepage": "https://github.com/vulcanize/erc20-watcher#readme", "dependencies": { "@types/lodash": "^4.14.168", + "apollo-type-bigint": "^0.1.3", "express": "^4.17.1", "express-graphql": "^0.12.0", "graphql": "^15.5.0", diff --git a/src/erc20.graphql b/src/erc20.graphql index e76e2896..690647b0 100644 --- a/src/erc20.graphql +++ b/src/erc20.graphql @@ -1,29 +1,100 @@ -type Author { - id: Int! - firstName: String - lastName: String - """ - the list of Posts by this author - """ - posts: [Post] +# +# ERC20 GQL schema +# + +# Types + +# Support uint256 values. +scalar BigInt + +# Proof for returned data. Serialized blob for now. +# Will be converted into a well defined structure later. +type Proof { + data: String! } -type Post { - id: Int! - title: String - author: Author - votes: Int +# Result type, with proof, for uint256 method return values. +type ResultUInt256 { + value: BigInt! + + # Proof from state/storage trie. + proof: Proof } -# the schema allows the following query: +# 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)` +type TransferEvent { + from: String! + to: String! + value: BigInt! +} + +# Approval Event +# Emittted by: `function approve(address _spender, uint256 _value) public returns (bool success)` +type ApprovalEvent { + owner: String! + spender: String! + value: BigInt! +} + +# All possible event types fired by an ERC20 contract. +union TokenEvent = TransferEvent | ApprovalEvent + +# Result type, with proof, for event return values. +type ResultEvent { + event: TokenEvent! + + # Proof from receipts trie. + proof: Proof +} + + +# +# Queries +# + type Query { - posts: [Post] - author(id: Int!): Author + + # `function balanceOf(address _owner) public view returns (uint256 balance)` + balanceOf( + blockHash: String! + token: String! + + owner: String! + ): ResultUInt256! + + # `function allowance(address _owner, address _spender) public view returns (uint256 remaining)` + allowance( + blockHash: String! + token: String! + + owner: String! + spender: String! + ): ResultUInt256! + + # Get token events at a certain block, optionally filter by event name. + events( + blockHash: String! + token: String! + name: String + ): [ResultEvent!] } -# this schema allows the following mutation: -type Mutation { - upvotePost ( - postId: Int! - ): Post +# +# Subscriptions +# +type Subscription { + + # Watch for token events (at head of chain). + onTokenEvent(token: String!): ResultEvent! } diff --git a/src/gql.ts b/src/gql.ts index ee02d482..2884d1dc 100644 --- a/src/gql.ts +++ b/src/gql.ts @@ -1,36 +1,51 @@ +import 'lodash'; import 'graphql-import-node'; -import { find, filter } from 'lodash'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import BigInt from 'apollo-type-bigint'; import * as typeDefs from './erc20.graphql'; -import data from './mock-data'; - -const { posts, authors } = data; +import { blocks } from './mock-data'; const resolvers = { - Query: { - posts: () => posts, - author: (_, { id }) => find(authors, { id }), - }, + BigInt: new BigInt('bigInt'), - Mutation: { - upvotePost: (_, { postId }) => { - const post = find(posts, { id: postId }); - if (!post) { - throw new Error(`Couldn't find post with id ${postId}`); + TokenEvent: { + __resolveType: (obj) => { + if (obj.owner) { + return 'ApprovalEvent'; + } + + return 'TransferEvent'; + } + }, + + Query: { + + balanceOf: (_, { blockHash, token, owner }) => { + console.log('balanceOf', blockHash, token, owner); + + return { + value: blocks[blockHash][token].balanceOf[owner], + proof: { data: '' } } - post.votes += 1; - return post; }, - }, - Author: { - posts: author => filter(posts, { authorId: author.id }), - }, + allowance: (_, { blockHash, token, owner, spender }) => { + console.log('allowance', blockHash, token, owner, spender); - Post: { - author: post => find(authors, { id: post.authorId }), - }, + return { + value: blocks[blockHash][token].allowance[owner][spender], + proof: { data: '' } + } + }, + + events: (_, { blockHash, token, name }) => { + console.log('events', blockHash, token, name); + return blocks[blockHash][token].events + .filter(e => !name || name === e.name) + .map(e => ({ 'event': e })); + } + } }; export const schema = makeExecutableSchema({ diff --git a/src/mock-data.ts b/src/mock-data.ts index 994d9e10..08327859 100644 --- a/src/mock-data.ts +++ b/src/mock-data.ts @@ -1,17 +1,43 @@ -const authors = [ - { id: 1, firstName: 'Tom', lastName: 'Coleman' }, - { id: 2, firstName: 'Sashko', lastName: 'Stubailo' }, - { id: 3, firstName: 'Mikhail', lastName: 'Novikov' }, -]; +// TODO: Pull mock data for 5 tokens from rinkeby. -const posts = [ - { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 }, - { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 }, - { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 }, - { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 }, -]; - -export default { - posts, - authors +export const tokens = { + '0xd87fea54f506972e3267239ec8e159548892074a': { + name: 'ChainLink Token', + symbol: 'LINK', + decimals: 18, + totalSupply: '1000000' + } +}; + +export const blocks = { + // Block hash. + '0x77b5479a5856dd8ec63df6aabf9ce0913071a6dda3a3d54f3c9c940574bcb8ab': { + + // ERC20 token address. + '0xd87fea54f506972e3267239ec8e159548892074a': { + balanceOf: { + '0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc': 10000, + '0xCA6D29232D1435D8198E3E5302495417dD073d61': 500 + }, + allowance: { + '0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc': { + '0xCA6D29232D1435D8198E3E5302495417dD073d61': 100 + } + }, + events: [ + { + name: 'Transfer', + from: '0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc', + to: '0xCA6D29232D1435D8198E3E5302495417dD073d61', + value: 500 + }, + { + name: 'Approval', + owner: '0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc', + spender: '0xCA6D29232D1435D8198E3E5302495417dD073d61', + value: 100 + } + ] + } + } }; diff --git a/yarn.lock b/yarn.lock index 6f3fb5b7..ff7363a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -943,6 +943,11 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +apollo-type-bigint@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/apollo-type-bigint/-/apollo-type-bigint-0.1.3.tgz#9242115ca909b9467ba5c4bc6493a56a06984c0b" + integrity sha512-nyfwEWRZ+kon3Nnot20DufGm2EHZrkJoryYzw3soD+USdxhkcW434w1c/n+mjMLQDl86Z6EvlkvMX5Lordf2Wg== + apollo-upload-client@14.1.3: version "14.1.3" resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-14.1.3.tgz#91f39011897bd08e99c0de0164e77ad2f3402247"