Home

Subgraph Development Tips and Tricks - Graph Protocol

Subgraph is used to provide the data available on the blockchain in a consumable format (basically JSON). It can help to create dashboards and make the data aggregation possible.

Subgraph uses Assembly Script. Some of the basic operations can become difficult to do using the Assembly Script, such as

This article tries to provide easy-to-follow solutions to the common problems

Creating custom types in Subgraph

PROBLEM: In typescript, you can simply do this (but you cannot in Subgraph - Assembly Script)

interface DayMonthYear {
  day: number;
  month: number;
  year: number;
}

SOLUTION: In Subgraph, you need to create a class as shown below

class DayMonthYear {
  day: BigInt;
  month: BigInt;
  year: BigInt;

  constructor(day: BigInt, month: BigInt, year: BigInt) {
    this.day = day;
    this.month = month;
    this.year = year;
  }
}

Handling dates in Subgraph

Dates are not one of the built-in types of Subgraph - Assembly Script

So you need to create your own parser to convert unix timestamp to valid dates. And yes, this could be daunting.

import {
  BigInt,
  ethereum,
} from '@graphprotocol/graph-ts';

function toBigInt(integer: i32): BigInt {
  return BigInt.fromI32(integer);
}

const SECONDS_IN_DAY = BigInt.fromI32(86400);
const ZERO_BI = BigInt.fromI32(0);
const ONE_BI = BigInt.fromI32(1);

class DayMonthYear {
  day: BigInt;
  month: BigInt;
  year: BigInt;

  constructor(day: BigInt, month: BigInt, year: BigInt) {
    this.day = day;
    this.month = month;
    this.year = year;
  }
}

// Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days
export function dayMonthYearFromTimestamp(block: ethereum.Block): DayMonthYear {
  let unixEpoch: BigInt = block.timestamp;

  // you can have leap seconds apparently - but this is good enough for us ;)
  let daysSinceEpochStart = unixEpoch.div(SECONDS_IN_DAY);
  daysSinceEpochStart = daysSinceEpochStart.plus(toBigInt(719468));

  let era: BigInt = (daysSinceEpochStart >= ZERO_BI
    ? daysSinceEpochStart
    : daysSinceEpochStart.minus(toBigInt(146096))
  ).div(toBigInt(146097));

  let dayOfEra: BigInt = daysSinceEpochStart.minus(era.times(toBigInt(146097))); // [0, 146096]

  let yearOfEra: BigInt = dayOfEra
    .minus(dayOfEra.div(toBigInt(1460)))
    .plus(dayOfEra.div(toBigInt(36524)))
    .minus(dayOfEra.div(toBigInt(146096)))
    .div(toBigInt(365)); // [0, 399]

  let year: BigInt = yearOfEra.plus(era.times(toBigInt(400)));

  let dayOfYear: BigInt = dayOfEra.minus(
    toBigInt(365)
      .times(yearOfEra)
      .plus(yearOfEra.div(toBigInt(4)))
      .minus(yearOfEra.div(toBigInt(100)))
  ); // [0, 365]

  let monthZeroIndexed = toBigInt(5)
    .times(dayOfYear)
    .plus(toBigInt(2))
    .div(toBigInt(153)); // [0, 11]

  let day = dayOfYear.minus(
    toBigInt(153)
      .times(monthZeroIndexed)
      .plus(toBigInt(2))
      .div(toBigInt(5))
      .plus(toBigInt(1))
  ); // [1, 31]

  let month = monthZeroIndexed.plus(monthZeroIndexed < toBigInt(10) ? toBigInt(3) : toBigInt(-9)); // [1, 12]

  year = month <= toBigInt(2) ? year.plus(ONE_BI) : year;

  return new DayMonthYear(day, month, year);
}

If you need more functionality, then you can look at the BokkyPooBah's DateTime Library And rewrite the necessary function in Assembly Script

Unique IDs for entities in Subgraph

Every entity should have an unique ID, which is a valid and useful restriction in databases. But this might become tricky when working with smart contracts.

The reason this could become complicated is because

But there is a combination which is guaranteed to be unique - transaction hash and event log

Transaction hash: The transaction hash of the transaction of this log. Log index: The index of this log across all logs in the entire block.

const id = event.transaction.hash.toHexString().concat('-').concat(event.logIndex.toString())

Or you can use the block number instead of transaction hash

const id = event.block.number.toString().concat('-').concat(event.logIndex.toString())

String concatenation in Subgraph

This may not work in some cases

const result = 'a' + 'b'

This works all the time as it uses the functions made available by the language

const result = 'a'.concat('b')

Converting string to bytes32 in Subgraph

import { Bytes } from "@graphprotocol/graph-ts";

const str = Bytes.fromUTF8("hello-world").toHexString().padEnd(66, '0');

Converting string to decimal in Subgraph

import { BigDecimal } from "@graphprotocol/graph-ts";

const num = BigDecimal.fromString("123");

Converting integer to BigInt in Subgraph

import { BigInt } from "@graphprotocol/graph-ts";

const num = BigInt.fromI32(123);

keccak256 and abi.encodePacked in Subgraph

Consider this solidity code with keccak256 and abi.encodePacked

bytes32  a = 'a'
bytes32  b = 'b'
bytes32 result = keccak256(abi.encodePacked(a, b));

You can do the same in subgraph using the following code

import { Bytes, BigInt, ByteArray, ethereum } from '@graphprotocol/graph-ts'

function concat(a: ByteArray, b: ByteArray): ByteArray {
  let out = new Uint8Array(a.length + b.length)
  for (let i = 0; i < a.length; i++) {
    out[i] = a[i]
  }
  for (let j = 0; j < b.length; j++) {
    out[a.length + j] = b[j]
  }

  return changetype<ByteArray>(out)
}

function main () {
  const a = Bytes.fromUTF8("a").toHexString().padEnd(66, '0')
  const b = Bytes.fromUTF8("b").toHexString().padEnd(66, '0')

  const result = crypto.keccak256(
    concat(
      Bytes.fromHexString(a),
      Bytes.fromHexString(b)
    )
  )
}


Last Updated on

Next Post: GraphProtocol: TS2322 null assignment in Subgraph →

Comments