The Graph: zapytania o dane Web3
Tym razem przyjrzymy się bliżej protokołowi The Graph, który zasadniczo stał się częścią standardowego stosu do tworzenia aplikacji dapps w zeszłym roku. Zobaczmy najpierw, jak zrobilibyśmy rzeczy w tradycyjny sposób...
Bez The Graph...
Przejdźmy więc do prostego przykładu w celach ilustracyjnych. Wszyscy lubimy gry, więc wyobraźmy sobie prostą grę z użytkownikami zakładów:
1pragma solidity 0.7.1;23contract Game {4 uint256 totalGamesPlayerWon = 0;5 uint256 totalGamesPlayerLost = 0;6 event BetPlaced(address player, uint256 value, bool hasWon);78 function placeBet() external payable {9 bool hasWon = evaluateBetForPlayer(msg.sender);1011 if (hasWon) {12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');13 require(success, "Transfer failed");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}Pokaż wszystkoKopiuj
Załóżmy teraz, że w naszej aplikacji dapp chcemy wyświetlić całkowitą liczbę przegranych/wygranych gier, a także aktualizować ją za każdym razem, gdy ktoś gra ponownie. Podejście byłoby następujące:
- Pobierz
totalGamesPlayerWon
. - Pobierz
totalGamesPlayerlost
. - Subskrybuj zdarzenia
BetPlaceed
.
Możemy nasłuchiwać zdarzeń w sieci Web3(opens in a new tab), jak widać po prawej stronie, ale wymaga to obsługi kilku przypadków.
1GameContract.events.BetPlaced({2 fromBlock: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // event fired6})7.on('changed', function(event) {8 // event was removed again9})10.on('error', function(error, receipt) {11 // tx rejected12});Pokaż wszystkoKopiuj
W naszym prostym przykładzie jest to nadal poniekąd w porządku. Ale powiedzmy, że chcemy teraz wyświetlać kwoty przegranych/wygranych zakładów tylko dla aktualnego gracza. Cóż, nie mamy szczęścia, lepiej wdrożyć nowy kontrakt, który przechowuje te wartości i je pobiera. A teraz wyobraź sobie znacznie bardziej skomplikowany inteligentny kontrakt i dapp, rzeczy mogą szybko się popsuć.
Możesz zobaczyć, że to nieoptymalne:
- Nie działa dla już wdrożonych kontraktów.
- Dodatkowe koszty gazu za przechowywanie tych wartości.
- Wymaga kolejnego wywołania w celu pobrania danych dla węzła Ethereum.
Spójrzmy teraz na lepsze rozwiązanie.
Pozwól, że przedstawię Ci GraphQL
Najpierw porozmawiajmy o GraphQL, pierwotnie zaprojektowanym i zaimplementowanym przez Facebooka. Być może znasz tradycyjny model Rest API. Teraz wyobraź sobie, że zamiast tego możesz napisać zapytanie dla dokładnie tych danych, które chciałeś:
Te dwa obrazy w dużym stopniu oddają istotę GraphQL. Za pomocą zapytania po prawej stronie możemy zdefiniować dokładnie, jakich danych chcemy, dzięki czemu otrzymujemy wszystko w jednym żądaniu i nic więcej niż dokładnie to, czego potrzebujemy. Serwer GraphQL obsługuje pobieranie wszystkich wymaganych danych, dzięki czemu jest niezwykle łatwy w użyciu dla użytkownika frontendu. To dobre wyjaśnienie(opens in a new tab), jak dokładnie serwer obsługuje zapytanie, jeśli jesteś zainteresowany.
Teraz, mając tę wiedzę, w końcu wskoczmy w przestrzeń blockchain i The Graph.
Co to jest The Graph?
Blockchain to zdecentralizowana baza danych, ale w przeciwieństwie do tego, co zwykle ma miejsce, nie mamy języka zapytań dla tej bazy danych. Rozwiązania do odzyskiwania danych są bolesne lub całkowicie niemożliwe. The Graph to zdecentralizowany protokół do indeksowania i odpytywania danych blockchain. I mogłeś się domyślić, że używa GraphQL jako języka zapytań.
Przykłady są zawsze najlepsze, aby coś zrozumieć, więc użyjmy The Graph dla naszego przykładu GameContract.
Jak stworzyć Subgraph
Definicja sposobu indeksowania danych nazywana jest subgraph. Wymaga trzech komponentów:
- Manifest (subgraf.yaml)
- Schemat (schema.graphql)
- Mapping (mapping.ts)
Manifest (subgraf.yaml)
Manifest jest naszym plikiem konfiguracyjnym i definiuje:
- które inteligentne kontrakty indeksować (adres, sieć, ABI...)
- jakich zdarzeń nasłuchiwać
- inne rzeczy do słuchania, takie jak wywołania funkcji lub bloki
- wywoływane funkcje mapujące (zobacz mapping.ts poniżej)
Tutaj możesz zdefiniować wiele kontraktów i programów obsługi. Typowa konfiguracja miałaby folder podrzędny wewnątrz projektu Hardhat z własnym repozytorium. Wtedy możesz łatwo odwołać się do ABI.
Dla wygody możesz również użyć narzędzia szablonu, takiego jak wąsy. Następnie tworzysz subgraph.template.yaml i wstawiasz adresy oparte na najnowszych wdrożeniach. Aby zapoznać się z bardziej zaawansowaną przykładową konfiguracją, zobacz na przykład Repozytorium subgrafów Aave(opens in a new tab).
A pełną dokumentację można zobaczyć tutaj: https://thegraph.com/docs/define-a-subgraph#the-subgraph-manifest(opens in a new tab).
1specVersion: 0.0.12description: Placing Bets on Ethereum3repository: - GitHub link -4schema:5 file: ./schema.graphql6dataSources:7 - kind: ethereum/contract8 name: GameContract9 network: mainnet10 source:11 address: '0x2E6454...cf77eC'12 abi: GameContract13 startBlock: 617524414 mapping:15 kind: ethereum/events16 apiVersion: 0.0.117 language: wasm/assemblyscript18 entities:19 - GameContract20 abis:21 - name: GameContract22 file: ../build/contracts/GameContract.json23 eventHandlers:24 - event: PlacedBet(address,uint256,bool)25 handler: handleNewBet26 file: ./src/mapping.tsPokaż wszystko
Schemat (schema.graphql)
Schematem jest definicja danych GraphQL. Pozwoli to na zdefiniowanie istniejących obiektów i ich typów. Obsługiwane typy z wykresu to
- Bajty
- ID
- Tekst
- Boolean
- Wewnątrz
- BigInt
- BigDecimal
Możesz również używać obiektów jako typu do definiowania relacji. W naszym przykładzie definiujemy relacje od gracza do zakładów. ! oznacza, że wartość nie może być pusta. Pełną dokumentację można zobaczyć tutaj: https://thegraph.com/docs/define-a-subgraph#the-graphql-schema(opens in a new tab).
1type Bet @entity {2 id: ID!3 player: Player!4 playerHasWon: Boolean!5 time: Int!6}78type Player @entity {9 id: ID!10 totalPlayedCount: Int11 hasWonCount: Int12 hasLostCount: Int13 bets: [Bet]!14}Pokaż wszystko
Mapping (mapping.ts)
Plik mapowania na wykresie definiuje nasze funkcje, które przekształcają przychodzące zdarzenia w podmioty. Jest napisany w AssemblyScript, podzbiorze Typescript. Oznacza to, że może być skompilowany w WASM (WebAssembly) dla bardziej wydajnego i przenośnego wykonywania mapowania.
Musisz zdefiniować każdą funkcję nazwaną w pliku subgraph.yaml, więc w naszym przypadku potrzebujemy tylko jednego: handleNewBet. Najpierw próbujemy załadować obiekt Gracza z adresu nadawcy w postaci identyfikatora. Jeśli nie istnieje, tworzymy nową jednostkę i wypełniamy ją wartościami początkowymi.
Następnie tworzymy nową jednostkę zakładu. Identyfikatorem dla tego będzie event.transaction.hash.toHex() + "-" + event.logIndex.toString() zapewniający zawsze unikalną wartość. Używanie samego skrótu nie wystarczy, ponieważ ktoś może kilkakrotnie wywoływać funkcję placeBet w jednej transakcji za pośrednictwem inteligentnej umowy.
Na koniec możemy zaktualizować podmiot Player, który będzie zawierał wszystkie dane. Tablice nie mogą być wypychane bezpośrednio, ale muszą zostać zaktualizowane, jak pokazano tutaj. Używamy identyfikatora, aby odnieść się do zakładu. A .save() jest wymagane na końcu do przechowywania obiektu.
Pełną dokumentację można zobaczyć tutaj: https://thegraph.com/docs/define-a-subgraph#writing-mappings(opens in a new tab). Możesz także dodać dane wyjściowe rejestrowania do pliku mapowania, zobacz tutaj(opens in a new tab).
1import { Bet, Player } from "../generated/schema"2import { PlacedBet } from "../generated/GameContract/GameContract"34export function handleNewBet(event: PlacedBet): void {5 let player = Player.load(event.transaction.from.toHex())67 if (player == null) {8 // create if doesn't exist yet9 player = new Player(event.transaction.from.toHex())10 player.bets = new Array<string>(0)11 player.totalPlayedCount = 012 player.hasWonCount = 013 player.hasLostCount = 014 }1516 let bet = new Bet(17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()18 )19 bet.player = player.id20 bet.playerHasWon = event.params.hasWon21 bet.time = event.block.timestamp22 bet.save()2324 player.totalPlayedCount++25 if (event.params.hasWon) {26 player.hasWonCount++27 } else {28 player.hasLostCount++29 }3031 // update array like this32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 player.save()37}Pokaż wszystko
Używanie go w frontendzie
Używając czegoś takiego jak Apollo Boost, możesz łatwo zintegrować The Graph z React Dapp (lub Apollo-Vue). Zwłaszcza gdy używasz hooków React i Apollo, pobieranie danych jest tak proste, jak napisanie pojedynczego zapytania GraphQl w twoim komponencie. Typowa konfiguracja może wyglądać tak:
1// See all subgraphs: https://thegraph.com/explorer/2const client = new ApolloClient({3 uri: "{{ subgraphUrl }}",4})56ReactDOM.render(7 <ApolloProvider client={client}>8 <App />9 </ApolloProvider>,10 document.getElementById("root")11)Pokaż wszystko
A teraz możemy napisać na przykład takie zapytanie. Spowoduje to pobranie
- ile razy obecny użytkownik wygrał
- ile razy aktualny użytkownik przegrał
- listę sygnatur czasowych ze wszystkimi jego poprzednimi zakładami
Wszystko w jednym żądaniu do serwera GraphQL.
1const myGraphQlQuery = gql`2 players(where: { id: $currentUser }) {3 totalPlayedCount4 hasWonCount5 hasLostCount6 bets {7 time8 }9 }10`1112const { loading, error, data } = useQuery(myGraphQlQuery)1314React.useEffect(() => {15 if (!loading && !error && data) {16 console.log({ data })17 }18}, [loading, error, data])Pokaż wszystko
Ale brakuje nam ostatniego elementu układanki, a jest nim serwer. Możesz uruchomić go samodzielnie lub skorzystać z usługi hostowanej.
Serwer The Graph
Graph Explorer: usługa hostowana
Najprostszym sposobem jest skorzystanie z usługi hostowanej. Postępuj zgodnie z instrukcjami tutaj(opens in a new tab), aby wdrożyć subgraf. W przypadku wielu projektów można znaleźć istniejące podgrafy w eksploratorze pod adresem https://thegraph.com/explorer/(opens in a new tab).
Uruchamianie własnego węzła
Alternatywnie możesz uruchomić własny węzeł: https://github.com/graphprotocol/graph-node#quick-start(opens in a new tab). Jednym z powodów, aby to zrobić, może być korzystanie z sieci, która nie jest obsługiwana przez hostowaną usługę. Obecnie obsługiwane są sieć główna, Kovan, Rinkeby, Ropsten, Goerli, PoA-Core, xDAI i Sokol.
Zdecentralizowana przyszłość
GraphQL obsługuje również strumienie dla nowo przychodzących zdarzeń. Nie jest to jeszcze w pełni obsługiwane przez The Graph, ale zostanie wkrótce wydane.
Brakującym aspektem jest jednak wciąż decentralizacja. The Graph w przyszłości ma ostatecznie stać się w pełni zdecentralizowanym protokołem. Oto dwa świetne artykuły, które bardziej szczegółowo wyjaśniają plan:
- https://thegraph.com/blog/the-graph-network-in-depth-part-1(opens in a new tab)
- https://thegraph.com/blog/the-graph-network-in-depth-part-2(opens in a new tab)
Dwa kluczowe aspekty to:
- Użytkownicy będą płacić indeksatorom za zapytania.
- Indeksatorzy będą stakować tokeny Graph (GRT).
Ostatnia edycja: @sumitvekariya(opens in a new tab), 29 sierpnia 2024