Skip to content

使用Subsquid检索本地Moonbeam开发节点

作者:Erin Shaben

概览

在开发dApp时,使用本地的开发环境而非如测试网或主网等实际运作的网络来开发智能合约是有益的。在本地进行开发,可以消除在实际网络开发时会遇到的一些麻烦,例如必须为开发账户提供资金和等待区块生成等等。在Moonbeam,开发者可以启动他们自己的本地Moonbeam 开发节点以快速轻松地构建和测试应用。

但那些依赖检索器来检索区块链数据的dApp呢?这些应用的开发者该如何简化开发过程?Subsquid 是一个为Moonbeam等基于Substrate区块链开发的数据网络。它包含了超过100种区块链的数据,开发者现在也可以用它在本地开发环境(例如您的Moonbeam开发节点)上检索内容!

本教程将带您了解使用Subsquid在本地Moonbeam开发节点上检索数据的过程。我们将会创建一个ERC-20合约并使用Subsquid来检索我们的ERC-20的转账记录。

本教程基于Massimo Luraschi关于如何通过本地检索提高dApp开发效率的教程{target=_blank},但已针对Moonbeam开发节点进行了修改。

查看先决条件

要跟随此教程,您需要具备以下条件:

在后面的教程中我们将会配置Hardhat项目和创建Subsquid项目。

创建一个本地开发节点

首先,我们将使用Docker启动本地Moonbeam开发节点。出于本教程的教学目的,我们将配置我们的开发节点,使其以每四秒生成(密封)区块,这将简化调试过程。但是,您可以根据需求随意增加或减少这个时间,或者将您的节点配置为立即密封区块。使用即时密封设定时,区块链将在收到交易时创建一个区块。

在启动本地节点时,我们将会使用以下指令:

  • --dev - 指定使用开发链
  • --sealing 4000 - 每四秒(4000毫秒)密封一个区块
  • --rpc-external - 监听所有HTTP与WebSocket接口

要创建一个开发节点,您可以运行以下指令为Moonbeam提供最新的Docker映像:

docker run --rm --name moonbeam_development --network host \
moonbeamfoundation/moonbeam:v0.37.1 \
--dev --sealing 4000 --ws-external --rpc-external
docker run --rm --name moonbeam_development -p 9944:9944 \
moonbeamfoundation/moonbeam:v0.37.1 \
--dev --sealing 4000 --ws-external --rpc-external
docker run --rm --name moonbeam_development -p 9944:9944 ^
moonbeamfoundation/moonbeam:v0.37.1 ^
--dev --sealing 4000 --ws-external --rpc-external

这些指令将会启动我们的开发节点,您可以使用9944端口。

Spin up a Moonbeam development node

我们的开发节点具有10个预先拥有资金的账户。

开发账户地址和私钥
  • Alith:

    • Public Address: 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac
    • Private Key: 0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133
  • Baltathar:

    • Public Address: 0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0
    • Private Key: 0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b
  • Charleth:

    • Public Address: 0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc
    • Private Key: 0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b
  • Dorothy:

    • Public Address: 0x773539d4Ac0e786233D90A233654ccEE26a613D9
    • Private Key: 0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68
  • Ethan:

    • Public Address: 0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB
    • Private Key: 0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4
  • Faith:

    • Public Address: 0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d
    • Private Key: 0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df
  • Goliath:

    • Public Address: 0x7BF369283338E12C90514468aa3868A551AB2929
    • Private Key: 0x96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18
  • Heath:

    • Public Address: 0x931f3600a299fd9B24cEfB3BfF79388D19804BeA
    • Private Key: 0x0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab
  • Ida:

    • Public Address: 0xC41C5F1123ECCd5ce233578B2e7ebd5693869d73
    • Private Key: 0x4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8
  • Judith:

    • Public Address: 0x2898FE7a42Be376C8BC7AF536A940F7Fd5aDd423
    • Private Key: 0x94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b

关于更多运行Moonbeam开发节点的信息,请查看设置Moonbeam开发节点的教程。

设置一个Hardhat项目

你应该已经创建了一个空白的Hardhat项目,但如果你并没有创建Hardhat项目,你可以在我们的Hardhat文档页面查看创建一个Hardhat项目的教程。

在本部分教程中,我们将为本地Moonbeam开发节点配置我们的Hardhat项目,创建ERC-20合约,并编写脚本以部署我们的合约并与之交互。

在开始创建项目之前,首先要安装一些需要的依赖项:Hardhat Ethers插件OpenZeppelin合约。Hardhat Ethers插件提供了一种使用Ethers库与网络交互的便捷方式。我们将使用OpenZeppelin的基础ERC-20实现来创建ERC-20。要安装这两个依赖项,您可以运行以下指令:

npm install @nomicfoundation/hardhat-ethers ethers@6 @openzeppelin/contracts
yarn add @nomicfoundation/hardhat-ethers ethers@6 @openzeppelin/contracts

为一个本地开发节点配置Hardhat

在更新配置文件之前,我们需要获得其中一个开发帐户的私钥,该私钥将用于部署我们的合约和发送交易。在此例子中,我们将使用Alith的私钥:

0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133

注意事项

请勿将私钥存储在JavaScript或Python文件中。

开发账户中通常包含私钥,且这些账户存在于您自己的开发环境。然而,当您持续为如Moonbase Alpha或Moonbeam(超出本教程的范围)等实时网络建立索引时,您需要通过指定的secret manager管理器或类似服务管理您的私钥。

现在我们可以编辑hardhat.config.js为我们的Moonbeam开发节点包含以下网络和帐户配置:

require('@nomicfoundation/hardhat-ethers');

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.17",
  networks: {
    dev: {
      url: 'http://127.0.0.1:9944',
      chainId: 1281, // (hex: 0x501),
      accounts: [
        '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133',
      ], // Alith's private key
    },
  },
};

创建一个ERC-20合约

出于本教程目的,我们将创建一个简单的ERC-20合约。我们将依赖OpenZeppelin的ERC-20基础实现。我们将从为合约创建一个文件并将其命名为MyTok.sol开始:

mkdir -p contracts && touch contracts/MyTok.sol

现在我们可以编辑MyTok.sol文件并包含以下合约,它将生成MYTOK的初始供应数并仅允许合约所有者生成额外的Token:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyTok is ERC20, Ownable {
    constructor(uint256 initialSupply) ERC20("MyToken", "MYTOK") {
        _mint(msg.sender, initialSupply);
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

部署一个ERC-20合约

现在我们已经成功设置我们的合约,我们能够编译和部署合约。

您可以运行以下指令编译合约:

npx hardhat compile

Compile contracts using Hardhat

此指令将会编译合约并为其产生一个包含artifacts和合约ABI的目录。

要部署合约,我们需要创建一个部署脚本来部署我们的ERC-20合约并生成MYTOK的初始供应。我们将使用Alith的账户来部署合约,并指定初始供应量为1000 MYTOK。MYTOK的初始供应将被铸造并发送给合约所有者,也就是Alith。

让我们跟随以下步骤来部署我们的合约:

  1. 为我们的脚本创建一个目录的文件:

    mkdir -p scripts && touch scripts/deploy.js
    
  2. deploy.js文件中添加以下脚本:

    // We require the Hardhat Runtime Environment explicitly here. This is optional
    // but useful for running the script in a standalone fashion through `node <script>`.
    //
    // You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
    // will compile your contracts, add the Hardhat Runtime Environment's members to the
    // global scope, and execute the script.
    const hre = require('hardhat');
    
    async function main() {
      // Get ERC-20 Contract
      const MyTok = await hre.ethers.getContractFactory('MyTok');
    
      // Deploy it with Inital supply of 1000
      const myTok = await MyTok.deploy(1000000000000000000000n);
    
      // Wait for the Deployment
      await myTok.deployed();
    
      console.log(`Contract deployed to ${myTok.address}`);
    }
    
    // We recommend this pattern to be able to use async/await everywhere
    // and properly handle errors.
    main().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
    
  3. 使用我们在hardhat.config.js文件中设置的dev网络配置运行脚本:

    npx hardhat run scripts/deploy.js --network dev
    

部署合约的地址应当在终端出现,请保存该地址,我们将在后面的教程中用于合约交互。

Deploy contracts using Hardhat

转移ERC-20

由于我们将检索ERC-20的Transfer事件,我们需要发送一些交易,将一些Token从Alith的账户转移到我们的其他测试账户。为此,我们将创建一个简单的脚本,将10个MYTOK转移给Baltathar、Charleth、Dorothy和Ethan。请跟随以下步骤:

  1. 创建一个新的文件脚本以传送交易

    touch scripts/transactions.js
    
  2. transactions.js文件中添加以下脚本:

    // We require the Hardhat Runtime Environment explicitly here. This is optional
    // but useful for running the script in a standalone fashion through `node <script>`.
    //
    // You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
    // will compile your contracts, add the Hardhat Runtime Environment's members to the
    // global scope, and execute the script.
    const hre = require('hardhat');
    async function main() {
      // Get Contract ABI
      const MyTok = await hre.ethers.getContractFactory('MyTok');
    
      // Plug ABI to Address
      const myTok = await MyTok.attach(
        '0xc01Ee7f10EA4aF4673cFff62710E1D7792aBa8f3'
      );
    
      const value = hre.ethers.utils.parseUnits('10', 'ether');
    
      let tx;
      // Transfer to Baltathar
      tx = await myTok.transfer(
        '0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0',
        value
      );
      await tx.wait();
      console.log(`Transfer to Baltathar with TxHash ${tx.hash}`);
    
      // Transfer to Charleth
      tx = await myTok.transfer(
        '0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc',
        value
      );
      await tx.wait();
      console.log(`Transfer to Charleth with TxHash ${tx.hash}`);
    
      // Transfer to Dorothy
      tx = await myTok.transfer(
        '0x773539d4Ac0e786233D90A233654ccEE26a613D9',
        value
      );
      await tx.wait();
      console.log(`Transfer to Dorothy with TxHash ${tx.hash}`);
    
      // Transfer to Ethan
      tx = await myTok.transfer(
        '0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB',
        value
      );
      await tx.wait();
      console.log(`Transfer to Ethan with TxHash ${tx.hash}`);
    }
    // We recommend this pattern to be able to use async/await everywhere
    // and properly handle errors.
    main().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
    
  3. 运行脚本以传送交易:

    npx hardhat run scripts/transactions.js --network dev
    

当交易传送成功,您将会在终端中看到交易记录。

Send transactions using Hardhat

现在我们可以创建Squid以在本地开发节点检索数据。

创建一个Subsquid项目

现在我们将开始创建Subsquid项目。首先,我们需要安装Subsquid CLI

npm i -g @subsquid/cli

现在我们将能够使用sqd指令与我们的Squid项目进行交互。要创建我们的项目,我们将使用-t标志,这将从模板创建一个项目。我们将使用EVM Squid模板,这是一个用于检索EVM链的入门项目。

您可以运行以下指令创建一个名为local-squid的EVM Squid项目:

sqd init local-squid -t evm

这将会创建一个拥有所有必要依赖项的Squid项目。您可以继续操作并安装所有依赖项:

cd local-squid && npm install

现在我们已经完成我们项目的初始配置,我们需要配置我们的项目以从本地开发节点检索ERC-20Transfer事件。

检索一个本地Moonbeam开发节点

要检索本地开发节点,我们将使用Subsquid的EVM Archive。如果您不熟悉Subsquid,Archive是链上数据的数据湖,EVM Archive用于EVM数据。

EVM Archive通过Subsquid的subsquid/eth-archive-workerDocker映像提供。我们将Archive配置通过将其指向我们在9944运行的开发节点端口来检索我们的链上数据。

要开始进行操作,我们需要为Archive创建一个新的目录和Docker编译文件。

mkdir archive && touch archive/docker-compose.archive.yml

接着,我们可以添加以下代码至docker-compose.archive.yml文件中:

version: "3"

services:
  worker:
    image: subsquid/eth-archive-worker:latest
    environment:
      RUST_LOG: "info"
    ports:
      - 8080:8080
    command: [
            "/eth/eth-archive-worker",
            "--server-addr", "0.0.0.0:8080",
            "--db-path", "/data/db",
            "--data-path", "/data/parquet/files",
            "--request-timeout-secs", "300",
            "--connect-timeout-ms", "1000",
            "--block-batch-size", "10",
            "--http-req-concurrency", "10",
            "--best-block-offset", "10",
            "--rpc-urls", "http://host.docker.internal:9944/",
            "--max-resp-body-size", "30",
            "--resp-time-limit", "5000",
            "--query-concurrency", "16",
    ]
    # Uncomment this section on Linux machines.
    # The connection to local RPC node will not work otherwise.
    # extra_hosts:
    #   - "host.docker.internal:host-gateway"
    volumes:
      - database:/data/db

volumes:
  database:

注意事项

如果您使用的是Linux,别忘记取消extra_hosts部分的评论。

为了轻松运行Archive,让我们更新位于local-squid根目录中的先前存在的commands.json文件,以包含archive-uparchive-down指令,这将启动并根据需要关闭Archive:

{
    "$schema": "https://cdn.subsquid.io/schemas/commands.json",
    "commands": {
        "archive-up": {
            "description": "Start local Moonbeam Archive",
            "cmd": [
                "docker-compose",
                "-f",
                "archive/docker-compose.archive.yml",
                "up",
                "-d"
            ]
        },
        "archive-down": {
            "description": "Stop local Moonbeam Archive",
            "cmd": [
                "docker-compose",
                "-f",
                "archive/docker-compose.archive.yml",
                "down"
            ]
        },
        // ...
    }
}

注意事项

commands对象中添加两个新指令的位置并不重要。您可以随意将它们添加到列表顶部或您认为合适的任何位置。

现在我们可以运行以下指令开始我们的Archive:

sqd archive-up

这将会在8080端口运行我们的Archive。

Spin up a local Subsquid EVM Archive

Archive的部分就是这样!现在我们需要更新我们的Squid项目来检索ERC-20Transfer事件,然后我们就可以运行检索器了!

检索ERC-20转账

要检索ERC-20转账,我们需要执行以下步骤:

  1. 更新数据库结构并为数据生产模型
  2. 使用MyTok合约的ABI生成TypeScript接口,我们的Squid将使用这些来检索Transfer事件
  3. 配置处理器以处理来自我们本地开发节点和Archive的MyTok合约的Transfer事件。然后我们将添加逻辑以处理Transfer事件并保存处理过的传输数据

如先前所述,我们首先需要为传输数据定义数据库结构。为此,我们将编辑位于local-squid根目录中的schema.graphql文件,并创建一个Transfer实体:

type Transfer @entity {
  id: ID!
  block: Int!
  from: String! @index
  to: String! @index
  value: BigInt!
  txHash: String!
  timestamp: BigInt!
}

现在我们可以从结构中产生实体类,我们将会用这些信息处理转账数据:

sqd codegen

接着,我们可以使用我们列表上的第二个条目并用我们的合约ABI生成TypeScript接口类。为此,可以运行以下指令:

sqd typegen ../artifacts/contracts/MyTok.sol/MyTok.json

Run Subsquid commands

这将在src/abi/MyTok.ts文件中生成相关的TypeScript接口类。在本教程中,我们将专门使用events

在第三步,我们将开始更新处理器。处理器从Archive中获取链上数据,按照指定的方式转换数据,并保存结果。我们将在src/processor.ts文件中处理每一项。

我们将会采取以下步骤操作:

  1. 导入我们先前两个步骤产生的文件:数据模型和事件接口
  2. chain数据来源设置为我们的本地开发节点,并将archive设置为本地Archive
  3. 让处理器为MyTok合约处理EVM日志和筛选Transfer事件
  4. 添加逻辑以处理转账数据。我们将迭代与MyTok合约相关的每个区块和Transfer事件,解码它们,并将传输数据保存到数据库

您可以在src/processor.ts文件中使用以下代码取代所有原有的内容:

import { TypeormDatabase } from "@subsquid/typeorm-store";
import { EvmBatchProcessor } from "@subsquid/evm-processor";
import { Transfer } from "./model";
import { events } from "./abi/MyTok";

const contractAddress =
  '0xc01Ee7f10EA4aF4673cFff62710E1D7792aBa8f3'.toLowerCase();
const processor = new EvmBatchProcessor()
  .setDataSource({
    chain: "http://localhost:9944", // Local development node
    archive: "http://localhost:8080", // Local Archive
  })
  .addLog(contractAddress, {
    filter: [[events.Transfer.topic]],
    data: {
      evmLog: {
        topics: true,
        data: true,
      },
      transaction: {
        hash: true,
      },
    },
  });

processor.run(new TypeormDatabase(), async (ctx) => {
  const transfers: Transfer[] = [];
  for (let c of ctx.blocks) {
    for (let i of c.items) {
      if (i.address === contractAddress && i.kind === 'evmLog') {
        if (i.transaction) {
          const { from, to, value } = events.Transfer.decode(i.evmLog);
          transfers.push(
            new Transfer({
              id: `${String(c.header.height).padStart(
                10,
                '0'
              )}-${i.transaction.hash.slice(3, 8)}`,
              block: c.header.height,
              from: from,
              to: to,
              value: value.toBigInt(),
              timestamp: BigInt(c.header.timestamp),
              txHash: i.transaction.hash,
            })
          );
        }
      }
    }
  }
  await ctx.store.save(transfers);
});

现在我们已经完成所有必要的步骤,并已经准备好运行我们的检索器!

运行检索器

要运行检索器,我们需要运行一系列的sqd指令:

  1. 创建项目:

    sqd build
    
  2. 启动数据库:

    sqd up
    
  3. 删除EVM模板附带的数据库迁移文件,并为新数据库结构生成一个新文件:

    sqd migration:clean
    sqd migration:generate
    
  4. 启动检索器:

    sqd process
    

注意事项

您可以查看commands.json文件了解sqd指令执行了什么内容。

在终端中,您应当能看见检索器在处理区块!

Spin up a Subsquid indexer

如果您的Squid没有正确地检索区块,请确保您的开发节点正在使用--sealing标志运行。以本教程例子来说,你应该将标志设置为--sealing 4000,这样每四秒就会产生一个区块。您也可以根据需要随意编辑时间间隔。在您尝试再次启动您的Squid之前,请运行以下指令来关闭本地Archive和Squid:

sqd archive-down && sqd down

接着您可以启动本地Archive和备用Squid:

sqd archive-up && sqd up

最后您将能够重新继续检索:

sqd process

现在您的检索器将没有问题的检索您的开发节点!

检查检索器

要检查我们的检索器,我们需要在新的终端视窗中启动GraphQL服务器:

sqd serve

GraphQL服务器将被启动,您可以在localhost:4350/graphql进行访问。接着您将能够检查数据库中的所有转账数据:

query MyQuery {
  transfers {
    id
    block
    from
    to
    value
    txHash
  }
}

所有转账数据都应当出现,包含转账至Alith账户的初始供应转账,以及Alith与Baltathar、Charleth、Dorothy和Ethan之间的转账。

Query transfer data using the GraphQL server

就这样!您已经成功使用Subsquid在本地Moonbeam开发节点检索数据!您可以在GitHub上查看完整的项目内容。

本教程仅用于教育目的。 因此,不应在生产环境中使用本教程中创建的任何合约或代码。

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