如何在以太坊上構(gòu)建GraphQL API
本文轉(zhuǎn)載自微信公眾號(hào)「區(qū)塊鏈研究實(shí)驗(yàn)室」,作者鏈三豐。轉(zhuǎn)載本文請(qǐng)聯(lián)系區(qū)塊鏈研究實(shí)驗(yàn)室公眾號(hào)。
過(guò)去,開(kāi)發(fā)人員通過(guò)構(gòu)建自己的集中式索引服務(wù)器從區(qū)塊鏈中提取數(shù)據(jù),將數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫(kù)中,并通過(guò)API進(jìn)行公開(kāi)。這需要大量的工程和硬件資源,并且破壞了分散化所需的重要安全性。
本文將向大家介紹如何在去中心化Web基礎(chǔ)架構(gòu)-區(qū)塊鏈數(shù)據(jù)上輕松部署API。
分散Web基礎(chǔ)架構(gòu)
分布式互聯(lián)網(wǎng)的構(gòu)想和發(fā)展方向通常稱為Web3,Web3通過(guò)以下附加功能增強(qiáng)了我們今天所知道的互聯(lián)網(wǎng):
- 去中心化
- 可驗(yàn)證的
- 不信任
- 自我管理
為了實(shí)現(xiàn)分散化,協(xié)議定義了網(wǎng)絡(luò),這些網(wǎng)絡(luò)提供了一系列數(shù)字服務(wù),例如計(jì)算,存儲(chǔ),帶寬,身份以及其他沒(méi)有中介的Web基礎(chǔ)結(jié)構(gòu)。這些協(xié)議通常分布在多個(gè)節(jié)點(diǎn)(服務(wù)器)上,使大部分希望成為網(wǎng)絡(luò)并提供服務(wù)的任何人都能參與。
在圖上建立
在本文中,我們還將研究一種這樣的協(xié)議Graph,以及如何使用以太坊區(qū)塊鏈中存儲(chǔ)的數(shù)據(jù)來(lái)構(gòu)建和部署我們自己的GraphQL API。
Graph是一個(gè)索引協(xié)議,用于查詢以太坊等區(qū)塊鏈和IPFS等網(wǎng)絡(luò),任何人都可以構(gòu)建和發(fā)布稱為子圖的開(kāi)放API,從而使數(shù)據(jù)易于訪問(wèn)。
子圖定義了您希望通過(guò)GraphQL API提供的數(shù)據(jù),數(shù)據(jù)源和數(shù)據(jù)訪問(wèn)模式。作為一個(gè)開(kāi)發(fā)人員可以選擇使用一個(gè)子已經(jīng)部署的其他開(kāi)發(fā)人員,或者定義和部署自己的子圖,并使用它。
子圖由幾個(gè)主要部分組成:
1. GraphQL模式
GraphQL模式定義您要保存和查詢的數(shù)據(jù)類型/實(shí)體,您還可以在架構(gòu)中定義諸如關(guān)系和全文搜索功能之類的配置。
2.子圖清單(yaml配置)
清單定義了子圖索引的智能合約,它們的ABI,這些合約中要注意的事件以及如何將事件數(shù)據(jù)映射到Graph Node存儲(chǔ)并允許查詢的實(shí)體。
3. AssemblyScript映射
AssemblyScript映射使您可以保存要使用架構(gòu)中定義的實(shí)體類型建立索引的數(shù)據(jù);該圖表CLI還使用子圖的模式的組合與智能合約的ABI一起產(chǎn)生AssemblyScript類型。
開(kāi)始建造
現(xiàn)在我們對(duì)Graph及其工作原理有了很好的了解,讓我們開(kāi)始編寫一些代碼。
在本教程中,我們將構(gòu)建一個(gè)子圖,用于從Zora智能合約查詢NTF數(shù)據(jù),實(shí)現(xiàn)用于獲取NFT及其所有者的查詢,并在它們之間建立關(guān)系。
先決條件:
為了在本教程中取得成功,您應(yīng)該在計(jì)算機(jī)上安裝Node.js,我建議您使用NVM或FNM管理Node.js的版本。
在圖資源管理器中創(chuàng)建圖項(xiàng)目
首先,打開(kāi)Graph Explorer,然后登錄或創(chuàng)建一個(gè)新帳戶。接下來(lái),轉(zhuǎn)到儀表板,然后單擊“添加子圖”以創(chuàng)建一個(gè)新的子圖。
使用以下屬性配置子圖:
- 子圖名稱-Zoranft子圖
- 字幕-用于查詢NFT的子圖
- 可選-填寫說(shuō)明和GITHUB URL屬性
使用Graph CLI初始化新的子圖
接下來(lái),安裝Graph CLI:
- $ npm install -g @graphprotocol/graph-cli
- # or
- $ yarn global add @graphprotocol/graph-cli
安裝Graph CLI后,您可以使用Graph CLIinit命令初始化一個(gè)新的子圖。
兩種方法:
1 從示例子圖中
- $ graph init --from-example <GITHUB_USERNAME>/<SUBGRAPH_NAME> [<DIRECTORY>]
2 來(lái)自現(xiàn)有的智能合約
如果您已經(jīng)將智能合約部署到以太坊主網(wǎng)或測(cè)試網(wǎng)之一,則從該合約初始化新的子圖是啟動(dòng)和運(yùn)行的簡(jiǎn)便方法。
- $ graph init --from-contract <CONTRACT_ADDRESS> \
- [--network <ETHEREUM_NETWORK>] \
- [--abi <FILE>] \
- <GITHUB_USER>/<SUBGRAPH_NAME> [<DIRECTORY>]
在我們的例子中,我們將使用Zora令牌合約,因此我們可以通過(guò)使用--from-contract標(biāo)志傳遞合約地址來(lái)從該合約地址進(jìn)行初始化:
- $ graph init --from-contract 0xabEFBc9fD2F806065b4f3C237d4b59D9A97Bcac7 --network mainnet \
- --contract-name Token --index-events
- ? Subgraph name › your-username/Zoranftsubgraph
- ? Directory to create the subgraph in › Zoranftsubgraph
- ? Ethereum network › Mainnet
- ? Contract address › 0xabEFBc9fD2F806065b4f3C237d4b59D9A97Bcac7
- ? Contract Name · Token
此命令將根據(jù)作為參數(shù)傳入的合同地址生成一個(gè)基本子圖--from-contract。通過(guò)使用此合同地址,CLI將在項(xiàng)目中初始化一些內(nèi)容以幫助您入門。
子圖的主要配置和定義位于subgraph.yaml文件中,子圖代碼庫(kù)由幾個(gè)文件組成:
- subgraph.yaml:包含子圖清單的YAML文件。
- schema.graphql:一個(gè)GraphQL架構(gòu),用于定義為子圖存儲(chǔ)的數(shù)據(jù)以及如何通過(guò)GraphQL查詢數(shù)據(jù)。
- AssemblyScript映射:從以太坊中的事件數(shù)據(jù)轉(zhuǎn)換為架構(gòu)中定義的實(shí)體的AssemblyScript代碼。
我們將使用的subgraph.yaml中的條目是:
- description(可選):子圖是什么的可讀描述,子圖部署到Hosted Service時(shí),圖資源管理器將顯示此描述。
- repository(可選):可在其中找到子圖清單的存儲(chǔ)庫(kù)的URL。
- dataSources.source:子圖來(lái)源的智能合約的地址,以及要使用的智能合約的abi。
- dataSources.source.startBlock(可選):數(shù)據(jù)源從其開(kāi)始索引的塊的編號(hào)。
- dataSources.mapping.entities:數(shù)據(jù)源寫入存儲(chǔ)的實(shí)體,每個(gè)實(shí)體的架構(gòu)都在schema.graphql文件中定義。
- dataSources.mapping.abis:一個(gè)或多個(gè)命名ABI文件,用于源合同以及您在映射中與之交互的任何其他智能合同。
- dataSources.mapping.eventHandlers:列出該子圖所響應(yīng)的智能合約事件以及映射中的處理程序(示例中為./src/mapping.ts),這些處理程序?qū)⑦@些事件轉(zhuǎn)換為商店中的實(shí)體。
定義實(shí)體
使用The Graph,您可以在schema.graphql中定義實(shí)體類型,并且Graph Node將生成用于查詢?cè)搶?shí)體類型的單個(gè)實(shí)例和集合的頂級(jí)字段。每種應(yīng)為實(shí)體的類型都必須使用@entity指令進(jìn)行注釋。
我們將要建立索引的實(shí)體/數(shù)據(jù)是Token和User。這樣,我們可以索引用戶以及用戶自己創(chuàng)建的令牌。
為此,請(qǐng)使用以下代碼更新schema.graphql:
- type Token @entity {
- id: ID!
- tokenID: BigInt!
- contentURI: String!
- metadataURI: String!
- creator: User!
- owner: User!
- }
- type User @entity {
- id: ID!
- tokens: [Token!]! @derivedFrom(field: "owner")
- created: [Token!]! @derivedFrom(field: "creator")
- }
通過(guò)@derivedFrom(來(lái)自文檔)通過(guò)“關(guān)系”
可以通過(guò)@derivedFrom字段在實(shí)體上定義反向查找。這會(huì)在實(shí)體上創(chuàng)建一個(gè)虛擬字段,可以查詢?cè)撎摂M字段,但無(wú)法通過(guò)映射API手動(dòng)設(shè)置。
相反,它是從另一個(gè)實(shí)體上定義的關(guān)系派生的。對(duì)于此類關(guān)系,存儲(chǔ)關(guān)系的兩邊幾乎沒(méi)有意義,并且僅存儲(chǔ)一側(cè)而派生另一側(cè)時(shí),索引和查詢性能都將更好。
現(xiàn)在,我們已經(jīng)為我們的應(yīng)用程序創(chuàng)建了GraphQL模式,我們可以在本地生成實(shí)體,以開(kāi)始在mappingsCLI所創(chuàng)建的實(shí)體中使用:
- graph codegen
為了使工作中的智能合約,事件和實(shí)體變得容易且類型安全,Graph CLI從子圖的GraphQL模式和數(shù)據(jù)源中包含的合約ABI的組合中生成AssemblyScript類型。
使用實(shí)體和映射更新子圖
現(xiàn)在,我們可以配置subgraph.yaml以使用我們剛剛創(chuàng)建的實(shí)體并配置它們的映射。
為此,請(qǐng)先dataSources.mapping.entities使用User和Token實(shí)體更新字段:
- entities:
- - Token
- - User
接下來(lái),更新,dataSources.mapping.eventHandlers使其僅包括以下兩個(gè)事件處理程序:
- eventHandlers:
- - event: TokenURIUpdated(indexed uint256,address,string)
- handler: handleTokenURIUpdated
- - event: Transfer(indexed address,indexed address,indexed uint256)
- handler: handleTransfer
最后,更新配置以添加startBlock:
- source:
- address: "0xabEFBc9fD2F806065b4f3C237d4b59D9A97Bcac7"
- abi: Token
- startBlock: 11565020
Assemblyscript映射
接下來(lái),打開(kāi)src / mappings.ts來(lái)編寫我們?cè)谧訄Dsubgraph中定義的映射eventHandlers。
使用以下代碼更新文件:
- import {
- TokenURIUpdated as TokenURIUpdatedEvent,
- Transfer as TransferEvent,
- Token as TokenContract
- } from "../generated/Token/Token"
- import {
- Token, User
- } from '../generated/schema'
- export function handleTokenURIUpdated(event: TokenURIUpdatedEvent): void {
- let token = Token.load(event.params._tokenId.toString());
- token.contentURI = event.params._uri;
- token.save();
- }
- 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.metadataURI = tokenContract.tokenMetadataURI(event.params.tokenId);
- }
- 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();
- }
- }
這些映射將處理創(chuàng)建,傳輸或更新新令牌的事件。當(dāng)這些事件觸發(fā)時(shí),映射會(huì)將數(shù)據(jù)保存到子圖中。
運(yùn)行構(gòu)建
接下來(lái),讓我們運(yùn)行一個(gè)構(gòu)建以確保正確配置了所有內(nèi)容。為此,請(qǐng)運(yùn)行以下build命令:
- $ graph build
如果構(gòu)建成功,則應(yīng)該在根目錄中看到一個(gè)新的構(gòu)建文件夾。
部署子圖
要進(jìn)行部署,我們可以deploy使用Graph CLI運(yùn)行該命令。要進(jìn)行部署,您首先需要為在Graph Explorer中創(chuàng)建的子圖復(fù)制Access令牌:
接下來(lái),運(yùn)行以下命令:
- $ graph auth https://api.thegraph.com/deploy/ <ACCESS_TOKEN>
- $ yarn deploy
部署子圖后,您應(yīng)該看到它顯示在您的儀表板中:
當(dāng)您單擊子圖時(shí),它應(yīng)該打開(kāi)Graph資源管理器:
查詢數(shù)據(jù)
現(xiàn)在我們位于儀表板中,我們應(yīng)該能夠開(kāi)始查詢數(shù)據(jù)了。運(yùn)行以下查詢以獲取令牌及其元數(shù)據(jù)的列表:
- {
- tokens {
- id
- tokenID
- contentURI
- metadataURI
- }
- }
我們還可以配置訂單方向:
- {
- tokens(
- orderBy:id,
- orderDirection: desc
- ) {
- id
- tokenID
- contentURI
- metadataURI
- }
- }
或選擇跳過(guò)某些結(jié)果以實(shí)現(xiàn)一些基本分頁(yè):
- {
- tokens(
- skip: 100,
- orderBy:id,
- orderDirection: desc
- ) {
- id
- tokenID
- contentURI
- metadataURI
- }
- }
或查詢用戶及其相關(guān)內(nèi)容:
- {
- users {
- id
- tokens {
- id
- contentURI
- }
- }
- }
更新子圖
如果我們想要對(duì)子圖進(jìn)行一些更改然后重新部署,我們應(yīng)該怎么辦?假設(shè)我們要向子圖添加新功能,假設(shè)我們除了現(xiàn)有的查詢功能外,還想添加該功能以按創(chuàng)建NFT的時(shí)間戳進(jìn)行排序。
為此,我們需要先向?qū)嶓w添加一個(gè)新createdAtTimestamp字段Token:
- type Token @entity {
- id: ID!
- tokenID: BigInt!
- contentURI: String!
- metadataURI: String!
- creator: User!
- owner: User!
- "Add new createdAtTimesamp field"
- createdAtTimestamp: BigInt!
- }
現(xiàn)在,我們可以重新運(yùn)行代碼生成:
- graph codegen
接下來(lái),我們需要更新映射以保存此新字段:
- // update the handleTransfer function to add the createdAtTimestamp to the token object
- 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;
- // Add the createdAtTimestamp to the token object
- token.createdAtTimestamp = event.block.timestamp;
- let tokenContract = TokenContract.bind(event.address);
- token.contentURI = tokenContract.tokenURI(event.params.tokenId);
- token.metadataURI = tokenContract.tokenMetadataURI(event.params.tokenId);
- }
- 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();
- }
- }
現(xiàn)在我們可以重新部署子圖:
- $ yarn deploy
子圖重新部署后,我們現(xiàn)在可以按時(shí)間戳查詢以查看最近創(chuàng)建的NFTS:
- {
- tokens(
- orderBy:createdAtTimestamp,
- orderDirection: desc
- ) {
- id
- tokenID
- contentURI
- metadataURI
- }
- }}
【編輯推薦】




























