4
0

Goldsky 🧮によるBerachainのデータのインデックスとクエリ [Berachain翻訳]

Posted at

本記事は下記の翻訳となります。
『Index & Query Berachain Data with Goldsky 🧮』

image.png

Goldsky とは何か、なぜインデックス化に関心を持つ必要があるのか?

Goldskyは、開発者がブロックチェーンからデータを格納してクエリするGraphQL API を構築することができます。このデータは高度にカスタマイズ可能であり、トークン供給の成長などのトレンドを明らかにするために使用したり、ユーザーのトークン残高などの瞬時のデータを取得したりすることができます。

サブグラフには、ブロックチェーンからデータをインデックス化し、生データを変換して簡単にクエリできる形式で格納するためのロジックが含まれています。

このチュートリアルでは、Berachainネットワーク上のユーザーの ERC20 残高をクエリするサブグラフの開発方法を学びます。

必要要件 📋

  • Nodejs v20.11.0以上
  • pnpm
  • IDE(例:VSCode、Replit など)

既にThe Graphに展開されているサブグラフを Berchain/Goldsky に展開したい場合は、「Goldsky でのセットアップ」セクションに進んでください。Goldsky と The Graph のサブグラフは完全な互換性があります!

サブグラフの構築 🛠️

まず、ターミナルに入力してください:

mkdir goldsky-subgraph;
cd goldsky-subgraph;

pnpm init;

pnpm install @graphprotocol/graph-cli @graphprotocol/graph-ts;

# Accept all of the defaults, hitting enter when prompted

# name: (project-name) project-name
# version: (0.0.0) 0.0.1
# description: The Project Description
# entry point: //leave empty
# test command: //leave empty
# git repository: //the repositories url
# keywords: //leave empty
# author: // your name
# license: N/A

# @graphprotocol dependencies provide subgraph development tooling

プロジェクトのルートに、以下のプロジェクト構造を作成してください(空のファイルでも構いません):

# FROM: ./goldsky-subgraph;

.
├── abis
│   └── Erc20.json
├── package.json
├── schema.graphql
├── src
│   ├── mapping.ts
│   ├── utils.ts
└── subgraph.yaml

./abis/Erc20.json に以下の内容を貼り付けて、ERC20 コントラクトのインターフェースを定義します:

[
  {
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [{ "name": "", "type": "string" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "_spender", "type": "address" },
      { "name": "_value", "type": "uint256" }
    ],
    "name": "approve",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "totalSupply",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "_from", "type": "address" },
      { "name": "_to", "type": "address" },
      { "name": "_value", "type": "uint256" }
    ],
    "name": "transferFrom",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "decimals",
    "outputs": [{ "name": "", "type": "uint8" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{ "name": "_owner", "type": "address" }],
    "name": "balanceOf",
    "outputs": [{ "name": "balance", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "symbol",
    "outputs": [{ "name": "", "type": "string" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "_to", "type": "address" },
      { "name": "_value", "type": "uint256" }
    ],
    "name": "transfer",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      { "name": "_owner", "type": "address" },
      { "name": "_spender", "type": "address" }
    ],
    "name": "allowance",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  { "payable": true, "stateMutability": "payable", "type": "fallback" },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "owner", "type": "address" },
      { "indexed": true, "name": "spender", "type": "address" },
      { "indexed": false, "name": "value", "type": "uint256" }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "from", "type": "address" },
      { "indexed": true, "name": "to", "type": "address" },
      { "indexed": false, "name": "value", "type": "uint256" }
    ],
    "name": "Transfer",
    "type": "event"
  }
]

サブグラフの設定

サブグラフを作成する最初のステップは、データソースとデータ構造(エンティティ)を定義することです。これは、subgraph.yamlまたはサブグラフマニフェストで行います。

./subgraph.yamlに以下を貼り付けてください:

specVersion: 0.0.4
description: ERC-20 subgraph with event handlers & entities
schema:
  file: ./schema.graphql
dataSources:
  - kind: ethereum/contract
    name: Erc20
    network: berachain-bartio
    source:
      address: "0x1306D3c36eC7E38dd2c128fBe3097C2C2449af64"
      abi: Erc20
      startBlock: 88948
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:
        - Token
        - Account
        - TokenBalance
      abis:
        - name: Erc20
          file: ./abis/Erc20.json
      eventHandlers:
        - event: Transfer(indexed address,indexed address,uint256)
          handler: handleTransfer
      file: ./src/mapping.ts

このマニフェストでは、いくつかのポイントがあります:

  1. source: アドレスはbHONEY トークンであり、Erc20インターフェースを使用して、デプロイされたstartBlockからインデックスされます
  2. entitiesは、クエリ可能な JavaScript オブジェクトと考えることができます。エンティティはお互いに関連付けられることがあり、GraphQL スキーマで定義されます(以下参照)
  3. eventHandlers: トークンのTransferイベントが発生するたびに、./src/mapping.tshandleTransferメソッドが呼び出され、インデックスロジックが実行されます

スキーマの作成

./schema.graphql では、サブグラフ内に存在するさまざまなエンティティのプロパティと関係を定義します。

# Token details
type Token @entity {
  id: ID!
  #token name
  name: String!
  #token symbol
  symbol: String!
  #decimals used
  decimals: BigDecimal!
}

# account details
type Account @entity {
  #account address
  id: ID!
  #balances
  balances: [TokenBalance!]! @derivedFrom(field: "account")
}

# token balance details
type TokenBalance @entity {
  id: ID!
  #token
  token: Token!
  #account
  account: Account!
  #amount
  amount: BigDecimal!
}

Tokenエンティティは、ERC20 トークンのよく知られたプロパティを含んでいます。

Accountエンティティには、ウォレットアドレスのidと興味深いことに、TokenBalanceタイプのbalancesリストが含まれています。@derivedFromディレクティブは、アカウントのbalancesプロパティが、TokenBalanceエンティティのaccountプロパティに基づいて逆引きで定義されることを意味します。

TokenBalanceは、AccountエンティティとTokenエンティティの両方を活用して、各ユーザーのトークン残高を定義しています。

このチュートリアルでは、TokenエンティティとTokenBalanceエンティティの両方を持つ必要はありませんでした。ユーザーの MIM 残高は、単にAccountエンティティにキャプチャすることも考えられます。ただし、この設計により、複数のトークン残高をキャプチャするためにサブグラフを拡張することが可能になります。

マッピングの作成

マッピングファイルは、すべてが一緒になる場所です ✨ ブロックチェーンのデータは、スキーマで定義したエンティティと関連付けられます。以下では、handleTransfer イベントハンドラが呼び出されたときに発生する相互作用を定義しています。

./src/mapping.ts に以下のコードを追加してください:

//import event class from generated files
import { Transfer } from "../generated/Erc20/Erc20";
//import the functions defined in utils.ts
import { fetchTokenDetails, fetchAccount, updateTokenBalance } from "./utils";
//import datatype
import { BigInt } from "@graphprotocol/graph-ts";

export function handleTransfer(event: Transfer): void {
  // 1. Get token details
  let token = fetchTokenDetails(event);
  if (!token) {
    return;
  }

  // 2. Get account details
  let fromAddress = event.params.from.toHex();
  let toAddress = event.params.to.toHex();

  let fromAccount = fetchAccount(fromAddress);
  let toAccount = fetchAccount(toAddress);

  if (!fromAccount || !toAccount) {
    return;
  }

  // 3. Update the token balances
  // Setting the token balance of the 'from' account
  updateTokenBalance(
    token,
    fromAccount,
    BigInt.fromI32(0).minus(event.params.value)
  );

  // Setting the token balance of the 'to' account
  updateTokenBalance(token, toAccount, event.params.value);
}

handleTransferTransfer イベントをパラメータとして受け取り、情報 (fromAddress, toAddress, transferAmount) を含んでいます。この情報を使用して、ハンドラは次の機能を実行することができます:

  1. トークンの詳細を取得する
  2. アカウントの詳細を取得する
  3. アカウントの残高を更新する

高レベルでは、このハンドラのコードは、Transfer イベントが発生するたびに、ERC20 アカウントの残高を忠実に更新します。

エンティティの操作

mapping.tsのコードは非常にシンプルですが、エンティティとのやり取りの重要な部分はユーティリティファイルに抽象化されています。では、サブグラフのエンティティとの操作の基本を見ていきましょう。

./src/utils.tsに以下のコードを追加してください:

//import smart contract class from generated files
import { Erc20 } from "../generated/Erc20/Erc20";
//import entities
import { Account, Token, TokenBalance } from "../generated/schema";
//import datatypes
import { BigDecimal, ethereum, BigInt } from "@graphprotocol/graph-ts";

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

// Fetch token details
export function fetchTokenDetails(event: ethereum.Event): Token | null {
  //check if token details are already saved
  let token = Token.load(event.address.toHex());
  if (!token) {
    //if token details are not available
    //create a new token
    token = new Token(event.address.toHex());

    //set some default values
    token.name = "N/A";
    token.symbol = "N/A";
    token.decimals = BigDecimal.fromString("0");

    //bind the contract
    let erc20 = Erc20.bind(event.address);

    //fetch name
    let tokenName = erc20.try_name();
    if (!tokenName.reverted) {
      token.name = tokenName.value;
    }

    //fetch symbol
    let tokenSymbol = erc20.try_symbol();
    if (!tokenSymbol.reverted) {
      token.symbol = tokenSymbol.value;
    }

    //fetch decimals
    let tokenDecimal = erc20.try_decimals();
    if (!tokenDecimal.reverted) {
      token.decimals = BigDecimal.fromString(tokenDecimal.value.toString());
    }

    //save the details
    token.save();
  }
  return token;
}

// Fetch account details
export function fetchAccount(address: string): Account | null {
  //check if account details are already saved
  let account = Account.load(address);
  if (!account) {
    //if account details are not available
    //create new account
    account = new Account(address);
    account.save();
  }
  return account;
}

export function updateTokenBalance(
  token: Token,
  account: Account,
  amount: BigInt
): void {
  // Don't update zero address
  if (ZERO_ADDRESS == account.id) return;

  // Get existing account balance or create a new one
  let accountBalance = getOrCreateAccountBalance(account, token);
  let balance = accountBalance.amount.plus(bigIntToBigDecimal(amount));

  // Update the account balance
  accountBalance.amount = balance;
  accountBalance.save();
}

function getOrCreateAccountBalance(
  account: Account,
  token: Token
): TokenBalance {
  let id = token.id + "-" + account.id;
  let tokenBalance = TokenBalance.load(id);

  // If balance is not already saved
  // create a new TokenBalance instance
  if (!tokenBalance) {
    tokenBalance = new TokenBalance(id);
    tokenBalance.account = account.id;
    tokenBalance.token = token.id;
    tokenBalance.amount = BigDecimal.fromString("0");

    tokenBalance.save();
  }

  return tokenBalance;
}

function bigIntToBigDecimal(quantity: BigInt, decimals: i32 = 18): BigDecimal {
  return quantity.divDecimal(
    BigInt.fromI32(10)
      .pow(decimals as u8)
      .toBigDecimal()
  );
}
  • fetchAccount()Accountエンティティを返します。まず、渡されたaddressを持つAccountエンティティが存在するかどうかを確認します。存在しない場合は新しいエンティティを作成します。

  • fetchTokenDetails()Tokenエンティティを返します。Tokenが存在しない場合、トークンアドレスをERC20インターフェースにバインドして新しいインスタンスを作成します。これにより、トークンコントラクトから公開された読み取り関数にアクセスできるようになります。これにより、名前、シンボル、小数点以下の桁数などのトークンのプロパティを取得および設定できます。

  • updateTokenBalance()はおそらく最も重要な関数であり、各転送ごとにユーザーのTokenBalanceエンティティを更新します。mapping.tsファイルに戻ってみると、この関数はTransferイベントごとに 2 回呼び出されます。転送元の場合は負のamountが渡され、トークン残高の減少を示し、逆に受信者の場合は正のamountが渡されます。これにより、トークン残高の正確な会計が維持されます。

サブグラフのビルド

プロジェクトディレクトリのルートで、ターミナルで次のコマンドを実行します:

# FROM: ./goldsky-subgraph;

pnpm codegen;
pnpm build;

これらのコマンドは、コントラクトの ABI から TypeScript クラスファイルを生成し、コードをコンパイルし、/buildディレクトリにビルド出力を作成します。

デプロイする前に、Goldsky での設定が必要です。

Goldsky での設定方法

Goldsky は、サブグラフをホストし、必要なインデックス作業を行います。以下の手順に従ってアカウントを設定してください:

  1. app.goldsky.comでアカウントを作成します。
  2. 設定ページで API キーを作成します。
  3. Goldsky CLI をインストールします:
curl https://goldsky.com | sh

4. Log in with the API key created earlier:

goldsky login

サブグラフをデプロイする 🚀

プロジェクトのルートで、次のコマンドを実行します:

# FROM: ./goldsky-subgraph;

goldsky subgraph deploy erc20-subgraph/1.0.0 --path .

デプロイが成功したら、デプロイされたサブグラフを表示します。ただし、すぐに使用することはできません。インデックス作成プロセスでは、トークンの残高を更新するためにすべてのブロックを処理する必要があります。


Goldsky Subgraph Dashboard

データのクエリ

ダッシュボードでは、「パブリック GraphQL リンク」という項目が表示されます。これを使用してクエリを作成することができます。新しいサブグラフを使って次のクエリを試してみましょう。

{
  accounts {
    id
    balances {
      id
      token {
        id
        name
        symbol
        decimals
      }
      amount
    }
  }
}

一緒に試すための例が必要な場合は、このライブサブグラフを使用してください。

ユーザーの MIM 残高を Berachain のブロックエクスプローラと照合すると、サブグラフが正確にユーザーのトークン残高をインデックス化していることがわかります ✅


Comparison of Subgraph and Block Explorer Token Balances

まとめ

これが Berachain ウォレットのトークン残高をインデックス化するために Goldsky サブグラフを使用する方法です。Goldsky は、開発者が簡単にカスタマイズされたブロックチェーンデータを保存およびクエリするためのデータ可用性プラットフォームです。


🐻 Full Code Repository

最終的なコードを確認したり、他のガイドを見たりするには、Berachain Goldsky Guide Codeをチェックしてください。

🛠️ もっと作りたいですか?

Berachain でさらに多くのものを構築し、さらに多くの例を見たい場合は、Berachain GitHub Guides Repoをご覧ください。NextJS、Hardhat、Viem、Foundry など、さまざまな実装があります。

開発者サポートをお探しですか?

質問をするために、Berachain Discordサーバーに参加し、開発者チャンネルをチェックしてください。

❤️ この記事に対して愛を示すのを忘れないでください 👏🏼



【Sunrise とは】
Sunrise は Proof of Liquidity(PoL)と Fee Abstraction(手数料抽象化)を備えたデータ可用性レイヤーです。 私たちは DA の体験を再構築し、多様なエコシステムからのモジュラー型流動性を活用してロールアップを立ち上げています。

【Social Links】

【お問合せ】
Sunrise へのお問い合わせはこちらから 👉 Google Form

1080x360.jpeg

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0