Zum Hauptinhalt springen

The Graph: Web3-Datenabfragen reparieren

Solidity
Smart Contracts
Abfragen
the graph
React
Fortgeschritten
Markus Waas
6. September 2020
8 Minuten Lesezeit

Dieses Mal werfen wir einen genaueren Blick auf The Graph, das im letzten Jahr im Wesentlichen Teil des Standard-Stacks für die Entwicklung von Dapps geworden ist. Schauen wir uns zunächst an, wie wir die Dinge auf traditionelle Weise angehen würden...

Ohne The Graph...

Nehmen wir also ein einfaches Beispiel zur Veranschaulichung. Wir alle mögen Spiele, stell dir also ein einfaches Spiel vor, bei dem Benutzer Wetten platzieren:

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}

Nehmen wir nun an, wir möchten in unserer Dapp die Gesamtzahl der Wetten sowie die insgesamt verlorenen/gewonnenen Spiele anzeigen und diese auch aktualisieren, wenn jemand erneut spielt. Der Ansatz wäre:

  1. totalGamesPlayerWon abrufen.
  2. totalGamesPlayerLost abrufen.
  3. BetPlaced-Ereignisse abonnieren.

Wir können auf das Ereignis in Web3 (opens in a new tab) hören, wie rechts gezeigt, aber das erfordert die Behandlung ziemlich vieler Fälle.

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // Ereignis ausgelöst
6})
7.on('changed', function(event) {
8 // Ereignis wurde wieder entfernt
9})
10.on('error', function(error, receipt) {
11 // tx abgelehnt
12});

Für unser einfaches Beispiel ist das noch einigermaßen in Ordnung. Aber nehmen wir an, wir möchten nun die Anzahl der verlorenen/gewonnenen Wetten nur für den aktuellen Spieler anzeigen. Da haben wir Pech gehabt, du solltest besser einen neuen Smart Contract bereitstellen, der diese Werte speichert, und sie dann abrufen. Und nun stell dir einen viel komplizierteren Smart Contract und eine komplexere Dapp vor, da kann es schnell unübersichtlich werden.

One Does Not Simply Query

Du siehst, dass dies nicht optimal ist:

  • Funktioniert nicht für bereits bereitgestellte Smart Contracts.
  • Zusätzliche Gaskosten für die Speicherung dieser Werte.
  • Erfordert einen weiteren Aufruf, um die Daten für einen Ethereum-Blockchain-Knoten abzurufen.

Thats not good enough

Schauen wir uns nun eine bessere Lösung an.

Darf ich vorstellen: GraphQL

Lass uns zunächst über GraphQL sprechen, das ursprünglich von Facebook entworfen und implementiert wurde. Vielleicht bist du mit dem traditionellen REST-API-Modell vertraut. Stell dir nun vor, du könntest stattdessen eine Abfrage für genau die Daten schreiben, die du haben möchtest:

GraphQL API vs. REST API

Animated demonstration of a GraphQL query in The Graph playground

Die beiden Bilder erfassen ziemlich genau die Essenz von GraphQL. Mit der Abfrage auf der rechten Seite können wir genau definieren, welche Daten wir wollen, sodass wir alles in einer einzigen Anfrage erhalten und nicht mehr als genau das, was wir brauchen. Ein GraphQL-Server übernimmt das Abrufen aller erforderlichen Daten, sodass es für die Frontend-Verbraucherseite unglaublich einfach zu bedienen ist. Hier ist eine schöne Erklärung (opens in a new tab), wie genau der Server eine Abfrage verarbeitet, falls du interessiert bist.

Mit diesem Wissen wollen wir nun endlich in den Blockchain-Bereich und zu The Graph springen.

Was ist The Graph?

Eine Blockchain ist eine dezentralisierte Datenbank, aber im Gegensatz zum Normalfall haben wir keine Abfragesprache für diese Datenbank. Lösungen zum Abrufen von Daten sind mühsam oder völlig unmöglich. The Graph ist ein dezentralisiertes Protokoll zur Indizierung und Abfrage von Blockchain-Daten. Und du hast es vielleicht schon erraten, es verwendet GraphQL als Abfragesprache.

The Graph

Beispiele sind immer am besten, um etwas zu verstehen, also verwenden wir The Graph für unser GameContract-Beispiel.

Wie man einen Subgraph erstellt

Die Definition, wie Daten indiziert werden sollen, wird Subgraph genannt. Er erfordert drei Komponenten:

  1. Manifest (subgraph.yaml)
  2. Schema (schema.graphql)
  3. Mapping (mapping.ts)

Manifest (subgraph.yaml)

Das Manifest ist unsere Konfigurationsdatei und definiert:

  • welche Smart Contracts indiziert werden sollen (Adresse, Netzwerk, ABI...)
  • auf welche Ereignisse gehört werden soll
  • andere Dinge, auf die gehört werden soll, wie Funktionsaufrufe oder Blöcke
  • die aufgerufenen Mapping-Funktionen (siehe mapping.ts unten)

Du kannst hier mehrere Smart Contracts und Handler definieren. Ein typisches Setup hätte einen Subgraph-Ordner innerhalb des Hardhat-Projekts mit einem eigenen Repository. Dann kannst du leicht auf die ABI verweisen.

Aus Bequemlichkeitsgründen möchtest du vielleicht auch ein Vorlagen-Tool wie Mustache verwenden. Dann erstellst du eine subgraph.template.yaml und fügst die Adressen basierend auf den neuesten Bereitstellungen ein. Für ein fortgeschritteneres Beispiel-Setup siehe zum Beispiel das Aave-Subgraph-Repo (opens in a new tab).

Und die vollständige Dokumentation findest du hier (opens in a new tab).

1specVersion: 0.0.1
2description: Placing Bets on Ethereum
3repository: - GitHub link -
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

Schema (schema.graphql)

Das Schema ist die GraphQL-Datendefinition. Es ermöglicht dir zu definieren, welche Entitäten existieren und welche Typen sie haben. Unterstützte Typen von The Graph sind

  • Bytes
  • ID
  • String
  • Boolean
  • Int
  • BigInt
  • BigDecimal

Du kannst auch Entitäten als Typ verwenden, um Beziehungen zu definieren. In unserem Beispiel definieren wir eine 1-zu-n-Beziehung vom Spieler zu den Wetten. Das ! bedeutet, dass der Wert nicht leer sein darf. Die vollständige Dokumentation findest du hier (opens 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}

Mapping (mapping.ts)

Die Mapping-Datei in The Graph definiert unsere Funktionen, die eingehende Ereignisse in Entitäten umwandeln. Sie ist in AssemblyScript geschrieben, einer Teilmenge von TypeScript. Das bedeutet, dass sie in WASM (WebAssembly) kompiliert werden kann, um eine effizientere und portablere Ausführung des Mappings zu ermöglichen.

Du musst jede in der Datei subgraph.yaml benannte Funktion definieren, in unserem Fall benötigen wir also nur eine: handleNewBet. Wir versuchen zunächst, die Player-Entität von der Absenderadresse als ID zu laden. Wenn sie nicht existiert, erstellen wir eine neue Entität und füllen sie mit Startwerten.

Dann erstellen wir eine neue Bet-Entität. Die ID dafür wird event.transaction.hash.toHex() + "-" + event.logIndex.toString() sein, was immer einen eindeutigen Wert gewährleistet. Nur den Hash zu verwenden, reicht nicht aus, da jemand die Funktion placeBet mehrmals in einer Transaktion über einen Smart Contract aufrufen könnte.

Zuletzt können wir die Player-Entität mit allen Daten aktualisieren. Arrays können nicht direkt mit Push erweitert werden, sondern müssen wie hier gezeigt aktualisiert werden. Wir verwenden die ID, um auf die Wette zu verweisen. Und .save() ist am Ende erforderlich, um eine Entität zu speichern.

Die vollständige Dokumentation findest du hier: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings (opens in a new tab). Du kannst der Mapping-Datei auch Protokollausgaben hinzufügen, siehe hier (opens 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 // erstellen, falls noch nicht vorhanden
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 // Array so aktualisieren
32 let bets = player.bets
33 bets.push(bet.id)
34 player.bets = bets
35
36 player.save()
37}

Verwendung im Frontend

Mit etwas wie Apollo Boost kannst du The Graph ganz einfach in deine React-Dapp (oder Apollo-Vue) integrieren. Besonders bei der Verwendung von React Hooks und Apollo ist das Abrufen von Daten so einfach wie das Schreiben einer einzigen GraphQL-Abfrage in deiner Komponente. Ein typisches Setup könnte so aussehen:

1// Siehe alle Subgraphen: 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)

Und nun können wir zum Beispiel eine Abfrage wie diese schreiben. Das wird uns Folgendes abrufen:

  • wie oft der aktuelle Benutzer gewonnen hat
  • wie oft der aktuelle Benutzer verloren hat
  • eine Liste von Zeitstempeln mit all seinen vorherigen Wetten

Alles in einer einzigen Anfrage an den GraphQL-Server.

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

Magic

Aber uns fehlt noch ein letztes Puzzleteil, und das ist der Server. Du kannst ihn entweder selbst betreiben oder den gehosteten Dienst nutzen.

Der The Graph-Server

Graph Explorer: Der gehostete Dienst

Der einfachste Weg ist die Nutzung des gehosteten Dienstes. Folge den Anweisungen hier (opens in a new tab), um einen Subgraph bereitzustellen. Für viele Projekte kannst du tatsächlich bestehende Subgraphen im Explorer (opens in a new tab) finden.

The Graph-Explorer

Einen eigenen Blockchain-Knoten betreiben

Alternativ kannst du deinen eigenen Blockchain-Knoten betreiben. Die Dokumentation findest du hier (opens in a new tab). Ein Grund dafür könnte die Nutzung eines Netzwerks sein, das vom gehosteten Dienst nicht unterstützt wird. Die derzeit unterstützten Netzwerke sind hier zu finden (opens in a new tab).

Die dezentralisierte Zukunft

GraphQL unterstützt auch Streams für neu eingehende Ereignisse. Diese werden auf The Graph durch Substreams (opens in a new tab) unterstützt, die sich derzeit in der offenen Beta-Phase befinden.

Im Jahr 2021 (opens in a new tab) begann The Graph mit dem Übergang zu einem dezentralisierten Indizierungsnetzwerk. Du kannst mehr über die Architektur dieses dezentralisierten Indizierungsnetzwerks hier (opens in a new tab) lesen.

Zwei Schlüsselaspekte sind:

  1. Benutzer bezahlen die Indexer für Abfragen.
  2. Indexer hinterlegen Graph-Token (GRT) als Einsatz.

Letzte Aktualisierung der Seite: 3. März 2026

War dieses Tutorial hilfreich?