What you’ll need

  1. The contract address you’re interested in indexing.
  2. The ABI (Application Binary Interface) of the contract.

Walkthrough

1

Getting the ABI

If the contract you’re interested in indexing is a contract you deployed, then you’ll have the contract address and ABI handy. Otherwise, you can use a mix of public explorer tools to find this information. For example, if we’re interested in indexing the friend.tech contract…

  1. Find the contract address from Dappradar
  2. Click through to the block explorer where the ABI can be found under the Contract ABI section. You can also click here to download it.

Save the ABI to your local file system and make a note of the contract address. Also make a note of the block number the contract was deployed at, you’ll need this at a later step.

2

Creating the configuration file

The next step is to create the Instant Subgraph configuration file (e.g. friendtech-config.json). This file consists of five key sections:

  1. Config version number
  2. Config name
  3. ABIs
  4. Chains
  5. Contract instances

Version number

As of October 2023, our Instant Subgraph configuration system is on version 1. This may change in the future. This is not the version number of your subgraph, but of Goldsky’s configuration file format.

Config name

This is a name of your choice that helps you understand what this config is for. It is only used for internal debugging. For this guide, we’ll use friendtech.

ABIs, chains, and contract instances

These three sections are interconnected.

  1. Name your ABI and enter the path to the ABI file you saved earlier (relative to where this config file is located). In this case, ftshares and abi.json.
  2. Write out the contract instance, referencing the ABI you named earlier, address it’s deployed at, chain it’s on, the start block.
{
  "version": "1",
  "name": "friendtech",
  "abis": {
    "ftshares": {
      "path": "./abi.json"
    }
  },
  "instances": [
    {
      "abi": "ftshares",
      "address": "0xCF205808Ed36593aa40a44F10c7f7C2F67d4A4d4",
      "startBlock": 2430440,
      "chain": "base"
    }
  ]
}

The abi name in instances should match a key in abis, in this example, ftshares. It is possible to have more than one chains and more than one ABI. Multiple chains will result in multiple subgraphs. The file abi.json in this example should contain the friendtech ABI downloaded from here.

This configuration can handle multiple contracts with distinct ABIs, the same contract across multiple chains, or multiple contracts with distinct ABIs on multiple chains.

For a complete reference of the various properties, please see the Instant Subgraphs references docs

3

Deploying the subgraph

With your configuration file ready, it’s time to deploy the subgraph.

  1. Open the CLI and log in to your Goldsky account with the command: goldsky login.
  2. Deploy the subgraph using the command: goldsky subgraph deploy name/version --from-abi <path-to-config-file>, then pass in the path to the config file you created. Note - do NOT pass in the ABI itself, but rather the config file defined above. Example: goldsky subgraph deploy friendtech/1.0 --from-abi friendtech-config.json

Goldsky will generate all the necessary subgraph code, deploy it, and return an endpoint that you can start querying.

Clicking the endpoint link takes you to a web client where you can browse the schema and draft queries to integrate into your app.

Extending your subgraph with enrichments

Enrichments are a powerful way to add additional data to your subgraph by performing eth calls in the middle of an event or call handler.

See the enrichments configuration reference for more information on how to define these enrichments, and for an example configuration with enrichments.

Concepts

  • Enrichments are defined at the instance level, and executed at the trigger handler level. This means that you can have different enrichments for different data sources or templates and that all enrichment executions are isolated to the handler they are being called from.
    • any additional imports from @graphprotocol/graph-ts beyond BigInt, Bytes, and store can be declared in the options.imports field of the enrichment (e.g., BigDecimal).
  • Enrichments always begin by performing all eth calls first, if any eth calls are aborted then the enrichment as a whole is aborted.
    • calls marked as required or having another call declare them as a depends_on dependency will abort if the call is not successful, otherwise the call output value will remain as null.
    • calls support pre and post expressions for conditions to test before and after the call, if either fails the call is aborted. Since these are expressions, they can be dynamic or constant values.
    • call source is an expression and therefore allows for dynamic values using math or concatenations. If the source is simply a contract address then it will be automatically converted to an Address type.
    • call params is an expression list and can also be dynamic values or constants.
  • Enrichments support defining new entities as well as updating existing entities. If the entity name matches the trigger entity name, then the entity field mappings will be applied to the existing entity.
    • entity field mapping values are expressions and can be dynamic or constant values.
    • new enrichment entities are linked to the parent (trigger) entity that created them, with the parent (trigger) entity also linking to the new entity or entities in the opposite direction (always a collection type).
    • note that while you can define existing entities that are not the trigger entity, you may not update existing entities only create new instances of that entity.
    • entities support being created multiple times in a single enrichment, but require a unique id expression to be defined for each entity, id can by a dynamic value or a constant.

Examples

Below are some various examples of configurations for different scenarios. To keep each example brief, we will only show the enrich section of the configuration, and in most cases only the part of the enrich section that is relevant. See the enrichments configuration reference for the full configuration reference.

Options

Here we are enabling debugging for the enrichment (this will output the enrichment steps to the subgraph log), as well as importing BigDecimal for use in a calls or entities section.

"enrich": {
  "options": {
    "imports": ["BigDecimal"],
    "debugging": true
  }
}

Call self

Here we are calling a function on the same contract as the trigger event. This means we can omit the abi and source configuration fields, as they are implied in this scenario, we only need to include the name and params fields (if the function declares paramters). We can refer to the result of this call using calls.balance.

"calls": {
  "balance": {
    "name": "balanceOf",
    "params": "event.params.owner"
  }
}

Call dependency

Here we are creating a 2-call dependency, where the second call depends on the first call (the params are calls.owner meaning we need the value of the owner call before we can invoke balanceOf). This means that if the first call fails, the second call will not be executed. Calls are always executed in the order they are configured, so the second call will have access to the output of the first call (in this example, we use that output as a parameter to the second call). We can list multiple calls in the depends_on array to create a dependency graph (if needed). Adding a call to the depends_on array will not automatically re-order the calls, so be sure to list them in the correct order.

"calls": {
  "owner": {
    "name": "ownerOf",
    "params": "event.params.id"
  },
  "balance": {
    "depends_on": ["owner"],
    "name": "balanceOf",
    "params": "calls.owner"
  }
}

External contract call for known address

Here we are calling a function on an external contract, where we know the address of the contract ahead of time. In this case, we need to include the abi and source configuration fields.

"calls": {
  "usdc_balance": {
    "abi": "erc20",
    "source": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "name": "balanceOf",
    "params": "event.params.owner"
  }
}

External contract call for dynamic address

Here we are setting up a 2 call chain to first determine the contract address, then call a function on that contract. In our example, the contractAddress function is returning an Address type so we can use the call result directly in the source field of the second call. If contractAddress was instead returning a string type, then we would use "source": "Address.fromString(calls.contract_address)", though this would be an unusual case to observe.

"calls": {
  "contract_address": {
    "name": "contractAddress",
    "params": "event.params.id"
  },
  "balance": {
    "depends_on": ["contract_address"],
    "abi": "erc20",
    "source": "calls.contract_address",
    "name": "balanceOf",
    "params": "event.params.owner"
  }
}

Required call

Here we are marking a call as required, meaning that if the call fails then the enrichment as a whole will be aborted. This is useful when you do not want to create a new entity (or enrich an existing entity) if the call does not return any meaningful data. Also note that when using depends_on, the dependency call is automatically marked as required. This should be used when the address of the contract being called may not always implement the function being called.

"calls": {
  "balance": {
    "abi": "erc20",
    "name": "balanceOf",
    "source": "event.params.address",
    "params": "event.params.owner",
    "required": true
  }
}

Pre and post conditions

Here we are using conditions to prevent a call from being executed or to abort the enrichment if the call result is not satisfactory. Avoiding an eth call can have a big performance impact if the inputs to the call are often invalid. Avoiding the creation of an entity can save on entity counts if the entity is not needed or useful for various call results. Conditions are simply checked at their target site in the enrichment, and evaluated to its negation to check if an abort is necessary (e.g., true becomes !(true), which is always false and therefore never aborts). In this example, we’re excluding the call if the owner is in a deny list, and we’re aborting the enrichment if the balance is 0.

"calls": {
  "balance": {
    "name": "balanceOf",
    "params": "event.params.owner",
    "conditions": {
      "pre": "![Address.zero().toHexString(), \"0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03\"].includes(event.params.owner.toHexString())",
      "post": "result.value.gt(BigInt.zero())"
    }
  }
}

Simple entity field mapping constant

Here were are simply replicating the id field from the event params into our enrichment entity. This can be useful if you want to filter or sort the enrichment entities by this field.

  "MyEntity": {
    "id uint256": "event.params.id"
  },

Simple entity field mapping expression

Here we are applying a serialization function to the value of a call result. This is necessary as the enrichment code generator does not resolve the effective type of an expression, so if there is a type mismatch a serialization function must be applied (in this case String vs Address).

  "MyEntity": {
    "owner address": "calls.owner.toHexString()"
  },

Complex entity field mapping expression

Here we are conditionally setting the value of usd_balance on whether or not the usdc_balance call was successful. If the call was not successful, then we set the value to BigDecimal.zero(), otherwise we divide the call result by 10^6 (USDC decimals) to convert the balance to a USD value.

  "MyEntity": {
    "usd_balance fixed": "calls.usdc_balance === null ? BigDecimal.zero() : calls.usdc_balance!.divDecimal(BigInt.fromU32(10).pow(6).toBigDecimal())"
  },

Can't find what you're looking for? Reach out to us at support@goldsky.com for help.