[The Graph] Định nghĩa Subgraph

[The Graph] Định nghĩa Subgraph

Bài viết này nằm trong chuỗi bài viết hướng dẫn tạo Subgraph trên The Graph.

 

Một subgraph xác định dữ liệu nào The Graph sẽ lập chỉ mục từ Ethereum và cách nó sẽ lưu trữ. Sau khi được triển khai, nó sẽ tạo thành một phần của đồ thị toàn cầu về dữ liệu blockchain.

Subgraph được định nghĩa thông qua một số tệp:

  • subgraph.yaml: tệp YAML chứa subgraph manifest
  • schema.graphql: một lược đồ GraphQL xác định dữ liệu nào được lưu trữ cho subgraph và cách truy vấn nó qua GraphQL
  • AssemblyScript Mappings: AssemblyScript code dịch dữ liệu sự kiện trong Ethereum sang các thực thể được xác định trong lược đồ của bạn (ví dụ: mapping.ts trong hướng dẫn này)

Trước khi đi vào chi tiết về nội dung của tệp manifest, bạn cần cài đặt Graph CLI mà bạn sẽ cần để xây dựng và triển khai một subgraph.

Trước khi đi vào chi tiết về nội dung của tệp kê khai, bạn cần cài đặt Graph CLI mà bạn sẽ cần để xây dựng và triển khai một subgraph.

Cài đặt Graph CLI

Graph CLI được viết bằng JavaScript và bạn sẽ cần cài đặt yarn hoặc npm để sử dụng nó; giả định rằng bạn có yarn trong những gì sau đây. Hướng dẫn chi tiết để cài đặt yarn có thể được tìm thấy trong graph-cli repo

 

Khi bạn đã có yarn, hãy cài đặt Graph CLI bằng cách chạy

yarn global add @graphprotocol/graph-cli

 

Tạo một dự án Subgraph

Lệnh graph init có thể được sử dụng để thiết lập một dự án subgraph mới, từ một hợp đồng hiện có trên bất kỳ mạng Ethereum công khai nào hoặc từ một subgraph mẫu.

Từ một hợp đồng hiện có

Nếu bạn đã có một hợp đồng thông minh được triển khai trên Ethereum mainnet hoặc một trong các testnet, thì việc khởi động một subgraph mới từ hợp đồng này có thể là một cách tốt để bắt đầu.

Lệnh sau tạo một subgraph lập chỉ mục tất cả các sự kiện của một hợp đồng hiện có. Nó cố gắng lấy hợp đồng ABI từ Etherscan và quay lại yêu cầu đường dẫn tệp cục bộ. Nếu thiếu bất kỳ đối số tùy chọn nào, nó sẽ đưa bạn đến một biểu mẫu tương tác.

graph init \

  –from-contract <CONTRACT_ADDRESS> \

  [–network <ETHEREUM_NETWORK>] \

  [–abi <FILE>] \

  <GITHUB_USER>/<SUBGRAPH_NAME> [<DIRECTORY>]

 

<GITHUB_USER> là tên github của tổ chức hoặc người dùng github của bạn.

<SUBGRAPH_NAME> là tên cho subgraph của bạn.

<DIRECTORY> là tên tùy chọn của thư mục nơi mà graph init sẽ đặt bản kê khai subgraph mẫu.

<CONTRACT_ADDRESS> là địa chỉ của hợp đồng hiện tại của bạn. <ETHEREUM_NETWORK> là tên của mạng Ethereum mà hợp đồng sử dụng. 

<FILE> là một đường dẫn cục bộ đến tệp hợp đồng ABI. Cả –network–abi đều tùy chọn.

 

Các mạng được hỗ trợ trên Hosted Service là:

mainnet

kovan

rinkeby

ropsten

goerli

poa-core

xdai

poa-sokol

 

Từ một subgraph mẫu

Chế độ thứ hai mà graph init hỗ trợ đang tạo một dự án mới từ một subgraph mẫu. Lệnh sau thực hiện điều này:

graph init –from-example <GITHUB_USER>/<SUBGRAPH_NAME> [<DIRECTORY>]

Subgraph mẫu dựa trên hợp đồng Gravity của Dani Grant quản lý hình đại diện của người dùng và phát ra các sự kiện NewGravatar hoặc UpdateGravatar bất cứ khi nào hình đại diện được tạo hoặc cập nhật. Subgraph xử lý các sự kiện này bằng cách ghi các thực thể Gravatar vào kho lưu trữ Graph Node và đảm bảo chúng được cập nhật theo sự kiện. Các phần sau sẽ xem xét các tệp tạo nên subgraph cho ví dụ này.

 

Tệp kê khai Subgraph

Tệp kê khai subgraph subgraph.yaml xác định các hợp đồng thông minh mà subgraph của bạn sẽ lập chỉ mục, những sự kiện nào từ các hợp đồng này cần chú ý và cách ánh xạ dữ liệu sự kiện tới các thực thể mà Graph Node lưu trữ và cho phép truy vấn. Thông số kỹ thuật đầy đủ cho các tệp kê khai subgraph có thể được tìm thấy tại đây.

Đối với subgraph ví dụ, subgraph.yaml là:

specVersion: 0.0.1

description: Gravatar for Ethereum

repository: https://github.com/graphprotocol/example-subgraph

schema:

  file: ./schema.graphql

dataSources:

  – kind: ethereum/contract

    name: Gravity

    network: mainnet

    source:

      address: ‘0x2E645469f354BB4F5c8a05B3b30A929361cf77eC’

      abi: Gravity

      startBlock: 6175244

    mapping:

      kind: ethereum/events

      apiVersion: 0.0.1

      language: wasm/assemblyscript

      entities:

        – Gravatar

      abis:

        – name: Gravity

          file: ./abis/Gravity.json

      eventHandlers:

        – event: NewGravatar(uint256,address,string,string)

          handler: handleNewGravatar

        – event: UpdatedGravatar(uint256,address,string,string)

          handler: handleUpdatedGravatar

      callHandlers:

        – function: createGravatar(string,string)

          handler: handleCreateGravatar

      blockHandlers:

        – function: handleBlock

        – function: handleBlockWithCall

          filter:

            kind: call

      file: ./src/mapping.ts

Các mục quan trọng cần cập nhật cho tệp kê khai là:

  • description: mô tả mà con người có thể đọc được về nội dung của subgraph. Mô tả này được Graph Explorer hiển thị khi subgraph được triển khai cho Hosted Service.
  • repository: URL của kho lưu trữ – nơi có thể tìm thấy tệp kê khai subgraph. Dữ liệu này cũng được hiển thị trên Graph Explorer.
  • dataSources.source: address của hợp đồng thông minh mà các subgraph lấy nguồn, và abi của hợp đồng thông minh sẽ sử dụng. address là tùy chọn; nếu bỏ qua nó sẽ cho phép lập chỉ mục các sự kiện phù hợp từ tất cả các hợp đồng.
  • dataSources.source.startBlock: số tùy chọn của khối mà nguồn dữ liệu bắt đầu lập chỉ mục. Trong hầu hết các trường hợp, bạn nên sử dụng khối mà hợp đồng đã được tạo.
  • dataSources.mapping.entities: các thực thể mà nguồn dữ liệu ghi vào cửa hàng. Lược đồ cho mỗi thực thể được xác định trong tệp schema.graphql.
  • dataSources.mapping.abis: một hoặc nhiều tệp ABI được đặt tên cho hợp đồng nguồn cũng như bất kỳ hợp đồng thông minh nào khác mà bạn tương tác từ bên trong ánh xạ.
  • dataSources.mapping.eventHandlers: liệt kê các sự kiện hợp đồng thông minh mà subgraph này có phản ứng và các trình xử lý trong ánh xạ— ./src/mapping.ts trong ví dụ — chuyển đổi các sự kiện này thành các thực thể trong cửa hàng.
  • dataSources.mapping.callHandlers: liệt kê các chức năng hợp đồng thông minh mà subgraph này phản ứng và xử lý trong ánh xạ chuyển đổi các đầu vào và đầu ra cho các lệnh gọi hàm thành các thực thể trong cửa hàng.
  • dataSources.mapping.blockHandlers: liệt kê các khối mà subgraph này phản ứng và các trình xử lý trong ánh xạ để chạy khi một khối được nối vào chuỗi. Nếu không có bộ lọc, trình xử lý khối sẽ được chạy mỗi khối. Một bộ lọc tùy chọn có thể được cung cấp các loại sau đây: call. Một bộ lọc call sẽ chạy trình xử lý nếu khối chứa ít nhất một lệnh gọi đến hợp đồng nguồn dữ liệu.

Một subgraph có thể lập chỉ mục dữ liệu từ nhiều hợp đồng thông minh. Thêm một mục nhập cho mỗi hợp đồng mà từ đó dữ liệu cần được lập chỉ mục vào mảng dataSources.

Các trình kích hoạt cho nguồn dữ liệu trong một khối được sắp xếp theo quy trình sau:

  1. Trình kích hoạt sự kiện và gọi được sắp xếp đầu tiên theo chỉ mục giao dịch trong khối.
  2. Trình kích hoạt sự kiện và gọi trong cùng một giao dịch được sắp xếp theo quy ước: trình kích hoạt sự kiện trước rồi đến trình kích hoạt gọi, mỗi loại tuân theo thứ tự mà chúng được xác định trong tệp kê khai.
  3. Trình kích hoạt khối được chạy sau trình kích hoạt sự kiện và gọi, theo thứ tự được xác định trong tệp kê khai.

Các quy tắc đặt lệnh này có thể thay đổi.

Lấy ABIs

(Các) tệp ABI phải khớp với (các) hợp đồng của bạn. Có một số cách để lấy tệp ABI:

  • Nếu bạn đang xây dựng dự án của riêng mình, bạn có thể sẽ có quyền truy cập vào các ABI mới nhất của mình.
  • Nếu bạn đang xây dựng một subgraph cho một dự án công khai, bạn có thể tải dự án đó xuống máy tính của mình và lấy ABI bằng cách sử dụng truffle compile hoặc sử dụng solc để biên dịch.
  • Bạn cũng có thể tìm thấy ABI trên Etherscan , nhưng không phải lúc nào cũng đáng tin cậy, vì ABI được tải lên có thể đã lỗi thời. Đảm bảo rằng bạn có ABI phù hợp, nếu không việc chạy subgraph của bạn sẽ không thành công.

Lược đồ GraphQL

Lược đồ (schema) cho subgraph của bạn có trong tệp schema.graphql. Các lược đồ GraphQL được định nghĩa bằng ngôn ngữ định nghĩa giao diện GraphQL. Nếu bạn chưa bao giờ viết một schema GraphQL, bạn nên kiểm tra primer này trên hệ thống loại GraphQL. Bạn có thể tìm thấy tài liệu tham khảo cho lược đồ GraphQL trong phần API GraphQL.

Xác định các thực thể

Trước khi xác định các thực thể, điều quan trọng là phải lùi lại một bước và suy nghĩ về cách dữ liệu của bạn được cấu trúc và liên kết. Tất cả các truy vấn sẽ được thực hiện dựa trên mô hình dữ liệu được xác định trong lược đồ subgraph và các thực thể được lập chỉ mục bởi subgraph. Vì vậy sẽ rất tốt nếu xác định lược đồ subgraph theo cách phù hợp với nhu cầu của dApp của bạn. Có thể hữu ích khi hình dung các thực thể là “đối tượng chứa dữ liệu”, thay vì là sự kiện hoặc chức năng.

Với The Graph, bạn chỉ cần xác định các loại thực thể trong schema.graphql và Graph Node sẽ tạo các trường cấp cao nhất để truy vấn các trường hợp và tập hợp đơn lẻ của loại thực thể đó. Mỗi loại phải là một thực thể bắt buộc phải được chú thích bằng một @entity điều khiển.

Ví dụ tốt

Thực thể Gravatar dưới đây được cấu trúc xung quanh một đối tượng Gravatar và là một ví dụ điển hình về cách một thực thể có thể được xác định.

type Gravatar @entity {

  id: ID!

  owner: Bytes

  displayName: String

  imageUrl: String

  accepted: Boolean

}

Ví dụ chưa tốt

Ví dụ thực thể GravatarAcceptedGravatarDeclined dưới đây dựa trên các sự kiện. Không nên ánh xạ các sự kiện hoặc lệnh gọi hàm tới các thực thể 1: 1.

type GravatarAccepted @entity {

  id: ID!

  owner: Bytes

  displayName: String

  imageUrl: String

}

Các trường tùy chọn và bắt buộc

Các trường thực thể có thể được xác định theo yêu cầu hoặc tùy chọn. Các trường bắt buộc được biểu thị bằng ! trong lược đồ. Nếu trường bắt buộc không được đặt trong ánh xạ, bạn sẽ nhận được lỗi này khi truy vấn trường:

Null value resolved for non-null field ‘name’

Mỗi thực thể phải có một id cho trường, thuộc loại ID! (chuỗi). Các trường id hoạt động như khóa chính, và cần phải là duy nhất trong số tất cả các đơn vị cùng loại.

Các loại vô hướng tích hợp

GraphQL Vô hướng được hỗ trợ

The Graph  hỗ trợ các đại lượng vô hướng sau trong API GraphQL:

Kiểu Sự miêu tả
Bytes Mảng byte, được biểu diễn dưới dạng chuỗi thập lục phân. Thường được sử dụng cho các hash (mã băm) và địa chỉ Ethereum.
ID Được lưu trữ dưới dạng một string.
String Vô hướng cho các giá trị string. Các ký tự rỗng không được hỗ trợ và tự động bị xóa.
Boolean Vô hướng cho các giá trị boolean.
Int Thông số GraphQL xác định Int có kích thước 32 byte.
BigInt Số nguyên lớn. Sử dụng cho các loại uint32, int64, uint64, …, uint256 của Ethereum. Lưu ý: Mọi thứ bên dưới uint32, chẳng hạn như int32, uint24 hoặc int8 được biểu thị bằng i32.
BigDecimal BigDecimal Các số thập phân có độ chính xác cao được biểu diễn dưới dạng dấu hiệu và số mũ. Phạm vi số mũ là từ −6143 đến +6144. Làm tròn đến 34 chữ số có nghĩa.

Enums

Bạn cũng có thể tạo enums trong một lược đồ. Enums có cú pháp sau:

enum TokenStatus {

  OriginalOwner

  SecondOwner

  ThirdOwner

}

Khi enum được xác định trong lược đồ, bạn có thể sử dụng biểu diễn chuỗi của giá trị enum để đặt một trường enum trên một thực thể. Ví dụ, bạn có thể thiết lập tokenStatus đến SecondOwner bằng cách đầu tiên xác định đối tượng của bạn và sau đó thiết lập trường với entity.tokenStatus = “SecondOwner. Ví dụ dưới đây minh họa Token thực thể sẽ trông như thế nào với trường enum:

type Token @entity {

  id: ID!

  name: String!

  symbol: String!

  decimals: Int!

  tokenStatus: TokenStatus!

}

Bạn có thể tìm thấy thêm chi tiết về cách viết enums trong tài liệu GraphQL.

Mối quan hệ thực thể

Một thực thể có thể có mối quan hệ với một hoặc nhiều thực thể khác trong lược đồ của bạn. Những mối quan hệ này có thể được duyệt qua trong các truy vấn của bạn. Các mối quan hệ trong The Graph là một chiều. Có thể mô phỏng mối quan hệ hai chiều bằng cách xác định mối quan hệ một chiều ở một trong hai “điểm cuối” của mối quan hệ.

Mối quan hệ được xác định trên các thực thể giống như bất kỳ trường nào khác ngoại trừ kiểu được chỉ định là của một thực thể khác.

Mối quan hệ một-một

Xác định một loại thực thể Transaction với mối quan hệ một-một tùy chọn với một loại thực thể TransactionReceipt:

type Transaction @entity {

  id: ID!

  transactionReceipt: TransactionReceipt

}

 

type TransactionReceipt @entity {

  id: ID!

  transaction: Transaction

}

Mối quan hệ một-nhiều

Xác định một loại thực thể TokenBalance với mối quan hệ bắt buộc một-nhiều với một loại thực thể Token:

type Token @entity {

  id: ID!

}

 

type TokenBalance @entity {

  id: ID!

  amount: Int!

  token: Token!

}

Tra cứu ngược

Tra cứu ngược có thể được xác định trên một thực thể thông qua trường @derivedFrom. Điều này tạo ra một trường ảo trên thực thể có thể được truy vấn nhưng không thể được đặt theo cách thủ công thông qua API ánh xạ. Đúng hơn, nó có nguồn gốc từ mối quan hệ được xác định trên thực thể khác. Đối với các mối quan hệ như vậy, hiếm khi có ý nghĩa khi lưu trữ cả hai bên của mối quan hệ và cả hiệu suất lập chỉ mục và truy vấn sẽ tốt hơn khi chỉ một bên được lưu trữ và bên kia được dẫn xuất.

Đối với các mối quan hệ một – nhiều, mối quan hệ phải luôn được lưu trữ ở phía ‘một’ và bên ‘nhiều’ phải luôn được dẫn xuất. Lưu trữ mối quan hệ theo cách này, thay vì lưu trữ một mảng các thực thể ở phía ‘nhiều’, sẽ dẫn đến hiệu suất tốt hơn đáng kể cho cả lập chỉ mục và truy vấn subgraph. Nói chung, việc lưu trữ các mảng thực thể nên tránh càng nhiều càng tốt.

Ví dụ

Chúng ta có thể tạo số dư cho một token có thể truy cập được từ token bằng cách dẫn xuất một trường tokenBalances:

type Token @entity {

  id: ID!

  tokenBalances: [TokenBalance!]! @derivedFrom(field: “token”)

}

 

type TokenBalance @entity {

  id: ID!

  amount: Int!

  token: Token!

}

Mối quan hệ nhiều-nhiều

Đối với các mối quan hệ nhiều-nhiều, chẳng hạn như người dùng mà mỗi người có thể thuộc bất kỳ số lượng tổ chức nào, thì cách đơn giản nhất, nhưng không phải là hoạt động tốt nhất, để mô hình hóa mối quan hệ là một mảng trong mỗi hai thực thể có liên quan. Nếu mối quan hệ là đối xứng, chỉ một mặt của mối quan hệ cần được lưu trữ và mặt còn lại có thể được suy ra.

Ví dụ

Xác định tra cứu ngược lại từ một loại thực thể User sang một loại thực thể Organization. Trong ví dụ bên dưới, điều này đạt được bằng cách tra cứu thuộc tính members từ bên trong thực thể Organization. Trong các truy vấn, trường organizations trên User sẽ được giải quyết bằng cách tìm tất cả các thực thể Organization bao gồm ID của người dùng.

type Organization @entity {

  id: ID!

  name: String!

  members: [User!]!

}

 

type User @entity {

  id: ID!

  name: String!

  organizations: [Organization!]! @derivedFrom(field: “members”)

}

Một cách hiệu quả hơn để lưu trữ mối quan hệ này là thông qua một bảng ánh xạ có một mục nhập cho mỗi cặp User / Organization với một lược đồ như

type Organization @entity {

  id: ID!

  name: String!

  members: [UserOrganization]! @derivedFrom(field: “user”)

}

 

type User @entity {

  id: ID!

  name: String!

  organizations: [UserOrganization!] @derivedFrom(field: “organization”)

}

 

type UserOrganization @entity {

  id: ID!   # Set to `${user.id}-${organization.id}`

  user: User!

  organization: Organization!

}

Cách tiếp cận này yêu cầu các truy vấn giảm xuống một cấp bổ sung để truy xuất, ví dụ: tổ chức cho người dùng:

query usersWithOrganizations {

  users {

    organizations { # this is a UserOrganization entity

      organization {

        name

      }

    }

  }

}

Cách lưu trữ mối quan hệ nhiều-nhiều phức tạp hơn này sẽ dẫn đến việc lưu trữ ít dữ liệu hơn cho subgraph và do đó, một subgraph thường nhanh hơn đáng kể để lập chỉ mục và truy vấn.

Thêm nhận xét vào lược đồ

Theo thông số kỹ thuật GraphQL, các nhận xét có thể được thêm vào bên trên các thuộc tính thực thể lược đồ bằng cách sử dụng dấu ngoặc kép “”. Điều này được minh họa trong ví dụ dưới đây:

 type MyFirstEntity @entity {

    “unique identifier and primary key of the entity”

    id: ID!

    address: Bytes!

  }

Xác định Trường Tìm kiếm Toàn Văn Bản

Truy vấn tìm kiếm fulltext (toàn văn bản) lọc và xếp hạng các thực thể dựa trên đầu vào tìm kiếm văn bản. Các truy vấn toàn văn bản có thể trả về các kết quả phù hợp cho các từ tương tự bằng cách xử lý đầu vào văn bản truy vấn thành các gốc trước khi so sánh với dữ liệu văn bản đã được lập chỉ mục.

Định nghĩa truy vấn toàn văn bao gồm tên truy vấn, từ điển ngôn ngữ được sử dụng để xử lý các trường văn bản, thuật toán xếp hạng được sử dụng để sắp xếp kết quả và các trường được bao gồm trong tìm kiếm. Mỗi truy vấn toàn văn bản có thể kéo dài nhiều trường, nhưng tất cả các trường được bao gồm phải từ một loại thực thể duy nhất.

Để thêm truy vấn toàn văn bản, hãy bao gồm một loại _Schema_ có chỉ thị toàn văn bản trong lược đồ GraphQL.

type _Schema_

  @fulltext(

    name: “bandSearch”,

    language: en

    algorithm: rank,

    include: [

      {

        entity: “Band”,

        fields: [

          { name: “name” },

          { name: “description” },

          { name: “bio” },

        ]

      }

    ]

  )

 

type Band @entity {

    id: ID!

    name: String!

    description: String!

    bio: String

    wallet: Address

    labels: [Label!]!

    discography: [Album!]!

    members: [Musician!]!

}

Ví dụ trường bandSearch có thể được sử dụng trong các truy vấn để lọc các thực thể Band dựa trên các tài liệu văn bản trong các trường name, descriptionbio. Đến phần GraphQL  API – Truy vấn để biết mô tả về tìm kiếm toàn văn bản API và ví dụ về cách sử dụng.

query {

  bandSearch(text: “breaks & electro & detroit”) {

    id

    name

    description

    wallet

  }

}

Ngôn ngữ được hỗ trợ

Việc chọn một ngôn ngữ khác sẽ có tác động dứt khoát, mặc dù đôi khi hơi tế nhị, đối với API tìm kiếm toàn văn bản. Các trường được bao phủ bởi trường truy vấn toàn văn bản được kiểm tra trong ngữ cảnh của ngôn ngữ đã chọn, vì vậy các từ vựng được tạo ra bởi phân tích và truy vấn tìm kiếm khác nhau giữa ngôn ngữ. Ví dụ: khi sử dụng từ điển tiếng Thổ Nhĩ Kỳ được hỗ trợ, “token” được đặt gốc thành “toke” trong khi tất nhiên, từ điển tiếng Anh sẽ bắt gốc từ “token”.

Từ điển ngôn ngữ được hỗ trợ:

Từ điển
đơn giản Chung
cho tiếng Đan Mạch
nl tiếng Hà Lan
en tiếng Anh
fi tiếng Phần lan
fr tiếng Pháp
de tiếng Đức
hu tiếng Hungary
it tiếng Ý
no tiếng Nauy
pt tiếng Bồ Đào Nha
ro tiếng Rumani
ru tiếng Nga
es tiếng Tây Ban Nha
sv tiếng Thụy Điển
tr tiếng Thổ nhĩ kỳ

Thuật toán xếp hạng

Các thuật toán được hỗ trợ để sắp xếp kết quả:

Thuật toán Mô tả
rank Sử dụng chất lượng đối sánh (0-1) của truy vấn toàn văn để sắp xếp kết quả.
proximityRank Tương tự như rank nhưng cũng bao gồm mức gần gũi của các đối sánh.

Viết Ánh Xạ

Các ánh xạ biến đổi dữ liệu Ethereum mà các ánh xạ của bạn đang cung cấp thành các thực thể được xác định trong lược đồ của bạn. Các ánh xạ được viết trong một tập con của TypeScript được gọi là AssemblyScript có thể được biên dịch thành WASM (WebAssembly). AssemblyScript nghiêm ngặt hơn TypeScript bình thường, nhưng cung cấp một cú pháp quen thuộc.

Đối với mỗi trình xử lý sự kiện được quy định tại subgraph.yaml dưới mapping.eventHandlers, tạo ra một chức năng xuất của cùng tên. Mỗi trình xử lý phải chấp nhận một tham số duy nhất được gọi event với loại tương ứng với tên của sự kiện đang được xử lý.

Trong subgraph ví dụ, src/mapping.ts chứa các trình xử lý cho các sự kiện NewGravatarUpdatedGravatar:

import { NewGravatar, UpdatedGravatar } from ‘../generated/Gravity/Gravity’

import { Gravatar } from ‘../generated/schema’

 

export function handleNewGravatar(event: NewGravatar): void {

  let gravatar = new Gravatar(event.params.id.toHex())

  gravatar.owner = event.params.owner

  gravatar.displayName = event.params.displayName

  gravatar.imageUrl = event.params.imageUrl

  gravatar.save()

}

 

export function handleUpdatedGravatar(event: UpdatedGravatar): void {

  let id = event.params.id.toHex()

  let gravatar = Gravatar.load(id)

  if (gravatar == null) {

    gravatar = new Gravatar(id)

  }

  gravatar.owner = event.params.owner

  gravatar.displayName = event.params.displayName

  gravatar.imageUrl = event.params.imageUrl

  gravatar.save()

}

Trình xử lý đầu tiên nhận một sự kiện NewGravatar và tạo một thực thể Gravatar mới new Gravatar(event.params.id.toHex()), điền các trường thực thể bằng cách sử dụng các tham số sự kiện tương ứng. Các thể thực thể này được đại diện bởi biến gravatar, với một giá trị idevent.params.id.toHex().

Trình xử lý thứ hai cố gắng tải Gravatar hiện có từ bộ lưu trữ của Graph Node. Nếu nó chưa tồn tại, nó được tạo ra theo yêu cầu. Thực thể sau đó được cập nhật để khớp với các thông số sự kiện mới, trước khi nó được lưu trở lại bộ lưu trữ bằng cách sử dụng gravatar.save().

Các ID được đề xuất để tạo các thực thể mới

Mọi thực thể phải có một id duy nhất trong số tất cả các thực thể cùng loại. Giá trị id của thực thể được đặt khi thực thể được tạo. Dưới đây là một số giá trị id đề xuất cần xem xét khi tạo các thực thể mới. LƯU Ý: Giá trị của id phải là một string.

 

  • event.params.id.toHex()
  • event.transaction.from.toHex()
  • event.transaction.hash.toHex() + “-” + event.logIndex.toString()

 

Chúng tôi cung cấp Thư viện Graph Typescript chứa các tiện ích để tương tác với bộ lưu trữ The Graph Node và các tiện ích để xử lý dữ liệu hợp đồng thông minh và các thực thể. Bạn có thể sử dụng thư viện này trong ánh xạ của mình bằng cách nhập @graphprotocol/graph-ts vào mapping.ts.

Tạo mã

Để khiến các hợp đồng hoạt động, sự kiện và thực thể hoạt động dễ dàng và an toàn về kiểu, Graph CLI có thể tạo các loại AssemblyScript từ lược đồ GraphQL của subgraph và các ABI hợp đồng có trong các nguồn dữ liệu.

Điều này được thực hiện với

graph codegen [–output-dir <OUTPUT_DIR>] [<MANIFEST>]

nhưng trong hầu hết các trường hợp, subgraph đã được định cấu hình trước thông qua package.json để cho phép bạn chỉ cần chạy một trong những thao tác sau để đạt được điều tương tự:

# Yarn

yarn codegen

 

# NPM

npm run codegen

Điều này sẽ tạo ra một lớp AssemblyScript cho mọi hợp đồng thông minh trong các tệp ABI được đề cập trong subgraph.yaml, cho phép bạn ràng buộc các hợp đồng này với các địa chỉ cụ thể trong ánh xạ và gọi các phương thức hợp đồng chỉ đọc đối với khối đang được xử lý. Nó cũng sẽ tạo ra một lớp cho mọi sự kiện hợp đồng để cung cấp khả năng truy cập dễ dàng vào các tham số sự kiện cũng như khối và giao dịch mà sự kiện bắt nguồn từ đó. Tất cả các loại này đều được viết tới <OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts. Trong subgraph ví dụ, điều này sẽ generated/Gravity/Gravity.ts cho phép các ánh xạ nhập các loại này với

import {

  // The contract class:

  Gravity,

  // The events classes:

  NewGravatar,

  UpdatedGravatar,

} from ‘../generated/Gravity/Gravity’

Ngoài ra, một lớp được tạo cho mỗi loại thực thể trong lược đồ GraphQL của subgraph. Các lớp này cung cấp quyền truy cập tải, đọc và ghi thực thể an toàn kiểu đối với các trường thực thể cũng như một phương thức save() để ghi các thực thể để lưu trữ. Tất cả các lớp thực thể được ghi vào <OUTPUT_DIR>/schema.ts, cho phép ánh xạ để nhập chúng với

import { Gravatar } from ‘../generated/schema’

 

Lưu ý: Việc tạo mã phải được thực hiện lại sau mỗi lần thay đổi đối với lược đồ GraphQL hoặc ABI có trong tệp kê khai. Nó cũng phải được thực hiện ít nhất một lần trước khi xây dựng hoặc triển khai subgraph.

 

Việc tạo mã không kiểm tra mã ánh xạ của bạn trong src/mapping.ts. Nếu bạn muốn kiểm tra điều đó trước khi cố gắng triển khai subgraph của mình cho Graph Explorer, bạn có thể chạy yarn build và sửa bất kỳ lỗi cú pháp nào mà trình biên dịch TypeScript có thể tìm thấy.

Nếu các phương thức chỉ đọc trong hợp đồng của bạn có thể hoàn nguyên, thì bạn nên xử lý điều đó bằng cách gọi phương thức hợp đồng đã tạo có tiền tố try_. Ví dụ, hợp đồng Gravity cho thấy phương thức gravatarToOwner. Mã này sẽ có thể xử lý việc hoàn nguyên trong phương thức đó:

 let gravity = Gravity.bind(event.address)

  let callResult = gravity.try_gravatarToOwner(gravatar)

  if (callResult.reverted) {

    log.info(“getGravatar reverted”, [])

  } else {

    let owner = callResult.value

  }

Lưu ý rằng một Graph Node được kết nối với máy khách Geth hoặc Infura có thể không phát hiện tất cả các lần hoàn nguyên, nếu bạn dựa vào điều này, bạn nên sử dụng Greaph Node được kết nối với máy khách Parity.

Trong phần tiếp theo, chúng ta sẽ xem cách triển khai subgraph của bạn bằng Graph Explorer.

Mẫu nguồn dữ liệu

Một mẫu phổ biến trong các hợp đồng thông minh Ethereum là sử dụng hợp đồng đăng ký hoặc hợp đồng chế tạo (factory), trong đó một hợp đồng tạo, quản lý hoặc tham chiếu đến một số lượng tùy ý các hợp đồng khác mà mỗi hợp đồng có trạng thái và sự kiện riêng. Địa chỉ của các hợp đồng phụ này có thể được biết trước hoặc có thể không được biết trước và nhiều hợp đồng này có thể được tạo và / hoặc thêm vào theo thời gian. Đây là lý do tại sao, trong những trường hợp như vậy, việc xác định một nguồn dữ liệu đơn lẻ hoặc một số lượng nguồn dữ liệu cố định là không thể và cần có một cách tiếp cận năng động hơn: các mẫu nguồn dữ liệu .

Nguồn dữ liệu cho Hợp đồng chính

Đầu tiên, bạn xác định nguồn dữ liệu thông thường cho hợp đồng chính. Đoạn mã dưới đây hiển thị nguồn dữ liệu mẫu đơn giản cho hợp đồng chế tạo sàn giao dịch Uniswap. Lưu ý trình xử lý sự kiện NewExchange(address,address). Nó được tạo ra khi một hợp đồng sàn giao dịch mới được tạo ra trên chuỗi bởi hợp đồng chế tạo.

dataSources:

  – kind: ethereum/contract

    name: Factory

    network: mainnet

    source:

      address: ‘0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95’

      abi: Factory

    mapping:

      kind: ethereum/events

      apiVersion: 0.0.2

      language: wasm/assemblyscript

      file: ./src/mappings/factory.ts

      entities:

        – Directory

      abis:

        – name: Factory

          file: ./abis/factory.json

      eventHandlers:

        – event: NewExchange(address,address)

          handler: handleNewExchange

Mẫu nguồn dữ liệu cho các hợp đồng được tạo động

Sau đó, bạn thêm mẫu nguồn dữ liệu vào tệp kê khai. Chúng giống với các nguồn dữ liệu thông thường, ngoại trừ việc chúng thiếu địa chỉ hợp đồng được xác định trước theo source. Thông thường, bạn sẽ xác định một mẫu cho từng loại hợp đồng phụ được quản lý hoặc tham chiếu bởi hợp đồng mẹ.

dataSources:

  – kind: ethereum/contract

    name: Factory

    # … other source fields for the main contract …

templates:

  – name: Exchange

    kind: ethereum/contract

    network: mainnet

    source:

      abi: Exchange

    mapping:

      kind: ethereum/events

      apiVersion: 0.0.1

      language: wasm/assemblyscript

      file: ./src/mappings/exchange.ts

      entities:

        – Exchange

      abis:

        – name: Exchange

          file: ./abis/exchange.json

      eventHandlers:

        – event: TokenPurchase(address,uint256,uint256)

          handler: handleTokenPurchase

        – event: EthPurchase(address,uint256,uint256)

          handler: handleEthPurchase

        – event: AddLiquidity(address,uint256,uint256)

          handler: handleAddLiquidity

        – event: RemoveLiquidity(address,uint256,uint256)

          handler: handleRemoveLiquidity

Tạo mẫu nguồn dữ liệu

Trong bước cuối cùng, bạn cập nhật ánh xạ hợp đồng chính của mình để tạo phiên bản nguồn dữ liệu động từ một trong các mẫu. Trong ví dụ này, bạn sẽ thay đổi ánh xạ hợp đồng chính để nhập mẫu Exchange và gọi phương thức Exchange.create(address) trên đó để bắt đầu lập chỉ mục hợp đồng sàn giao dịch mới.

import { Exchange } from ‘../generated/templates’

 

export function handleNewExchange(event: NewExchange): void {

  // Start indexing the exchange; `event.params.exchange` is the

  // address of the new exchange contract

  Exchange.create(event.params.exchange)

}

 

Lưu ý: Nguồn dữ liệu mới sẽ chỉ xử lý các lệnh gọi và sự kiện cho khối mà nó được tạo và tất cả các khối tiếp theo, sẽ không xử lý dữ liệu lịch sử, tức là dữ liệu được chứa trong các khối trước đó.

Nếu các khối trước đó chứa dữ liệu liên quan đến nguồn dữ liệu mới, tốt nhất là lập chỉ mục dữ liệu đó bằng cách đọc trạng thái hiện tại của hợp đồng và tạo các thực thể đại diện cho trạng thái đó tại thời điểm nguồn dữ liệu mới được tạo.

 

Lưu ý: Nguồn dữ liệu mới sẽ chỉ xử lý các lệnh gọi và sự kiện cho khối mà nó được tạo và tất cả các khối tiếp theo, nhưng sẽ không xử lý dữ liệu lịch sử, tức là dữ liệu được chứa trong các khối trước đó.

Nếu các khối trước đó chứa dữ liệu liên quan đến nguồn dữ liệu mới, tốt nhất là lập chỉ mục dữ liệu đó bằng cách đọc trạng thái hiện tại của hợp đồng và tạo các thực thể đại diện cho trạng thái đó tại thời điểm nguồn dữ liệu mới được tạo.

Bối cảnh nguồn dữ liệu

Các nối cảnh nguồn dữ liệu cho phép đi qua cấu hình bổ sung khi khởi tạo một mẫu. Trong ví dụ, giả sử các sàn giao dịch được liên kết với một cặp giao dịch cụ thể, được bao gồm trong sự kiện NewExchange. Thông tin đó có thể được chuyển vào nguồn dữ liệu được khởi tạo, như sau:

import { Exchange } from ‘../generated/templates’

 

export function handleNewExchange(event: NewExchange): void {

  let context = new DataSourceContext()

  context.setString(“tradingPair”, event.params.tradingPair)

  Exchange.createWithContext(event.params.exchange, context)

}

Bên trong ánh xạ của mẫu Exchange, ngữ cảnh sau đó có thể được truy cập:

import { dataSource } from “@graphprotocol/graph-ts”

 

let context = dataSource.context()

let tradingPair = context.getString(“tradingPair”)

Có những setters và getters giống như setStringgetString cho tất cả các loại giá trị.

Bắt đầu khối

startBlock là cài đặt tùy chọn cho phép bạn xác định từ khối nào trong chuỗi mà nguồn dữ liệu sẽ bắt đầu lập chỉ mục. Đặt khối bắt đầu cho phép nguồn dữ liệu bỏ qua hàng triệu khối có khả năng không liên quan. Thông thường, một nhà phát triển subgraph sẽ đặt startBlock vào khối mà hợp đồng thông minh của nguồn dữ liệu được tạo.

dataSources:

 – kind: ethereum/contract

   name: ExampleSource

   network: mainnet

   source:

     address: ‘0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95’

     abi: ExampleContract

     startBlock: 6627917

   mapping:

     kind: ethereum/events

     apiVersion: 0.0.2

     language: wasm/assemblyscript

     file: ./src/mappings/factory.ts

     entities:

       – User

     abis:

       – name: ExampleContract

         file: ./abis/ExampleContract.json

     eventHandlers:

       – event: NewEvent(address,address)

         handler: handleNewEvent

Lưu ý: Khối tạo hợp đồng có thể được tra cứu nhanh chóng trên Etherscan: 1. Tìm kiếm hợp đồng bằng cách nhập địa chỉ của nó vào thanh tìm kiếm. 2. Nhấp vào hash giao dịch tạo trong phần Contract Creator. 3. Tải trang chi tiết giao dịch nơi bạn sẽ tìm thấy khối bắt đầu cho hợp đồng đó.

Trình xử lý cuộc gọi

Trong khi các sự kiện cung cấp một cách hiệu quả để thu thập các thay đổi liên quan đến trạng thái của hợp đồng, nhiều hợp đồng tránh tạo nhật ký để tối ưu hóa phí gas. Trong những trường hợp này, một subgraph có thể đăng ký các lệnh gọi được thực hiện tới hợp đồng nguồn dữ liệu. Điều này đạt được bằng cách xác định trình xử lý cuộc gọi tham chiếu đến chữ ký hàm và trình xử lý ánh xạ sẽ xử lý các cuộc gọi đến hàm này. Để xử lý các cuộc gọi này, trình xử lý ánh xạ sẽ nhận được ethereum.Call như một đối số với các đầu vào và đầu ra đã nhập từ cuộc gọi. Các cuộc gọi được thực hiện ở bất kỳ độ sâu nào trong chuỗi cuộc gọi của giao dịch sẽ kích hoạt ánh xạ, cho phép ghi lại hoạt động với hợp đồng nguồn dữ liệu thông qua các hợp đồng ủy quyền.

Trình xử lý cuộc gọi sẽ chỉ kích hoạt trong một trong hai trường hợp: khi hàm được chỉ định được gọi bởi một tài khoản khác với chính hợp đồng hoặc khi nó được đánh dấu là bên ngoài trong Solidity và được gọi như một phần của một hàm khác trong cùng một hợp đồng.

Lưu ý: Trình xử lý cuộc gọi không được hỗ trợ trên Rinkeby và Ganache. Các trình xử lý cuộc gọi phụ thuộc vào API theo dõi chẵn lẻ và cả Rinkeby và Ganache đều không hỗ trợ điều này (Rinkeby chỉ dành cho Geth).

Xác định Trình xử lý cuộc gọi

Để xác định trình xử lý cuộc gọi trong tệp kê khai của bạn, chỉ cần thêm một mảng callHandlers bên dưới nguồn dữ liệu mà bạn muốn đăng ký.

dataSources:

  – kind: ethereum/contract

    name: Gravity

    network: mainnet

    source:

      address: ‘0x731a10897d267e19b34503ad902d0a29173ba4b1’

      abi: Gravity

    mapping:

      kind: ethereum/events

      apiVersion: 0.0.2

      language: wasm/assemblyscript

      entities:

        – Gravatar

        – Transaction

      abis:

        – name: Gravity

          file: ./abis/Gravity.json

      callHandlers:

        – function: createGravatar(string,string)

          handler: handleCreateGravatar

function là chữ ký hàm chuẩn hóa để lọc các cuộc gọi theo. Các tính chất handler là tên của các chức năng trong ánh xạ của bạn, bạn muốn thực hiện khi hàm mục tiêu được gọi là trong hợp đồng nguồn dữ liệu.

Chức năng ánh xạ

Mỗi trình xử lý cuộc gọi nhận một tham số duy nhất có kiểu tương ứng với tên của hàm được gọi. Trong subgraph ví dụ ở trên, ánh xạ chứa một trình xử lý khi chức năng createGravatar được gọi và nhận một CreateGravatarCall như đối số:

import { CreateGravatarCall } from ‘../generated/Gravity/Gravity’

import { Transaction } from ‘../generated/schema’

 

export function handleCreateGravatar(call: CreateGravatarCall): void {

  let id = call.transaction.hash.toHex()

  let transaction = new Transaction(id)

  transaction.displayName = call.inputs._displayName

  transaction.imageUrl = call.inputs._imageUrl

  transaction.save()

}

Chức năng handleCreateGravatar phải lấy một CreateGravatarCall mới mà là một lớp con của ethereum.Call, được cung cấp bởi @graphprotocol/graph-ts, trong đó bao gồm các yếu tố đầu vào được nhập và kết quả của cuộc gọi. Các loại CreateGravatarCall được tạo ra cho bạn khi bạn chạy graph codegen.

Trình xử lý khối

Ngoài việc đăng ký các sự kiện hợp đồng hoặc lệnh gọi hàm, một subgraph có thể muốn cập nhật dữ liệu của nó khi các khối mới được nối vào chuỗi. Để đạt được điều này, một subgraph có thể chạy một hàm sau mỗi khối hoặc sau các khối phù hợp với bộ lọc được xác định trước.

Bộ lọc được hỗ trợ

filter:

  kind: call

Trình xử lý được xác định sẽ được gọi một lần cho mỗi khối chứa lệnh gọi đến hợp đồng (nguồn dữ liệu) mà trình xử lý được định nghĩa theo.

Tổng bộ lọc cho trình xử lý khối sẽ đảm bảo rằng trình xử lý được gọi là mọi khối. Nguồn dữ liệu chỉ có thể chứa một trình xử lý khối cho mỗi loại bộ lọc.

dataSources:

  – kind: ethereum/contract

    name: Gravity

    network: dev

    source:

      address: ‘0x731a10897d267e19b34503ad902d0a29173ba4b1’

      abi: Gravity

    mapping:

      kind: ethereum/events

      apiVersion: 0.0.2

      language: wasm/assemblyscript

      entities:

        – Gravatar

        – Transaction

      abis:

        – name: Gravity

          file: ./abis/Gravity.json

      blockHandlers:

        – handler: handleBlock

        – handler: handleBlockWithCallToContract

          filter:

            kind: call

Chức năng ánh xạ

Hàm ánh xạ sẽ nhận một ethereum.Block như là đối số duy nhất của nó. Giống như các hàm ánh xạ cho các sự kiện, hàm này có thể truy cập các thực thể subgraph hiện có trong bộ lưu trữ, gọi các hợp đồng thông minh và tạo hoặc cập nhật các thực thể.

import { ethereum } from ‘@graphprotocol/graph-ts’

 

export function handleBlock(block: ethereum.Block): void {

  let id = block.hash.toHex()

  let entity = new Block(id)

  entity.save()

}

Sự kiện ẩn danh

Nếu bạn cần xử lý các sự kiện ẩn danh trong Solidity, bạn có thể đạt được điều đó bằng cách cung cấp chủ đề 0 của sự kiện, như trong ví dụ:

eventHandlers:

  – event: LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)

    topic0: “0xbaa8529c00000000000000000000000000000000000000000000000000000000”

    handler: handleGive

Một sự kiện sẽ chỉ được kích hoạt khi cả chữ ký và chủ đề 0 trùng khớp. Theo mặc định, topic0 bằng giá trị hash của chữ ký sự kiện.

Ghim IPFS

Một trường hợp sử dụng phổ biến để kết hợp IPFS với Ethereum là lưu trữ dữ liệu trên IPFS sẽ quá đắt để duy trì trên chuỗi và tham chiếu hàm băm IPFS trong các hợp đồng Ethereum.

Với các hàm băm IPFS như vậy, các subgraph có thể đọc các tệp tương ứng từ IPFS bằng cách sử dụng ipfs.catipfs.map. Tuy nhiên, để thực hiện việc này một cách đáng tin cậy, yêu cầu các tệp này phải được ghim trên nút IPFS mà Graph Node lập chỉ mục mà subgraph kết nối. Trong trường hợp của hosted service, đây là https://api.thegraph.com/ipfs/.

Để giúp các nhà phát triển subgraph trở nên dễ dàng, nhóm The Graph đã viết một công cụ để truyền tệp từ node IPFS này sang nút IPFS khác, được gọi là ipfs-sync.

Jade Vo

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.