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.
La definizione del subgraph consiste in alcuni file:
-
subgraph.yaml
: un file YAML contenente il manifesto del subgraph -
schema.graphql
: uno schema GraphQL che definisce quali dati sono memorizzati per il subgraph e come interrogarli via GraphQL -
AssemblyScript Mappings
: codice che traduce i dati dell'evento nelle entità definite nello schema (ad esempiomapping.ts
in questo tutorial)
In order to use your subgraph on The Graph's decentralized network, you will need to . It is recommended that you to your subgraph with at least .
Before you go into detail about the contents of the manifest file, you need to install the which you will need to build and deploy a subgraph.
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.
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.
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 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.
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 nuovodataSource
dovrebbe utilizzare glieventHandler
&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
.
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 .
Per il subgraph di esempio, subgraph.yaml
è:
specVersion: 0.0.4description: Gravatar for Ethereumrepository: https://github.com/graphprotocol/graph-toolingschema:file: ./schema.graphqlindexerHints:prune: autodataSources:- kind: ethereum/contractname: Gravitynetwork: mainnetsource:address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'abi: GravitystartBlock: 6175244endBlock: 7175245context:foo:type: Booldata: truebar:type: Stringdata: 'bar'mapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatarabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: NewGravatar(uint256,address,string,string)handler: handleNewGravatar- event: UpdatedGravatar(uint256,address,string,string)handler: handleUpdatedGravatarcallHandlers:- function: createGravatar(string,string)handler: handleCreateGravatarblockHandlers:- handler: handleBlock- handler: handleBlockWithCallfilter:kind: callfile: ./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 is1.2.0
. See 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. -
indexerHints.prune
: Defines the retention of historical block data for a subgraph. See in 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 comeBool
,String
,Int
,Int8
,BigDecimal
,Bytes
,List
, eBigInt
. Ogni variabile deve specificare il suotipo
edati
. 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 campofilter
conkind: 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.
I trigger per una data source all'interno di un blocco sono ordinati secondo il seguente processo:
- I trigger di eventi e chiamate sono ordinati prima per indice di transazione all'interno del blocco.
- 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.
- 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 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.
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.
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 totopic2
, and so on, up totopic3
, since the Ethereum Virtual Machine (EVM) allows up to three indexed arguments per event.
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Token {// Event declaration with indexed parameters for addressesevent Transfer(address indexed from, address indexed to, uint256 value);// Function to simulate transferring tokensfunction transfer(address to, uint256 value) public {// Emitting the Transfer event with from, to, and valueemit Transfer(msg.sender, to, value);}}
In this example:
- The
Transfer
event is used to log transactions of tokens between addresses. - The
from
andto
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.
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: handleSomeEventtopic1: ['0xValue1', '0xValue2']topic2: ['0xAddress1', '0xAddress2']topic3: ['0xValue3']
In this setup:
topic1
corresponds to the first indexed argument of the event,topic2
to the second, andtopic3
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.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleDirectedTransfertopic1: ['0xAddressA'] # Sender Addresstopic2: ['0xAddressB'] # Receiver Address
In this configuration:
topic1
is configured to filterTransfer
events where0xAddressA
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
is the receiver.- The subgraph will only index transactions that occur directly from
0xAddressA
to0xAddressB
.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleTransferToOrFromtopic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Addresstopic2: ['0xAddressB', '0xAddressC'] # Receiver Address
In this configuration:
topic1
is configured to filterTransfer
events where0xAddressA
,0xAddressB
,0xAddressC
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
and0xAddressC
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.
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).
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:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- Call 3 (Token Holdings): Takes 4 seconds
Total time taken = 3 + 2 + 4 = 9 seconds
With this feature, you can declare these calls to be executed in parallel:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- 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
- Declarative Definition: In the subgraph manifest, you declare the Ethereum calls in a way that indicates they can be executed in parallel.
- Parallel Execution Engine: The Graph Node's execution engine recognizes these declarations and runs the calls simultaneously.
- Result Aggregation: Once all calls are complete, the results are aggregated and used by the subgraph for further processing.
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: handleSwapcalls:global0X128: Pool[event.address].feeGrowthGlobal0X128()global1X128: Pool[event.address].feeGrowthGlobal1X128()
Details for the example above:
global0X128
is the declaredeth_call
.- The text before colon(
global0X128
) is the label for thiseth_call
which is used when logging errors. - The text (
Pool[event.address].feeGrowthGlobal0X128()
) is the actualeth_call
that will be executed, which is in the form ofContract[address].function(arguments)
- The
address
andarguments
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()
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 o usando solc per compilare.
- È possibile trovare l'ABI anche su , 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 .
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: BytesdisplayName: StringimageUrl: Stringaccepted: 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: BytesdisplayName: StringimageUrl: String}type GravatarDeclined @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}
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
.
Nella nostra API GraphQL supportiamo i seguenti scalari:
Tipo | Descrizione |
---|---|
Bytes | Byte array, rappresentato come una stringa esadecimale. Comunemente utilizzato per gli hash e gli indirizzi di Ethereum. |
String | Scalare per valori string . I caratteri nulli non sono supportati e vengono rimossi automaticamente. |
Boolean | Scalare per valori boolean . |
Int | The GraphQL spec defines Int to be a signed 32-bit integer. |
Int8 | Un 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. |
BigInt | Numeri 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 . |
BigDecimal | BigDecimal 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. |
Timestamp | It 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 {OriginalOwnerSecondOwnerThirdOwner}
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 .
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à.
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}
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!}
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 entityorganization {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.
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 entityid: Bytes!address: Bytes!}
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: enalgorithm: rankinclude: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }])type Band @entity {id: Bytes!name: String!description: String!bio: Stringwallet: Addresslabels: [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 per una descrizione dell'API di ricerca fulltext e per altri esempi di utilizzo.
query {bandSearch(text: "breaks & electro & detroit") {idnamedescriptionwallet}}
**: **A partire dalla specVersion
0.0.4
, fullTextSearch
deve essere dichiarato nella sezione features
del manifesto del subgraph.
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:
Codice | Dizionario |
---|---|
semplice | Generale |
da | Danese |
nl | Olandese |
en | Inglese |
fi | Finlandese |
fr | Francese |
de | Tedesco |
hu | Ungherese |
it | Italiano |
no | Norvegese |
pt | Portoghese |
ro | Rumeno |
ru | Russo |
es | Spagnolo |
sv | Svedese |
tr | Turco |
Algoritmi supportati per ordinare i risultati:
Algoritmo | Descrizione |
---|---|
rank | Utilizza la qualità della corrispondenza (0-1) della query fulltext per ordinare i risultati. |
proximityRank | Simile a rank, ma include anche la vicinanza degli incontri. |
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 chiamato che può essere compilato in WASM (). 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.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.save()}export function handleUpdatedGravatar(event: UpdatedGravatar): void {let id = event.params.idlet gravatar = Gravatar.load(id)if (gravatar == null) {gravatar = new Gravatar(id)}gravatar.owner = event.params.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.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()
.
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 aBytes
as theid
is beneficial. Determining theid
would look like
let dayID = event.block.timestamp.toI32() / 86400let id = Bytes.fromI32(dayID)
- Convert constant addresses to
Bytes
.
const id = Bytes.fromHexString('0xdead...beef')
There is a 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
.
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.
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:
# Yarnyarn codegen# NPMnpm 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.
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.
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 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/contractname: Factorynetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: Factorymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Directoryabis:- name: Factoryfile: ./abis/factory.jsoneventHandlers:- event: NewExchange(address,address)handler: handleNewExchange
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/contractname: Factory# ... other source fields for the main contract ...templates:- name: Exchangekind: ethereum/contractnetwork: mainnetsource:abi: Exchangemapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/exchange.tsentities:- Exchangeabis:- name: Exchangefile: ./abis/exchange.jsoneventHandlers:- 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
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 contractExchange.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.
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.
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/contractname: ExampleSourcenetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: ExampleContractstartBlock: 6627917mapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Userabis:- name: ExampleContractfile: ./abis/ExampleContract.jsoneventHandlers:- event: NewEvent(address,address)handler: handleNewEvent
Nota: Il blocco di creazione del contratto può essere rapidamente consultato su Etherscan:
- Cercare il contratto inserendo l'indirizzo nella barra di ricerca.
- Fare clic sull'hash della transazione di creazione nella sezione
Contract Creator
. - 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:
"never"
: No pruning of historical data; retains the entire history."auto"
: Retains the minimum necessary history as set by the indexer, optimizing query performance.- 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:
- , which enable querying the past states of these entities at specific blocks throughout the subgraph's history
- Using the subgraph as a 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 , 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 :
{indexingStatuses(subgraphs: ["Qm..."]) {subgraphsyncedhealthchains {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.
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/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: Approval(address,address,uint256)handler: handleApproval- event: Transfer(address,address,uint256)handler: handleTransfertopic1: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0xc8dA6BF26964aF9D7eEd9e03E53415D37aA96325'] # Optional topic filter which filters only events with the specified topic.
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.
Per definire un gestore di chiamate nel manifesto, basta aggiungere un array callHandlers
sotto la data source che si desidera sottoscrivere.
dataSources:- kind: ethereum/contractname: Factorynetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: Factorymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Directoryabis:- name: Factoryfile: ./abis/factory.jsoneventHandlers:- 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.
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.hashlet transaction = new Transaction(id)transaction.displayName = call.inputs._displayNametransaction.imageUrl = call.inputs._imageUrltransaction.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
.
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.
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/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsonblockHandlers:- handler: handleBlock- handler: handleBlockWithCallToContractfilter:kind: call
Richiede specVersion
>= 0.0.8
Nota: I filtri di polling sono disponibili solo su dataSources di kind: ethereum
.
blockHandlers:- handler: handleBlockfilter:kind: pollingevery: 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: handleOncefilter: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()}
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.hashlet 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.
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: handleNewGravatarreceipt: 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
.
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:
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.4description: Gravatar for Ethereumfeatures:- fullTextSearch- nonFatalErrorsdataSources: ...
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 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")}
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.
hour
: sets the timeseries period every hour, on the hour.day
: sets the timeseries period every day, starting and ending at 00:00.
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.
{stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {idtimestampsum}}
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.
about Timeseries and Aggregations.
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.4description: Gravatar for Ethereumfeatures:- 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"}]
Nota: non è consigliabile utilizzare il grafting quando si effettua l'aggiornamento iniziale a The Graph Network. Per saperne di più, leggi .
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 subgraphblock: 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
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 , which are used to dynamically create new chain-based data sources.
Questo sostituisce l'API esistente ipfs.cat
Le data source file richiedono l'uso di graph-ts >=0.29.0 and graph-cli >=0.33.1
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: BigIntowner: User!}
Entità nuova, divisa:
type Token @entity {id: ID!tokenID: BigInt!tokenURI: String!ipfsURI: TokenMetadataupdatedAtTimestamp: BigIntowner: 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!
È l'origine dati che verrà generata quando viene identificato un file di interesse.
templates:- name: TokenMetadatakind: file/ipfsmapping:apiVersion: 0.0.7language: wasm/assemblyscriptfile: ./src/mapping.tshandler: handleMetadataentities:- TokenMetadataabis:- name: Tokenfile: ./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 for more details.
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 ().
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()}}
È 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 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 from an Arweave gateway (). Arweave supports transactions uploaded via Irys (previously Bundlr), and Graph Node can also fetch files based on .
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.tokenIdtoken.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 = tokenIpfsHashTokenMetadataTemplate.create(tokenIpfsHash)}token.updatedAtTimestamp = event.block.timestamptoken.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!
È 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.
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 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"
I data source dei file attualmente richiedono le ABI, anche se le ABI non sono utilizzate (). 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" (). Workaround is to create file data source handlers in a dedicated file.