Как получить real-time данные смарт-контракта, используя The Graph (пишем свой сабграф)

от автора

Всем привет! В предыдущих статьях (1 и 2) я рассказывал про концепцию индексирования данных смарт-контрактов на блокчейне в общем и в частности через средства The Graph, а также про то, как использовать готовые «сабграфы» на The Graph Hosted Service, чтобы, не написав ни строки кода, делать к ним GraphQL запросы и получать данные популярных децентрализованных приложений. Однако, если вы присматриваетесь к Web3 разработке, то вероятно вам и самим придется разрабатывать такие сабграфы для своего приложения. Эту тему (разработка собственных сабграфов стандарта The Graph) я бы и хотел осветить в данном материале. Чтобы пример был не сферический и в вакууме, будем рассматривать существующий смарт-контракт проекта TornadoCash.

Прежде всего, что такое The Graph?

Во-первых, The Graph — это децентрализованный протокол, который позволяет получать доступ к данным смарт контрактов на блокчейне, индексируемым децентрализованными так называемыми «индексаторами», курируемым — «кураторами» и спонсируемым — «делегаторами». Простыми словами, это децентрализованная сеть, в которой есть три роли, которые действуют в своих интересах, вследствие чего вы как внешний пользователь можете подключиться к сети, заплатить денег (в токенах) и делать GraphQL-запросы как к «базе данных» с готовыми данными. Детально можно прочитать о протоколе на thegraph.com.

Во-вторых, The Graph также является технологией, которая помогает создать ETL-процесс (Extract-Transform-Load), или так называемый «сабграф», который будет собирать необходимые данные, хранит их в базе данных и делает их доступными с помощью GraphQL.

Кроме того, чем еще хороша эта технологи — это ее принятие индустрией. В частности, вам не нужно запускать какое-либо специальную инфраструктуру, архивные блокчейн-ноды, индексаторы и т. д., потому что вы можете использовать уже существующих провайдеров инфраструктуры The Graph и деплоить свои сабграфы в один из них, получая взамен GraphQL эндпоинт.

Все это делает сабграфы привлекательным и удобным инструментом для разработчиков Web3, а также аналитиков или исследователей. Итак, приступим.

Как начать работу со сабграфами

Шаблон кода сабграфа можно создать с помощью командной утилиты graph-cli. Чтобы установить ее, выполните следующую команду:

npm install -g @graphprotocol/graph-cli

или

yarn global add @graphprotocol/graph-cli

Далее нужно запустить команду инициализации проекта «graph init», в которую удобно сразу передать все необходимые параметры (хотя можно вводить их по запросу):

graph init tornado_subgraph /path/to/new/project/tornado \   --protocol=ethereum --product=hosted-service \   --allow-simple-name --contract-name TornadoContract \   --from-contract=0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF \   --index-events --start-block=17000000 --network=mainnet

Вы также можете изменить параметр «start-block» на блок, с которого вы действительно хотели бы начать. Например, это может быть блок, когда контракт был задеплоен. Вы можете перейти на Etherscan, найти этот контракт по адресу 0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF (адрес смарт-контракта как правило можно найти на сайте проекта) и пролистать до первой транзакции. Номер блока в данном случае будет 9117720. Кроме того, здеcь указано, что логгировать будем данные с ethereum mainnet (поля —protocol и —network).

В результате выполнения этой команды мы получим папку проекта, которую можно развернуть на любом хостинге сабграфов. Но в этом случае данные будут ограничены только переменными, которые логгируются событиями (events в коде смарт-контрактов на Solidity).

Что означают «переменные, логгируемые событиями»

Если вы откроете код этого смарт-контракта на языке Solidity, вы увидите несколько классов Contract, включающих некоторые функции, такие как deposit или withdraw:

function deposit(bytes32 _commitment) external payable nonReentrant {     require(!commitments[_commitment], "The commitment has been submitted");      uint32 insertedIndex = _insert(_commitment);     commitments[_commitment] = true;     _processDeposit();      emit Deposit(_commitment, insertedIndex, block.timestamp);   }  function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable nonReentrant {     require(_fee <= denomination, "Fee exceeds transfer value");     require(!nullifierHashes[_nullifierHash], "The note has been already spent");     require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one     require(verifier.verifyProof(_proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]), "Invalid withdraw proof");      nullifierHashes[_nullifierHash] = true;     _processWithdraw(_recipient, _relayer, _fee, _refund);     emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);   }

Вы можете заметить, что в конце этих функций генерируется событие Deposit/Withdrawal (строки emit Deposit… и emit Withdrawal…). Это означает, что переменные в скобках будут сохранены в журнале блокчейна, к которому легко можно получить доступ с помощью нашего проекта сабграфа (который мы только что создали). Эти события в коде контракта описаны следующим образом:

event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);  event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);

Если вам нужны только эти переменные, вы просто можете развернуть сабграф на платформе хостинга сабграфов, и все — вы получите GraphQL-эндпоинт, который можно вызвать с помощью GraphQL следующим образом (пример для события Withdrawal):

{   withdrawals(first: 10) {     id     to     nullifierHash     relayer     fee     blockNumber     blockTimestamp     transactionHash   } }

Так, когда вы имеете код сабграфа и понимание, как делать к нему запросы, остается только задеплоить его на хостинг и дождаться, когда он будет синхронизирован. Здесь вы можете посмотреть туториал и демо о том, как развернуть сабграф на платформе Chainstack, однако вы также можете использовать любую другую платформу.

Теперь пришло время посмотреть, что мы фактически сгенерировали с помощью команды «graph init«. Чтобы управлять поведением сабграфа, вам понадобится работать только с 3 файлами.

Первый файл — «subgraph.yaml» — называется манифестом. Этот код будет выглядеть следующим образом:

specVersion: 0.0.5 schema:   file: ./schema.graphql dataSources:   - kind: ethereum     name: TornadoContract     network: mainnet     source:       address: "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF"       abi: TornadoContract       startBlock: 17000000     mapping:       kind: ethereum/events       apiVersion: 0.0.7       language: wasm/assemblyscript       entities:         - Deposit         - Withdrawal       abis:         - name: TornadoContract           file: ./abis/TornadoContract.json       eventHandlers:         - event: Deposit(indexed bytes32,uint32,uint256)           handler: handleDeposit         - event: Withdrawal(address,bytes32,indexed address,uint256)           handler: handleWithdrawal       file: ./src/tornado-contract.ts

Важными вещами являются (упоминали при генерировании сабграфа командой graph init): chain/network, startBlock, и, кроме того, названия событий и пути к исходным файлам. Все понятно интуитивно. Вы можете оставить этот файл без изменений.

Второй файл, schema.graphql, описывает, как будут храниться наши данные из событий. Файл по умолчанию для этого смарт-контракта будет выглядеть следующим образом:

type Deposit @entity(immutable: true) {   id: Bytes!   commitment: Bytes! # bytes32   leafIndex: BigInt! # uint32   timestamp: BigInt! # uint256   blockNumber: BigInt!   blockTimestamp: BigInt!   transactionHash: Bytes! }  type Withdrawal @entity(immutable: true) {   id: Bytes!   to: Bytes! # address   nullifierHash: Bytes! # bytes32   relayer: Bytes! # address   fee: BigInt! # uint256   blockNumber: BigInt!   blockTimestamp: BigInt!   transactionHash: Bytes! }

Если вы хотите добавить что-то, вы можете изменить это прямо в этом файле. Но это только описание того, как сохранить данные, а не о том, как их получить. Здесь вы можете найти материал, объясняющий как устроены схемы сабграфов.

Третий файл, src/tornado-contract.ts — содержит фактическую логику того, как получить данные из событий (и не только из событий!) и как поместить их в таблицы, которые мы только что описали выше. Этот файл выглядит так:

import {   Deposit as DepositEvent,   Withdrawal as WithdrawalEvent } from "../generated/TornadoContract/TornadoContract" import { Deposit, Withdrawal } from "../generated/schema" import { Address, BigInt } from "@graphprotocol/graph-ts" import { TornadoContract } from "../generated/TornadoContract/TornadoContract"  export function handleDeposit(event: DepositEvent): void {   let entity = new Deposit(     event.transaction.hash.concatI32(event.logIndex.toI32())   )    entity.commitment = event.params.commitment   entity.leafIndex = event.params.leafIndex   entity.timestamp = event.params.timestamp    entity.blockNumber = event.block.number   entity.blockTimestamp = event.block.timestamp   entity.transactionHash = event.transaction.hash    entity.save() }  export function handleWithdrawal(event: WithdrawalEvent): void {   let entity = new Withdrawal(     event.transaction.hash.concatI32(event.logIndex.toI32())   )   entity.to = event.params.to   entity.nullifierHash = event.params.nullifierHash   entity.relayer = event.params.relayer   entity.fee = event.params.fee    entity.blockNumber = event.block.number   entity.blockTimestamp = event.block.timestamp   entity.transactionHash = event.transaction.hash    entity.save() }

Как видите, в этом примере данные просто копируются данные из полей переменной «event» в объект Deposit/Withdrawal в соответствующие заполнители. Этот код был сгенерирован и может быть развернут без внесения изменений.

Но что если для каждой транзакции, связанной с Tornado Cash, нам необходима дополнительная информация? Например, в информации отсутствует адрес, который отправляет свои «деньги» на контракт Tornado Cash. Давайте добавим его в несколько строк кода.

Одна вещь, которую вам нужно знать здесь. Когда вы получаете переменную «event», она также содержит гораздо больше информации помимо переданных параметров. Полные сущности данных, которые могут быть легко извлечены, включают:

class Event {   address: Address   logIndex: BigInt   transactionLogIndex: BigInt   logType: string | null   block: Block   transaction: Transaction   parameters: Array<EventParam>   receipt: TransactionReceipt | null }  class Block {   hash: Bytes   parentHash: Bytes   unclesHash: Bytes   author: Address   stateRoot: Bytes   transactionsRoot: Bytes   receiptsRoot: Bytes   number: BigInt   gasUsed: BigInt   gasLimit: BigInt   timestamp: BigInt   difficulty: BigInt   totalDifficulty: BigInt   size: BigInt | null   baseFeePerGas: BigInt | null }  class Transaction {   hash: Bytes   index: BigInt   from: Address   to: Address | null   value: BigInt   gasLimit: BigInt   gasPrice: BigInt   input: Bytes   nonce: BigInt }  class TransactionReceipt {   transactionHash: Bytes   transactionIndex: BigInt   blockHash: Bytes   blockNumber: BigInt   cumulativeGasUsed: BigInt   gasUsed: BigInt   contractAddress: Address   logs: Array<Log>   status: BigInt   root: Bytes   logsBloom: Bytes }  class Log {   address: Address   topics: Array<Bytes>   data: Bytes   blockHash: Bytes   blockNumber: Bytes   transactionHash: Bytes   transactionIndex: BigInt   logIndex: BigInt   transactionLogIndex: BigInt   logType: string   removed: bool | null }

Допустим, я хочу добавить поля “from” и “value” из структуры Transaction. Чтобы это сделать, нужно добавить несколько строк кода в src/tornado-contract.ts:

  entity.blockNumber = event.block.number   entity.blockTimestamp = event.block.timestamp   entity.transactionHash = event.transaction.hash    // LINE#1 The address that triggered the event can be accessed via event.transaction.from   entity.from_ = event.transaction.from    // LINE#2 The value of the transaction in Wei can be accessed via event.transaction.value   entity.value_ = event.transaction.value    entity.save()

Также нужно добавить пару строк в schema.graphql:

type Deposit @entity(immutable: true) {   id: Bytes!   from_: Bytes! # LINE#1   value_: BigInt! # LINE#2   commitment: Bytes! # bytes32   leafIndex: BigInt! # uint32   timestamp: BigInt! # uint256   blockNumber: BigInt!   blockTimestamp: BigInt!   transactionHash: Bytes! }

Осталось задеплоить сабграф на хостинг. Команда будет выглядеть вот так:

graph deploy \   --node https://api.graph-eu.p2pify.com/3a57099edc73524c2807cafeefaa82e1/deploy --ipfs https://api.graph-eu.p2pify.com/3a57099ec3635c2807cafeefaa82e1/ipfs \   tornado_subgraph

Далее данные можно получать через GraphQL такими запросами (в UI):

{   deposits(first: 10) {     id     commitment     leafIndex     timestamp     transactionHash     from_     value_   } }

или через командную строку:

curl -g \\   -X POST \\   -H "Content-Type: application/json" \\   -d '{"query":"{deposits(first: 10) { id  commitment leafIndex timestamp transactionHash from_ value_}}"}' \\      https://ethereum-mainnet.graph-eu.p2pify.com/3c6e0b8ac43232a8228b9a98ca1531d/tornado_subgraph

Но что, если вам нужно сохранить результаты вызова смарт-контракта как значение? Это тоже можно сделать из сабграфа, запустив eth_call прямо из кода. Вы можете самостоятельно протестировать эту возможность, если пройдете туториал «Индексирование баланса токенов ERC-20».

Список доп. материалов по теме:

Кроме того, много ссылок, видео и мануалов собрано в репозитории awesome-subgraphs.

Если у вас есть вопросы по разработке/использованию сабграфов, их можно задать их в Telegram-чате комьюнити разработчиков сабграфов Subgraphs Experience Sharing.


ссылка на оригинал статьи https://habr.com/ru/articles/754460/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *