AssemblyScript API Part 2

Storage API

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

The storage API allows to load, save, and delete entities from the HyperGraph node storage.

There is a one-to-one correspondence between the entities written in the storage mapping table and the @entity type defined in the schema.graphql file of the subgraph. In order to facilitate the use of these entities, Graph CLI (HyperGraph, please use the graph codegen command provided by @hgdotnetwork/graph-cli) to generate entity classes, which are subclasses of the built-in Entity type, with the attribute getters and setters of the fields in the Schema and load ) And save (save) methods to manipulate these entities.

Create entity

The following are common patterns for creating entities based on Ethereum events.

// Import the Transfer event class generated from the ERC20 ABI
import { Transfer as TransferEvent } from '../generated/ERC20/ERC20'

// Import the Transfer entity type generated from the GraphQL schema
import { Transfer } from '../generated/schema'

// Transfer event handler
export 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 processing blockchain data encounters a Transfer event, it will use the generated Transfer type (actually an alias of TransferEvent to avoid naming conflicts with the entity type) and pass it to the handleTransfer event handler. This type allows access to data, such as the parent transaction of the event and its parameters.

Each entity must have a unique ID to avoid conflicts with other entities. It is quite common for event parameters to contain unique identifiers that can be used. Note: Using the transaction hash as the ID assumes that no other event in the same transaction creates an entity that uses the hash as the ID.

Loading an entity from storage If the entity already exists, you can load it from storage using the following methods:

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

// Use the Transfer entity as before

Since the entity may not yet exist in storage, the type returned by the load method is Transfer or null. Therefore, it may be necessary to check whether the value is null before using it.

Note: You only need to load the entity if the changes made during the mapping implementation depend on the previous data of the entity. See the next section for two ways to update existing entities.

Updating existing entities There are two ways to update existing entities:

Load the entity, such as Transfer.load(id), set properties on the entity, and then .save() save the updated entity to storage.

Just use new Transfer(id) to create an entity, set properties on the entity, and then use .save() to save the entity to storage. If the entity already exists, the changes will be merged into it. In most cases, because the property setter (setter method) is generated, changing the property is simple:

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

You can also use one of the following two commands to unset attributes:

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

This only applies to optional attributes, that is, attributes without! In the declared attributes.

This only applies to optional properties, that is, properties without! In the declared properties of GraphQL. Two examples of such attributes are:

owner:Bytes or amount:BigInt

Updating the array properties takes more effort, because getting the array from the entity creates a copy of the array. This means that you must explicitly set the array properties again after changing the array. The following assumes that the entity has a numeric array field: [BigInt!]! field.

// This won't work
entity.numbers.push(BigInt.fromI32(1))
entity.save()

// This will work
let numbers = entity.numbers
numbers.push(BigInt.fromI32(1))
entity.numbers = numbers
entity.save()

Deleting entities from storage Currently, entities cannot be deleted from the generated type. On the contrary, to delete an entity, you need to pass the name of the entity type and the entity ID to store.remove to delete:

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, and blocks.

Support for Ethereum types Like entities, the graph codegen command generates corresponding classes for all smart contracts and events used in the subgraph. For this, the contract ABI must be part of the data source in the subgraph declaration list.

Usually, ABI files are stored in the abis/ folder. Using the generated class, the conversion between the Ethereum type and the built-in type is performed in the underlying mechanism, so the subgraph developer does not need to care.

The following example illustrates this. Given a schema definition of a subgraph, such as

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

Then there is a Transfer(address,address,uint256) event signature on Ethereum, three parameters: from, to and Amount, the types are address, address and uint256 respectively. In data processing, address and uint256 will be converted to Address and BigInt, so that they can be passed 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.from
transfer.to = event.params.to
transfer.amount = event.params.amount
transfer.save()

Event, block/transaction data

The Ethereum event passed to the event handler, such as the Transfer event in the previous example, not only provides access to the event parameters, but also access to its parent transaction and the block to which it belongs. The following data can be obtained from the event instance (these classes are 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
  gasUsed: BigInt
  gasPrice: BigInt
  input: Bytes
}

Access to smart contract status The code generated by the graph codegen command also includes the smart contract classes used in the subgraph. These can be used to access public state variables and call other methods of the smart contract in the current block. A common pattern is the smart contract that accesses the origin of the event. This can be achieved with the following code:

// Import the generated contract class
import { ERC20Contract } from '../generated/ERC20Contract/ERC20Contract'
// Import the generated entity class
import { 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()
}

The ERC20 smart contract on Ethereum has a public read-only function called symbol, which can be called using .symbol(). For public state variables, a method with the same name will be automatically created. Any other contract of the subgraph can be imported from the generated code and can be bound to a valid address.

Logging and debugging

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

The log API allows the subgraph to log information to the standard output of the HyperGraph node and the Graph browser. You can use different log levels to log messages. A basic format string syntax is provided to compose log messages based on parameter variables.

The log API includes the following functions: log.debug(fmt: string, args: Array): void——Record debugging messages. log.info(fmt: string, args: Array): void——Record prompt messages. log.warning(fmt: string, args: Array): void——log warning log.error(fmt: string, args: Array): void——log error message. log.critical(fmt: string, args: Array): void——log important messages and terminate the subgraph. The log API accepts format strings and arrays of string values. Then, it replaces the placeholders with string values in the array. The first {} placeholder is replaced with the first value in the array, the second {} placeholder is replaced with the second value, and so on.

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

Record one or more values Record a single value

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

let myValue = 'A'

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

Record a single entry from an existing array In the following example, although the array contains three values, only the first value of the parameter array is recorded.

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)
}

Record multiple entries from an existing array. Each entry in the arguments array needs to have its own placeholder {} in the log message string. The following example includes three placeholders {} in the log message. Therefore, all three values in myArray will be recorded.

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,
  )
}

Record a specific entry from an existing array To display a specific value in the array, an index value must be provided.

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

Record event information The following example records the block number, block hash, and transaction hash of the 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 sometimes anchor IPFS files on the blockchain. This allows the mapping to obtain the IPFS hash from the smart contract and read the corresponding file from IPFS. The file data will be returned in the Bytes data format, which usually requires further processing, such as using the JSON API mentioned later in this section.

Given an IPFS hash or path, you can read files from IPFS as follows:

// Put this inside an event handler in the mapping
let hash = 'QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D'
let data = ipfs.cat(hash)

// Paths like `QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D/Makefile`
// that include files in directories are also supported
let path = 'QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D/Makefile'
let data = ipfs.cat(path)

Note: ipfs.cat has not yet been determined. If the file cannot be retrieved through the IPFS network before the request times out, it will return a null value (null). Therefore, it is necessary to always check whether the result is null. In order to ensure that the files can be retrieved, they must be associated with the IPFS node corresponding to the HyperGraph node. If you use hosting services, the Heco network uses https://f.hg.netwoprk/, and the BSC network uses https://pf.hg.network.

You can also use ipfs.map to stream larger files. This function requires the hash or path of the IPFS file, the name of the callback, and the flags used 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').toString()
  let title = obj.get('title').toString()

  // Callbacks can also created entities
  let newItem = new Item(id)
  item.title = title
  item.parent = userData.toString() // Set parent to "parentId"
  item.save()
}

// Put this inside an event handler in the mapping
ipfs.map('Qm...', 'processItem', Value.fromString('parentId'), ['json'])

// Alternatively, use `ipfs.mapJSON`
ipfs.mapJSON('Qm...', 'processItem', Value.fromString('parentId'))

Currently the only supported flag is json, which must be passed to ipfs.map. With the json flag, the IPFS file must contain a series of JSON values, one value per line. Calling ipfs.map will read every line in the file, deserialize it into JSONValue and call the callback function for each of them. The callback can then use entity operations to store data from JSONValue. The changed content of the entity is stored only when the handler calling ipfs.map completes successfully. At the same time, they are stored in memory, so the file size that ipfs.map can handle is limited.

After success, ipfs.map will return void. If any call of the callback results in an error, the handler calling ipfs.map will abort and mark the submap as failed.

Encryption API

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

The encryption API makes functions related to encryption algorithms available for mapping. Currently 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-Parsing JSON data from the Bytes array The JSONValue class provides a method to extract values from any JSON document. Since JSON values can be booleans, numbers, arrays, etc., JSONValue has a property to check the value type:

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

In addition, there is a way to check whether the value is empty:

value.isNull():boolean

When the type of the value is determined, 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 ——(Then use one of the above 5 methods to convert JSONValue)

Type conversion reference

Source (s)

Destination

Conversion function

Address

Bytes

none

Address

ID

s.toHexString()

Address

String

s.toHexString()

BigDecimal

String

s.toString()

BigInt

BigDecimal

s.toBigDecimal()

BigInt

String (hexadecimal)

s.toHexString() or s.toHex()

BigInt

String (unicode)

s.toString()

BigInt

i32

s.toI32()

Boolean

Boolean

none

Bytes (signed)

BigInt

BigInt.fromSignedBytes(s)

Bytes (unsigned)

BigInt

BigInt.fromUnsignedBytes(s)

Bytes

String (hexadecimal)

s.toHexString() or s.toHex()

Bytes

String (unicode)

s.toString()

Bytes

String (base58)

s.toBase58()

Bytes

i32

s.toI32()

Bytes

u32

s.toU32()

Bytes

JSON

json.fromBytes(s)

int8

i32

none

int32

i32

none

int32

BigInt

Bigint.fromI32(s)

uint24

i32

none

int64 - int256

BigInt

none

uint32 - uint256

BigInt

none

JSON

boolean

s.toBool()

JSON

i64

s.toI64()

JSON

u64

s.toU64()

JSON

f64

s.toF64()

JSON

BigInt

s.toBigInt()

JSON

string

s.toString()

JSON

Array

s.toArray()

JSON

Object

s.toObject()

String

Address

Address.fromString(s)

String

BigDecimal

BigDecimal.fromString(s)

String (hexadecimal)

Bytes

ByteArray.fromHexString(s)

String (UTF-8)

Bytes

ByteArray.fromUTF8(s)

Data source metadata

You can check the smart contract address, network and context of the data source calling the handler through the dataSource namespace:

  • dataSource.address(): Address

  • dataSource.network(): string

  • dataSource.context(): DataSourceContext

Entity and DataSourceContext The basic Entity class and sub-DataSourceContext class have tool methods to dynamically set and get the ability of 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

Last updated