The Graph: Oprava dotazování na data ve Web3
Tentokrát se podíváme blíže na The Graph, který se v posledním roce v podstatě stal součástí standardního stacku pro vývoj decentralizovaných aplikací (dapp). Pojďme se nejprve podívat, jak bychom to dělali tradičním způsobem...
Bez The Graph...
Pro ilustraci si tedy vezměme jednoduchý příklad. Všichni máme rádi hry, takže si představte jednoduchou hru, kde uživatelé uzavírají sázky:
pragma solidity 0.7.1;
contract Game {
uint256 totalGamesPlayerWon = 0;
uint256 totalGamesPlayerLost = 0;
event BetPlaced(address player, uint256 value, bool hasWon);
function placeBet() external payable {
bool hasWon = evaluateBetForPlayer(msg.sender);
if (hasWon) {
(bool success, ) = msg.sender.call{ value: msg.value * 2 }('');
require(success, "Transfer failed");
totalGamesPlayerWon++;
} else {
totalGamesPlayerLost++;
}
emit BetPlaced(msg.sender, msg.value, hasWon);
}
}
Řekněme, že v naší decentralizované aplikaci (dapp) chceme zobrazit celkové sázky, celkový počet prohraných/vyhraných her a také to aktualizovat, kdykoli někdo hraje znovu. Postup by byl následující:
- Načíst
totalGamesPlayerWon. - Načíst
totalGamesPlayerLost. - Přihlásit se k odběru událostí
BetPlaced.
Můžeme naslouchat události ve Web3 (opens in a new tab), jak je znázorněno vpravo, ale vyžaduje to ošetření poměrně velkého množství případů.
GameContract.events.BetPlaced({
fromBlock: 0
}, function(error, event) { console.log(event); })
.on('data', function(event) {
// událost vyvolána
})
.on('changed', function(event) {
// událost byla znovu odstraněna
})
.on('error', function(error, receipt) {
// transakce zamítnuta
});
Pro náš jednoduchý příklad je to stále ještě docela v pořádku. Ale řekněme, že nyní chceme zobrazit částky prohraných/vyhraných sázek pouze pro aktuálního hráče. Máme smůlu, raději byste měli nasadit nový kontrakt, který tyto hodnoty ukládá, a načítat je z něj. A teď si představte mnohem složitější chytrý kontrakt a dapp, věci se mohou rychle zkomplikovat.
Sami vidíte, že to není optimální:
- Nefunguje pro již nasazené kontrakty.
- Dodatečné náklady na gas za ukládání těchto hodnot.
- Vyžaduje další volání pro načtení dat z uzlu Etherea.
Nyní se podívejme na lepší řešení.
Dovolte mi představit vám GraphQL
Nejprve si promluvme o GraphQL, které původně navrhl a implementoval Facebook. Možná znáte tradiční model REST API. Nyní si místo toho představte, že byste mohli napsat dotaz přesně na ta data, která chcete:
Tyto dva obrázky v podstatě vystihují podstatu GraphQL. Pomocí dotazu vpravo můžeme přesně definovat, jaká data chceme, takže získáme vše v jednom požadavku a nic víc než to, co přesně potřebujeme. Server GraphQL se stará o načtení všech požadovaných dat, takže je pro frontendového konzumenta neuvěřitelně snadné jej používat. Pokud vás to zajímá, zde je pěkné vysvětlení (opens in a new tab) toho, jak přesně server zpracovává dotaz.
S těmito znalostmi se konečně vrhněme do prostoru 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í na data z blockchainu. A jak jste už asi uhodli, jako dotazovací jazyk používá GraphQL.
Příklady jsou vždy nejlepší pro pochopení čehokoli, takže použijme The Graph pro náš příklad s GameContract.
Jak vytvořit podgraf
Definice toho, jak indexovat data, se nazývá podgraf. Vyžaduje tři komponenty:
- Manifest (
subgraph.yaml) - Schéma (
schema.graphql) - Mapování (
mapping.ts)
Manifest (subgraph.yaml)
Manifest je náš konfigurační soubor a definuje:
- které chytré kontrakty se mají indexovat (adresa, síť, ABI...)
- kterým událostem se má naslouchat
- další věci k naslouchání, jako jsou volání funkcí nebo bloky
- volané mapovací funkce (viz
mapping.tsníže)
Zde můžete definovat více kontraktů a handlerů. Typické nastavení by mělo složku podgrafu uvnitř projektu Hardhat s vlastním repozitářem. Pak můžete snadno odkazovat na ABI.
Z důvodu pohodlí možná budete chtít použít také šablonovací nástroj, jako je mustache. Pak 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ář podgrafu Aave (opens in a new tab).
A celou dokumentaci si můžete prohlédnout zde (opens in a new tab).
specVersion: 0.0.1
description: Placing Bets on Ethereum
repository: - GitHub link -
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: GameContract
network: mainnet
source:
address: '0x2E6454...cf77eC'
abi: GameContract
startBlock: 6175244
mapping:
kind: ethereum/events
apiVersion: 0.0.1
language: wasm/assemblyscript
entities:
- GameContract
abis:
- name: GameContract
file: ../build/contracts/GameContract.json
eventHandlers:
- event: PlacedBet(address,uint256,bool)
handler: handleNewBet
file: ./src/mapping.ts
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
- Bytes
- ID
- String
- Boolean
- Int
- BigInt
- BigDecimal
Entity můžete také použít jako typ k definování vztahů. V našem příkladu definujeme vztah 1:N (jeden k mnoha) od hráče k sázkám. Znak ! znamená, že hodnota nesmí být prázdná. Celou dokumentaci si můžete prohlédnout zde (opens in a new tab).
type Bet @entity {
id: ID!
player: Player!
playerHasWon: Boolean!
time: Int!
}
type Player @entity {
id: ID!
totalPlayedCount: Int
hasWonCount: Int
hasLostCount: Int
bets: [Bet]!
}
Mapování (mapping.ts)
Soubor mapování v The Graph definuje naše funkce, které transformují příchozí události na entity. Je napsán v AssemblyScriptu, 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.
Poté vytvoříme novou entitu Bet. Její id bude event.transaction.hash.toHex() + "-" + event.logIndex.toString(), což zajistí vždy jedinečnou hodnotu. Použití pouze hashe nestačí, protože někdo může volat funkci placeBet několikrát v jedné transakci prostřednictvím chytrého kontraktu.
Nakonec můžeme aktualizovat entitu Player všemi daty. Do polí nelze přidávat prvky přímo (push), ale musí se aktualizovat tak, jak je ukázáno zde. K odkazování na sázku používáme id. A na konci je vyžadováno .save() pro uložení entity.
Celou dokumentaci si můžete prohlédnout zde: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings (opens in a new tab). Do souboru mapování můžete také přidat výstup protokolování (logging), viz zde (opens in a new tab).
import { Bet, Player } from "../generated/schema"
import { PlacedBet } from "../generated/GameContract/GameContract"
export function handleNewBet(event: PlacedBet): void {
let player = Player.load(event.transaction.from.toHex())
if (player == null) {
// vytvořit, pokud ještě neexistuje
player = new Player(event.transaction.from.toHex())
player.bets = new Array<string>(0)
player.totalPlayedCount = 0
player.hasWonCount = 0
player.hasLostCount = 0
}
let bet = new Bet(
event.transaction.hash.toHex() + "-" + event.logIndex.toString()
)
bet.player = player.id
bet.playerHasWon = event.params.hasWon
bet.time = event.block.timestamp
bet.save()
player.totalPlayedCount++
if (event.params.hasWon) {
player.hasWonCount++
} else {
player.hasLostCount++
}
// aktualizovat pole takto
let bets = player.bets
bets.push(bet.id)
player.bets = bets
player.save()
}
Použití ve frontendu
Pomocí něčeho jako Apollo Boost můžete The Graph snadno integrovat do své decentralizované aplikace (dapp) v Reactu (nebo Apollo-Vue). Zejména při použití React hooks a Apolla je načítání dat tak jednoduché jako napsání jediného dotazu GraphQL ve vaší komponentě. Typické nastavení může vypadat takto:
// Zobrazit všechny podgrafy: https://thegraph.com/explorer/
const client = new ApolloClient({
uri: "{{ subgraphUrl }}",
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
)
A nyní můžeme napsat například takovýto dotaz. Ten 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 jednom jediném požadavku na server GraphQL.
const myGraphQlQuery = gql`
players(where: { id: $currentUser }) {
totalPlayedCount
hasWonCount
hasLostCount
bets {
time
}
}
`
const { loading, error, data } = useQuery(myGraphQlQuery)
React.useEffect(() => {
if (!loading && !error && data) {
console.log({ data })
}
}, [loading, error, data])
Chybí nám ale poslední kousek skládačky, a tím je server. Můžete jej buď provozovat sami, nebo využít hostovanou službu.
Server The Graph
Graph Explorer: Hostovaná služba
Nejjednodušší způsob je použít hostovanou službu. Pro nasazení podgrafu postupujte podle pokynů zde (opens in a new tab). Pro mnoho projektů můžete ve skutečnosti najít existující podgrafy v exploreru (opens in a new tab).
Provozování vlastního uzlu
Případně můžete provozovat svůj vlastní uzel. Dokumentace je zde (opens in a new tab). Jedním z důvodů, proč to udělat, může být použití sítě, která není podporována hostovanou službou. Aktuálně podporované sítě najdete zde (opens in a new tab).
Decentralizovaná budoucnost
GraphQL podporuje také streamy pro nově příchozí události. Ty jsou na The Graph podporovány prostřednictvím Substreams (opens in a new tab), které jsou v současné době v otevřené beta verzi.
V roce 2021 (opens in a new tab) zahájil The Graph přechod na decentralizovanou indexovací síť. Více o architektuře této decentralizované indexovací sítě si můžete přečíst zde (opens in a new tab).
Dva klíčové aspekty jsou:
- Uživatelé platí indexerům za dotazy.
- Indexeři stakují tokeny Graph (GRT).






