├── .eslintignore ├── server ├── util │ ├── index.js │ ├── getURLProtocol.js │ ├── makeWebSocketConnection.js │ └── makeRPCRequest.js ├── constants │ ├── index.js │ └── methods.js ├── query │ ├── version.js │ ├── coinbase.js │ ├── mining.js │ ├── gasPrice.js │ ├── blockNumber.js │ ├── peerCount.js │ ├── hashrate.js │ ├── listening.js │ ├── protocolVersion.js │ ├── accounts.js │ ├── getBalance.js │ ├── getTransactionCount.js │ ├── getBlockByHash.js │ ├── getTransactionByHash.js │ ├── getBlockByNumber.js │ ├── getBlockTransactionCountByHash.js │ ├── getBlockTransactionCountByNumber.js │ ├── getTransactionByBlockHashAndIndex.js │ ├── getTransactionByBlockNumberAndIndex.js │ ├── accountsWithBalances.js │ └── index.js ├── index.js └── schema.js ├── .gitignore ├── README.md ├── LICENSE ├── .eslintrc.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | test/**/*.js 2 | -------------------------------------------------------------------------------- /server/util/index.js: -------------------------------------------------------------------------------- 1 | const getURLProtocol = require('./getURLProtocol') 2 | const makeRPCRequest = require('./makeRPCRequest') 3 | const makeWebSocketConnection = require('./makeWebSocketConnection') 4 | 5 | module.exports = { 6 | getURLProtocol, 7 | makeRPCRequest, 8 | makeWebSocketConnection, 9 | } 10 | -------------------------------------------------------------------------------- /server/constants/index.js: -------------------------------------------------------------------------------- 1 | const METHODS = require('./methods.js') 2 | 3 | // Set env variables in non-production environments 4 | if (process.env.NODE_ENV !== 'production') { 5 | require('dotenv').config({ path: 'server/.env' }) 6 | } 7 | 8 | const { RPC_ENDPOINT } = process.env 9 | 10 | module.exports = { 11 | METHODS, 12 | RPC_ENDPOINT, 13 | } 14 | -------------------------------------------------------------------------------- /server/util/getURLProtocol.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extract the protocol of a URL 3 | * 4 | * @example getURLProtocol('ws://localhost:8545') //=> 'ws:' 5 | * 6 | * @param {String} url URL to analyse 7 | * @return {String} Protocol 8 | */ 9 | const getURLProtocol = url => { 10 | return url.match(/^(.*)+?\/\//)[1] 11 | } 12 | 13 | module.exports = getURLProtocol 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | Icon? 9 | ehthumbs.db 10 | Thumbs.db 11 | 12 | # Logs # 13 | ###################### 14 | *.log 15 | 16 | # Dependencies # 17 | ###################### 18 | node_modules/ 19 | 20 | # Environment # 21 | ###################### 22 | .env 23 | -------------------------------------------------------------------------------- /server/util/makeWebSocketConnection.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | 3 | /** 4 | * Connect to WebSocket 5 | * 6 | * @param {String} address WebSocket server address 7 | * @param {Object} eventEmitter Node.js EventEmitter 8 | * @return {Object} WebSocket connection 9 | */ 10 | const makeWebSocketConnection = (address, eventEmitter) => { 11 | const ws = new WebSocket(address) 12 | 13 | ws.on('error', error => { 14 | return eventEmitter.emit('error', error) 15 | }) 16 | 17 | ws.on('message', data => { 18 | const response = JSON.parse(data) 19 | 20 | return eventEmitter.emit(response.id, response.result) 21 | }) 22 | 23 | return ws 24 | } 25 | 26 | module.exports = makeWebSocketConnection 27 | -------------------------------------------------------------------------------- /server/query/version.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the current network id. 5 | * 6 | * @example query { version } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#net_version 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {String} 15 | */ 16 | const version = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.net.version) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = version 31 | -------------------------------------------------------------------------------- /server/query/coinbase.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the client coinbase address. 5 | * 6 | * @example query { coinbase } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_coinbase 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {String} 15 | */ 16 | const coinbase = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.eth.coinbase) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = coinbase 31 | -------------------------------------------------------------------------------- /server/query/mining.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns if client is actively mining new blocks. 5 | * 6 | * @example query { mining } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_mining 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {Boolean} 15 | */ 16 | const mining = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.eth.mining) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = mining 31 | -------------------------------------------------------------------------------- /server/query/gasPrice.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the current price per gas in wei. 5 | * 6 | * @example query { gasPrice } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gasprice 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {String} 15 | */ 16 | const gasPrice = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.eth.gasPrice) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = gasPrice 31 | -------------------------------------------------------------------------------- /server/query/blockNumber.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the number of most recent block. 5 | * 6 | * @example query { blockNumber } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_blocknumber 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {String} 15 | */ 16 | const blockNumber = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.eth.blockNumber) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = blockNumber 31 | -------------------------------------------------------------------------------- /server/query/peerCount.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns number of peers currently connected to the client. 5 | * 6 | * @example query { peerCount } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#net_peercount 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {String} 15 | */ 16 | const peerCount = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.net.peerCount) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = peerCount 31 | -------------------------------------------------------------------------------- /server/query/hashrate.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the number of hashes per second that the node is mining with. 5 | * 6 | * @example query { hashrate } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_hashrate 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {String} 15 | */ 16 | const hashrate = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.eth.hashrate) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = hashrate 31 | -------------------------------------------------------------------------------- /server/query/listening.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns if client is actively listening for network connections. 5 | * 6 | * @example query { listening } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#net_listening 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {Boolean} 15 | */ 16 | const listening = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.net.listening) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = listening 31 | -------------------------------------------------------------------------------- /server/query/protocolVersion.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the current ethereum protocol version. 5 | * 6 | * @example query { protocolVersion } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_protocolversion 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Object} context GraphQL request context 13 | * @param {Object} context.makeRPCRequest RPC request factory 14 | * @return {String} 15 | */ 16 | const protocolVersion = async (_, args, { makeRPCRequest }) => { 17 | try { 18 | const rpc = await makeRPCRequest(METHODS.eth.protocolVersion) 19 | 20 | if (rpc.error) { 21 | throw new Error(rpc.error.message) 22 | } 23 | 24 | return rpc.result 25 | } catch (e) { 26 | console.error(e) 27 | } 28 | } 29 | 30 | module.exports = protocolVersion 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | > Just a proof-of-concept for now with a few methods implemented! 6 | 7 | ## Setup 8 | ```bash 9 | npm install 10 | ``` 11 | 12 | ## Run 13 | ```bash 14 | npm start 15 | ``` 16 | 17 | ## Try 18 | 19 | Test it out on `http://localhost:4000/`: 20 | ```js 21 | query { 22 | blockNumber 23 | accounts 24 | getBalance(params:["0x1f0dff2d80c4812adaf0c93349b28abaa9a84710"]) 25 | } 26 | ``` 27 | 28 | ![graphql-playground](https://user-images.githubusercontent.com/1913316/35271739-fb22580c-002a-11e8-9df7-3e513730e362.png) 29 | 30 | Or with `curl`: 31 | ```bash 32 | curl 'http://localhost:4000/' -H 'Content-Type: application/json' --data-binary '{"query":"query {\n blockNumber\n accounts\n getBalance(params:[\"0x1f0dff2d80c4812adaf0c93349b28abaa9a84710\"])\n}","variables":{},"operationName":null}' 33 | ``` 34 | -------------------------------------------------------------------------------- /server/query/accounts.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns a list of addresses owned by client. 5 | * 6 | * @example query { accounts } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_accounts 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {Array} 16 | */ 17 | const accounts = async (_, { params }, { makeRPCRequest }) => { 18 | try { 19 | const rpc = await makeRPCRequest(METHODS.eth.accounts, params) 20 | 21 | if (rpc.error) { 22 | throw new Error(rpc.error.message) 23 | } 24 | 25 | return rpc.result 26 | } catch (e) { 27 | console.error(e) 28 | } 29 | } 30 | 31 | module.exports = accounts 32 | -------------------------------------------------------------------------------- /server/query/getBalance.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the balance of the account of given address. 5 | * 6 | * @example query { getBalance } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getbalance 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {String} 16 | */ 17 | const getBalance = async (_, { params }, { makeRPCRequest }) => { 18 | try { 19 | const rpc = await makeRPCRequest(METHODS.eth.getBalance, params) 20 | 21 | if (rpc.error) { 22 | throw new Error(rpc.error.message) 23 | } 24 | 25 | return rpc.result 26 | } catch (e) { 27 | console.error(e) 28 | } 29 | } 30 | 31 | module.exports = getBalance 32 | -------------------------------------------------------------------------------- /server/constants/methods.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | eth: { 3 | accounts: 'eth_accounts', 4 | blockNumber: 'eth_blockNumber', 5 | coinbase: 'eth_coinbase', 6 | gasPrice: 'eth_gasPrice', 7 | getBalance: 'eth_getBalance', 8 | getBlockByHash: 'eth_getBlockByHash', 9 | getBlockByNumber: 'eth_getBlockByNumber', 10 | getBlockTransactionCountByHash: 'eth_getBlockTransactionCountByHash', 11 | getBlockTransactionCountByNumber: 'eth_getBlockTransactionCountByNumber', 12 | getTransactionByBlockHashAndIndex: 'eth_getTransactionByBlockHashAndIndex', 13 | getTransactionByBlockNumberAndIndex: 14 | 'eth_getTransactionByBlockNumberAndIndex', 15 | getTransactionByHash: 'eth_getTransactionByHash', 16 | getTransactionCount: 'eth_getTransactionCount', 17 | hashrate: 'eth_hashrate', 18 | mining: 'eth_mining', 19 | protocolVersion: 'eth_protocolVersion', 20 | }, 21 | net: { 22 | listening: 'net_listening', 23 | peerCount: 'net_peerCount', 24 | version: 'net_version', 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /server/query/getTransactionCount.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the number of transactions sent from an address. 5 | * 6 | * @example query { getTransactionCount(params:["0x..."]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactioncount 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {String} 16 | */ 17 | const getTransactionCount = async (_, { params }, { makeRPCRequest }) => { 18 | try { 19 | const rpc = await makeRPCRequest(METHODS.eth.getTransactionCount, params) 20 | 21 | if (rpc.error) { 22 | throw new Error(rpc.error.message) 23 | } 24 | 25 | return rpc.result 26 | } catch (e) { 27 | console.error(e) 28 | } 29 | } 30 | 31 | module.exports = getTransactionCount 32 | -------------------------------------------------------------------------------- /server/query/getBlockByHash.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns information about a block by block hash. 5 | * 6 | * @example query { getBlockByHash(params:["0x..."]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {Object} 16 | */ 17 | const getBlockByHash = async (_, { params }, { makeRPCRequest }) => { 18 | try { 19 | const rpc = await makeRPCRequest(METHODS.eth.getBlockByHash, [ 20 | ...params, 21 | true, 22 | ]) 23 | 24 | if (rpc.error) { 25 | throw new Error(rpc.error.message) 26 | } 27 | 28 | return rpc.result 29 | } catch (e) { 30 | console.error(e) 31 | } 32 | } 33 | 34 | module.exports = getBlockByHash 35 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const { GraphQLServer } = require('graphql-yoga') 2 | const boxen = require('boxen') 3 | const chalk = require('chalk') 4 | const EventEmitter = require('events') 5 | 6 | const { RPC_ENDPOINT } = require('./constants') 7 | const Query = require('./query') 8 | const schema = require('./schema.js') 9 | const { makeRPCRequest } = require('./util') 10 | 11 | const eventEmitter = new EventEmitter() 12 | 13 | eventEmitter.setMaxListeners(0) 14 | 15 | const typeDefs = schema 16 | 17 | const resolvers = { 18 | Query: Query, 19 | } 20 | 21 | const context = { 22 | makeRPCRequest: makeRPCRequest(RPC_ENDPOINT, eventEmitter), 23 | } 24 | 25 | const server = new GraphQLServer({ typeDefs, resolvers, context }) 26 | 27 | server.start(() => { 28 | const message = 29 | chalk.yellow.bold('eth-graphql') + 30 | '\n\nServer is running on http://localhost:4000\n\n' + 31 | `RPC_ENDPOINT => ${RPC_ENDPOINT}` 32 | 33 | console.log( 34 | boxen(message, { 35 | align: 'center', 36 | borderColor: 'magenta', 37 | margin: 1, 38 | padding: 1, 39 | }) 40 | ) 41 | }) 42 | -------------------------------------------------------------------------------- /server/query/getTransactionByHash.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns information about a transaction requested by transaction hash. 5 | * 6 | * @example query { getTransactionByHash(params:["0x..."]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyhash 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {Object} 16 | */ 17 | const getTransactionByHash = async (_, { params }, { makeRPCRequest }) => { 18 | try { 19 | const rpc = await makeRPCRequest(METHODS.eth.getTransactionByHash, params) 20 | 21 | if (rpc.error) { 22 | throw new Error(rpc.error.message) 23 | } 24 | 25 | return rpc.result 26 | } catch (e) { 27 | console.error(e) 28 | } 29 | } 30 | 31 | module.exports = getTransactionByHash 32 | -------------------------------------------------------------------------------- /server/query/getBlockByNumber.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns information about a block by block number. 5 | * 6 | * @example query { getBlockByNumber(params:["latest"]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {Object} 16 | */ 17 | const getBlockByNumber = async (_, { params }, { makeRPCRequest }) => { 18 | try { 19 | const rpc = await makeRPCRequest(METHODS.eth.getBlockByNumber, [ 20 | ...params, 21 | true, 22 | ]) 23 | 24 | if (rpc.error) { 25 | throw new Error(rpc.error.message) 26 | } 27 | 28 | return rpc.result 29 | } catch (e) { 30 | console.error(e) 31 | } 32 | } 33 | 34 | module.exports = getBlockByNumber 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luke Hedger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended" 8 | ], 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "experimentalObjectRestSpread": true 12 | }, 13 | "ecmaVersion": 8, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "eqeqeq": [ 18 | "error", 19 | "always" 20 | ], 21 | "keyword-spacing": [ 22 | "error", 23 | { 24 | "before": true, 25 | "after": true 26 | } 27 | ], 28 | "no-console": "off", 29 | "no-irregular-whitespace": "error", 30 | "no-var": "error", 31 | "padding-line-between-statements": [ 32 | "error", 33 | { 34 | "blankLine": "always", 35 | "prev": "*", 36 | "next": ["block-like", "function", "return"] 37 | }, 38 | { 39 | "blankLine": "always", 40 | "prev": ["const", "let", "var"], 41 | "next": "*" 42 | }, 43 | { 44 | "blankLine": "any", 45 | "prev": ["const", "let", "var"], 46 | "next": ["const", "let", "var"] 47 | } 48 | ], 49 | "prefer-const": "error", 50 | "valid-jsdoc": "warn" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/query/getBlockTransactionCountByHash.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the number of transactions in a block from a block matching the given block hash. 5 | * 6 | * @example query { getBlockTransactionCountByHash(params:["0x..."]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblocktransactioncountbyhash 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {String} 16 | */ 17 | const getBlockTransactionCountByHash = async ( 18 | _, 19 | { params }, 20 | { makeRPCRequest } 21 | ) => { 22 | try { 23 | const rpc = await makeRPCRequest( 24 | METHODS.eth.getBlockTransactionCountByHash, 25 | params 26 | ) 27 | 28 | if (rpc.error) { 29 | throw new Error(rpc.error.message) 30 | } 31 | 32 | return rpc.result 33 | } catch (e) { 34 | console.error(e) 35 | } 36 | } 37 | 38 | module.exports = getBlockTransactionCountByHash 39 | -------------------------------------------------------------------------------- /server/query/getBlockTransactionCountByNumber.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns the number of transactions in a block matching the given block number. 5 | * 6 | * @example query { getBlockTransactionCountByNumber(params:["0x..."]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblocktransactioncountbynumber 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {String} 16 | */ 17 | const getBlockTransactionCountByNumber = async ( 18 | _, 19 | { params }, 20 | { makeRPCRequest } 21 | ) => { 22 | try { 23 | const rpc = await makeRPCRequest( 24 | METHODS.eth.getBlockTransactionCountByNumber, 25 | params 26 | ) 27 | 28 | if (rpc.error) { 29 | throw new Error(rpc.error.message) 30 | } 31 | 32 | return rpc.result 33 | } catch (e) { 34 | console.error(e) 35 | } 36 | } 37 | 38 | module.exports = getBlockTransactionCountByNumber 39 | -------------------------------------------------------------------------------- /server/query/getTransactionByBlockHashAndIndex.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns information about a transaction by block hash and transaction index position. 5 | * 6 | * @example query { getTransactionByBlockHashAndIndex(params:["0x...", "0x0"]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyblockhashandindex 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {Object} 16 | */ 17 | const getTransactionByBlockHashAndIndex = async ( 18 | _, 19 | { params }, 20 | { makeRPCRequest } 21 | ) => { 22 | try { 23 | const rpc = await makeRPCRequest( 24 | METHODS.eth.getTransactionByBlockHashAndIndex, 25 | params 26 | ) 27 | 28 | if (rpc.error) { 29 | throw new Error(rpc.error.message) 30 | } 31 | 32 | return rpc.result 33 | } catch (e) { 34 | console.error(e) 35 | } 36 | } 37 | 38 | module.exports = getTransactionByBlockHashAndIndex 39 | -------------------------------------------------------------------------------- /server/query/getTransactionByBlockNumberAndIndex.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('../constants') 2 | 3 | /** 4 | * Returns information about a transaction by block number and transaction index position. 5 | * 6 | * @example query { getTransactionByBlockNumberAndIndex(params:["0x29c", "0x0"]) } 7 | * 8 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyblocknumberandindex 9 | * 10 | * @param {Object} _ Parent resolver context 11 | * @param {Object} args Query arguments 12 | * @param {Array} args.params RPC request parameters 13 | * @param {Object} context GraphQL request context 14 | * @param {Object} context.makeRPCRequest RPC request factory 15 | * @return {Object} 16 | */ 17 | const getTransactionByBlockNumberAndIndex = async ( 18 | _, 19 | { params }, 20 | { makeRPCRequest } 21 | ) => { 22 | try { 23 | const rpc = await makeRPCRequest( 24 | METHODS.eth.getTransactionByBlockNumberAndIndex, 25 | params 26 | ) 27 | 28 | if (rpc.error) { 29 | throw new Error(rpc.error.message) 30 | } 31 | 32 | return rpc.result 33 | } catch (e) { 34 | console.error(e) 35 | } 36 | } 37 | 38 | module.exports = getTransactionByBlockNumberAndIndex 39 | -------------------------------------------------------------------------------- /server/query/accountsWithBalances.js: -------------------------------------------------------------------------------- 1 | const unit = require('ethjs-unit') 2 | 3 | const { METHODS } = require('../constants') 4 | 5 | /** 6 | * Returns a list of addresses owned by client with their balance. 7 | * 8 | * @example query { accountsWithBalances { address balance } } 9 | * 10 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_accounts 11 | * @see https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getbalance 12 | * 13 | * @param {Object} _ Parent resolver context 14 | * @param {Object} args Query arguments 15 | * @param {Object} context GraphQL request context 16 | * @param {Object} context.makeRPCRequest RPC request factory 17 | * @return {Array} 18 | */ 19 | const accountsWithBalances = async (_, args, { makeRPCRequest }) => { 20 | try { 21 | const { result: accounts } = await makeRPCRequest(METHODS.eth.accounts) 22 | 23 | const balances = await Promise.all( 24 | accounts.map(account => makeRPCRequest(METHODS.eth.getBalance, [account])) 25 | ) 26 | 27 | const result = balances.map(({ result: balance }, index) => { 28 | return { 29 | address: accounts[index], 30 | balance: unit.fromWei(balance, 'ether'), 31 | } 32 | }) 33 | 34 | return result 35 | } catch (e) { 36 | console.error(e) 37 | } 38 | } 39 | 40 | module.exports = accountsWithBalances 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-graphql", 3 | "version": "1.0.0", 4 | "description": "Ethereum GraphQL API", 5 | "main": "server/index.js", 6 | "lint-staged": { 7 | "*.js": [ 8 | "eslint --fix \"server/**/*.js\"", 9 | "prettier --write --no-semi --single-quote --trailing-comma=es5", 10 | "git add" 11 | ] 12 | }, 13 | "pre-commit": { 14 | "run": [ 15 | "lint:staged", 16 | "test" 17 | ], 18 | "silent": true 19 | }, 20 | "scripts": { 21 | "blockchain": "ganache-cli", 22 | "lint:staged": "lint-staged", 23 | "start": "nodemon --watch server server", 24 | "start:prod": "node server", 25 | "test": "jest" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/lukehedger/eth-graphql.git" 30 | }, 31 | "keywords": [ 32 | "ethereum", 33 | "graphql" 34 | ], 35 | "author": "Luke Hedger", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/lukehedger/eth-graphql/issues" 39 | }, 40 | "homepage": "https://github.com/lukehedger/eth-graphql#readme", 41 | "dependencies": { 42 | "boxen": "^1.3.0", 43 | "chalk": "^2.3.0", 44 | "ethjs-unit": "^0.1.6", 45 | "graphql-yoga": "^0.9.0", 46 | "node-fetch": "^1.7.3", 47 | "ws": "^4.0.0" 48 | }, 49 | "devDependencies": { 50 | "dotenv": "^4.0.0", 51 | "eslint": "^4.16.0", 52 | "ganache-cli": "^6.1.0-beta.2", 53 | "jest": "^22.1.4", 54 | "lint-staged": "^6.0.1", 55 | "nodemon": "^1.14.11", 56 | "pre-commit": "^1.2.2", 57 | "prettier": "^1.10.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/query/index.js: -------------------------------------------------------------------------------- 1 | const accounts = require('./accounts.js') 2 | const accountsWithBalances = require('./accountsWithBalances.js') 3 | const blockNumber = require('./blockNumber.js') 4 | const coinbase = require('./coinbase.js') 5 | const gasPrice = require('./gasPrice.js') 6 | const getBalance = require('./getBalance.js') 7 | const getBlockByHash = require('./getBlockByHash.js') 8 | const getBlockByNumber = require('./getBlockByNumber.js') 9 | const getBlockTransactionCountByHash = require('./getBlockTransactionCountByHash.js') 10 | const getBlockTransactionCountByNumber = require('./getBlockTransactionCountByNumber.js') 11 | const getTransactionByBlockHashAndIndex = require('./getTransactionByBlockHashAndIndex.js') 12 | const getTransactionByBlockNumberAndIndex = require('./getTransactionByBlockNumberAndIndex.js') 13 | const getTransactionByHash = require('./getTransactionByHash.js') 14 | const getTransactionCount = require('./getTransactionCount.js') 15 | const hashrate = require('./hashrate.js') 16 | const listening = require('./listening.js') 17 | const mining = require('./mining.js') 18 | const peerCount = require('./peerCount.js') 19 | const protocolVersion = require('./protocolVersion.js') 20 | const version = require('./version.js') 21 | 22 | module.exports = { 23 | accounts, 24 | accountsWithBalances, 25 | blockNumber, 26 | coinbase, 27 | gasPrice, 28 | getBalance, 29 | getBlockByHash, 30 | getBlockByNumber, 31 | getBlockTransactionCountByHash, 32 | getBlockTransactionCountByNumber, 33 | getTransactionByBlockHashAndIndex, 34 | getTransactionByBlockNumberAndIndex, 35 | getTransactionByHash, 36 | getTransactionCount, 37 | hashrate, 38 | listening, 39 | mining, 40 | peerCount, 41 | protocolVersion, 42 | version, 43 | } 44 | -------------------------------------------------------------------------------- /server/schema.js: -------------------------------------------------------------------------------- 1 | const typeDefs = ` 2 | type AccountWithBalance { 3 | address: String! 4 | balance: String! 5 | } 6 | 7 | type Block { 8 | difficulty: String! 9 | extraData: String! 10 | gasLimit: String! 11 | gasUsed: String! 12 | hash: String! 13 | logsBloom: String! 14 | nonce: String! 15 | miner: String! 16 | number: String! 17 | parentHash: String! 18 | receiptsRoot: String! 19 | sha3Uncles: String! 20 | size: String! 21 | stateRoot: String! 22 | totalDifficulty: String! 23 | transactionsRoot: String! 24 | timestamp: String! 25 | transactions: [String]! 26 | uncles: [String]! 27 | } 28 | 29 | type Transaction { 30 | blockHash: String! 31 | blockNumber: String! 32 | from: String! 33 | gas: String! 34 | gasPrice: String! 35 | hash: String! 36 | input: String! 37 | nonce: String! 38 | to: String! 39 | transactionIndex: String! 40 | value: String! 41 | } 42 | 43 | type Query { 44 | accounts(params: [String] = []): [String]! 45 | accountsWithBalances: [AccountWithBalance]! 46 | blockNumber(params: [String] = []): String! 47 | coinbase: String! 48 | gasPrice: String! 49 | getBalance(params: [String] = []): String! 50 | getBlockByHash(params: [String] = []): Block! 51 | getBlockByNumber(params: [String] = []): Block! 52 | getBlockTransactionCountByHash(params: [String] = []): String! 53 | getBlockTransactionCountByNumber(params: [String] = []): String! 54 | getTransactionByBlockHashAndIndex(params: [String] = []): Transaction! 55 | getTransactionByBlockNumberAndIndex(params: [String] = []): Transaction! 56 | getTransactionByHash(params: [String] = []): Transaction! 57 | getTransactionCount(params: [String] = []): String! 58 | hashrate: String! 59 | listening: Boolean! 60 | mining: Boolean! 61 | peerCount: String! 62 | protocolVersion: String! 63 | version: String! 64 | } 65 | ` 66 | 67 | module.exports = typeDefs 68 | -------------------------------------------------------------------------------- /server/util/makeRPCRequest.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const getURLProtocol = require('./getURLProtocol') 3 | const makeWebSocketConnection = require('./makeWebSocketConnection') 4 | 5 | /** 6 | * Generate RPC request data 7 | * 8 | * @param {String} method RPC method 9 | * @param {Array} params RPC parameters 10 | * @param {Number} [id=1] RPC request ID 11 | * @return {String} Stringified request data 12 | */ 13 | const getRPCRequestBody = (method, params, id = 1) => { 14 | return JSON.stringify({ 15 | jsonrpc: '2.0', 16 | method: method, 17 | params: params, 18 | id: id, 19 | }) 20 | } 21 | 22 | /** 23 | * Send RPC request over HTTP 24 | * 25 | * @param {String} endpoint RPC endpoint 26 | * @param {String} method RPC method 27 | * @param {Array} params RPC parameters 28 | * @return {Object} RPC response body 29 | */ 30 | const httpRPCRequest = endpoint => (method, params) => { 31 | return fetch(endpoint, { 32 | method: 'POST', 33 | headers: { 'Content-Type': 'application/json' }, 34 | body: getRPCRequestBody(method, params), 35 | }).then(res => res.json()) 36 | } 37 | 38 | /** 39 | * Send RPC request over WebSocket 40 | * 41 | * @param {Object} ws RPC WebSocket connection 42 | * @param {Object} eventEmitter Node.js EventEmitter 43 | * @param {String} method RPC method 44 | * @param {Array} params RPC parameters 45 | * @return {Object} RPC response body 46 | */ 47 | const wsRPCRequest = (ws, eventEmitter) => (method, params) => { 48 | return new Promise((resolve, reject) => { 49 | const id = new Date().getTime() 50 | 51 | // send message 52 | ws.send(getRPCRequestBody(method, params, id)) 53 | 54 | // on error callback 55 | const onError = error => reject({ error }) 56 | 57 | // on message callback 58 | const onMessage = result => { 59 | eventEmitter.removeListener('error', onError) 60 | 61 | return resolve({ result }) 62 | } 63 | 64 | // listen for error 65 | eventEmitter.once('error', onError) 66 | 67 | // listen for result 68 | eventEmitter.once(id, onMessage) 69 | }) 70 | } 71 | 72 | /** 73 | * Construct RPC request 74 | * 75 | * @param {String} endpoint RPC endpoint 76 | * @param {Object} eventEmitter Node.js EventEmitter 77 | * @return {Object} RPC response body 78 | */ 79 | const makeRPCRequest = (endpoint, eventEmitter) => { 80 | const protocol = getURLProtocol(endpoint) 81 | 82 | let rpcRequest 83 | 84 | switch (protocol) { 85 | case 'ws:': 86 | rpcRequest = wsRPCRequest( 87 | makeWebSocketConnection(endpoint, eventEmitter), 88 | eventEmitter 89 | ) 90 | break 91 | case 'http:': 92 | case 'https:': 93 | rpcRequest = httpRPCRequest(endpoint) 94 | break 95 | } 96 | 97 | return rpcRequest 98 | } 99 | 100 | module.exports = makeRPCRequest 101 | --------------------------------------------------------------------------------