Skip to content

therealemjy/eth-graphql

Repository files navigation

Query blockchain data using GraphQL

eth-graphql is an Apollo link for retrieving contract data from EVM-compatible blockchains using GraphQL

Killer features

🔄 Use an automatically generated GraphQL schema to fetch data from any EVM-compatible blockchain contract

🌐 Fully client-side, no centralized API

⚡️ Requests are batched using multicall for optimal efficiency

🛠 Leverage all the features of GraphQL and Apollo Client tools, such as optimistic updates and cache normalization

🔬 Comprehensive TypeScript support via GraphQL Codegen

Getting started

Install eth-graphql and its peer dependencies

Using yarn:

yarn add eth-graphql @apollo/client@^3.x ethers@^5.x

or npm:

npm install eth-graphql @apollo/client@^3.x ethers@^5.x

Setting up configuration File

Create a .js or .ts file (e.g.: ethGraphQlConfig.ts) which exports a config object as default:

import { Config } from 'eth-graphql';
import { providers } from 'ethers';

import erc20Abi from './abis/erc20.json';

const RPC_PROVIDER_URL = 'https://ethereum.publicnode.com';
const provider = new providers.JsonRpcProvider(RPC_PROVIDER_URL);

const config: Config = {
  chains: {
    1: {
      provider,
    },
  },
  contracts: [
    {
      name: 'SHIB',
      address: {
        1: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE',
      },
      abi: erc20Abi,
    },
  ],
};

export default config;

Note that if you want to use the example above, the ABI of the ERC20 contract can be found here.

Link creation and connection to Apollo Client

import { ApolloClient, InMemoryCache } from '@apollo/client';
import { createLink } from 'eth-graphql';

import config from './ethGraphQlConfig';

const link = createLink(config);

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link,
});

export default client;

Crafting your first query

You're now ready to write your first GraphQL query to fetch data from contracts on the blockchain!

import { gql } from '@apollo/client';

const { data } = await client.query({
  query: gql`
    query GetTokens {
      contracts(chainId: 1) {
        SHIB {
          decimals
        }
      }
    }
  `,
});

console.log(data.contracts.SHIB.decimals); // 18

GraphiQL interface

To explore the generated client-side schema and experiment with your queries, you can start a GraphiQL server using the following command:

yarn eth-graphql --config ./ethGraphQlConfig.ts

or with npm:

npm run eth-graphql --config ./ethGraphQlConfig.ts

Note that the config option signifies the location of your config file, relative to the directory from which you're executing the command.

The command will automatically open http://localhost:8008/eth-call-graphiql in your browser.

Multi-chain support

In the previous example, we've configured only one chain. However, the configuration can be adapted to support contract calls on multiple chains:

import { Config } from 'eth-graphql';
import { providers } from 'ethers';

import erc20Abi from './abis/erc20.json';

const MAINNET_RPC_PROVIDER_URL = 'https://ethereum.publicnode.com';
const mainnetProvider = new providers.JsonRpcProvider(MAINNET_RPC_PROVIDER_URL);

const TESTNET_RPC_PROVIDER_URL = 'https://ethereum-goerli.publicnode.com';
const testnetProvider = new providers.JsonRpcProvider(TESTNET_RPC_PROVIDER_URL);

const config: Config = {
  chains: {
    1: {
      provider: mainnetProvider,
    },
    5: {
      provider: testnetProvider,
    },
  },
  contracts: [
    {
      name: 'SHIB',
      address: {
        1: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE',
        5: '0x058d6Fb2828608C0422BB6C89F77CCaA9ea7A9b4',
      },
      abi: erc20Abi,
    },
  ],
};

export default config;

You can then select which chain to fetch data from when making your query:

import { gql } from '@apollo/client';

const { data: mainnetData } = await client.query({
  query: gql`
    query GetTokens {
      contracts(chainId: 1) {
        SHIB {
          decimals
        }
      }
    }
  `,
});

const { data: testnetData } = await client.query({
  query: gql`
    query GetTokens {
      contracts(chainId: 2) {
        SHIB {
          decimals
        }
      }
    }
  `,
});

Calling contracts using dynamic addresses

You can omit to set an address property for a contract:

import { Config } from 'eth-graphql';
import { providers } from 'ethers';

import erc20Abi from './abis/erc20.json';

const RPC_PROVIDER_URL = 'https://ethereum.publicnode.com';
const provider = new providers.JsonRpcProvider(RPC_PROVIDER_URL);

const config: Config = {
  ...
  contracts: [
    {
      name: 'ERC20',
      abi: erc20Abi,
    },
  ],
};

export default config;

In which case you will be able to pass an array of contract addresses to fetch when making your query. Results will be returned as an array of outputs, sorted in the same order as the input addresses:

import { gql } from '@apollo/client';

const { data } = await client.query({
  query: gql`
    query GetTokens {
      contracts(chainId: 1) {
        ERC20(
          addresses: [
            "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
            "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
          ]
        ) {
          name
        }
      }
    }
  `,
});

console.log(data.contracts.ERC20);
/* 
[{
  name: 'USD Coin'
}, {
  name: 'Uniswap'
}]
*/

Calling the same method multiple times

In our generated GraphQL schema, each field that can accept arguments and that signifies a callable contract method will have a corresponding field appended with "_MULT". This additional field accepts a single args argument, which represents an array of argument sets.

The mult field lets you call a specific contract method multiple times using different sets of arguments. Results will be returned as an array of outputs, sorted in the same order as the input arguments.

To illustrate, let's consider our previous example: the balanceOf method in the ERC20 token contract takes a _owner argument. This means we can use its corresponding mult field to retrieve the balance of multiple addresses.

import { gql } from '@apollo/client';

const { data } = await client.query({
  query: gql`
    query GetTokens {
      contracts(chainId: 1) {
        SHIB {
          balanceOf_MULT(
            args: [
              { _owner: "0x5a52e96bacdabb82fd05763e25335261b270efcb" }
              { _owner: "0xf3b0073e3a7f747c7a38b36b805247b222c302a3" }
            ]
          )
        }
      }
    }
  `,
});

console.log(data.contracts.SHIB.balanceOf_MULT);
// ["44926053923695454210260771799275", "43924775092079295515337024597625"]

Using custom multicall addresses

eth-graphql uses the multicall contract from 0xsequence to batch all the calls of a query into one. You can find the list of supported networks here.

In case you want to interact with contracts on an unsupported network, or prefer to use your own multicall contract, you can deploy 0xsequence's multicall contract. The code can be found here. Once deployed, include the contract's address in your configuration as follows:

import { Config } from 'eth-graphql';
import { providers } from 'ethers';

import erc20Abi from './abis/erc20.json';

const RPC_PROVIDER_URL = 'https://ethereum.publicnode.com';
const provider = new providers.JsonRpcProvider(RPC_PROVIDER_URL);

const config: Config = {
  chains: {
    1: {
      provider,
      multicallAddress: '0x' // Add multicall contract address here
    },
  },
  ...
};

export default config;

Automatically generating types for queries with graphql-codegen

You can automatically generate TypeScript types for your queries using graphql-codegen and the createSchema method from eth-graphql:

// codegenConfig.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
import { createSchema } from 'eth-graphql';
import { printSchema } from 'graphql';

import ethGraphQlConfig from './ethGraphQlConfig';

const config: CodegenConfig = {
  overwrite: true,
  schema: printSchema(createSchema(ethGraphQlConfig)),
  documents: '**/*.tsx',
  generates: {
    '.gql/': {
      preset: 'client',
      plugins: [],
      config: {
        scalars: {
          BigInt: 'string',
        },
      },
    },
  },
};

export default config;

Visit graphql-codegen's documentation to see how to install it on your project and automatically generate TypeScript types for all your queries. You can also check out the example app to see a working demonstration.

Choices taken

This library is opinionated and choices were taken in order to make the GraphQL query language work with any contract ABI.

Uint and Int

Mapped to the custom scalar BigInt. It accepts an integer or a string as input and always returns a string in the GraphQL schema.

e.g. ABI:

[
  {
    "constant": true,
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "name": "",
        "type": "uint256"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  totalSupply: BigInt!
}

Bytes, string, address, enum

Mapped to a string.

e.g. ABI:

[
  {
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  name: String!
}

Struct

Mapped to an object using a generated GraphQL type (input or output).

e.g. ABI:

[
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "id",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          }
        ],
        "internalType": "struct ContractName.Category",
        "name": "",
        "type": "tuple"
      }
    ],
    "name": "getMovie",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "id",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "title",
            "type": "string"
          }
        ],
        "internalType": "struct ContractName.Movie",
        "name": "",
        "type": "tuple"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  getMovie(category: ContractName_CategoryInput!): ContractName_Movie!
  # A getMovie_MULT field will also be created, as described in the "Calling the same method multiple times" section
}

input ContractName_CategoryInput {
  id: BigInt!
  name: String!
}

type ContractName_Movie {
  id: BigInt!
  title: String!
}

Void output

Non-mutating functions that return nothing will return the custom scalar Void, which returns null in the GraphQL schema.

e.g. ABI:

[
  {
    "inputs": [],
    "name": "getNothing",
    "outputs": [],
    "stateMutability": "pure",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  getNothing: Void
}

Tuples (input and output)

GraphQL lacks the capability to assign a fixed length to arrays, whether they're used as input or output. Hence, tuples are represented as arrays with unspecified length in the GraphQL schema. However, supplying an array with an incorrect length, as defined by the respective contract ABI, will trigger an error.

e.g. ABI:

[
  {
    "inputs": [
      {
        "internalType": "string[3]",
        "name": "someTuple",
        "type": "string[3]"
      }
    ],
    "name": "getTuple",
    "outputs": [
      {
        "internalType": "string[3]",
        "name": "",
        "type": "string[3]"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  getTuple(someTuple: [String!]!): [String!]!
  # A getTuple_MULT field will also be created, as described in the "Calling the same method multiple times" section
}

Unnamed input arguments

In the GraphQL schema, unnamed input arguments are assigned a generated name following the pattern: argX, where X represents the argument's position within the function arguments.

e.g. ABI:

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "passUnnamedString",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  passUnnamedString(arg0: String!, arg1: BigInt!): String!
  # A passUnnamedString_MULT field will also be created, as described in the "Calling the same method multiple times" section
}

Unnamed output properties

In the GraphQL schema, unnamed output properties are assigned a generated name following the pattern: valueX, where X represents the property's position within the function output.

e.g. ABI:

[
  {
    "inputs": [],
    "name": "getUnnamedValues",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  getUnnamedValues: ContractName_getUnnamedValuesOutput!
}

type ContractName_getUnnamedValuesOutput {
  value0: String!
  value1: BigInt!
}

Overloaded functions

Overloaded functions are treated as separate functions, differentiated by their names suffixed with their respective index within the ABI.

e.g. ABI:

[
  {
    "inputs": [],
    "name": "overloadedFn",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "name": "overloadedFn",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  }
]

Generated GraphQL:

type ContractName {
  overloadedFn0: ContractName_overloadedFnOutput!
  overloadedFn1(arg0: BigInt!): ContractName_overloadedFnOutput!
  # An overloadedFn1_MULT field will also be created, as described in the "Calling the same method multiple times" section
}

type ContractName_overloadedFnOutput {
  id: BigInt!
}