Skip to content

v0.6.0

Latest
Compare
Choose a tag to compare
@dimitrovmaksim dimitrovmaksim released this 06 Oct 10:38
· 4 commits to main since this release
7ccefb9

What's Changed

Breaking Changes:

It seems there's an issue with the -m1 binaries, where the binary will encounter memory error when tests fail. Either use rosetta or run matchstick using docker. When using the graph-cli, you can use the -d flag with the graph test command to run matchstick with docker, e.g. graph test -d

Requires matcshtick-as 0.6.0

Dropped support for MacOs 10.15 - Action runner deprecated

Dropped support for Ubuntu 18.04 - Action runner deprecated

Dropped support for Ubuntu 20.04 - Issues with building graph-node dependencies

Changed handling derived fields

Looking up derived entities will no longer be possible by directly calling the derived field of the entity , e.g. entity.derived_field, and they will not be present when logging the store with logStore().

To load the derived entities in your tests, you will need to use loadRelated, e.g. entity.derived_field.load(). To log the derived entities we have introduced a new function logEntity(entity: string, id: string, showRelated: boolean) which will load the parent entity and optionally can be called to load the related entities, similarly how queries return the derived fields.

Added support for loadRelated

Previously it was possible to get the derived entities by accessing them as Entity fields/properties

let entity = ExampleEntity.load("id")
let derivedEntity = entity.derived_entity

This was causing a lot of issues, because we had to track and save all entity change into our store. With adding Bytes as possible type. for the entity ID, things became even more complicated. With graph-node adding loadRelated we decided take a more graph-node approach. Derived fields actually do not exists in the Store/Database and are dynamically collected when you perform a query or use loadRelated. Now with the added support for loadRelated you can get the derived entities the same way you would do in your handlers.

let entity = ExampleEntity.load("id")
let derivedEntities = entity.derived_entities.load()

logEntity

Due to the changes to how we handle the derived fields (we are trying to be as close to graph-node as possible), logStore will no longer print the derived fields. For that reason we added another function logEntity(entity: string, id: string, showRelated: boolean) which takes the Entity type, entity id and showRelated boolean argument to indicate if we want to print the related derived entities.

Added support for loadInBlock

By default loadInBlock will return null. We have introduced mockInBlockStore that will allow users to mock entities into the block cache.

import { afterAll, beforeAll, describe, mockInBlockStore, test } from "matchstick-as"
import { Gravatar } from "../../generated/schema"

describe("loadInBlock", () => {
  beforeAll(() => {
    let gravatar = new Gravatar("gravatarId0")
    gravatar.displayName = "Gravatar 0"
    gravatar.owner = Address.fromString("0x90cBa2Bbb19ecc291A12066Fd8329D65FA1f1947")
    gravatar.imageUrl = "https://example.com/gravatarId0.png"

    mockInBlockStore("Gravatar", "gravatarId0", gravatar);
  })

  afterAll(() => {
    clearInBlockStore()
  })

  test("Can use entity.loadInBlock() to retrieve entity from cache store in the current block", () => {
    let retrievedGravatar = Gravatar.loadInBlock("gravatarId0")
    assert.stringEquals("gravatarId0", retrievedGravatar!.get("id")!.toString())
  })

  test("Returns null when calling entity.loadInBlock() if an entity doesn't exist in the current block", () => {
    let retrievedGravatar = Gravatar.loadInBlock("IDoNotExist")
    assert.assertNull(retrievedGravatar)
  })
})

Added support for dynamic DataSource creation testing

It is now possible to test if a new DataSource has been created from a template. It supports both ethereum/contract and file/ipfs templates. We added four new functions:

assert.dataSourceCount(templateName, expectedCount) - assert the expected count of DataSources from the specified template
assert.dataSourceExists(templateName, address/ipfsHash) - assert that a DataSource with the specified identifier (could be a contract address or IPFS file hash) from a specified template was created
logDataSources(templateName) - Print all DataSources from the specified template to the console for debugging purposes.
readFile(path) - Function that reads a json file that represents an IPFS file and returns the content as Bytes

Testing ethereum/contract templates

test("ethereum/contract dataSource creation example", () => {
    // Assert there are no dataSources created from GraphTokenLockWallet template
    assert.dataSourceCount("GraphTokenLockWallet", 0);

    // Create a new GraphTokenLockWallet datasource with address 0xA16081F360e3847006dB660bae1c6d1b2e17eC2A
    GraphTokenLockWallet.create(Address.fromString("0xA16081F360e3847006dB660bae1c6d1b2e17eC2A"));
    
    // Assert the dataSource has been created
    assert.dataSourceCount("GraphTokenLockWallet", 1);
    
    // Add a second dataSource with context
    let context = new DataSourceContext()
    context.set("contextVal", Value.fromI32(325))
    
    GraphTokenLockWallet.createWithContext(Address.fromString("0xA16081F360e3847006dB660bae1c6d1b2e17eC2B"), context);
    
    // Assert there are now 2 dataSources 
    assert.dataSourceCount("GraphTokenLockWallet", 2);

    // Assert that a dataSource with address "0xA16081F360e3847006dB660bae1c6d1b2e17eC2B" was created
    // Keep in mind that `Address` type is transformed to lower case when decoded, so you have to pass the address as all lower case when asserting if it exists
    assert.dataSourceExists("GraphTokenLockWallet", "0xA16081F360e3847006dB660bae1c6d1b2e17eC2B".toLowerCase());
    
    logDataSources("GraphTokenLockWallet");
  })

logDataSource:

🛠  {
  "0xa16081f360e3847006db660bae1c6d1b2e17ec2a": {
    "kind": "ethereum/contract",
    "name": "GraphTokenLockWallet",
    "address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2a",
    "context": null
  },
  "0xa16081f360e3847006db660bae1c6d1b2e17ec2b": {
    "kind": "ethereum/contract",
    "name": "GraphTokenLockWallet",
    "address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2b",
    "context": {
      "contextVal": {
        "type": "Int",
        "data": 325
      }
    }
  }
}

Testing file/ipfs templates

Similarly to contract dynamic DataSources it is now possible to test File DataSources and their handlers

subgraph.yaml:

...
templates:
 - kind: file/ipfs
    name: GraphTokenLockMetadata
    network: mainnet
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.6
      language: wasm/assemblyscript
      file: ./src/token-lock-wallet.ts
      handler: handleMetadata
      entities:
        - TokenLockMetadata
      abis:
        - name: GraphTokenLockWallet
          file: ./abis/GraphTokenLockWallet.json

schema.graphql:

"""
Token Lock Wallets which hold locked GRT
"""
type TokenLockMetadata @entity {
  "The address of the token lock wallet"
  id: ID!
  "Start time of the release schedule"
  startTime: BigInt!
  "End time of the release schedule"
  endTime: BigInt!
  "Number of periods between start time and end time"
  periods: BigInt!
  "Time when the releases start"
  releaseStartTime: BigInt!
}

metadata.json

{
    "startTime": 1,
    "endTime": 1,
    "periods": 1,
    "releaseStartTime": 1
}

./src/token-lock-wallet.ts:

export function handleMetadata(content: Bytes): void {
  // dataSource.stringParams() returns the File DataSource CID
  // stringParam() will be mocked in the handler test
  // for more info https://thegraph.com/docs/en/developing/creating-a-subgraph/#create-a-new-handler-to-process-files
  let tokenMetadata = new TokenLockMetadata(dataSource.stringParam());
  const value = json.fromBytes(content).toObject()
  
  if (value) {
    const startTime = value.get('startTime')
    const endTime = value.get('endTime')
    const periods = value.get('periods')
    const releaseStartTime = value.get('releaseStartTime')

    if (startTime && endTime && periods && releaseStartTime) {
      tokenMetadata.startTime = startTime.toBigInt()
      tokenMetadata.endTime = endTime.toBigInt()
      tokenMetadata.periods = periods.toBigInt()
      tokenMetadata.releaseStartTime = releaseStartTime.toBigInt()
    }

    tokenMetadata.save()
  }
}

./tests/token-lock-wallet.test.ts:

import { assert, test,  dataSourceMock, readFile } from "matchstick-as"
import { Address, BigInt, Bytes, DataSourceContext, ipfs, json, store, Value } from "@graphprotocol/graph-ts"

import { handleMetadata } from "../../src/token-lock-wallet"
import { TokenLockMetadata } from "../../generated/schema"
import { GraphTokenLockMetadata } from "../../generated/templates"

test("file/ipfs dataSource creation example", () => {
  // Generate the dataSource CID from the ipfsHash + ipfs path file
  // For example QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm/example.json
  const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
  const CID = `${ipfshash}/example.json`
    
  // Create a new dataSource using the generated CID
  GraphTokenLockMetadata.create(CID);

  // Assert the dataSource has been created
  assert.dataSourceCount("GraphTokenLockMetadata", 1);
  assert.dataSourceExists("GraphTokenLockMetadata", CID);
  logDataSources("GraphTokenLockMetadata");

  // Now we have to mock the dataSource metadata and specifically dataSource.stringParam()
  // dataSource.stringParams actually uses the value of dataSource.address(), so we will mock the address using dataSourceMock from  matchstick-as 
 // First we will reset the values and then use dataSourceMock.setAddress() to set the CID
  dataSourceMock.resetValues()
  dataSourceMock.setAddress(CID);

  // Now we need to generate the Bytes to pass to the dataSource handler
  // For this case we introduced a new function readFile, that reads a local json and returns the content as Bytes
  const content = readFile(`path/to/metadata.json`)
  handleMetadata(content)
    
 // Now we will test if a TokenLockMetadata was created
 const metadata = TokenLockMetadata.load(CID);
    
  assert.bigIntEquals(metadata!.endTime, BigInt.fromI32(1))
  assert.bigIntEquals(metadata!.periods, BigInt.fromI32(1))
  assert.bigIntEquals(metadata!.releaseStartTime, BigInt.fromI32(1))
  assert.bigIntEquals(metadata!.startTime, BigInt.fromI32(1))
  })

Added custom messages to asserts

All asserts now support setting a custom error message

assert.fieldEquals("Gravatar", "0x123", "id", "0x123", "Id should be 0x123");
assert.equals(ethereum.Value.fromI32(1), ethereum.Value.fromI32(1), "Value should equal 1");
assert.notInStore("Gravatar", "0x124", "Gravatar should not be in store");
assert.addressEquals(Address.zero(), Address.zero(), "Address should be zero");
assert.bytesEquals(Bytes.fromUTF8("0x123"), Bytes.fromUTF8("0x123"), "Bytes should be equal");
assert.i32Equals(2, 2, "I32 should equal 2");
assert.bigIntEquals(BigInt.fromI32(1), BigInt.fromI32(1), "BigInt should equal 1");
assert.booleanEquals(true, true, "Boolean should be true");
assert.stringEquals("1", "1", "String should equal 1");
assert.arrayEquals([ethereum.Value.fromI32(1)], [ethereum.Value.fromI32(1)], "Arrays should be equal");
assert.tupleEquals(changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]), changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]), "Tuples should be equal");
assert.assertTrue(true, "Should be true");
assert.assertNull(null, "Should be null");
assert.assertNotNull("not null", "Should be not null");
assert.entityCount("Gravatar", 1, "There should be 2 gravatars");
assert.dataSourceCount("GraphTokenLockWallet", 1, "GraphTokenLockWallet template should have one data source");
assert.dataSourceExists("GraphTokenLockWallet", Address.zero().toHexString(), "GraphTokenLockWallet should have a data source for zero address");

New Contributors

Full Changelog: 0.5.4...0.6.0