Skip to main content

AssemblyScript API

Note: if you created a subgraph prior to graph-cli/graph-ts version 0.23.0, you're using an older version of AssemblyScript, we recommend taking a look at the Migration Guide

This page documents what built-in APIs can be used when writing subgraph mappings. Two kinds of APIs are available out of the box:

It is also possible to add other libraries as dependencies, as long as they are compatible with AssemblyScript. Since this is the language mappings are written in, the AssemblyScript wiki is a good source for language and standard library features.

Installation#

Subgraphs created with graph init come with preconfigured dependencies. All that is required to install these dependencies is to run one of the following commands:

yarn install # Yarnnpm install  # NPM

If the subgraph was created from scratch, one of the following two commands will install the Graph TypeScript library as a dependency:

yarn add --dev @graphprotocol/graph-ts         # Yarnnpm install --save-dev @graphprotocol/graph-ts # NPM

API Reference#

The @graphprotocol/graph-ts library provides the following APIs:

  • An ethereum API for working with Ethereum smart contracts, events, blocks, transactions, and Ethereum values.
  • A store API to load and save entities from and to the Graph Node store.
  • A log API to log messages to the Graph Node output and the Graph Explorer.
  • An ipfs API to load files from IPFS.
  • A json API to parse JSON data.
  • A crypto API to use cryptographic functions.
  • Low-level primitives to translate between different type systems such as Ethereum, JSON, GraphQL and AssemblyScript.

Built-in Types#

Documentation on the base types built into AssemblyScript can be found in the AssemblyScript wiki.

The following additional types are provided by @graphprotocol/graph-ts.

ByteArray#

import { ByteArray } from '@graphprotocol/graph-ts'

ByteArray represents an array of u8.

Construction

  • fromI32(x: i32): ByteArray - Decomposes x into bytes.
  • fromHexString(hex: string): ByteArray - Input length must be even. Prefixing with 0x is optional.

Type conversions

  • toHexString(): string - Converts to a hex string prefixed with 0x.
  • toString(): string - Interprets the bytes as a UTF-8 string.
  • toBase58(): string - Encodes the bytes into a base58 string.
  • toU32(): u32 - Interprets the bytes as a little-endian u32. Throws in case of overflow.
  • toI32(): i32 - Interprets the byte array as a little-endian i32. Throws in case of overflow.

Operators

  • equals(y: ByteArray): bool – can be written as x == y.

BigDecimal#

import { BigDecimal } from '@graphprotocol/graph-ts'

BigDecimal is used to represent arbitrary precision decimals.

Construction

  • constructor(bigInt: BigInt) – creates a BigDecimal from an BigInt.
  • static fromString(s: string): BigDecimal – parses from a decimal string.

Type conversions

  • toString(): string – prints to a decimal string.

Math

  • plus(y: BigDecimal): BigDecimal – can be written as x + y.
  • minus(y: BigDecimal): BigDecimal – can be written as x - y.
  • times(y: BigDecimal): BigDecimal – can be written as x * y.
  • dividedBy(y: BigDecimal): BigDecimal – can be written as x / y.
  • equals(y: BigDecimal): bool – can be written as x == y.
  • notEqual(y: BigDecimal): bool – can be written as x != y.
  • lt(y: BigDecimal): bool – can be written as x < y.
  • le(y: BigDecimal): bool – can be written as x <= y.
  • gt(y: BigDecimal): bool – can be written as x > y.
  • ge(y: BigDecimal): bool – can be written as x >= y.
  • neg(): BigDecimal - can be written as -x.

BigInt#

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

BigInt is used to represent big integers. This includes Ethereum values of type uint32 to uint256 and int64 to int256. Everything below uint32, such as int32, uint24 or int8 is represented as i32.

The BigInt class has the following API:

Construction

  • BigInt.fromI32(x: i32): BigInt – creates a BigInt from an i32.

  • BigInt.fromString(s: string): BigInt– Parses a BigInt from a string.

  • BigInt.fromUnsignedBytes(x: Bytes): BigInt – Interprets bytes as an unsigned, little-endian integer. If your input is big-endian, call .reverse() first.

  • BigInt.fromSignedBytes(x: Bytes): BigInt – Interprets bytes as a signed, little-endian integer. If your input is big-endian, call .reverse() first.

    Type conversions

  • x.toHex(): string – turns BigInt into a string of hexadecimal characters.

  • x.toString(): string – turns BigInt into a decimal number string.

  • x.toI32(): i32 – returns the BigInt as an i32; fails if it the value does not fit into i32. It's a good idea to first check x.isI32().

  • x.toBigDecimal(): BigDecimal - converts into a decimal with no fractional part.

Math

  • x.plus(y: BigInt): BigInt – can be written as x + y.
  • x.minus(y: BigInt): BigInt – can be written as x - y.
  • x.times(y: BigInt): BigInt – can be written as x * y.
  • x.dividedBy(y: BigInt): BigInt – can be written as x / y.
  • x.mod(y: BigInt): BigInt – can be written as x % y.
  • x.equals(y: BigInt): bool – can be written as x == y.
  • x.notEqual(y: BigInt): bool – can be written as x != y.
  • x.lt(y: BigInt): bool – can be written as x < y.
  • x.le(y: BigInt): bool – can be written as x <= y.
  • x.gt(y: BigInt): bool – can be written as x > y.
  • x.ge(y: BigInt): bool – can be written as x >= y.
  • x.neg(): BigInt – can be written as -x.
  • x.divDecimal(y: BigDecimal): BigDecimal – divides by a decimal, giving a decimal result.
  • x.isZero(): bool – Convenience for checking if the number is zero.
  • x.isI32(): bool – Check if the number fits in an i32.
  • x.abs(): BigInt – Absolute value.
  • x.pow(exp: u8): BigInt – Exponentiation.
  • bitOr(x: BigInt, y: BigInt): BigInt – can be written as x | y.
  • bitAnd(x: BigInt, y: BigInt): BigInt – can be written as x & y.
  • leftShift(x: BigInt, bits: u8): BigInt – can be written as x << y.
  • rightShift(x: BigInt, bits: u8): BigInt – can be written as x >> y.

TypedMap#

import { TypedMap } from '@graphprotocol/graph-ts'

TypedMap can be used to stored key-value pairs. See this example.

The TypedMap class has the following API:

  • new TypedMap<K, V>() – creates an empty map with keys of type K and values of type T
  • map.set(key: K, value: V): void – sets the value of key to value
  • map.getEntry(key: K): TypedMapEntry<K, V> | null – returns the key-value pair for a key or null if the key does not exist in the map
  • map.get(key: K): V | null – returns the value for a key or null if the key does not exist in the map
  • map.isSet(key: K): bool – returns true if the key exists in the map and false if it does not

Bytes#

import { Bytes } from '@graphprotocol/graph-ts'

Bytes is used to represent arbitrary-length arrays of bytes. This includes Ethereum values of type bytes, bytes32 etc.

The Bytes class extends AssemblyScript's Uint8Array and this supports all the Uint8Array functionality, plus the following new methods:

  • b.toHex() – returns a hexadecimal string representing the bytes in the array
  • b.toString() – converts the bytes in the array to a string of unicode characters
  • b.toBase58() – turns an Ethereum Bytes value to base58 encoding (used for IPFS hashes)

Address#

import { Address } from '@graphprotocol/graph-ts'

Address extends Bytes to represent Ethereum address values.

It adds the following method on top of the Bytes API:

  • Address.fromString(s: string): Address – creates an Address from a hexadecimal string

Store API#

import { store } from '@graphprotocol/graph-ts'

The store API allows to load, save and remove entities from and to the Graph Node store.

Entities written to the store map one-to-one to the @entity types defined in the subgraph's GraphQL schema. To make working with these entities convenient, the graph codegen command provided by the Graph CLI generates entity classes, which are subclasses of the built-in Entity type, with property getters and setters for the fields in the schema as well as methods to load and save these entities.

Creating entities#

The following is a common pattern for creating entities from Ethereum events.

// Import the Transfer event class generated from the ERC20 ABIimport { Transfer as TransferEvent } from '../generated/ERC20/ERC20'
// Import the Transfer entity type generated from the GraphQL schemaimport { Transfer } from '../generated/schema'
// Transfer event handlerexport function handleTransfer(event: TransferEvent): void {  // Create a Transfer entity, using the hexadecimal string representation  // of the transaction hash as the entity ID  let id = event.transaction.hash.toHex()  let transfer = new Transfer(id)
  // Set properties on the entity, using the event parameters  transfer.from = event.params.from  transfer.to = event.params.to  transfer.amount = event.params.amount
  // Save the entity to the store  transfer.save()}

When a Transfer event is encountered while processing the chain, it is passed to the handleTransfer event handler using the generated Transfer type (aliased to TransferEvent here to avoid a naming conflict with the entity type). This type allows accessing data such as the event's parent transaction and its parameters.

Each entity must have a unique ID to avoid collisions with other entities. It is fairly common for event parameters to include a unique identifier that can be used. Note: Using the transaction hash as the ID assumes that no other events in the same transaction create entities with this hash as the ID.

Loading entities from the store#

If an entity already exists, it can be loaded from the store with the following:

let id = event.transaction.hash.toHex() // or however the ID is constructedlet transfer = Transfer.load(id)if (transfer == null) {  transfer = new Transfer(id)}
// Use the Transfer entity as before

As the entity may not exist in the store yet, the load method returns a value of type Transfer | null. It may thus be necessary to check for the null case before using the value.

Note: Loading entities is only necessary if the changes made in the mapping depend on the previous data of an entity. See the next section for the two ways of updating existing entities.

Updating existing entities#

There are two ways to update an existing entity:

  1. Load the entity with e.g. Transfer.load(id), set properties on the entity, then .save() it back to the store.
  2. Simply create the entity with e.g. new Transfer(id), set properties on the entity, then .save() it to the store. If the entity already exists, the changes are merged into it.

Changing properties is straight forward in most cases, thanks to the generated property setters:

let transfer = new Transfer(id)transfer.from = ...transfer.to = ...transfer.amount = ...

It is also possible to unset properties with one of the following two instructions:

transfer.from.unset()transfer.from = null

This only works with optional properties, i.e. properties that are declared without a ! in GraphQL. Two examples would be owner: Bytes or amount: BigInt.

Updating array properties is a little more involved, as the getting an array from an entity creates a copy of that array. This means array properties have to be set again explicitly after changing the array. The following assumes entity has a numbers: [BigInt!]! field.

// This won't workentity.numbers.push(BigInt.fromI32(1))entity.save()
// This will worklet numbers = entity.numbersnumbers.push(BigInt.fromI32(1))entity.numbers = numbersentity.save()

Removing entities from the store#

There is currently no way to remove an entity via the generated types. Instead, removing an entity requires passing the name of the entity type and the entity ID to store.remove:

import { store } from '@graphprotocol/graph-ts'...let id = event.transaction.hash.toHex()store.remove('Transfer', id)

Ethereum API#

The Ethereum API provides access to smart contracts, public state variables, contract functions, events, transactions, blocks and the encoding/decoding Ethereum data.

Support for Ethereum Types#

As with entities, graph codegen generates classes for all smart contracts and events used in a subgraph. For this, the contract ABIs need to be part of the data source in the subgraph manifest. Typically, the ABI files are stored in an abis/ folder.

With the generated classes, conversions between Ethereum types and the built-in types take place behind the scenes so that subgraph authors do not have to worry about them.

The following example illustrates this. Given a subgraph schema like

type Transfer @entity {  from: Bytes!  to: Bytes!  amount: BigInt!}

and a Transfer(address,address,uint256) event signature on Ethereum, the from, to and amount values of type address, address and uint256 are converted to Address and BigInt, allowing them to be passed on to the Bytes! and BigInt! properties of the Transfer entity:

let id = event.transaction.hash.toHex()let transfer = new Transfer(id)transfer.from = event.params.fromtransfer.to = event.params.totransfer.amount = event.params.amounttransfer.save()

Events and Block/Transaction Data#

Ethereum events passed to event handlers, such as the Transfer event in the previous examples, not only provide access to the event parameters but also to their parent transaction and the block they are part of. The following data can be obtained from event instances (these classes are a part of the ethereum module in graph-ts):

class Event {  address: Address  logIndex: BigInt  transactionLogIndex: BigInt  logType: string | null  block: Block  transaction: Transaction  parameters: Array<EventParam>}
class Block {  hash: Bytes  parentHash: Bytes  unclesHash: Bytes  author: Address  stateRoot: Bytes  transactionsRoot: Bytes  receiptsRoot: Bytes  number: BigInt  gasUsed: BigInt  gasLimit: BigInt  timestamp: BigInt  difficulty: BigInt  totalDifficulty: BigInt  size: BigInt | null}
class Transaction {  hash: Bytes  index: BigInt  from: Address  to: Address | null  value: BigInt  gasLimit: BigInt  gasPrice: BigInt  input: Bytes}

Access to Smart Contract State#

The code generated by graph codegen also includes classes for the smart contracts used in the subgraph. These can be used to access public state variables and call functions of the contract at the current block.

A common pattern is to access the contract from which an event originates. This is achieved with the following code:

// Import the generated contract classimport { ERC20Contract } from '../generated/ERC20Contract/ERC20Contract'// Import the generated entity classimport { Transfer } from '../generated/schema'
export function handleTransfer(event: Transfer) {  // Bind the contract to the address that emitted the event  let contract = ERC20Contract.bind(event.address)
  // Access state variables and functions by calling them  let erc20Symbol = contract.symbol()}

As long as the ERC20Contract on Ethereum has a public read-only function called symbol, it can be called with .symbol(). For public state variables a method with the same name is created automatically.

Any other contract that is part of the subgraph can be imported from the generated code and can be bound to a valid address.

Handling Reverted Calls#

If the read-only methods of your contract may revert, then you should handle that by calling the generated contract method prefixed with try_. For example, the Gravity contract exposes the gravatarToOwner method. This code would be able to handle a revert in that method:

let gravity = Gravity.bind(event.address);let callResult = gravity.try_gravatarToOwner(gravatar);if (callResult.reverted) {  log.info('getGravatar reverted', []);} else {  let owner = callResult.value;}

Note that a Graph node connected to a Geth or Infura client may not detect all reverts, if you rely on this we recommend using a Graph node connected to a Parity client.

Encoding/Decoding ABI#

Data can be encoded and decoded according to Ethereum's ABI encoding format using the encode and decode functions in the ethereum module.

import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts";
let tupleArray: Array<ethereum.Value> = [  ethereum.Value.fromAddress(Address.fromString("0x0000000000000000000000000000000000000420")),  ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(62))];
let tuple = tupleArray as ethereum.Tuple;
let encoded = ethereum.encode(ethereum.Value.fromTuple(tuple))!;
let decoded = ethereum.decode("(address,uint256)", encoded);

For more information:

Logging and Debugging#

import { log } from '@graphprotocol/graph-ts'

The log API allows subgraphs to log information to the Graph Node standard output as well as the Graph Explorer. Messages can be logged using different log levels. A basic format string syntax is provided to compose log messages from argument.

The log API includes the following functions:

  • log.debug(fmt: string, args: Array<string>): void - logs a debug message.
  • log.info(fmt: string, args: Array<string>): void - logs an informational message.
  • log.warning(fmt: string, args: Array<string>): void - logs a warning.
  • log.error(fmt: string, args: Array<string>): void - logs an error message.
  • log.critical(fmt: string, args: Array<string>): void – logs a critical message and terminates the subgraph.

The log API takes a format string and an array of string values. It then replaces placeholders with the string values from the array. The first {} placeholder gets replaced by the first value in the array, the second {} placeholder gets replaced by the second value and so on.

log.info('Message to be displayed: {}, {}, {}', [  value.toString(),  anotherValue.toString(),  'already a string',])

Logging one or more values#

Logging a single value#

In the example below, the string value "A" is passed into an array to become['A'] before being logged:

let myValue = 'A'
export function handleSomeEvent(event: SomeEvent): void {  // Displays : "My value is: A"  log.info('My value is: {}', [myValue])}

Logging a single entry from an existing array#

In the example below, only the first value of the argument array is logged, despite the array containing three values.

let myArray = ['A', 'B', 'C']
export function handleSomeEvent(event: SomeEvent): void {  // Displays : "My value is: A"  (Even though three values are passed to `log.info`)  log.info('My value is: {}', myArray)}

Logging multiple entries from an existing array#

Each entry in the arguments array requires its own placeholder {} in the log message string. The below example contains three placeholders {} in the log message. Because of this, all three values in myArray are logged.

let myArray = ['A', 'B', 'C']
export function handleSomeEvent(event: SomeEvent): void {  // Displays : "My first value is: A, second value is: B, third value is: C"  log.info(    'My first value is: {}, second value is: {}, third value is: {}',    myArray,  )}

Logging a specific entry from an existing array#

To display a specific value in the array, the indexed value must be provided.

export function handleSomeEvent(event: SomeEvent): void {  // Displays : "My third value is C"  log.info('My third value is: {}', [myArray[2]])}

Logging event information#

The example below logs the block number, block hash and transaction hash from an event:

import { log } from '@graphprotocol/graph-ts'
export function handleSomeEvent(event: SomeEvent): void {  log.debug('Block number: {}, block hash: {}, transaction hash: {}', [    event.block.number.toString(), // "47596000"    event.block.hash.toHexString(), // "0x..."    event.transaction.hash.toHexString(), // "0x..."  ])}

IPFS API#

import { ipfs } from '@graphprotocol/graph-ts'

Smart contracts occasionally anchor IPFS files on chain. This allows mappings to obtain the IPFS hashes from the contract and read the corresponding files from IPFS. The file data will be returned as Bytes, which usually requires further processing, e.g. with the json API documented later on this page.

Given an IPFS hash or path, reading a file from IPFS is done as follows:

// Put this inside an event handler in the mappinglet hash = 'QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D'let data = ipfs.cat(hash)
// Paths like `QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D/Makefile`// that include files in directories are also supportedlet path = 'QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D/Makefile'let data = ipfs.cat(path)

Note: ipfs.cat is not deterministic at the moment. If the file cannot be retrieved over the IPFS network before the request times out, it will return null. Due to this, it's always worth checking the result for null. To ensure that files can be retrieved, they have to be pinned to the IPFS node that Graph Node connects to. On the hosted service, this is https://api.thegraph.com/ipfs/. See the IPFS pinning section for more information.

It is also possible to process larger files in a streaming fashion with ipfs.map. The function expects the hash or path for an IPFS file, the name of a callback, and flags to modify its behavior:

import { JSONValue, Value } from '@graphprotocol/graph-ts'
export function processItem(value: JSONValue, userData: Value): void {  // See the JSONValue documentation for details on dealing  // with JSON values  let obj = value.toObject()  let id = obj.get('id')  let title = obj.get('title')
  if (!id || !title) {    return  }
  // Callbacks can also created entities  let newItem = new Item(id.toString())  newItem.title = title.toString()  newitem.parent = userData.toString() // Set parent to "parentId"  newitem.save()}
// Put this inside an event handler in the mappingipfs.map('Qm...', 'processItem', Value.fromString('parentId'), ['json'])
// Alternatively, use `ipfs.mapJSON`ipfs.mapJSON('Qm...', 'processItem', Value.fromString('parentId'))

The only flag currently supported is json, which must be passed to ipfs.map. With the json flag, the IPFS file must consist of a series of JSON values, one value per line. The call to ipfs.map will read each line in the file, deserialize it into a JSONValue and call the callback for each of them. The callback can then use entity operations to store data from the JSONValue. Entity changes are stored only when the handler that called ipfs.map finishes successfully; in the meantime, they are kept in memory, and the size of the file that ipfs.map can process is therefore limited.

On success, ipfs.map returns void. If any invocation of the callback causes an error, the handler that invoked ipfs.map is aborted, and the subgraph is marked as failed.

Crypto API#

import { crypto } from '@graphprotocol/graph-ts'

The crypto API makes a cryptographic functions available for use in mappings. Right now, there is only one:

  • crypto.keccak256(input: ByteArray): ByteArray

JSON API#

import { json, JSONValueKind } from '@graphprotocol/graph-ts'

JSON data can be parsed using the json API:

  • json.fromBytes(data: Bytes): JSONValue – parses JSON data from a Bytes array

The JSONValue class provides a way to pull values out of an arbitrary JSON document. Since JSON values can be booleans, numbers, arrays and more, JSONValue comes with a kind property to check the type of a value:

let value = json.fromBytes(...)if (value.kind == JSONValueKind.BOOL) {  ...}

In addition, there is a method to check if the value is null:

  • value.isNull(): boolean

When the type of a value is certain, it can be converted to a built-in type using one of the following methods:

  • value.toBool(): boolean
  • value.toI64(): i64
  • value.toF64(): f64
  • value.toBigInt(): BigInt
  • value.toString(): string
  • value.toArray(): Array<JSONValue> - (and then convert JSONValue with one of the 5 methods above)

Type Conversions Reference#

Source (s)DestinationConversion function
AddressBytesnone
AddressIDs.toHexString()
AddressStrings.toHexString()
BigDecimalStrings.toString()
BigIntBigDecimals.toBigDecimal()
BigIntString (hexadecimal)s.toHexString() or s.toHex()
BigIntString (unicode)s.toString()
BigInti32s.toI32()
BooleanBooleannone
Bytes (signed)BigIntBigInt.fromSignedBytes(s)
Bytes (unsigned)BigIntBigInt.fromUnsignedBytes(s)
BytesString (hexadecimal)s.toHexString() or s.toHex()
BytesString (unicode)s.toString()
BytesString (base58)s.toBase58()
Bytesi32s.toI32()
Bytesu32s.toU32()
BytesJSONjson.fromBytes(s)
int8i32none
int32i32none
int32BigIntBigint.fromI32(s)
uint24i32none
int64 - int256BigIntnone
uint32 - uint256BigIntnone
JSONbooleans.toBool()
JSONi64s.toI64()
JSONu64s.toU64()
JSONf64s.toF64()
JSONBigInts.toBigInt()
JSONstrings.toString()
JSONArrays.toArray()
JSONObjects.toObject()
StringAddressAddress.fromString(s)
StringBigDecimalBigDecimal.fromString(s)
String (hexadecimal)BytesByteArray.fromHexString(s)
String (UTF-8)BytesByteArray.fromUTF8(s)

Data Source Metadata#

You can inspect the contract address, network and context of the data source that invoked the handler through the dataSource namespace:

  • dataSource.address(): Address
  • dataSource.network(): string
  • dataSource.context(): DataSourceContext

Entity and DataSourceContext#

The base Entity class and the child DataSourceContext class have helpers to dynamically set and get fields:

  • setString(key: string, value: string): void
  • setI32(key: string, value: i32): void
  • setBigInt(key: string, value: BigInt): void
  • setBytes(key: string, value: Bytes): void
  • setBoolean(key: string, value: bool): void
  • setBigDecimal(key, value: BigDecimal): void
  • getString(key: string): string
  • getI32(key: string): i32
  • getBigInt(key: string): BigInt
  • getBytes(key: string): Bytes
  • getBoolean(key: string): boolean
  • getBigDecimal(key: string): BigDecimal