BlockchainのデータをGraphQLで取得する【SubQuery】
SubQueryについて雑に書いていきます。
event()
ブロックチェーンにあるデータをフロント側で管理することや情報を抽出することが難しい問題がありますが、TheGraphやSubQueryというサービスがその問題を解決してくれます。
スマートコントラクトでeventを発火すると引数で指定したデータがトランザクションログに保存されます。TheGraphやSubQueryはそのデータをオフチェーンのサーバーに蓄積しGraphQLでqueryできるようにします。eventの発火はフロント側で監視することもできます。こういったサービスが出る前は自前でサーバー立ててやっていたようです。
対応チェーンはTheGraphはethereum、SubQueryはPolkadot,substrate,Avalanche,Cosmos等に対応しています。
eventはfunction内などに仕込んでemitを使うことで発火させます。
以下はadd()というeventを定義してaddList()内でemitを使い発火させています。
ちなみにindexedを使うとclient側でフィルターをかけられたりします。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Emitter {
struct Transactor {
address sender;
address reciever;
uint256 amount;
}
Transactor[] list;
// 最大3つまでindexed修飾子を追加できる
event add(address indexed from, address indexed reciever, uint256 amount);
function addList(address _reciver, uint256 _amount) public {
list.push(Transactor(msg.sender, _reciver, _amount));
emit add(msg.sender, _reciver, _amount);
}
}
SubQuery
今回はSubQueryを例にして進めていきます。
プロジェクトを構成する3つのファイル
SubQueryを構成する重要なファイルが3つあります。この構成はTheGraphとほぼ変わりません。
-
project.yaml
- インデックスを作成するスマートコントラクトのaddress,abi,networkなどを記載する
- どのeventをリッスンするか
- 呼び出されるマッピング関数
-
mappings
- 発火されたeventをentityに変換する関数の定義
- project.yamlで記述された各関数を定義する必要がある
-
schema.graphql
- GraphQLのスキーマ定義
プロジェクト作成
cliのインストールからです。
% npm install -g @subql/cli
% subql init <project name>
% cd <project name>
% yarn
% yarn codegen
% yarn build
% yarn start:docker
ここまでするとlocal:3000でgraphqlのplaygroundが立ち上がります。
既にコントラクトからeventが発火されていればplaygroundでqueryできると思います。
project.yaml
project.yamlではコントラクトのaddressやabi(application binary interface)などを記載していきます。またどのeventを監視 するか、そのeventをどのマッピング関数に紐付けるかを決めます。mappingについては後ほど。
変更が必要そうなdataSources配下をピックアップ。
- abiのvalueはassetsのkeyである必要がある
- addressはコントラクトのもの
- assetsのfileにabiの置き場所を記載
- mappingのhandlerは関数名
- topicsはevent
- 下のファイルでnullになってる箇所はeventのindex値を入れてフィルターできる
dataSources:
- kind: substrate/FrontierEvm
startBlock: 752073
processor:
file: "./node_modules/@subql/frontier-evm-processor/dist/bundle.js"
options:
# abi の value は assets の keyである必要がある
abi: erc20
# Contract address (or recipient if transfer) to filter, if `null` should be for contract creation
address: "0x6bd193ee6d2104f14f94e2ca6efefae561a4334b"
assets:
erc20:
file: ./erc20.abi.json
mapping:
file: ./dist/index.js
handlers:
- handler: handleFrontierEvmEvent
kind: substrate/FrontierEvmEvent
filter:
topics:
- "Transfer(address indexed from,address indexed to,uint256 value)"
# topics[1]〜topics[3]はインデックス値
- null
- null
- null
- handler: handleFrontierEvmCall
kind: substrate/FrontierEvmCall
filter:
## The function can either be the function fragment or signature
# function: '0x095ea7b3'
# function: '0x7ff36ab500000000000000000000000000000000000000000000000000000000'
# function: approve(address,uint256)
function: "approve(address to,uint256 value)"
mapping
mappingはtypescriptのファイルです。
mapping関数にはいくつか種類があるので主要なものをピックアップします。
Block handlers
- 新しいブロックがチェーンに追加されるたびに情報を取得できる
- 定義済みのBlockHandlerが各Blockに対して1回呼び出される
- パフォーマンス的にあまり使わないほうが良いらしい
import { SubstrateBlock } from "@subql/types";
export async function handleBlock(block: SubstrateBlock): Promise<void> {
// Create a new BlockEntity with the block hash as it's ID
const record = new BlockEntity(block.block.header.hash.toString());
record.field1 = block.block.header.number.toNumber();
await record.save();
}
Event handlers
- 新しいブロックに特定のイベントが含まれている時に情報を取得できる
import { SubstrateEvent } from "@subql/types";
export async function handleEvent(event: SubstrateEvent): Promise<void> {
const {event: {data: [account, balance]}} = event;
const record = new EventEntity(event.extrinsic.block.block.header.hash.toString());
record.field2 = account.toString();
record.field3 = (balance as Balance).toBigInt();
await record.save();
}
Call handlers
- 特定のSubstrate extrinsicsに関する情報をキャッチアップする場合に仕様される
export async function handleCall(extrinsic: SubstrateExtrinsic): Promise<void> {
const record = new CallEntity(extrinsic.block.block.header.hash.toString());
record.field4 = extrinsic.block.timestamp;
await record.save();
}
schema.graphql
プロジェクト作成時はこんな感じのファイルができてるかなと思います。
type Transaction @entity {
id: ID! # Transaction hash
value: BigInt!
to: String!
from: String!
contractAddress: String!
}
type Approval @entity {
id: ID! # Transaction hash
value: BigInt!
owner: String!
spender: String!
contractAddress: String!
}
mappingのファイル内にnew EventEntity(hoge)
みたいな記述がありましたが、hogeの部分でidを指定してentityを作ろうとしてます。今回で言うとnew Transaction(hoge)
みたいな感じです。save()で保存します。
集計関数
GraphQLでの集計は以下に基づいているらしいです。
playground見るとStringFilterとかIntFilterとかDateFilterとかいろいろ型が見れるのでそこみると良さそうです。
例
idが0x0001056180590bf3c810a527a93c17b7bbd7f4c6c2ff74c076e9d998d3c6946e
と同じ
query {
logCardMinteds(filter: {id: {equalTo: "0x0001056180590bf3c810a527a93c17b7bbd7f4c6c2ff74c076e9d998d3c6946e"}} ) {
nodes {
nodeId
id
blockNumber
buyer
tokenId
cardTypeId
}
}
}
Discussion