Přeskočit na hlavní obsah

The Graph: Oprava dotazování dat ve Web3

solidity
smart kontrakt účty
dotazování
the graph
react
Středně pokročilý
Markus Waas
6. září 2020
7 minuta čtení

Tentokrát se podíváme blíže na The Graph, který se v podstatě stal součástí standardní sady nástrojů pro vývoj dapps v posledním roce. Nejprve se podívejme, jak bychom to dělali tradičním způsobem...

Bez The Graph...

Pro názornost si tedy uveďme jednoduchý příklad. Všichni máme rádi hry, takže si představte jednoduchou hru, ve které uživatelé sázejí:

1pragma solidity 0.7.1;
2
3contract Game {
4 uint256 totalGamesPlayerWon = 0;
5 uint256 totalGamesPlayerLost = 0;
6 event BetPlaced(address player, uint256 value, bool hasWon);
7
8 function placeBet() external payable {
9 bool hasWon = evaluateBetForPlayer(msg.sender);
10
11 if (hasWon) {
12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');
13 require(success, "Transfer failed");
14 totalGamesPlayerWon++;
15 } else {
16 totalGamesPlayerLost++;
17 }
18
19 emit BetPlaced(msg.sender, msg.value, hasWon);
20 }
21}
Zobrazit vše

Řekněme, že v naší dapp chceme zobrazit celkový počet sázek, celkový počet prohraných/vyhraných her a také jej aktualizovat, kdykoli někdo hraje znovu. Přístup by byl následující:

  1. Načíst totalGamesPlayerWon.
  2. Načíst totalGamesPlayerLost.
  3. Přihlásit se k odběru událostí BetPlaced.

Můžeme naslouchat události ve Web3opens in a new tab, jak je znázorněno vpravo, ale vyžaduje to řešení několika případů.

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // událost spuštěna
6})
7.on('changed', function(event) {
8 // událost byla znovu odstraněna
9})
10.on('error', function(error, receipt) {
11 // tx zamítnuta
12});
Zobrazit vše

Pro náš jednoduchý příklad je to stále jakž takž v pořádku. Ale řekněme, že nyní chceme zobrazit výši prohraných/vyhraných sázek pouze pro aktuálního hráče. No, máme smůlu, raději nasaďte nový kontrakt, který tyto hodnoty uloží a načte. A teď si představte mnohem složitější chytrý kontrakt a dapp, věci se mohou rychle zkomplikovat.

Jen tak se prostě nedotazuje

Vidíte, že to není optimální:

  • Nefunguje pro již nasazené kontrakty.
  • Dodatečné náklady na palivo pro uložení těchto hodnot.
  • Vyžaduje další volání k načtení dat pro uzel Etherea.

To není dost dobré

Nyní se podívejme na lepší řešení.

Dovolte mi, abych vám představil GraphQL

Nejprve si povíme něco o GraphQL, původně navrženém a implementovaném společností Facebook. Možná jste obeznámeni s tradičním modelem REST API. Nyní si představte, že byste místo toho mohli napsat dotaz na přesně ta data, která jste chtěli:

GraphQL API vs. REST API

Tyto dva obrázky v podstatě vystihují podstatu GraphQL. S dotazem vpravo můžeme definovat přesně, jaká data chceme, takže získáme vše v jednom požadavku a nic víc, než přesně to, co potřebujeme. GraphQL server se stará o načítání všech požadovaných dat, takže je pro spotřebitelskou stranu frontendu neuvěřitelně snadno použitelný. Zde je pěkné vysvětleníopens in a new tab, jak přesně server zpracovává dotaz, pokud vás to zajímá.

S těmito znalostmi se konečně vrhněme do světa blockchainu a The Graph.

Co je The Graph?

Blockchain je decentralizovaná databáze, ale na rozdíl od toho, co je obvyklé, pro tuto databázi nemáme dotazovací jazyk. Řešení pro získávání dat jsou bolestivá nebo zcela nemožná. The Graph je decentralizovaný protokol pro indexování a dotazování dat na blockchainu. A možná jste to uhodli, jako dotazovací jazyk používá GraphQL.

The Graph

Příklady jsou vždy nejlepší pro pochopení, takže použijme The Graph pro náš příklad GameContract.

Jak vytvořit Subgraph

Definice způsobu indexování dat se nazývá subgraph. Vyžaduje tři komponenty:

  1. Manifest (subgraph.yaml)
  2. Schéma (schema.graphql)
  3. Mapování (mapping.ts)

Manifest (subgraph.yaml)

Manifest je náš konfigurační soubor a definuje:

  • které chytré kontrakty indexovat (adresa, síť, ABI...)
  • kterým událostem naslouchat
  • další věci, kterým naslouchat, jako jsou volání funkcí nebo bloky
  • volané mapovací funkce (viz mapping.ts níže)

Zde můžete definovat více kontraktů a handlerů. Typické nastavení by mělo složku subgraph uvnitř projektu Hardhat s vlastním repozitářem. Potom můžete snadno odkazovat na ABI.

Z důvodu pohodlí můžete také chtít použít nástroj pro šablony, jako je mustache. Potom vytvoříte subgraph.template.yaml a vložíte adresy na základě nejnovějších nasazení. Pro pokročilejší příklad nastavení se podívejte například na repozitář subgraphu Aaveopens in a new tab.

A kompletní dokumentaci si můžete prohlédnout zdeopens in a new tab.

1specVersion: 0.0.1
2description: Sázení na Ethereu
3repository: - odkaz na GitHub -
4schema:
5 file: ./schema.graphql
6dataSources:
7 - kind: ethereum/contract
8 name: GameContract
9 network: mainnet
10 source:
11 address: '0x2E6454...cf77eC'
12 abi: GameContract
13 startBlock: 6175244
14 mapping:
15 kind: ethereum/events
16 apiVersion: 0.0.1
17 language: wasm/assemblyscript
18 entities:
19 - GameContract
20 abis:
21 - name: GameContract
22 file: ../build/contracts/GameContract.json
23 eventHandlers:
24 - event: PlacedBet(address,uint256,bool)
25 handler: handleNewBet
26 file: ./src/mapping.ts
Zobrazit vše

Schéma (schema.graphql)

Schéma je definice dat GraphQL. Umožní vám definovat, které entity existují a jaké jsou jejich typy. Podporované typy z The Graph jsou

  • Bajty
  • ID
  • String
  • Boolean
  • Int
  • BigInt
  • BigDecimal

Můžete také použít entity jako typ k definování vztahů. V našem příkladu definujeme vztah 1 ku mnoha od hráče k sázkám. Znak ! znamená, že hodnota nemůže být prázdná. Kompletní dokumentaci si můžete prohlédnout zdeopens in a new tab.

1type Bet @entity {
2 id: ID!
3 player: Player!
4 playerHasWon: Boolean!
5 time: Int!
6}
7
8type Player @entity {
9 id: ID!
10 totalPlayedCount: Int
11 hasWonCount: Int
12 hasLostCount: Int
13 bets: [Bet]!
14}
Zobrazit vše

Mapování (mapping.ts)

Mapovací soubor v The Graph definuje naše funkce, které transformují příchozí události na entity. Je napsán v AssemblyScript, což je podmnožina Typescriptu. To znamená, že jej lze zkompilovat do WASM (WebAssembly) pro efektivnější a přenositelnější provádění mapování.

Budete muset definovat každou funkci pojmenovanou v souboru subgraph.yaml, takže v našem případě potřebujeme pouze jednu: handleNewBet. Nejprve se pokusíme načíst entitu Player z adresy odesílatele jako id. Pokud neexistuje, vytvoříme novou entitu a naplníme ji počátečními hodnotami.

Potom vytvoříme novou entitu Bet. ID pro ni bude event.transaction.hash.toHex() + "-" + event.logIndex.toString(), což zajišťuje vždy jedinečnou hodnotu. Použití pouze haše nestačí, protože by někdo mohl volat funkci placeBet několikrát v jedné transakci prostřednictvím chytrého kontraktu.

Nakonec můžeme aktualizovat entitu Player se všemi daty. Pole nelze přímo doplňovat (push), ale je třeba je aktualizovat, jak je zde ukázáno. Používáme id k odkazování na sázku. A .save() je na konci vyžadováno k uložení entity.

Kompletní dokumentaci si můžete prohlédnout zde: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappingsopens in a new tab. Můžete také přidat výstup protokolování do mapovacího souboru, viz zdeopens in a new tab.

1import { Bet, Player } from "../generated/schema"
2import { PlacedBet } from "../generated/GameContract/GameContract"
3
4export function handleNewBet(event: PlacedBet): void {
5 let player = Player.load(event.transaction.from.toHex())
6
7 if (player == null) {
8 // vytvořit, pokud ještě neexistuje
9 player = new Player(event.transaction.from.toHex())
10 player.bets = new Array<string>(0)
11 player.totalPlayedCount = 0
12 player.hasWonCount = 0
13 player.hasLostCount = 0
14 }
15
16 let bet = new Bet(
17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()
18 )
19 bet.player = player.id
20 bet.playerHasWon = event.params.hasWon
21 bet.time = event.block.timestamp
22 bet.save()
23
24 player.totalPlayedCount++
25 if (event.params.hasWon) {
26 player.hasWonCount++
27 } else {
28 player.hasLostCount++
29 }
30
31 // aktualizovat pole tímto způsobem
32 let bets = player.bets
33 bets.push(bet.id)
34 player.bets = bets
35
36 player.save()
37}
Zobrazit vše

Použití ve frontendu

Pomocí něčeho jako Apollo Boost můžete snadno integrovat The Graph do své React dapp (nebo Apollo-Vue). Zejména při použití React hooks a Apollo je načítání dat tak jednoduché jako napsání jediného GraphQL dotazu ve vaší komponentě. Typické nastavení může vypadat takto:

1// Zobrazit všechny subgraphy: https://thegraph.com/explorer/
2const client = new ApolloClient({
3 uri: "{{ subgraphUrl }}",
4})
5
6ReactDOM.render(
7 <ApolloProvider client={client}>
8 <App />
9 </ApolloProvider>,
10 document.getElementById("root")
11)
Zobrazit vše

A nyní můžeme napsat například takovýto dotaz. Tímto se nám načte

  • kolikrát aktuální uživatel vyhrál
  • kolikrát aktuální uživatel prohrál
  • seznam časových razítek se všemi jeho předchozími sázkami

Vše v jediném požadavku na server GraphQL.

1const myGraphQlQuery = gql`
2 players(where: { id: $currentUser }) {
3 totalPlayedCount
4 hasWonCount
5 hasLostCount
6 bets {
7 time
8 }
9 }
10`
11
12const { loading, error, data } = useQuery(myGraphQlQuery)
13
14React.useEffect(() => {
15 if (!loading && !error && data) {
16 console.log({ data })
17 }
18}, [loading, error, data])
Zobrazit vše

Kouzlo

Ale chybí nám poslední dílek skládačky, a to je server. Můžete si ho buď spustit sami, nebo použít hostovanou službu.

Server The Graph

Graph Explorer: Hostovaná služba

Nejjednodušší způsob je použít hostovanou službu. Postupujte podle pokynů zdeopens in a new tab a nasaďte subgraph. Pro mnoho projektů můžete skutečně najít existující subgraphy v průzkumníkoviopens in a new tab.

The Graph-Explorer

Provozování vlastního uzlu

Alternativně si můžete spustit vlastní uzel. Dokumentace zdeopens in a new tab. Jedním z důvodů může být použití sítě, která není podporována hostovanou službou. Aktuálně podporované sítě naleznete zdeopens in a new tab.

Decentralizovaná budoucnost

GraphQL podporuje také streamy pro nově příchozí události. Ty jsou na grafu podporovány prostřednictvím Substreamsopens in a new tab, které jsou v současné době v otevřené beta verzi.

V roce 2021opens in a new tab začal The Graph přecházet na decentralizovanou indexovací síť. Více o architektuře této decentralizované indexovací sítě si můžete přečíst zdeopens in a new tab.

Dva klíčové aspekty jsou:

  1. Uživatelé platí indexerům za dotazy.
  2. Indexeři stakují tokeny Graph (GRT).

Stránka naposledy aktualizována: 24. června 2025

Byl tento tutoriál užitečný?