Skip to content

在Moonbeam上使用Subsquid进行检索

Subsquid Banner

概览

Subsquid为基于Substrate区块链所使用的检索节点框架。简单而言,Subsquid可以被当成一个包含GraphQL服务器的ETL(提取、转换和加载)工具,提供全面的筛选、分页甚至是全文字搜索等服务。

Subsquid具有来自以太坊虚拟机(EVM)和Substrate数据的原生完整支持,允许开发者在任何Moonbeam网络中的任何项目提取链上数据并运行EVM记录和Substrate实体(事件、extrinsics和储存项),并利用单一个GraphQL端点提供搜索结果的相关数据。通过Subsquid,开发者既能够根据EVM主题、合约地址以及区块编号进行筛选。

本教程将会包含如何在Moonriver网络上创建一个Subsquid项目(也就是“Squid")检索ERC-721 Token的转移记录。因此,您将会专注于Transfer EVM事件主题中。此教程也同样适用于Moonbeam或Moonbase Alpha。

此处提供的信息仅供参考,由第三方提供。 Moonbeam文档网站(https://docs.moonbeam.network/)上列出和描述的任何项目与Moonbeam立场无关。

查看先决条件

要顺利运行Squid项目,您需要安装以下软件:

创建一个项目

您可以使用Subsquid提供的模板代码库来创建项目,您可以跟随以下步骤进行操作:

  1. 导向至GitHub上的squid-template代码库

  2. 点击Use this template按钮

  3. 选取账户以及用于您项目的代码库名称

  4. 复制创建的代码库(请记得将<account>更换为您的GitHub账户):

    git clone git@github.com:<account>/squid-template.git
    
  5. 接着安装项目目录中的依赖项:

    cd squid-template && npm i
    
  6. 同时,您还需要安装些许额外的依赖项以检索EVM数据:

    npm i @ethersproject/abi ethers @subsquid/substrate-evm-processor @subsquid/evm-typegen
    

Image from Gyazo

下个部分将会使用模板并根据个人需求一步一步的进行修改,以获得和运行正确的数据。要查看完整的项目,您可以导向至在GitHub上的squid-evm-template代码库

定义实体模式

要在此教程中根据需求修改项目,您将会需要在模式中进行改变并定义追踪的实体,包含如下:

  • Token转移
  • Token所有权
  • 合约以及其铸造的Token

您可以编辑schema.graphql文件进行修改:

type Token @entity {
  id: ID!
  owner: Owner
  uri: String
  transfers: [Transfer!]! @derivedFrom(field: "token")
  contract: Contract
}

type Owner @entity {
  id: ID!
  ownedTokens: [Token!]! @derivedFrom(field: "owner")
  balance: BigInt
}

type Contract @entity {
  id: ID!
  name: String
  symbol: String
  totalSupply: BigInt
  mintedTokens: [Token!]! @derivedFrom(field: "contract")
}

type Transfer @entity {
  id: ID!
  token: Token!
  from: Owner
  to: Owner
  timestamp: BigInt!
  block: Int!
  transactionHash: String!
}

其中有几点关于模式定义,值得注意的部分如下所示:

  • @entity —— 表示此类型将被转换在数据库中储存的ORM模型
  • @derivedFrom —— 表示数据中的区域将不会永久保持,而会进行变化
  • type references(如: from: Owner)—— 作为两个实体之间的关系连接

要为模式定义生产TypeScript实体,您可以运行codegen工具:

npx sqd codegen

您可以在src/model/generated找到自动生成的文件。

Subsquid Project structure

ABI定义和Wrapper

Subsquid支持为Substrate数据源(事件、extrinsics和储存项)自动构建TypeScript类型的安全接口,并会在Runtime中自动检测更变。要专门为EVM日志生成TypeScript接口和解码函数,可以使用Subsquid的evm-typegen工具。

要提取和运行ERC-721数据,您必须获取其应用二进制接口(Application Binary Interface, ABI)的定义。您可以在JSON文件中获取,并在其后导入项目之中。

  1. 创建一个abis文件夹并为ERC-721 ABI创建一个JSON文件

    mkdir src/abis
    touch src/abis/ERC721.json
    
  2. 复制ERC-721接口的ABI,并粘贴至ERC721.json文件

注意事项

ERC-721 ABI定义了合约里面所有事件的签名。Transfer事件有三个函数,分别为 fromtotokenId,其类型分别为addressaddressuint256。因此,Transfer事件真正的定义将为Transfer(address, address, uint256)

修改TypeScript配置

要在TypeScript代码中读取并导入ABI JSON文件,您需要为tsconfig.json文件添加一个选项。请打开文件并在"compilerOptions"部分添加"resolveJsonModule": true选项:

// tsconfig.json
{
  "compilerOptions": {
    ...
    "resolveJsonModule": true
  },
  ...
}

使用ABI以获得和解码事件数据

要从ABI定义自动生成TypeScript接口并解码事件数据,只需从项目的根文件夹运行以下命令:

npx squid-evm-typegen --abi src/abi/ERC721.json --output src/abi/erc721.ts

abi参数指向之前创建的JSON文件,而output参数是将由命令本身生成的文件的名称。

定义和绑定事件处理程序

Subsquid SDK提供用户处理器,被称为SubstrateProcessor,在特定情况下被称为SubstrateEvmProcessor。*处理器连接至 Subsquid archive以获取链上数据。其自开始设定的开始区块运作,直到设定的最后一个区块或是新的数据被加入至链上时停止。

处理器提供会“处理”如同Substrate事件、extrinsics、储存项或是EVM记录等特定数据的附加函数。这函数能够通过指定事件、extrinsic名称、EVM记录合约地址进行配置。当处理器正在处理数据时,如果其遇到配置的事件名称,他将会执行“处理”函数内的内容。

在开始使用事件处理程序之前,有必要定义一些常量和一些辅助函数来管理EVM合约。您可以为这些项目创建一个附加文件:

touch src/contract.ts

管理EVM合约

src/contract.ts 文件中,您将执行以下步骤:

  1. 定义链节点端点(可选)
  2. 创建一个合约接口来存储地址和ABI等信息
  3. 定义函数以从数据库中获取合约实体或创建一个
  4. 定义processTransfer EVM日志处理程序,实现跟踪令牌传输的逻辑
// src/contracts.ts
import { assertNotNull, Store } from "@subsquid/substrate-evm-processor";
import { ethers } from "ethers";
import * as erc721 from "./abi/erc721";
import { Contract } from "./model";

export const CHAIN_NODE = "wss://wss.api.moonriver.moonbeam.network";

export const contract = new ethers.Contract(
  "0xb654611f84a8dc429ba3cb4fda9fad236c505a1a",
  erc721.abi,
  new ethers.providers.WebSocketProvider(assertNotNull(CHAIN_NODE))
);

export function createContractEntity(): Contract {
  return new Contract({
    id: contract.address,
    name: "Moonsama",
    symbol: "MSAMA",
    totalSupply: 1000n,
  });
}

let contractEntity: Contract | undefined;

export async function getContractEntity({
  store,
}: {
  store: Store;
}): Promise<Contract> {
  if (contractEntity == null) {
    contractEntity = await store.get(Contract, contract.address);
  }
  return assertNotNull(contractEntity);
}

async function processTransfer(ctx: EvmLogHandlerContext): Promise<void> {
  const transfer =
    events["Transfer(address,address,uint256)"].decode(ctx);

  let from = await ctx.store.get(Owner, transfer.from);
  if (from == null) {
    from = new Owner({ id: transfer.from, balance: 0n });
    await ctx.store.save(from);
  }

  let to = await ctx.store.get(Owner, transfer.to);
  if (to == null) {
    to = new Owner({ id: transfer.to, balance: 0n });
    await ctx.store.save(to);
  }

  let token = await ctx.store.get(Token, transfer.tokenId.toString());
  if (token == null) {
    token = new Token({
      id: transfer.tokenId.toString(),
      uri: await contract.tokenURI(transfer.tokenId),
      contract: await getContractEntity(ctx),
      owner: to,
    });
    await ctx.store.save(token);
  } else {
    token.owner = to;
    await ctx.store.save(token);
  }

  await ctx.store.save(
    new Transfer({
      id: ctx.txHash,
      token,
      from,
      to,
      timestamp: BigInt(ctx.substrate.block.timestamp),
      block: ctx.substrate.block.height,
      transactionHash: ctx.txHash,
    })
  );
}

“处理程序”函数包含一个Context的正确类型(此例为EvmLogHandlerContext)。此情况包含触发事件和接口以储存数据,并用于提取、运行和储存数据至数据库中。

注意事项

关于活动处理程序,其同样能够在处理器中被绑定为”箭头函数“。

创建处理器并附带处理程序

现在您可以将处理程序函数附加至处理器并设置处理其确保能够顺利执行。您可以编辑src/processor.ts文件以进行操作。

  1. 移除先前存在的代码
  2. 更新导入以包含CHAIN_NODEcontract常量、getContractEntitycreateContractEntity辅助函数、processTransfer处理函数和events映射
  3. 使用SubstrateEvmProcessor创建一个处理器并自行命名。举例而言,您可以使用moonriver-substrate或是根据您使用的网络命名
  4. 更新数据源和类型包
  5. 附加EVM记录处理程序函数和区块前的触发器,其将会在数据库中创建和储存合约实体
// src/processor.ts
import {
  EvmLogHandlerContext,
  SubstrateEvmProcessor,
} from "@subsquid/substrate-evm-processor";
import { lookupArchive } from "@subsquid/archive-registry";
import { CHAIN_NODE, contract, createContractEntity, getContractEntity, processTransfer } from "./contract";
import { events } from "./abi/erc721";
import { Owner, Token, Transfer } from "./model";

const processor = new SubstrateEvmProcessor("moonriver-substrate");

processor.setDataSource({
  chain: CHAIN_NODE,
  archive: lookupArchive("moonriver")[0].url,
});

processor.addPreHook({ range: { from: 0, to: 0 } }, async (ctx) => {
  await ctx.store.save(createContractEntity());
});

processor.addEvmLogHandler(
  contract.address,
  {
    filter: [events["Transfer(address,address,uint256)"].topic],
  },
  processTransfer
);

processor.run();

如果您希望在Moonbeam或Moonbase Alpha上进行操作,请确认数据源已更新至正确网路:

processor.setDataSource({
  chain: CHAIN_NODE,
  archive: lookupArchive("moonbeam")[0].url,
});
processor.setDataSource({
  chain: CHAIN_NODE,
  archive: lookupArchive("moonriver")[0].url,
});
processor.setDataSource({
  chain: CHAIN_NODE,
  archive: lookupArchive("moonbase")[0].url,
});

注意事项

lookupArchive为用于查询archive registry并根据网络名称获取存档地址。网络名称均需为小写。

启动和设置数据库

当您在本地运行项目,在此教程中您可以使用docker-compose.yml文件的模板启动一个PostgreSQL容器。您可以在您的终端中运行以下命令进行操作:

docker-compose up -d

Image from Gyazo

注意事项

-d参数可自行决定是否使用,其将以daemon模式启动容器,如此一来终端将不会被阻挡且不会出现其他额外输出。

Squid项目皆会通过ORM abstraction自动管理数据库连接和模式。

要设置数据库,您可以跟随以下步骤进行操作:

  1. 构建代码

    npm run build
    
  2. 移除模板的默认迁移

    rm -rf db/migrations/*.js
    
  3. 确认Postgres Docker容器squid-template_db_1正在运行

    docker ps -a
    
  4. 关闭目前数据库(如果您先前并未运行任何项目则无需执行此动作),并创建新的数据库、创建初始迁移并应用迁移

    npx sqd db drop
    npx sqd db create
    npx sqd db create-migration Init
    npx sqd db migrate
    

    Drop the database, re-create it, generate a migration and apply it

启动项目

您可以使用以下命令启动处理器(其将会阻挡当前终端):

node -r dotenv/config lib/processor.js

Image from Gyazo

最终,在个别的终端视窗中启动GraphQL服务器:

npx squid-graphql-server

导向至localhost:4350/graphql以获取GraphiQl控制器。在此视窗中,您可以进行执行如下所示的查询,以搜索拥有最大余额的账户持有人:

query MyQuery {
  owners(limit: 10, where: {}, orderBy: balance_DESC) {
    balance
    id
  }
}

或是查看特定用户所持有的Token:

query MyQuery {
  tokens(where: {owner: {id_eq: "0x495E889d1A6cEB447a57dcc1C68410299392380c"}}) {
    uri
    contract {
      id
      name
      symbol
      totalSupply
    }
  }
}

GraphiQL playground with some sample queries

恭喜您已成功设置,您可以开始使用此搜索功能了!

发布项目

Subsquid提供一个SaaS解决方案以托管其社区创建的项目。请查看Subsquid官方文档网站上部署您的Squid的教程以获取更多信息。

您同样可以导向至Aquarium查看其托管的项目。

示例项目代码库

您可以在GitHub上查看完成的完整项目

Subsquid文档包含许多信息且为最适合新手开始的平台,如果您希望了解除教程外的更多信息,请查看官方文档网站。

本网站的所有信息由第三方提供,仅供参考之用。Moonbeam文档网站(https://docs.moonbeam.network/)上列出和描述的任何项目与Moonbeam立场无关。Moonbeam Foundation不保证网站信息的准确性、完整性或真实性。如使用或依赖本网站信息,需自行承担相关风险,Moonbeam Foundation不承担任何责任和义务。这些材料的所有陈述和/或意见由提供方个人或实体负责,与Moonbeam Foundation立场无关,概不构成任何投资建议。对于任何特定事项或情况,应寻求专业权威人士的建议。此处的信息可能会包含或链接至第三方提供的信息与/或第三方服务(包括任何第三方网站等)。这类链接网站不受Moonbeam Foundation控制。Moonbeam Foundation对此类链接网站的内容(包括此类链接网站上包含的任何信息或资料)概不负责也不认可。这些链接内容仅为方便访客而提供,Moonbeam Foundation对因您使用此信息或任何第三方网站或服务提供的信息而产生的所有责任概不负责。

Last update: February 24, 2023
Created: May 10, 2022