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
- String concatenation
- Date manipulation
- Creating new types or interfaces
- Other common issues
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
- One transaction can emit multiple same events, therefore
event.transaction.hash
is not reliable - One block can have multiple events, so
event.block.number
is also not useful
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
Comments