본문으로 건너뛰기

Setting up a Project with The Graph

This guide will walk you through the process of deploying a subgraph for smart contracts on Milkomeda C1 using TheGraph.

While Milkomeda chains are not currently supported on TheGraph's decentralized network, Milkomeda runs it's own Graph Node to index the Milkomeda C1 chains.

To start, we will guide you through the steps of creating a subgraph in a local environment so you can test and develop your smart contract. Once you are satisfied with the result, we will show you how to deploy it on a Milkomeda C1 Devnet.

Creating a Smart Contract to be indexed

For our smart contract we will use a simple example of a Simple Storage contract with one single event that gets emitted every time the number is changed. Let's create a Hardhat dev environment to create and deploy our contract.

yarn init -y
yarn add hardhat
npx hardhat

In the contracts folder, we'll create a file named Storage.sol with the following:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Storage {
uint256 number;

event ChangeNumber(
uint256 timestamp,
uint256 oldValue,
uint256 newValue,
address caller
);

function store(uint256 num) public {
uint prevNumber = number;
number = num;
emit ChangeNumber(block.timestamp, prevNumber, number, msg.sender);
}

function retrieve() public view returns (uint256){
return number;
}
}

The parameters of the event is what will be indexed by the subgraph. The event will simple record the timestamp, the previous number, the new number and the contract caller.

To compile the contract run:

npx hardhat compile

Local Subgraph environment

To run a graph node locally, we'll use Docker to run a ganache, IPFS, postgres and a graph-node containers. Here is the necessary docker-compose.yml file:

version: '3'
services:
ganache:
image: trufflesuite/ganache:v7.5.0
command: -m "test test test test test test test test test test test junk"
-a 5
--wallet.hdPath "m/44'/60'/0'"
ports:
- 127.0.0.1:8545:8545
ipfs:
image: ipfs/go-ipfs:v0.10.0
ports:
- '5001:5001'
volumes:
- ./data/ipfs:/data/ipfs
postgres:
image: postgres
ports:
- '5432:5432'
command:
[
"postgres",
"-cshared_preload_libraries=pg_stat_statements"
]
environment:
POSTGRES_USER: graph-node
POSTGRES_PASSWORD: let-me-in
POSTGRES_DB: graph-node
PGDATA: "/var/lib/postgresql/data"
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
volumes:
- ./data/postgres:/var/lib/postgresql/data
graph-node:
image: graphprotocol/graph-node
ports:
- '8000:8000'
- '8001:8001'
- '8020:8020'
- '8030:8030'
- '8040:8040'
depends_on:
- ipfs
- postgres
extra_hosts:
- 192.168.80.1:host-gateway
environment:
postgres_host: postgres
postgres_user: graph-node
postgres_pass: let-me-in
postgres_db: graph-node
ipfs: 'ipfs:5001'
ethereum: 'ganache:http://ganache:8545'
GRAPH_LOG: info

Start the containers by running:

docker compose up

To be able to deploy to the local ganahache chain in docker container, we'll have to add it to the hardhat.config.ts. We will also already add the Milkomeda C1 Devnet. The private key for Ganache is for one of the funded accounts in the ganache chain.

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
require("dotenv").config();

const config: HardhatUserConfig = {
solidity: "0.8.18",
networks: {
c1_testnet: {
url: `https://rpc-devnet-cardano-evm.c1.milkomeda.com`,
accounts: [`0x${process.env.PRIVATE_KEY}`]
},
ganache: {
url: 'http://localhost:8545',
chainId: 1337,
accounts: ['0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5']
}
}
};

export default config;

Now we'll create a deployment script (deploy.ts) in the scripts folder:

import { ethers } from "hardhat";

async function main() {
const Storage = await hre.ethers.getContractFactory("Storage");
const storage = await Storage.deploy();
await storage.deployed();
console.log(`Storage contract deployed to ${storage.address}`);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

To deploy the contract to the ganache chain, run:

npx hardhat run scripts/deploy.ts --network ganache

Configure Subgraph

First, we will need the Graph CLI:

yarn add @graphprotocol/graph-cli @graphprotocol/graph-ts

To deploy a subgraph to our local graph-node we will need three files:

  • subgraph.yaml - for the subgraph parameters
  • schema.graphql - to define the subgraph entities
  • mappings.ts - AssemblyScript code to define the indexing logic. Basically how the blockchain events get mapped to the entities

There are several ways to store these files, but we will create subgraph.yaml in the root folder of our project and then create a folder called subgraph for the schema.graphql and mappings.ts

First, create a file named subgraph.yaml with the required config:

specVersion: 0.0.5
description: Milkomeda Subgraph Tutorial
schema:
file: ./subgraph/schema.graphql
dataSources:
- kind: ethereum/contract
name: Storage
network: ganache
source:
address: ''
abi: Storage
startBlock: 1
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
file: ./subgraph/mapping.ts
entities:
- Values
- User
abis:
- name: Storage
file: ./artifacts/contracts/Storage.sol/Storage.json
eventHandlers:
- event: 'ChangeNumber(uint256,uint256,uint256,address)'
handler: handleChangeNumber

The important parts to take notice are:

  • GraphQL schema, which we will be creating next
  • Network name that we define in the environment variables of the graph-node container, in this case "ganache"
  • Source/address of the deployed smart contract
  • ABI for the smart contract
  • Mapping file, which we will create
  • Entities
  • Event handlers

First let's tweak our deployment script so that the contract address gets updated in the subgraph.yaml for each deployment.

import { ethers } from "hardhat";
const fs = require('fs');
const yaml = require('js-yaml');

const FILENAME = 'subgraph.yaml'
const changeAddress = (address) => {
let doc = yaml.load(fs.readFileSync(FILENAME, 'utf-8'));
doc.dataSources[0].source.address = address
fs.writeFileSync(FILENAME, yaml.dump(doc));
}

async function main() {
const Storage = await hre.ethers.getContractFactory("Storage");
const storage = await Storage.deploy();
await storage.deployed();
console.log(`Storage contract deployed to ${storage.address}`);
changeAddress(storage.address)
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Now let's create our schema.graphql in the subgraph folder.

type StoredValue @entity {
id: ID!
oldValue: BigInt!
newValue: BigInt!
caller: User!
}

type User @entity {
id: ID!
values: [StoredValue!]! @derivedFrom(field: "caller")
}

We are creating two entities, the StoredValue and the User.

We'll also create a file called mappings.ts in the subgraph folder.

import {  ChangeNumber } from "./types/Storage/Storage";
import { User, StoredValue } from "./types/schema";

export function handleChangeNumber(event: ChangeNumber): void {

// USER
let userId = event.params.caller.toHex()
let user = User.load(userId)

if (!user) {
user = new User(userId)
user.save()
}

// VALUE
let token = new StoredValue(event.params.timestamp.toString())
token.oldValue = event.params.oldValue
token.newValue = event.params.newValue
token.caller = userId
token.save()
}

In order to make it easy and type-safe to work with smart contracts, events and entities, the Graph CLI can generate AssemblyScript types from the subgraph's GraphQL schema and the contract ABIs included in the data sources.

graph codegen --output-dir subgraph/types/

Now we can create an deploy the subgraph.

graph create milkomeda/subgraph_tutorial --node http://127.0.0.1:8020

graph deploy milkomeda/subgraph_tutorial --ipfs http://localhost:5001 --node http://localhost:8020 --output-dir artifacts/subgraph -l 0.1

After successfully creation and deployment of the subgraph, a link will be shown in the terminal to access the GraphQL frontend interface.

At the moment, if you query all records of the entity User and StoredValue, the results will be empty, because there have been to transactions to be indexed.

The query to get all results would be:

{
users {
id
}
storedValues {
id
oldValue
newValue
caller {
id
}
}
}

And the results would be:

{
"data": {
"users": [],
"storedValues": []
}
}

But now let's call the contract to store a new value. Create a file names callContract.ts in the scripts folder with the following. We can read the contract address from the subgraph.yaml file just to be sure we are using the same contract.

const hre = require("hardhat");
const yaml = require('js-yaml');
const fs = require('fs');

async function main() {
const FILENAME = 'subgraph.yaml'
let doc = yaml.load(fs.readFileSync(FILENAME, 'utf-8'));
const contractAddress = doc.dataSources[0].source.address;

const storage = await hre.ethers.getContractAt("Storage", contractAddress);
const res = await storage.store(5);
console.log("Trx hash:", res.hash);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

Then, run:

npx hardhat run scripts/callContract.ts --network ganache

If we run the query again, the event emitted from the contract call should have been indexed. And here is the result:

{
"data": {
"users": [
{
"id": "0x1e59ce931b4cfea3fe4b875411e280e173cb7a9c"
}
],
"storedValues": [
{
"id": "1679441122",
"oldValue": "0",
"newValue": "5",
"caller": {
"id": "0x1e59ce931b4cfea3fe4b875411e280e173cb7a9c"
}
}
]
}
}

Deploying a Subgraph for Milkomeda C1 Devnet

After testing that our contract and subgraph are working well together, we can deploy the smart contract to the devnet and the subgraph to Milkomeda's TheGraph and IPFS nodes.

IPFS Node

Node with enough public access that can be used by us and users that want to deploy their own thegraph deployments, but don’t want to keep a local IPFS node. that url can be used also for any public subgraph deployment (it’s network agnostic since ipfs network is just one, so there is not separate url for devnet/mainnet)

TheGraph node

JSON-RPC (for managing deployments) - is the specific network to be used by scripts to deploy subgraphs (it is fully public)

The changes required are very simple. From the current setup, we only need to change three things:

  1. We no longer need the docker containers, since the smart contract will be deployed to Milkomeda C1 Devnet and the subgraph will be deployed to Milkomeda's Thegraph and IPFS nodes. We can therefore delete or ignore the docker-compose.yml file.

  2. The network name for the contract in the dataSources in the subgraph.yaml file should be changed to "Milkomeda-C1-Devnet".

dataSources:
- kind: ethereum/contract
name: Storage
network: "Milkomeda-C1-Devnet"
  1. The commands for the creation and deployment of the subgraph should now point to the IPFS and TheGraph (Milkomeda) nodes.

Now we can deploy our smart contract to Milkomeda A1 Devnet (we had already added the chain to hardhat.config.ts).

npx hardhat run scripts/deploy.ts --network c1_testnet

And create and deploy the subgraph to the new containers:

graph create milkomeda/subgraph_tutorial_c1 --node https://c1-devnet-thegraph.milkomeda.com

graph deploy milkomeda/subgraph_tutorial_c1 --ipfs https://ipfs.milkomeda.com --node https://c1-devnet-thegraph.milkomeda.com --output-dir artifacts/subgraph -l 0.1

Executing there commands should show an output similar to the one below, containing the GraphQL endpoint and a link to a GraphQL queyr frontend: https://c1-devnet-thegraph.milkomeda.com:443/subgraphs/name/milkomeda/subgraph_tutorial_c1

✔ Upload subgraph to IPFS

Build completed: QmS1iG3oaBQuYhAxCtTV1jJApjC6MTjTNzKSYAL5SJUzcc

Deployed to https://c1-devnet-thegraph.milkomeda.com:443/subgraphs/name/milkomeda/subgraph_tutorial_c1/graphql

Subgraph endpoints:
Queries (HTTP): https://c1-devnet-thegraph.milkomeda.com:443/subgraphs/name/milkomeda/subgraph_tutorial_c1

In order to see indexed events, call the contract to store a value. It might take a few minutes until the event gets indexed.

npx hardhat run scripts/callContract.ts --network c1_testnet

We can now query our subgraph and it will have indexed the events from our contract on Milkomeda C1 Devnet.

{
"data": {
"users": [
{
"id": "0xcd9e29b0b51ac983b59bf7c8ecf57bcc430e8dd3"
}
],
"storedValues": [
{
"id": "1679443522",
"oldValue": "0",
"newValue": "5",
"caller": {
"id": "0xcd9e29b0b51ac983b59bf7c8ecf57bcc430e8dd3"
}
}
]
}
}

This guide explained how to deploy a subgraph for smart contracts on Milkomeda C1 using TheGraph.

After creating a smart contract for testing and development, we set up a local subgraph environment using Docker to run a ganache, IPFS, postgres, and a graph-node containers. Then, we created a deployment script to deploy the contract to the ganache chain and we explained how to configure subgraph using The Graph CLI.

Finally, we ajusted to setup to deploy the Smart Contract on the Milkomeda C1 chain and index it from a subgraph deployed on Milkomeda's IPFS and th