Developing > Creare un subgraph

Creare un subgraph

Reading time: 46 min

Un subgraph estrae i dati da una blockchain, li elabora e li memorizza in modo che possano essere facilmente interrogati tramite GraphQL.

Definizione di un Subgraph

La definizione del subgraph consiste in alcuni file:

In order to use your subgraph on The Graph's decentralized network, you will need to create an API key. It is recommended that you add signal to your subgraph with at least 3,000 GRT.

Before you go into detail about the contents of the manifest file, you need to install the Graph CLI which you will need to build and deploy a subgraph.

Installare the Graph CLI

Collegamento a questa sezione

The Graph CLI è scritta in JavaScript e per utilizzarla è necessario installare yarn oppure npm; in quanto segue si presume che si disponga di yarn.

Una volta che si dispone di yarn, installare the Graph CLI eseguendo

Installare con yarn:

yarn global add @graphprotocol/graph-cli

Installare con npm:

npm install -g @graphprotocol/graph-cli

Once installed, the graph init command can be used to set up a new subgraph project, either from an existing contract or from an example subgraph. This command can be used to create a subgraph in Subgraph Studio by passing in graph init --product subgraph-studio. If you already have a smart contract deployed to your preferred network, bootstrapping a new subgraph from that contract can be a good way to get started.

Da un contratto esistente

Collegamento a questa sezione

Il comando seguente crea un subgraph che indicizza tutti gli eventi di un contratto esistente. Tenta di recuperare l'ABI del contratto da Etherscan e torna a richiedere il percorso di un file locale. Se manca uno qualsiasi degli argomenti opzionali, il comando viene eseguito attraverso un modulo interattivo.

graph init \
--product subgraph-studio
--from-contract <CONTRACT_ADDRESS> \
[--network <ETHEREUM_NETWORK>] \
[--abi <FILE>] \
<SUBGRAPH_SLUG> [<DIRECTORY>]

Il <SUBGRAPH_SLUG> è l'ID del subgraph in Subgraph Studio, che si trova nella pagina dei dettagli del subgraph.

Da un subgraph di esempio

Collegamento a questa sezione

La seconda modalità supportata da graph init è la creazione di un nuovo progetto a partire da un subgraph di esempio. Il comando seguente esegue questa operazione:

graph init --studio <SUBGRAPH_SLUG>

The example subgraph is based on the Gravity contract by Dani Grant that manages user avatars and emits NewGravatar or UpdateGravatar events whenever avatars are created or updated. The subgraph handles these events by writing Gravatar entities to the Graph Node store and ensuring these are updated according to the events. The following sections will go over the files that make up the subgraph manifest for this example.

Aggiungere nuove data sources a un subgraph esistente

Collegamento a questa sezione

Dalla v0.31.0 il graph-cli supporta l'aggiunta di nuove sorgenti di dati a un subgraph esistente tramite il comando graph add.

graph add <address> [<subgraph-manifest default: "./subgraph.yaml">]
Opzioni:
--abi <path> Percorso dell'ABI del contratto (predefinito: download from Etherscan)
--contract-name Nome del contratto (predefinito: Contract)
--merge-entities Se unire entità con lo stesso nome (predefinito: false)
--network-file <path> Percorso del file di configurazione della rete (predefinito: "./networks.json")

Il comando add recupera l'ABI da Etherscan (a meno che non sia specificato un percorso ABI con l'opzione --abi) e crea una nuova dataSource nello stesso modo in cui il comando graph init crea una dataSource -from-contract, aggiornando di conseguenza lo schema e le mappature.

L'opzione --merge-entities identifica il modo in cui lo sviluppatore desidera gestire i conflitti tra i nomi di entità e evento:

  • If true: il nuovo dataSource dovrebbe utilizzare gli eventHandler & entità esistenti.
  • If false: una nuova entità & il gestore dell'evento deve essere creato con ${dataSourceName}{EventName}.

Il contratto address sarà scritto in networks.json per la rete rilevante.

Nota: Quando si utilizza il cli interattivo, dopo aver eseguito con successo graph init, verrà richiesto di aggiungere un nuovo dataSource.

Manifesto di Subgraph

Collegamento a questa sezione

Il manifesto del subgraph subgraph.yaml definisce gli smart contract che il subgraph indicizza, a quali eventi di questi contratti prestare attenzione e come mappare i dati degli eventi alle entità che Graph Node memorizza e permette di effettuare query. Le specifiche complete dei manifesti dei subgraph sono disponibili qui.

Per il subgraph di esempio, subgraph.yaml è:

specVersion: 0.0.4
description: Gravatar for Ethereum
repository: https://github.com/graphprotocol/graph-tooling
schema:
file: ./schema.graphql
indexerHints:
prune: auto
dataSources:
- kind: ethereum/contract
name: Gravity
network: mainnet
source:
address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'
abi: Gravity
startBlock: 6175244
endBlock: 7175245
context:
foo:
type: Bool
data: true
bar:
type: String
data: 'bar'
mapping:
kind: ethereum/events
apiVersion: 0.0.6
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:
- handler: handleBlock
- handler: handleBlockWithCall
filter:
kind: call
file: ./src/mapping.ts

Le voci importanti da aggiornare per il manifesto sono:

  • specVersion: a semver version that identifies the supported manifest structure and functionality for the subgraph. The latest version is 1.2.0. See specVersion releases section to see more details on features & releases.

  • description: a human-readable description of what the subgraph is. This description is displayed in Graph Explorer when the subgraph is deployed to Subgraph Studio.

  • repository: the URL of the repository where the subgraph manifest can be found. This is also displayed in Graph Explorer.

  • features: un elenco di tutti i nomi delle caratteristiche utilizzati.

  • indexerHints.prune: Defines the retention of historical block data for a subgraph. See prune in indexerHints section.

  • dataSources.source: l'indirizzo dello smart contract di cui il subgraph è fonte e l'ABI dello smart contract da utilizzare. L'indirizzo è facoltativo; omettendolo, si possono indicizzare gli eventi corrispondenti di tutti i contratti.

  • dataSources.source.startBlock: il numero opzionale del blocco da cui l'origine dati inizia l'indicizzazione. Nella maggior parte dei casi, si consiglia di utilizzare il blocco in cui è stato creato il contratto.

  • dataSources.source.endBlock: Il numero opzionale del blocco a cui il data source interrompe l'indicizzazione, incluso quel blocco. È richiesta la versione minima delle specifiche: 0.0.9.

  • dataSources.context: coppie del valore chiave che possono essere usate nelle mappature dei subgraph. Supporta vari tipi di dati come Bool, String, Int, Int8, BigDecimal, Bytes, List, e BigInt. Ogni variabile deve specificare il suo tipo e dati. Queste variabili di contesto sono poi accessibili nei file di mappatura, offrendo più opzioni configurabili per lo sviluppo del subgraph.

  • dataSources.mapping.entities: le entità che l'origine dati scrive nell'archivio. Lo schema di ciascuna entità è definito nel file schema.graphql.

  • dataSources.mapping.abis: uno o più file ABI denominati per il contratto sorgente e per tutti gli altri smart contract con cui si interagisce all'interno delle mappature.

  • dataSources.mapping.eventHandlers: elenca gli eventi dello smart contract a cui questo subgraph reagisce e i gestori nella mappatura -./src/mapping.ts nell'esempio - che trasformano questi eventi in entità dell'archivio.

  • dataSources.mapping.callHandlers: elenca le funzioni dello smart contract a cui questo subgraph reagisce e i gestori della mappatura che trasformano gli input e gli output delle chiamate di funzione in entità dell'archivio.

  • dataSources.mapping.blockHandlers: elenca i blocchi a cui questo subgraph reagisce e i gestori nella mappatura da eseguire quando un blocco viene aggiunto alla chain. Senza un filtro, il gestore del blocco verrà eseguito ogni blocco. È possibile fornire un filtro di chiamata opzionale aggiungendo al gestore un campo filter con kind: call. Questo eseguirà il gestore solo se il blocco contiene almeno una chiamata al contratto sorgente.

Un singolo subgraph può indicizzare i dati di più smart contract. Aggiungere all'array dataSources una voce per ogni contratto da cui devono essere indicizzati i dati.

Order of Triggering Handlers

Collegamento a questa sezione

I trigger per una data source all'interno di un blocco sono ordinati secondo il seguente processo:

  1. I trigger di eventi e chiamate sono ordinati prima per indice di transazione all'interno del blocco.
  2. I trigger di eventi e chiamate all'interno della stessa transazione sono ordinati secondo una convenzione: prima i trigger di eventi e poi quelli di chiamate, rispettando l'ordine in cui sono definiti nel manifesto.
  3. I trigger di blocco vengono eseguiti dopo i trigger di evento e di chiamata, nell'ordine in cui sono definiti nel manifesto.

Queste regole di ordinazione sono soggette a modifiche.

Note: When new dynamic data source are created, the handlers defined for dynamic data sources will only start processing after all existing data source handlers are processed, and will repeat in the same sequence whenever triggered.

Indexed Argument Filters / Topic Filters

Collegamento a questa sezione

Requires: SpecVersion >= 1.2.0

Topic filters, also known as indexed argument filters, are a powerful feature in subgraphs that allow users to precisely filter blockchain events based on the values of their indexed arguments.

  • These filters help isolate specific events of interest from the vast stream of events on the blockchain, allowing subgraphs to operate more efficiently by focusing only on relevant data.

  • This is useful for creating personal subgraphs that track specific addresses and their interactions with various smart contracts on the blockchain.

How Topic Filters Work

Collegamento a questa sezione

When a smart contract emits an event, any arguments that are marked as indexed can be used as filters in a subgraph's manifest. This allows the subgraph to listen selectively for events that match these indexed arguments.

  • The event's first indexed argument corresponds to topic1, the second to topic2, and so on, up to topic3, since the Ethereum Virtual Machine (EVM) allows up to three indexed arguments per event.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Token {
// Event declaration with indexed parameters for addresses
event Transfer(address indexed from, address indexed to, uint256 value);
// Function to simulate transferring tokens
function transfer(address to, uint256 value) public {
// Emitting the Transfer event with from, to, and value
emit Transfer(msg.sender, to, value);
}
}

In this example:

  • The Transfer event is used to log transactions of tokens between addresses.
  • The from and to parameters are indexed, allowing event listeners to filter and monitor transfers involving specific addresses.
  • The transfer function is a simple representation of a token transfer action, emitting the Transfer event whenever it is called.

Configuration in Subgraphs

Collegamento a questa sezione

Topic filters are defined directly within the event handler configuration in the subgraph manifest. Here is how they are configured:

eventHandlers:
- event: SomeEvent(indexed uint256, indexed address, indexed uint256)
handler: handleSomeEvent
topic1: ['0xValue1', '0xValue2']
topic2: ['0xAddress1', '0xAddress2']
topic3: ['0xValue3']

In this setup:

  • topic1 corresponds to the first indexed argument of the event, topic2 to the second, and topic3 to the third.
  • Each topic can have one or more values, and an event is only processed if it matches one of the values in each specified topic.
  • Within a Single Topic: The logic functions as an OR condition. The event will be processed if it matches any one of the listed values in a given topic.
  • Between Different Topics: The logic functions as an AND condition. An event must satisfy all specified conditions across different topics to trigger the associated handler.

Example 1: Tracking Direct Transfers from Address A to Address B

Collegamento a questa sezione
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleDirectedTransfer
topic1: ['0xAddressA'] # Sender Address
topic2: ['0xAddressB'] # Receiver Address

In this configuration:

  • topic1 is configured to filter Transfer events where 0xAddressA is the sender.
  • topic2 is configured to filter Transfer events where 0xAddressB is the receiver.
  • The subgraph will only index transactions that occur directly from 0xAddressA to 0xAddressB.

Example 2: Tracking Transactions in Either Direction Between Two or More Addresses

Collegamento a questa sezione
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransferToOrFrom
topic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Address
topic2: ['0xAddressB', '0xAddressC'] # Receiver Address

In this configuration:

  • topic1 is configured to filter Transfer events where 0xAddressA, 0xAddressB, 0xAddressC is the sender.
  • topic2 is configured to filter Transfer events where 0xAddressB and 0xAddressC is the receiver.
  • The subgraph will index transactions that occur in either direction between multiple addresses allowing for comprehensive monitoring of interactions involving all addresses.

Declared eth_call

Collegamento a questa sezione

Requires: SpecVersion >= 1.2.0. Currently, eth_calls can only be declared for event handlers.

Declarative eth_calls are a valuable subgraph feature that allows eth_calls to be executed ahead of time, enabling graph-node to execute them in parallel.

This feature does the following:

  • Significantly improves the performance of fetching data from the Ethereum blockchain by reducing the total time for multiple calls and optimizing the subgraph's overall efficiency.
  • Allows faster data fetching, resulting in quicker query responses and a better user experience.
  • Reduces wait times for applications that need to aggregate data from multiple Ethereum calls, making the data retrieval process more efficient.
  • Declarative eth_calls: Ethereum calls that are defined to be executed in parallel rather than sequentially.
  • Parallel Execution: Instead of waiting for one call to finish before starting the next, multiple calls can be initiated simultaneously.
  • Time Efficiency: The total time taken for all the calls changes from the sum of the individual call times (sequential) to the time taken by the longest call (parallel).

Scenario without Declarative eth_calls

Collegamento a questa sezione

Imagine you have a subgraph that needs to make three Ethereum calls to fetch data about a user's transactions, balance, and token holdings.

Traditionally, these calls might be made sequentially:

  1. Call 1 (Transactions): Takes 3 seconds
  2. Call 2 (Balance): Takes 2 seconds
  3. Call 3 (Token Holdings): Takes 4 seconds

Total time taken = 3 + 2 + 4 = 9 seconds

Scenario with Declarative eth_calls

Collegamento a questa sezione

With this feature, you can declare these calls to be executed in parallel:

  1. Call 1 (Transactions): Takes 3 seconds
  2. Call 2 (Balance): Takes 2 seconds
  3. Call 3 (Token Holdings): Takes 4 seconds

Since these calls are executed in parallel, the total time taken is equal to the time taken by the longest call.

Total time taken = max (3, 2, 4) = 4 seconds

  1. Declarative Definition: In the subgraph manifest, you declare the Ethereum calls in a way that indicates they can be executed in parallel.
  2. Parallel Execution Engine: The Graph Node's execution engine recognizes these declarations and runs the calls simultaneously.
  3. Result Aggregation: Once all calls are complete, the results are aggregated and used by the subgraph for further processing.

Example Configuration in Subgraph Manifest

Collegamento a questa sezione

Declared eth_calls can access the event.address of the underlying event as well as all the event.params.

Subgraph.yaml using event.address:

eventHandlers:
event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)
handler: handleSwap
calls:
global0X128: Pool[event.address].feeGrowthGlobal0X128()
global1X128: Pool[event.address].feeGrowthGlobal1X128()

Details for the example above:

  • global0X128 is the declared eth_call.
  • The text before colon(global0X128) is the label for this eth_call which is used when logging errors.
  • The text (Pool[event.address].feeGrowthGlobal0X128()) is the actual eth_call that will be executed, which is in the form of Contract[address].function(arguments)
  • The address and arguments can be replaced with variables that will be available when the handler is executed.

Subgraph.yaml using event.params

calls:
- ERC20DecimalsToken0: ERC20[event.params.token0].decimals()

SpecVersion Releases

Collegamento a questa sezione
VersionRelease notes
1.2.0Added support for Indexed Argument Filtering & declared eth_call
1.1.0Supports Timeseries & Aggregations. Added support for type Int8 for id.
1.0.0Supports indexerHints feature to prune subgraphs
0.0.9Supports endBlock feature
0.0.8Added support for polling Block Handlers and Initialisation Handlers.
0.0.7Added support for File Data Sources.
0.0.6Supports fast Proof of Indexing calculation variant.
0.0.5Added support for event handlers having access to transaction receipts.
0.0.4Added support for managing subgraph features.

I file ABI devono corrispondere al vostro contratto. Esistono diversi modi per ottenere i file ABI:

  • Se state costruendo il vostro progetto, probabilmente avrete accesso alle ABI più recenti.
  • Se state costruendo un subgraph per un progetto pubblico, potete scaricare il progetto sul vostro computer e ottenere l'ABI usando truffle compile o usando solc per compilare.
  • È possibile trovare l'ABI anche su Etherscan, ma non è sempre affidabile, in quanto l'ABI caricato su questo sito potrebbe non essere aggiornato. Assicuratevi di avere l'ABI corretto, altrimenti l'esecuzione del subgraph fallirà.

Lo schema del subgraph si trova nel file schema.graphql. Gli schemi GraphQL sono definiti utilizzando il linguaggio di definizione dell'interfaccia GraphQL. Se non hai mai scritto uno schema GraphQL, si consiglia di dare un'occhiata a questa guida sul sistema di tipi GraphQL. La documentazione di riferimento per gli schemi GraphQL si trova nella sezione GraphQL API.

Definire le entità

Collegamento a questa sezione

Prima di definire le entità, è importante fare un passo indietro e pensare a come i dati sono strutturati e collegati. Tutte le query saranno fatte sul modello di dati definito nello schema del subgraph e sulle entità indicizzate dal subgraph. Per questo motivo, è bene definire lo schema del subgraph in modo che corrisponda alle esigenze della propria applicazione. Può essere utile immaginare le entità come "oggetti contenenti dati", piuttosto che come eventi o funzioni.

Con The Graph, è sufficiente definire i tipi di entità in schema.graphql e Graph Node genererà campi di primo livello per interrogare singole istanze e collezioni di quel tipo di entità. Ogni tipo che dovrebbe essere un'entità deve essere annotato con una direttiva @entity. Per impostazione predefinita, le entità sono mutabili, il che significa che le mappature possono caricare entità esistenti, modificarle e memorizzare una nuova versione di quell'entità. La mutabilità ha un prezzo e per i tipi di entità per i quali si sa che non saranno mai modificati, ad esempio perché contengono semplicemente dati estratti alla lettera dalla chain, si raccomanda di contrassegnarli come immutabili con @entity(immutable: true). I mapping possono apportare modifiche alle entità immutabili, purché tali modifiche avvengano nello stesso blocco in cui l'entità è stata creata. Le entità immutabili sono molto più veloci da scrivere e da effettuare query e quindi dovrebbero essere utilizzate ogni volta che è possibile.

L'entità Gravatar qui sotto è strutturata intorno a un oggetto Gravatar ed è un buon esempio di come potrebbe essere definita un'entità.

type Gravatar @entity(immutable: true) {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
accepted: Boolean
}

Gli esempi di entità GravatarAccepted e GravatarDeclined che seguono sono basati su eventi. Non è consigliabile mappare gli eventi o le chiamate di funzione alle entità 1:1.

type GravatarAccepted @entity {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
}
type GravatarDeclined @entity {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
}

Campi opzionali e obbligatori

Collegamento a questa sezione

I campi delle entità possono essere definiti come obbligatori o opzionali. I campi obbligatori sono indicati da ! nello schema. Se un campo obbligatorio non è impostato nella mappatura, si riceverà questo errore quando si interroga il campo:

Null value resolved for non-null field 'name'

Ogni entità deve avere un campo id, che deve essere di tipo Bytes! oppure String!. In genere si raccomanda di usare Bytes!, a meno che il id non contenga testo leggibile dall'umano, poiché le entità con id Bytes! saranno più veloci da scrivere e da effettuare query rispetto a quelle con String! id. Il campo id serve come chiave primaria e deve essere unico per tutte le entità dello stesso tipo. Per ragioni storiche, è accettato anche il tipo ID!, sinonimo di String!.

Per alcuni tipi di entità, l'id è costruito a partire dagli id di altre due entità; ciò è possibile usando concat, ad esempio, let id = left.id.concat(right.id) per formare l'id dagli id di left e right. Allo stesso modo, per costruire un id a partire dall'id di un'entità esistente e da un contatore count, si può usare let id = left.id.concatI32(count). La concatenazione è garantita per produrre id unici, purché la lunghezza di left sia la stessa per tutte queste entità, ad esempio perché left.id è un Adress.

Tipi scalari integrati

Collegamento a questa sezione

Scalari supportati da GraphQL

Collegamento a questa sezione

Nella nostra API GraphQL supportiamo i seguenti scalari:

TipoDescrizione
BytesByte array, rappresentato come una stringa esadecimale. Comunemente utilizzato per gli hash e gli indirizzi di Ethereum.
StringScalare per valori string. I caratteri nulli non sono supportati e vengono rimossi automaticamente.
BooleanScalare per valori boolean.
IntThe GraphQL spec defines Int to be a signed 32-bit integer.
Int8Un intero firmato a 8 byte, noto anche come intero firmato a 64 bit, può memorizzare valori nell'intervallo da -9,223,372,036,854,775,808 a 9,223,372,036,854,775,807. È preferibile utilizzare questo per rappresentare i64 da ethereum.
BigIntNumeri interi grandi. Utilizzati per i tipi uint32, int64, uint64, ..., uint256 di Ethereum. Nota: Tutto ciò che è inferiore a uint32 come int32, uint24 oppure int8 è rappresentato come i32.
BigDecimalBigDecimal Decimali ad alta precisione rappresentati come un significante e un esponente. L'intervallo degli esponenti va da -6143 a +6144. Arrotondato a 34 cifre significative.
TimestampIt is an i64 value in microseconds. Commonly used for timestamp fields for timeseries and aggregations.

È possibile creare enum anche all'interno di uno schema. Gli enum hanno la seguente sintassi:

enum TokenStatus {
OriginalOwner
SecondOwner
ThirdOwner
}

Una volta che l'enum è definito nello schema, si può usare la rappresentazione in stringa del valore dell'enum per impostare un campo enum su un'entità. Ad esempio, si può impostare il tokenStatus su SecondOwner definendo prima l'entità e poi impostando il campo con entity.tokenStatus = "SecondOwner". L'esempio seguente mostra l'aspetto dell'entità Token con un campo enum:

Maggiori dettagli sulla scrittura degli enum si trovano nella documentazione di GraphQL.

Relazioni tra entità

Collegamento a questa sezione

Un'entità può avere una relazione con una o più altre entità dello schema. Queste relazioni possono essere attraversate nelle query. Le relazioni in The Graph sono unidirezionali. È possibile simulare relazioni bidirezionali definendo una relazione unidirezionale su entrambe le "estremità" della relazione.

Le relazioni sono definite sulle entità come qualsiasi altro campo, tranne per il fatto che il tipo specificato è quello di un'altra entità.

Rapporti uno-a-uno

Collegamento a questa sezione

Definire un tipo di entità Transaction con una relazione opzionale uno-a-uno con un tipo di entità TransactionReceipt:

type Transaction @entity(immutable: true) {
id: Bytes!
transactionReceipt: TransactionReceipt
}
type TransactionReceipt @entity(immutable: true) {
id: Bytes!
transaction: Transaction
}

Relazioni uno-a-molti

Collegamento a questa sezione

Definire un tipo di entità TokenBalance con una relazione obbligatoria uno-a-molti con un tipo di entità Token:

type Token @entity(immutable: true) {
id: Bytes!
}
type TokenBalance @entity {
id: Bytes!
amount: Int!
token: Token!
}

Le ricerche inverse possono essere definite su un'entità attraverso il campo @derivedFrom. Questo crea un campo virtuale sull'entità che può essere interrogato, ma non può essere impostato manualmente attraverso l'API dei mapping. Piuttosto, è derivato dalla relazione definita sull'altra entità. Per tali relazioni, raramente ha senso memorizzare entrambi i lati della relazione e sia l'indicizzazione che le prestazioni delle query saranno migliori quando solo un lato è memorizzato e l'altro è derivato.

Per le relazioni uno-a-molti, la relazione deve sempre essere memorizzata sul lato "uno" e il lato "molti" deve sempre essere derivato. Memorizzare la relazione in questo modo, piuttosto che memorizzare un array di entità sul lato "molti", migliorerà notevolmente le prestazioni sia per l'indicizzazione che per l'interrogazione del subgraph. In generale, la memorizzazione di array di entità dovrebbe essere evitata per quanto possibile.

Possiamo rendere accessibili i saldi di un token dal token stesso, derivando un campo tokenBalances:

type Token @entity(immutable: true) {
id: Bytes!
tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")
}
type TokenBalance @entity {
id: Bytes!
amount: Int!
token: Token!
}

Relazioni molti-a-molti

Collegamento a questa sezione

Per le relazioni molti-a-molti, come ad esempio gli utenti che possono appartenere a un numero qualsiasi di organizzazioni, il modo più semplice, ma generalmente non il più performante, di modellare la relazione è come un array in ciascuna delle due entità coinvolte. Se la relazione è simmetrica, è necessario memorizzare solo un lato della relazione e l'altro lato può essere derivato.

Definire una ricerca inversa da un tipo di entità User a un tipo di entità Organization. Nell'esempio seguente, questo si ottiene cercando l'attributo members dall'entità Organization. Nelle query, il campo organizations su User verrà risolto trovando tutte le entità Organization che includono l'ID dell'utente.

type Organization @entity {
id: Bytes!
name: String!
members: [User!]!
}
type User @entity {
id: Bytes!
name: String!
organizations: [Organization!]! @derivedFrom(field: "members")
}

Un modo più performante per memorizzare questa relazione è una tabella di mappatura che ha una voce per ogni coppia User / Organization con uno schema come

type Organization @entity {
id: Bytes!
name: String!
members: [UserOrganization!]! @derivedFrom(field: "organization")
}
type User @entity {
id: Bytes!
name: String!
organizations: [UserOrganization!] @derivedFrom(field: "user")
}
type UserOrganization @entity {
id: Bytes! # Set to `user.id.concat(organization.id)`
user: User!
organization: Organization!
}

Questo approccio richiede che le query scendano di un ulteriore livello per recuperare, ad esempio, le organizzazioni degli utenti:

query usersWithOrganizations {
users {
organizations {
# this is a UserOrganization entity
organization {
name
}
}
}
}

Questo modo più elaborato di memorizzare le relazioni molti-a-molti si traduce in una minore quantità di dati memorizzati per il subgraph e quindi in un subgraph che spesso è molto più veloce da indicizzare e da effettuare query.

Aggiungere commenti allo schema

Collegamento a questa sezione

As per GraphQL spec, comments can be added above schema entity attributes using the hash symble #. This is illustrated in the example below:

type MyFirstEntity @entity {
# unique identifier and primary key of the entity
id: Bytes!
address: Bytes!
}

Definizione dei campi di ricerca fulltext

Collegamento a questa sezione

Le query di ricerca fulltext filtrano e classificano le entità in base a un input di ricerca testuale. Le query full-text sono in grado di restituire corrispondenze per parole simili, elaborando il testo della query in gambi prima di confrontarli con i dati di testo indicizzati.

La definizione di una query fulltext include il nome della query, il dizionario linguistico utilizzato per elaborare i campi di testo, l'algoritmo di classificazione utilizzato per ordinare i risultati e i campi inclusi nella ricerca. Ogni query fulltext può comprendere più campi, ma tutti i campi inclusi devono appartenere a un unico tipo di entità.

Per aggiungere una query fulltext, includere un tipo _Schema_ con una direttiva fulltext nello schema GraphQL.

type _Schema_
@fulltext(
name: "bandSearch"
language: en
algorithm: rank
include: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }]
)
type Band @entity {
id: Bytes!
name: String!
description: String!
bio: String
wallet: Address
labels: [Label!]!
discography: [Album!]!
members: [Musician!]!
}

Il campo di esempio bandSearch può essere utilizzato nelle query per filtrare le entità Band in base ai documenti di testo nei campi name, description, e bio. Passare a GraphQL API - Queries per una descrizione dell'API di ricerca fulltext e per altri esempi di utilizzo.

query {
bandSearch(text: "breaks & electro & detroit") {
id
name
description
wallet
}
}

**Feature Management: **A partire dalla specVersion 0.0.4, fullTextSearch deve essere dichiarato nella sezione features del manifesto del subgraph.

Lingue supportate

Collegamento a questa sezione

La scelta di una lingua diversa avrà un effetto definitivo, anche se talvolta sottile, sull'API di ricerca fulltext. I campi coperti da una query fulltext vengono esaminati nel contesto della lingua scelta, quindi i lessemi prodotti dall'analisi e dalle query di ricerca variano da lingua a lingua. Ad esempio, quando si utilizza il dizionario turco supportato, "token" viene ridotto a "toke", mentre il dizionario inglese lo riduce a "token".

Dizionari linguistici supportati:

CodiceDizionario
sempliceGenerale
daDanese
nlOlandese
enInglese
fiFinlandese
frFrancese
deTedesco
huUngherese
itItaliano
noNorvegese
ptPortoghese
roRumeno
ruRusso
esSpagnolo
svSvedese
trTurco

Algoritmi di classificazione

Collegamento a questa sezione

Algoritmi supportati per ordinare i risultati:

AlgoritmoDescrizione
rankUtilizza la qualità della corrispondenza (0-1) della query fulltext per ordinare i risultati.
proximityRankSimile a rank, ma include anche la vicinanza degli incontri.

Scrivere le mappature

Collegamento a questa sezione

Le mappature prendono i dati da una particolare fonte e li trasformano in entità definite nello schema. Le mappature sono scritte in un sottoinsieme di TypeScript chiamato AssemblyScript che può essere compilato in WASM (WebAssembly). AssemblyScript è più rigoroso del normale TypeScript, ma offre una sintassi familiare.

Per ogni gestore di eventi definito in subgraph.yaml sotto mapping.eventHandlers, creare una funzione esportata con lo stesso nome. Ogni gestore deve accettare un singolo parametro, chiamato event, con un tipo corrispondente al nome dell'evento da gestire.

Nel subgraph di esempio, src/mapping.ts contiene gestori per gli eventi NewGravatar e UpdatedGravatar:

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)
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
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()
}

Il primo gestore prende un evento NewGravatar e crea una nuova entità Gravatar con new Gravatar(event.params.id.toHex()), popolando i campi dell'entità usando i parametri corrispondenti dell'evento. Questa istanza di entità è rappresentata dalla variabile gravatar, con un valore id di event.params.id.toHex().

Il secondo gestore cerca di caricare il Gravatar esistente dal negozio dei Graph Node.Se non esiste ancora, viene creato su richiesta. L'entità viene quindi aggiornata in base ai nuovi parametri dell'evento, prima di essere salvata nel negozio con gravatar.save().

ID consigliati per la creazione di nuove entità

Collegamento a questa sezione

It is highly recommended to use Bytes as the type for id fields, and only use String for attributes that truly contain human-readable text, like the name of a token. Below are some recommended id values to consider when creating new entities.

  • transfer.id = event.transaction.hash

  • let id = event.transaction.hash.concatI32(event.logIndex.toI32())

  • For entities that store aggregated data, for e.g, daily trade volumes, the id usually contains the day number. Here, using a Bytes as the id is beneficial. Determining the id would look like

let dayID = event.block.timestamp.toI32() / 86400
let id = Bytes.fromI32(dayID)
  • Convert constant addresses to Bytes.

const id = Bytes.fromHexString('0xdead...beef')

There is a Graph Typescript Library which contains utilities for interacting with the Graph Node store and conveniences for handling smart contract data and entities. It can be imported into mapping.ts from @graphprotocol/graph-ts.

Handling of entities with identical IDs

Collegamento a questa sezione

When creating and saving a new entity, if an entity with the same ID already exists, the properties of the new entity are always preferred during the merge process. This means that the existing entity will be updated with the values from the new entity.

If a null value is intentionally set for a field in the new entity with the same ID, the existing entity will be updated with the null value.

If no value is set for a field in the new entity with the same ID, the field will result in null as well.

Generazione del codice

Collegamento a questa sezione

Per rendere semplice e sicuro il lavoro con gli smart contract, gli eventi e le entità, la Graph CLI può generare tipi AssemblyScript dallo schema GraphQL del subgraph e dagli ABI dei contratti inclusi nelle data source.

Questo viene fatto con

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

ma nella maggior parte dei casi, i subgraph sono già preconfigurati tramite package.json per consentire di eseguire semplicemente uno dei seguenti comandi per ottenere lo stesso risultato:

# Yarn
yarn codegen
# NPM
npm run codegen

Questo genera una classe AssemblyScript per ogni smart contract nei file ABI menzionati in subgraph.yaml, consentendo di legare questi contratti a indirizzi specifici nelle mappature e di chiamare i metodi del contratto in sola lettura contro il blocco in elaborazione. Verrà inoltre generata una classe per ogni evento contrattuale, per fornire un facile accesso ai parametri dell'evento, nonché al blocco e alla transazione da cui l'evento ha avuto origine. Tutti questi tipi sono scritti in <OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts. Nel subgraph di esempio, questo sarebbe generated/Gravity/Gravity.ts, consentendo ai mapping di importare questi tipi con.

import {
// The contract class:
Gravity,
// The events classes:
NewGravatar,
UpdatedGravatar,
} from '../generated/Gravity/Gravity'

Inoltre, viene generata una classe per ogni tipo di entità nello schema GraphQL del subgraph. Queste classi forniscono il caricamento sicuro del tipo di entità, l'accesso in lettura e scrittura ai campi dell'entità e un metodo save() per scrivere le entità nella memoria. Tutte le classi di entità sono scritte in <OUTPUT_DIR>/schema.ts, consentendo alle mappature di importarle con

import { Gravatar } from '../generated/schema'

Nota: La generazione del codice deve essere eseguita nuovamente dopo ogni modifica allo schema GraphQL o alle ABI incluse nel manifesto. Inoltre, deve essere eseguita almeno una volta prima di costruire o distribuire il subgraph.

Code generation does not check your mapping code in src/mapping.ts. If you want to check that before trying to deploy your subgraph to Graph Explorer, you can run yarn build and fix any syntax errors that the TypeScript compiler might find.

Modelli di Data Source

Collegamento a questa sezione

Un modello comune negli smart contract compatibili con EVM è l'uso di contratti di registro o di fabbrica, in cui un contratto crea, gestisce o fa riferimento a un numero arbitrario di altri contratti che hanno ciascuno il proprio stato e i propri eventi.

Gli indirizzi di questi subcontratti possono o meno essere noti in anticipo e molti di questi contratti possono essere creati e/o aggiunti nel tempo. Per questo motivo, in questi casi, la definizione di una singola data source o di un numero fisso di data source è impossibile e occorre un approccio più dinamico: i modelli di data source.

Data Source per il contratto principale

Collegamento a questa sezione

Per prima cosa, si definisce un data source regolare per il contratto principale. Lo snippet seguente mostra un esempio semplificato di data source per il contratto Uniswap exchange factory. Si noti il gestore di eventi NewExchange(address,address). Questo viene emesso quando un nuovo contratto di scambio viene creato sulla chain dal contratto di fabbrica.

dataSources:
- kind: ethereum/contract
name: Factory
network: mainnet
source:
address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
abi: Factory
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/factory.ts
entities:
- Directory
abis:
- name: Factory
file: ./abis/factory.json
eventHandlers:
- event: NewExchange(address,address)
handler: handleNewExchange

Modelli di data source per contratti creati dinamicamente

Collegamento a questa sezione

Quindi, si aggiungono modelli di data source al manifesto. Questi sono identici alle normali data source, tranne per il fatto che non hanno un indirizzo di contratto predefinito sotto source. In genere, si definisce un modello per ogni tipo di subcontratto gestito o referenziato dal contratto principale.

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.6
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

Istanziare un modello di data source

Collegamento a questa sezione

Nella fase finale, si aggiorna la mappatura del contratto principale per creare un'istanza di origine dati dinamica da uno dei modelli. In questo esempio, si modificherà la mappatura del contratto principale per importare il modello Exchange e richiamare il metodo Exchange.create(address) per avviare l'indicizzazione del nuovo contratto exchange.

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)
}

Nota: Una nuova data source elaborerà solo le chiamate e gli eventi del blocco in cui è stata creata e di tutti i blocchi successivi, ma non elaborerà i dati storici, cioè quelli contenuti nei blocchi precedenti.

Se i blocchi precedenti contengono dati rilevanti per la nuova data source, è meglio indicizzare tali dati leggendo lo stato attuale del contratto e creando entità che rappresentino tale stato al momento della creazione della nuova data source.

Contesto del Data Source

Collegamento a questa sezione

I contesti delle data source consentono di passare una configurazione aggiuntiva quando si istanzia un modello. Nel nostro esempio, diciamo che gli scambi sono associati a una particolare coppia di trading, che è inclusa nell'evento NewExchange. Queste informazioni possono essere passate nell'origine dati istanziata, in questo modo:

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)
}

All'interno di una mappatura del modello Exchange, è possibile accedere al contesto:

import { dataSource } from '@graphprotocol/graph-ts'
let context = dataSource.context()
let tradingPair = context.getString('tradingPair')

Esistono setter e getter come setString e getString per tutti i tipi di valore.

Blocchi di partenza

Collegamento a questa sezione

L'opzione startBlock è un'impostazione opzionale che consente di definire da quale blocco della chain l'origine dati inizierà l'indicizzazione. L'impostazione del blocco iniziale consente al data source di saltare potenzialmente milioni di blocchi irrilevanti. In genere, lo sviluppatore di un subgraph imposta startBlock sul blocco in cui è stato creato lo smart contract del data source.

dataSources:
- kind: ethereum/contract
name: ExampleSource
network: mainnet
source:
address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
abi: ExampleContract
startBlock: 6627917
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/factory.ts
entities:
- User
abis:
- name: ExampleContract
file: ./abis/ExampleContract.json
eventHandlers:
- event: NewEvent(address,address)
handler: handleNewEvent

Nota: Il blocco di creazione del contratto può essere rapidamente consultato su Etherscan:

  1. Cercare il contratto inserendo l'indirizzo nella barra di ricerca.
  2. Fare clic sull'hash della transazione di creazione nella sezione Contract Creator.
  3. Caricare la pagina dei dettagli della transazione, dove si trova il blocco iniziale per quel contratto.

The indexerHints setting in a subgraph's manifest provides directives for indexers on processing and managing a subgraph. It influences operational decisions across data handling, indexing strategies, and optimizations. Presently, it features the prune option for managing historical data retention or pruning.

This feature is available from specVersion: 1.0.0

indexerHints.prune: Defines the retention of historical block data for a subgraph. Options include:

  1. "never": No pruning of historical data; retains the entire history.
  2. "auto": Retains the minimum necessary history as set by the indexer, optimizing query performance.
  3. A specific number: Sets a custom limit on the number of historical blocks to retain.
indexerHints:
prune: auto

The term "history" in this context of subgraphs is about storing data that reflects the old states of mutable entities.

History as of a given block is required for:

  • Time travel queries, which enable querying the past states of these entities at specific blocks throughout the subgraph's history
  • Using the subgraph as a graft base in another subgraph, at that block
  • Rewinding the subgraph back to that block

If historical data as of the block has been pruned, the above capabilities will not be available.

Using "auto" is generally recommended as it maximizes query performance and is sufficient for most users who do not require access to extensive historical data.

For subgraphs leveraging time travel queries, it's advisable to either set a specific number of blocks for historical data retention or use prune: never to keep all historical entity states. Below are examples of how to configure both options in your subgraph's settings:

To retain a specific amount of historical data:

indexerHints:
prune: 1000 # Replace 1000 with the desired number of blocks to retain

To preserve the complete history of entity states:

indexerHints:
prune: never

You can check the earliest block (with historical state) for a given subgraph by querying the Indexing Status API:

{
indexingStatuses(subgraphs: ["Qm..."]) {
subgraph
synced
health
chains {
earliestBlock {
number
}
latestBlock {
number
}
chainHeadBlock { number }
}
}
}

Note that the earliestBlock is the earliest block with historical data, which will be more recent than the startBlock specified in the manifest, if the subgraph has been pruned.

Event handlers in a subgraph react to specific events emitted by smart contracts on the blockchain and trigger handlers defined in the subgraph's manifest. This enables subgraphs to process and store event data according to defined logic.

Defining an Event Handler

Collegamento a questa sezione

An event handler is declared within a data source in the subgraph's YAML configuration. It specifies which events to listen for and the corresponding function to execute when those events are detected.

dataSources:
- kind: ethereum/contract
name: Gravity
network: dev
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
eventHandlers:
- event: Approval(address,address,uint256)
handler: handleApproval
- event: Transfer(address,address,uint256)
handler: handleTransfer
topic1: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0xc8dA6BF26964aF9D7eEd9e03E53415D37aA96325'] # Optional topic filter which filters only events with the specified topic.

Gestori di chiamate

Collegamento a questa sezione

Sebbene gli eventi rappresentino un modo efficace per raccogliere le modifiche rilevanti allo stato di un contratto, molti contratti evitano di generare log per ottimizzare i costi del gas. In questi casi, un subgraph può sottoscrivere le chiamate fatte al contratto dell'origine dati. Ciò si ottiene definendo gestori di chiamate che fanno riferimento alla firma della funzione e al gestore di mappatura che elaborerà le chiamate a questa funzione. Per elaborare queste chiamate, il gestore della mappatura riceverà un ethereum.Call come argomento con gli input e gli output digitati della chiamata. Le chiamate effettuate a qualsiasi profondità nella chain di chiamate di una transazione attiveranno la mappatura, consentendo di catturare l'attività con il contratto della data source attraverso i contratti proxy.

I gestori di chiamate si attivano solo in uno dei due casi: quando la funzione specificata viene chiamata da un conto diverso dal contratto stesso o quando è contrassegnata come esterna in Solidity e chiamata come parte di un'altra funzione nello stesso contratto.

Nota: I gestori delle chiamate dipendono attualmente dall'API di tracciamento Parity. Alcune reti, come la chain BNB e Arbitrum, non supportano questa API. Se un subgraph che indicizza una di queste reti contiene uno o più gestori di chiamate, non inizierà la sincronizzazione. Gli sviluppatori di subgraph dovrebbero invece utilizzare i gestori di eventi. Questi sono molto più performanti dei gestori di chiamate e sono supportati da tutte le reti evm.

Definire un gestore di chiamate

Collegamento a questa sezione

Per definire un gestore di chiamate nel manifesto, basta aggiungere un array callHandlers sotto la data source che si desidera sottoscrivere.

dataSources:
- kind: ethereum/contract
name: Factory
network: mainnet
source:
address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
abi: Factory
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/factory.ts
entities:
- Directory
abis:
- name: Factory
file: ./abis/factory.json
eventHandlers:
- event: NewExchange(address,address)
handler: handleNewExchange

La function è la firma della funzione normalizzata per filtrare le chiamate. La proprietà handler è il nome della funzione della mappatura che si desidera eseguire quando la funzione di destinazione viene chiamata nel contratto del data source.

Funzione di mappatura

Collegamento a questa sezione

Ogni gestore di chiamate accetta un singolo parametro di tipo corrispondente al nome della funzione chiamata. Nel subgraph di esempio sopra, la mappatura contiene un gestore per quando la funzione createGravatar viene chiamata e riceve un parametro CreateGravatarCall come argomento:

import { CreateGravatarCall } from '../generated/Gravity/Gravity'
import { Transaction } from '../generated/schema'
export function handleCreateGravatar(call: CreateGravatarCall): void {
let id = call.transaction.hash
let transaction = new Transaction(id)
transaction.displayName = call.inputs._displayName
transaction.imageUrl = call.inputs._imageUrl
transaction.save()
}

La funzione handleCreateGravatar prende una nuova CreateGravatarCall che è una subclasse di ethereum.Call, fornita da @graphprotocol/graph-ts, che include gli input e gli output tipizzati della chiamata. Il tipo CreateGravatarCall viene generato quando si esegue graph codegen.

Gestori di blocchi

Collegamento a questa sezione

Oltre a sottoscrivere eventi di contratto o chiamate di funzione, un subgraph può voler aggiornare i propri dati quando nuovi blocchi vengono aggiunti alla chain. A tale scopo, un subgraph può eseguire una funzione dopo ogni blocco o dopo i blocchi che corrispondono a un filtro predefinito.

Filtri supportati

Collegamento a questa sezione

Filtro di chiamata

Collegamento a questa sezione
filter:
kind: call

Il gestore definito sarà richiamato una volta per ogni blocco che contiene una chiamata al contratto (data source) sotto il quale il gestore è definito.

Nota: Il filtro call dipende attualmente dall'API di tracciamento Parity. Alcune reti, come la chain BNB e Arbitrum, non supportano questa API. Se un subgraph che indicizza una di queste reti contiene uno o più gestori di blocchi con un filtro call, non inizierà la sincronizzazione.

L'assenza di un filtro per un gestore di blocchi garantisce che il gestore venga chiamato a ogni blocco. Una data source può contenere un solo gestore di blocchi per ogni tipo di filtro.

dataSources:
- kind: ethereum/contract
name: Gravity
network: dev
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
blockHandlers:
- handler: handleBlock
- handler: handleBlockWithCallToContract
filter:
kind: call

Filtro di polling

Collegamento a questa sezione

Richiede specVersion >= 0.0.8

Nota: I filtri di polling sono disponibili solo su dataSources di kind: ethereum.

blockHandlers:
- handler: handleBlock
filter:
kind: polling
every: 10

Il gestore definito sarà chiamato una volta ogni n blocchi, dove n è il valore fornito nel campo every. Questa configurazione consente al subgraph di eseguire operazioni specifiche a intervalli regolari di blocco.

Richiede specVersion >= 0.0.8

Nota: I filtri once sono disponibili solo su dataSources di kind: ethereum.

blockHandlers:
- handler: handleOnce
filter:
kind: once

Il gestore definito con il filtro once sarà chiamato una sola volta prima dell'esecuzione di tutti gli altri gestori. Questa configurazione consente al subgraph di utilizzare il gestore come gestore di inizializzazione, eseguendo compiti specifici all'inizio dell'indicizzazione.

export function handleOnce(block: ethereum.Block): void {
let data = new InitialData(Bytes.fromUTF8('initial'))
data.data = 'Setup data here'
data.save()
}

Funzione di mappatura

Collegamento a questa sezione

La funzione di mappatura riceverà un Blocco Etereo come unico argomento. Come le funzioni di mappatura per gli eventi, questa funzione può accedere alle entità del subgraph esistenti nell'archivio, chiamare smart contract e creare o aggiornare entità.

import { ethereum } from '@graphprotocol/graph-ts'
export function handleBlock(block: ethereum.Block): void {
let id = block.hash
let entity = new Block(id)
entity.save()
}

Se è necessario elaborare eventi anonimi in Solidity, è possibile farlo fornendo l'argomento 0 dell'evento, come nell'esempio:

eventHandlers:
- event: LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)
topic0: '0x644843f351d3fba4abcd60109eaff9f54bac8fb8ccf0bab941009c21df21cf31'
handler: handleGive

Un evento viene attivato solo se la firma e l'argomento 0 corrispondono. Per impostazione predefinita, topic0 è uguale all'hash della firma dell'evento.

Ricevute di transazione nei gestori di eventi

Collegamento a questa sezione

A partire da specVersion 0.0.5 e apiVersion 0.0.7, i gestori di eventi possono avere accesso alla ricevuta della transazione che li ha emessi.

Per fare ciò, i gestori di eventi devono essere dichiarati nel manifesto del subgraph con la nuova chiave receipt: true, che è opzionale e ha come valore predefinito false.

eventHandlers:
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
receipt: true

All'interno della funzione handler, è possibile accedere alla ricevuta nel campo Event.receipt. Se la chiave receipt è impostata su false oppure omessa nel manifesto, verrà restituito un valore null.

Caratteristiche sperimentali

Collegamento a questa sezione

A partire da specVersion 0.0.4, le caratteristiche del subgraph devono essere dichiarate esplicitamente nella sezione features al livello superiore del file del manifesto, utilizzando il loro nome camelCase, come elencato nella tabella seguente:

CaratteristicaNome
Errori non fatalinonFatalErrors
Ricerca full-textfullTextSearch
Graftinggrafting

Ad esempio, se un subgraph utilizza le funzionalità Full-Text Search e Non-fatal Errors, il campo features del manifesto dovrebbe essere:

specVersion: 0.0.4
description: Gravatar for Ethereum
features:
- fullTextSearch
- nonFatalErrors
dataSources: ...

Si noti che l'uso di una caratteristica senza dichiararla incorrerà in un errore di validazione durante la distribuzione del subgraph, mentre non si verificherà alcun errore se una caratteristica viene dichiarata ma non utilizzata.

Timeseries and Aggregations

Collegamento a questa sezione

Timeseries and aggregations enable your subgraph to track statistics like daily average price, hourly total transfers, etc.

This feature introduces two new types of subgraph entity. Timeseries entities record data points with timestamps. Aggregation entities perform pre-declared calculations on the Timeseries data points on an hourly or daily basis, then store the results for easy access via GraphQL.

type Data @entity(timeseries: true) {
id: Int8!
timestamp: Timestamp!
price: BigDecimal!
}
type Stats @aggregation(intervals: ["hour", "day"], source: "Data") {
id: Int8!
timestamp: Timestamp!
sum: BigDecimal! @aggregate(fn: "sum", arg: "price")
}

Defining Timeseries and Aggregations

Collegamento a questa sezione

Timeseries entities are defined with @entity(timeseries: true) in schema.graphql. Every timeseries entity must have a unique ID of the int8 type, a timestamp of the Timestamp type, and include data that will be used for calculation by aggregation entities. These Timeseries entities can be saved in regular trigger handlers, and act as the “raw data” for the Aggregation entities.

Aggregation entities are defined with @aggregation in schema.graphql. Every aggregation entity defines the source from which it will gather data (which must be a Timeseries entity), sets the intervals (e.g., hour, day), and specifies the aggregation function it will use (e.g., sum, count, min, max, first, last). Aggregation entities are automatically calculated on the basis of the specified source at the end of the required interval.

Available Aggregation Intervals

Collegamento a questa sezione
  • hour: sets the timeseries period every hour, on the hour.
  • day: sets the timeseries period every day, starting and ending at 00:00.

Available Aggregation Functions

Collegamento a questa sezione
  • sum: Total of all values.
  • count: Number of values.
  • min: Minimum value.
  • max: Maximum value.
  • first: First value in the period.
  • last: Last value in the period.

Example Aggregations Query

Collegamento a questa sezione
{
stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {
id
timestamp
sum
}
}

Note:

To use Timeseries and Aggregations, a subgraph must have a spec version ≥1.1.0. Note that this feature might undergo significant changes that could affect backward compatibility.

Read more about Timeseries and Aggregations.

Errori non fatali

Collegamento a questa sezione

Gli errori di indicizzazione su subgraph già sincronizzati causano, per impostazione predefinita, il fallimento del subgraph e l'interruzione della sincronizzazione. In alternativa, i subgraph possono essere configurati per continuare la sincronizzazione in presenza di errori, ignorando le modifiche apportate dal gestore che ha provocato l'errore. In questo modo gli autori dei subgraph hanno il tempo di correggere i loro subgraph mentre le query continuano a essere servite rispetto al blocco più recente, anche se i risultati potrebbero essere incoerenti a causa del bug che ha causato l'errore. Si noti che alcuni errori sono sempre fatali. Per essere non fatale, l'errore deve essere noto come deterministico.

Nota: Il Graph Network non supporta ancora gli errori non fatali e gli sviluppatori non dovrebbero distribuire i subgraph che utilizzano questa funzionalità alla rete tramite Studio.

Per abilitare gli errori non fatali è necessario impostare il seguente flag di caratteristica nel manifesto del subgraph:

specVersion: 0.0.4
description: Gravatar for Ethereum
features:
- nonFatalErrors
...

La query deve anche scegliere di effettuare query dei dati con potenziali incongruenze attraverso l'argomento subgraphError. Si raccomanda anche di effettuare query di _meta per verificare se il subgraph ha saltato gli errori, come nell'esempio:

foos(first: 100, subgraphError: allow) {
id
}
_meta {
hasIndexingErrors
}

Se il subgrapg incontra un errore, la query restituirà sia i dati sia un errore graphql con il messaggio "indexing_error", come in questo esempio di risposta:

"data": {
"foos": [
{
"id": "0xdead"
}
],
"_meta": {
"hasIndexingErrors": true
}
},
"errors": [
{
"message": "indexing_error"
}
]

Grafting su subgraph esistenti

Collegamento a questa sezione

Nota: non è consigliabile utilizzare il grafting quando si effettua l'aggiornamento iniziale a The Graph Network. Per saperne di più, leggi qui.

Quando un subgraph viene distribuito per la prima volta, inizia l'indicizzazione degli eventi al blocco genesi della chain corrispondente (o al blocco startBlock definito con ciascuna data source). In alcune circostanze, è vantaggioso riutilizzare i dati di un subgraph esistente e iniziare l'indicizzazione in un blocco successivo. Questa modalità di indicizzazione è chiamata Grafting. Il grafting è utile, ad esempio, durante lo sviluppo per superare rapidamente semplici errori nelle mappature o per far funzionare di nuovo temporaneamente un subgraph esistente dopo che è fallito.

Un subgraph viene innestato su un subgraph di base quando il manifesto del subgraph in subgraph.yaml contiene un blocco graft al livello superiore:

description: ...
graft:
base: Qm... # Subgraph ID of base subgraph
block: 7345624 # Block number

Quando viene distribuito un subgraph il cui manifesto contiene un blocco di graft, Graph Node copierà i dati del subgraph di base fino al blocco indicato e compreso e continuerà a indicizzare il nuovo subgraph da quel blocco in poi. Il subgraph di base deve esistere sull'istanza del Graph Node di destinazione e deve essere indicizzato almeno fino al blocco dato. A causa di questa restrizione, il grafring dovrebbe essere usato solo durante lo sviluppo o in caso di emergenza per accelerare la produzione di un subgraph equivalente non grafted.

Poiché l'innesto copia piuttosto che indicizzare i dati di base, è molto più veloce portare il subgraph al blocco desiderato rispetto all'indicizzazione da zero, anche se la copia iniziale dei dati può richiedere diverse ore per subgraph molto grandi. Mentre il subgraph innestato viene inizializzato, il Graph Node registra le informazioni sui tipi di entità già copiati.

Il grafted subgraph può utilizzare uno schema GraphQL non identico a quello del subgraph di base, ma semplicemente compatibile con esso. Deve essere uno schema di subgraph valido di per sé, ma può discostarsi dallo schema del subgraph di base nei seguenti modi:

  • Aggiunge o rimuove i tipi di entità
  • Rimuove gli attributi dai tipi di entità
  • Aggiunge attributi annullabili ai tipi di entità
  • Trasforma gli attributi non nulli in attributi nulli
  • Aggiunge valori agli enum
  • Aggiunge o rimuove le interfacce
  • Cambia per quali tipi di entità viene implementata un'interfaccia

**Gestione delle caratteristiche: **grafting deve essere dichiarato tra le caratteristiche nel manifesto del subgraph.

IPFS/Arweave File Data Sources

Collegamento a questa sezione

I data source file sono una nuova funzionalità del subgraph per accedere ai dati fuori chain durante l'indicizzazione in modo robusto ed estendibile. I data source file supportano il recupero di file da IPFS e da Arweave.

Questo pone anche le basi per l'indicizzazione deterministica dei dati fuori chain e per la potenziale introduzione di dati arbitrari provenienti da HTTP.

Rather than fetching files "in line" during handler execution, this introduces templates which can be spawned as new data sources for a given file identifier. These new data sources fetch the files, retrying if they are unsuccessful, running a dedicated handler when the file is found.

This is similar to the existing data source templates, which are used to dynamically create new chain-based data sources.

Questo sostituisce l'API esistente ipfs.cat

Guida all'aggiornamento

Collegamento a questa sezione

Aggiornare graph-ts e graph-cli

Collegamento a questa sezione

Le data source file richiedono l'uso di graph-ts >=0.29.0 and graph-cli >=0.33.1

Aggiungere un nuovo tipo di entità che verrà aggiornato quando verranno trovati dei file

Collegamento a questa sezione

I data sources file non possono accedere o aggiornare le entità basate sulla chain, ma devono aggiornare le entità specifiche del file.

Ciò può significare dividere i campi delle entità esistenti in entità separate, collegate tra loro.

Entità combinata originale:

type Token @entity {
id: ID!
tokenID: BigInt!
tokenURI: String!
externalURL: String!
ipfsURI: String!
image: String!
name: String!
description: String!
type: String!
updatedAtTimestamp: BigInt
owner: User!
}

Entità nuova, divisa:

type Token @entity {
id: ID!
tokenID: BigInt!
tokenURI: String!
ipfsURI: TokenMetadata
updatedAtTimestamp: BigInt
owner: String!
}
type TokenMetadata @entity {
id: ID!
image: String!
externalURL: String!
name: String!
description: String!
}

Se la relazione è 1:1 tra l'entità genitore e l'entità data source file risultante, il modello più semplice è quello di collegare l'entità genitore a un'entità file risultante utilizzando il CID IPFS come lookup. Contattateci su Discord se avete difficoltà a modellare le vostre nuove entità basate su file!

You can use nested filters to filter parent entities on the basis of these nested entities.

Aggiungere una nuova data source templata con kind: file/ipfs oppure kind: file/arweave

Collegamento a questa sezione

È l'origine dati che verrà generata quando viene identificato un file di interesse.

templates:
- name: TokenMetadata
kind: file/ipfs
mapping:
apiVersion: 0.0.7
language: wasm/assemblyscript
file: ./src/mapping.ts
handler: handleMetadata
entities:
- TokenMetadata
abis:
- name: Token
file: ./abis/Token.json

Attualmente sono richiesti abis, anche se non è possibile richiamare i contratti dall'interno del data source file

The file data source must specifically mention all the entity types which it will interact with under entities. See limitations for more details.

Creare un nuovo gestore per elaborare i file

Collegamento a questa sezione

This handler should accept one Bytes parameter, which will be the contents of the file, when it is found, which can then be processed. This will often be a JSON file, which can be processed with graph-ts helpers (documentation).

Il CID del file, come stringa leggibile, è accessibile tramite dataSource come segue:

const cid = dataSource.stringParam()

Esempio di gestore:

import { json, Bytes, dataSource } from '@graphprotocol/graph-ts'
import { TokenMetadata } from '../generated/schema'
export function handleMetadata(content: Bytes): void {
let tokenMetadata = new TokenMetadata(dataSource.stringParam())
const value = json.fromBytes(content).toObject()
if (value) {
const image = value.get('image')
const name = value.get('name')
const description = value.get('description')
const externalURL = value.get('external_url')
if (name && image && description && externalURL) {
tokenMetadata.name = name.toString()
tokenMetadata.image = image.toString()
tokenMetadata.externalURL = externalURL.toString()
tokenMetadata.description = description.toString()
}
tokenMetadata.save()
}
}

Creare i data source file quando necessario

Collegamento a questa sezione

È ora possibile creare i data sources file durante l'esecuzione di gestori a chain:

  • Importare il modello dal templates generato automaticamente
  • chiamare TemplateName.create(cid: string) da una mappatura, dove il cid è un identificatore di contenuto valido per IPFS o Arweave

Per IPFS, Graph Node supporta identificatori di contenuto v0 e v1 e identificatori di contenuto con directory (ad esempio bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json).

For Arweave, as of version 0.33.0 Graph Node can fetch files stored on Arweave based on their transaction ID from an Arweave gateway (example file). Arweave supports transactions uploaded via Irys (previously Bundlr), and Graph Node can also fetch files based on Irys manifests.

Esempio:

import { TokenMetadata as TokenMetadataTemplate } from '../generated/templates'
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
//This example code is for a Crypto coven subgraph. The above ipfs hash is a directory with token metadata for all crypto coven NFTs.
export function handleTransfer(event: TransferEvent): void {
let token = Token.load(event.params.tokenId.toString())
if (!token) {
token = new Token(event.params.tokenId.toString())
token.tokenID = event.params.tokenId
token.tokenURI = '/' + event.params.tokenId.toString() + '.json'
const tokenIpfsHash = ipfshash + token.tokenURI
//This creates a path to the metadata for a single Crypto coven NFT. It concats the directory with "/" + filename + ".json"
token.ipfsURI = tokenIpfsHash
TokenMetadataTemplate.create(tokenIpfsHash)
}
token.updatedAtTimestamp = event.block.timestamp
token.owner = event.params.to.toHexString()
token.save()
}

Questo creerà una nuova data source file, che interrogherà l'endpoint IPFS o Arweave configurato del Graph Node, ritentando se non viene trovato. Quando il file viene trovato, viene eseguito il gestore del data source file.

Questo esempio utilizza il CID come ricerca tra l'entità genitore Token e l'entità risultante TokenMetadata.

In precedenza, questo è il punto in cui uno sviluppatore di subgraph avrebbe chiamato ipfs.cat(CID) per recuperare il file

Congratulazioni, state usando i data source file!

Distribuire i subgraph

Collegamento a questa sezione

È ora possibile build e deploy il proprio subgraph su qualsiasi Graph Node >=v0.30.0-rc.0.

I gestori e le entità di data source file sono isolati dalle altre entità del subgraph, assicurando che siano deterministici quando vengono eseguiti e garantendo che non ci sia contaminazione di data source basate sulla chain. Per essere precisi:

  • Le entità create di Data Source file sono immutabili e non possono essere aggiornate
  • I gestori di Data Source file non possono accedere alle entità di altre data source file
  • Le entità associate al Data Source file non sono accessibili ai gestori alla chain

Sebbene questo vincolo non dovrebbe essere problematico per la maggior parte dei casi d'uso, potrebbe introdurre complessità per alcuni. Contattate via Discord se avete problemi a modellare i vostri dati basati su file in un subgraph!

Inoltre, non è possibile creare data source da una data source file, sia essa una data source onchain o un'altra data source file. Questa restrizione potrebbe essere eliminata in futuro.

Migliori pratiche

Collegamento a questa sezione

Se si collegano i metadati NFT ai token corrispondenti, utilizzare l'hash IPFS dei metadati per fare riferimento a un'entità Metadata dall'entità Token. Salvare l'entità Metadata usando l'hash IPFS come ID.

You can use DataSource context when creating File Data Sources to pass extra information which will be available to the File Data Source handler.

Se si dispone di entità che vengono aggiornate più volte, creare entità univoche basate su file utilizzando l'hash IPFS & l'ID dell'entità e fare riferimento a queste entità utilizzando un campo derivato nell'entità basata sulla chain.

Stiamo lavorando per migliorare la raccomandazione di cui sopra, in modo che le query restituiscano solo la versione "più recente"

Problemi conosciuti

Collegamento a questa sezione

I data source dei file attualmente richiedono le ABI, anche se le ABI non sono utilizzate (problema). La soluzione è aggiungere qualsiasi ABI.

Handlers for File Data Sources cannot be in files which import eth_call contract bindings, failing with "unknown import: ethereum::ethereum.call has not been defined" (issue). Workaround is to create file data source handlers in a dedicated file.

Migrazione del Subgraph di Crypto Coven

Data Sources del file GIP

Modifica pagina

Precedente
Supported Networks
Successivo
API AssemblyScript
Modifica pagina