在以太坊上构建 GraphQL API
去中心化网络基础设施
生产dapp的数量持续激增,使用 Solidity 和其他区块链语言构建的开发人员的需求持续超过供应,导致工资越来越高。
作为一名初入这个领域的开发者,我很快意识到,我们与区块链交互和构建的方式与我过去在传统 Web 中习惯的方式存在诸多差异。以太坊(以及其他区块链)的数据并非以一种能够高效或轻松地被其他应用程序或前端直接使用的格式存储。问题在于,你需要对数据进行索引和组织,以便高效地检索。
传统上,这是数据库在集中式技术堆栈中所做的工作,但Web3 堆栈中缺少该索引层。
在传统的 Web 技术栈中,数据库、服务器和 API 会对数据进行查询、过滤、排序、分页、分组和连接操作,然后通常通过某种类型的 HTTP 请求将数据返回给应用程序。而直接从以太坊或其他区块链读取数据时,这些类型的数据转换是无法实现的。
过去,开发人员通过构建自己的中心化索引服务器来解决这个问题——从区块链中提取数据,将其存储在数据库中,然后通过 API 端点公开。这需要大量的工程和硬件资源,并且破坏了去中心化所需的重要安全属性。
我们如何在区块链数据之上构建一个可以轻松部署到去中心化 Web 基础设施的 API?让我们来一探究竟。
去中心化网络基础设施
去中心化互联网的愿景和发展方向通常被称为Web3。Web3通过以下新增特性增强了我们今天所熟知的互联网:
- 去中心化
- 可验证
- 无需信任
- 自治
要详细了解 Web3,请观看Juan Benet的这段视频
为了实现去中心化,协议定义了无需中介即可提供一系列数字服务的网络,例如计算、存储、带宽、身份和其他网络基础设施。这些协议通常分布在多个节点(服务器)上,使任何希望加入网络并提供服务的人都可以参与。
网络参与者受到激励,致力于为所有用户提供最高质量的服务。此外,网络还制定了规则,以确保网络本身的安全性和完整性。这通常通过结合各种共识机制来实现,这些机制被编程到智能合约中,并实现了各种类型的博弈论和加密经济设计。
什么使得服务真正去中心化?
基于图表构建
在这篇文章中,我们将研究一个这样的协议,The Graph,以及如何使用存储在以太坊区块链中的数据构建和部署我们自己的 GraphQL API。
The Graph 是一个用于查询以太坊等区块链和IPFS等网络的索引协议。任何人都可以构建和发布称为“子图”的开放 API,从而轻松访问数据。
子图定义了您希望通过 GraphQL API 提供的数据、数据源和数据访问模式。作为开发者,您可以选择使用其他开发者已经部署的子图,也可以定义并部署您自己的子图并使用它。
开发人员可以通过将其子图部署到托管服务或网络来创建开放 API,从而使他们能够根据 API 的使用量赚钱。
子图由几个主要部分组成:
1. GraphQL Schema
GraphQL Schema 定义了您想要保存和查询的数据类型/实体。您还可以在 Schema 中定义关系和全文搜索功能等配置。
2. Subgraph Manifest(yaml配置)
(来自文档)清单定义了子图索引的智能合约、它们的ABI、需要关注这些合约中的哪些事件,以及如何将事件数据映射到 Graph Node 存储并允许查询的实体。
3. AssemblyScript 映射
AssemblyScript 映射允许您使用架构中定义的实体类型保存要索引的数据。Graph CLI还会结合子图的架构和智能合约的 ABI 生成 AssemblyScript 类型。
托管服务 vs Subgraph Studio
构建子图有两种方法 -托管服务和Subgraph Studio。在本教程中,我们将为托管服务构建子图。
托管服务是一项中心化服务,免费,并将一直存在,直到去中心化网络的功能与其对等。您可以使用Subgraph Studio并按照本指南为去中心化网络构建子图。
在未来的某个时候,Subgraph Studio将成为构建子图的唯一方法。
让我们开始构建
现在我们已经很好地理解了 The Graph 及其工作原理,让我们开始编写一些代码。
在本教程中,我们将构建一个子图,用于从Foundation智能合约查询 NTF 数据,实现获取 NFT 及其所有者的查询,并建立它们之间的关系。
基金会还设有一个开发者门户,其中提供有关其智能合约代码的信息以及指向其自己的子图的链接。
先决条件
要成功完成本教程,您需要在计算机上安装Node.js。目前,我建议使用nvm或fnm来管理 Node.js 版本。
在 Graph Explorer 中创建 Graph 项目
首先,打开Graph Explorer并登录或创建一个新帐户。
接下来,转到仪表板并单击“添加子图”以创建一个新的子图。
使用以下属性配置子图:
- 子图名称 - Foundationsubgraph
- 副标题 -用于查询 NFT 的子图
- 可选 - 填写描述和 GITHUB URL 属性
一旦创建子图,我们将使用 Graph CLI 在本地初始化子图。
使用 Graph CLI 初始化新的子图
接下来,安装 Graph CLI:
$ npm install -g @graphprotocol/graph-cli
# or
$ yarn global add @graphprotocol/graph-cli
安装 Graph CLI 后,您可以使用 Graph CLIinit
命令初始化新的子图。
初始化新子图有两种方法:
1-来自示例子图
$ graph init --from-example <GITHUB_USERNAME>/<SUBGRAPH_NAME> [<DIRECTORY>]
2 - 来自现有的智能合约
如果您已经在以太坊主网或其中一个测试网上部署了智能合约,那么从该合约初始化新的子图是一种简单的启动和运行方法。
$ graph init --from-contract <CONTRACT_ADDRESS> \
[--network <ETHEREUM_NETWORK>] \
[--abi <FILE>] \
<GITHUB_USER>/<SUBGRAPH_NAME> [<DIRECTORY>]
在我们的例子中,我们将从Foundation NFT 合约开始,因此我们可以通过使用标志传入合约地址来从该合约地址进行初始化--from-contract
:
$ graph init --from-contract 0xc9fe4ffc4be41d93a1a7189975cd360504ee361a --network mainnet \
--contract-name Token --index-events
? Product for which to initialize › hosted-service
? Subgraph name › your-username/Foundationsubgraph
? Directory to create the subgraph in › Foundationsubgraph
? Ethereum network › Mainnet
? Contract address › 0xc9fe4ffc4be41d93a1a7189975cd360504ee361a
? Contract Name · Token
此命令将根据作为 参数传入的合约地址生成一个基本子图--from-contract
。通过使用此合约地址,CLI 将初始化项目中的一些内容以帮助您入门(包括获取abis
并将其保存在abis目录中)。
通过传递
--index-events
CLI,将根据从合同发出的事件自动在schema.graphql和src/mapping.ts中为我们填充一些代码。
子图的主要配置和定义位于subgraph.yaml文件中。子图代码库由以下几个文件组成:
- subgraph.yaml:包含子图清单的 YAML 文件
- schema.graphql:一个 GraphQL 模式,定义了子图存储的数据,以及如何通过 GraphQL 查询它
- AssemblyScript 映射:AssemblyScript 代码,将以太坊中的事件数据转换为架构中定义的实体(例如本教程中的 mapping.ts)
我们将使用的subgraph.yaml中的条目是:
description
(可选):子图的可读描述。当子图部署到托管服务时,Graph 浏览器会显示此描述。repository
(可选):子图清单所在的仓库 URL。Graph 浏览器也会显示该 URL。dataSources.source
:子图来源的智能合约地址,以及要使用的智能合约的 abi。地址是可选的;省略它可以索引所有合约中匹配的事件。dataSources.source.startBlock
(可选):数据源开始索引的区块编号。大多数情况下,我们建议使用合约创建时所在的区块。dataSources.mapping.entities
:数据源写入存储的实体。每个实体的架构在 schema.graphql 文件中定义。dataSources.mapping.abis
:源合约的一个或多个命名 ABI 文件以及您在映射中与之交互的任何其他智能合约。dataSources.mapping.eventHandlers
:列出此子图响应的智能合约事件以及映射中的处理程序(示例中为./src/mapping.ts),将这些事件转换为存储中的实体。
定义实体
使用 The Graph,您可以在schema.graphql中定义实体类型,Graph Node 将生成用于查询该实体类型的单个实例和集合的顶级字段。每个应为实体的类型都需要使用@entity
指令进行注释。
我们将要索引的实体/数据是Token
和User
。这样,我们就可以索引用户创建的令牌以及用户自己创建的令牌。
为此,请使用以下代码更新schema.graphql :
type Token @entity {
id: ID!
tokenID: BigInt!
contentURI: String
tokenIPFSPath: String
name: String!
createdAtTimestamp: BigInt!
creator: User!
owner: User!
}
type User @entity {
id: ID!
tokens: [Token!]! @derivedFrom(field: "owner")
created: [Token!]! @derivedFrom(field: "creator")
}
关于关系@derivedFrom
(来自文档):
可以通过字段在实体上定义反向查找@derivedFrom
。这会在实体上创建一个虚拟字段,该字段可以查询,但无法通过映射 API 手动设置。相反,它是从另一个实体上定义的关系派生而来的。对于此类关系,存储关系的两端几乎没有意义,如果只存储一侧,而派生另一侧,索引和查询性能都会更好。
对于一对多关系,关系应始终存储在“一”侧,而“多”侧应始终为派生关系。以这种方式存储关系,而不是在“多”侧存储实体数组,将显著提高索引和查询子图的性能。一般而言,应尽可能避免存储实体数组。
现在我们已经为我们的应用程序创建了 GraphQL 模式,我们可以在本地生成实体以开始在mappings
CLI 创建的中使用:
graph codegen
为了使智能合约、事件和实体的工作变得简单且类型安全,Graph CLI 从子图的 GraphQL 模式和数据源中包含的合约 ABI 的组合生成 AssemblyScript 类型。
使用实体和映射更新子图
现在我们可以配置subgraph.yaml来使用我们刚刚创建的实体并配置它们的映射。
为此,首先dataSources.mapping.entities
使用User
和Token
实体更新字段:
entities:
- Token
- User
接下来,更新dataSources.mapping.eventHandlers
以仅包含以下三个事件处理程序:
- event: TokenIPFSPathUpdated(indexed uint256,indexed string,string)
handler: handleTokenIPFSPathUpdated
- event: Transfer(indexed address,indexed address,indexed uint256)
handler: handleTransfer
最后更新配置添加startBlock,并将合约更改address
为主代理合约地址:
source:
address: "0x3B3ee1931Dc30C1957379FAc9aba94D1C48a5405"
abi: Token
startBlock: 11565020
Assemblyscript 映射
接下来,打开src/mappings.ts来写入我们在子图 subgraph 中定义的映射eventHandlers
。
使用以下代码更新文件:
import {
TokenIPFSPathUpdated as TokenIPFSPathUpdatedEvent,
Transfer as TransferEvent,
Token as TokenContract,
} from "../generated/Token/Token"
import {
Token, User
} from '../generated/schema'
export function handleTransfer(event: TransferEvent): void {
let token = Token.load(event.params.tokenId.toString());
if (!token) {
token = new Token(event.params.tokenId.toString());
token.creator = event.params.to.toHexString();
token.tokenID = event.params.tokenId;
let tokenContract = TokenContract.bind(event.address);
token.contentURI = tokenContract.tokenURI(event.params.tokenId);
token.tokenIPFSPath = tokenContract.getTokenIPFSPath(event.params.tokenId);
token.name = tokenContract.name();
token.createdAtTimestamp = event.block.timestamp;
}
token.owner = event.params.to.toHexString();
token.save();
let user = User.load(event.params.to.toHexString());
if (!user) {
user = new User(event.params.to.toHexString());
user.save();
}
}
export function handleTokenURIUpdated(event: TokenIPFSPathUpdatedEvent): void {
let token = Token.load(event.params.tokenId.toString());
if (!token) return
token.tokenIPFSPath = event.params.tokenIPFSPath;
token.save();
}
这些映射将处理新代币创建、转移或更新时的事件。当这些事件触发时,映射会将数据保存到子图中。
运行构建
接下来,让我们运行构建以确保所有配置正确。为此,请运行以下build
命令:
$ graph build
如果构建成功,您应该会看到在根目录中生成了一个新的构建文件夹。
部署子图
要部署,我们可以使用 Graph CLI 运行deploy
命令。要部署,您首先需要复制您帐户的访问令牌,该令牌可在Graph 浏览器中找到:
接下来运行以下命令:
$ graph auth
✔ Product for which to initialize · hosted-service
✔ Deploy key · ********************************
$ yarn deploy
一旦子图部署完毕,您就会看到它出现在您的仪表板中:
当您单击子图时,它应该打开图形浏览器:
查询数据
现在我们已经进入仪表板,应该可以开始查询数据了。运行以下查询以获取令牌及其元数据的列表:
{
tokens {
id
tokenID
contentURI
tokenIPFSPath
}
}
我们还可以配置订单方向:
{
tokens(
orderBy:id,
orderDirection: desc
) {
id
tokenID
contentURI
tokenIPFSPath
}
}
或者选择跳过一定数量的结果来实现一些基本的分页:
{
tokens(
skip: 100,
orderBy:id,
orderDirection: desc
) {
id
tokenID
contentURI
tokenIPFSPath
}
}
或者查询用户及其相关内容:
{
users {
id
tokens {
id
contentURI
}
}
}
我们还可以通过时间戳查询查看最近创建的 NFTS:
{
tokens(
orderBy: createdAtTimestamp,
orderDirection: desc
) {
id
tokenID
contentURI
metadataURI
}
}
该项目的代码库位于此处
IPFS元数据
托管服务还支持IPFS API,允许您与网络上存储的数据进行交互。
这对于 NFT 元数据特别有用,您可能拥有以 JSON 格式存储的有关 NFT 的其他信息。
如果您对其工作原理感兴趣,请查看利用此功能的我的Bored Ape Yacht Club API 。
后续步骤Next steps
如果您有兴趣了解有关 Web3、构建 Dapps 或构建子图的更多信息,请查看以下资源:
Twitter 上的 The Graph - @graphprotocol
Austin Griffith 在 Twitter 上@austingriffith和Scaffold Eth
文章来源:https://dev.to/edge-and-node/building-graphql-apis-on-ethereum-4poa