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 parametersschema.graphql
- to define the subgraph entitiesmappings.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)
- URL (mainnet): https://c1-mainnet-thegraph.milkomeda.com
- URL (devnet): https://c1-devnet-thegraph.milkomeda.com
The changes required are very simple. From the current setup, we only need to change three things:
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.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"
- 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