The Graph: Naprawa zapytań o dane w Web3
Tym razem przyjrzymy się bliżej The Graph, który w zeszłym roku stał się w zasadzie częścią standardowego stosu technologicznego do tworzenia zdecentralizowanych aplikacji (dapp). Zobaczmy najpierw, jak zrobilibyśmy to w tradycyjny sposób...
Bez The Graph...
Posłużmy się więc prostym przykładem w celach ilustracyjnych. Wszyscy lubimy gry, więc wyobraźmy sobie prostą grę, w której użytkownicy obstawiają zakłady:
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);
}
}
Załóżmy teraz, że w naszej zdecentralizowanej aplikacji (dapp) chcemy wyświetlać łączną liczbę zakładów, łączną liczbę przegranych/wygranych gier, a także aktualizować te dane za każdym razem, gdy ktoś ponownie zagra. Podejście wyglądałoby następująco:
- Pobranie
totalGamesPlayerWon. - Pobranie
totalGamesPlayerLost. - Subskrypcja zdarzeń
BetPlaced.
Możemy nasłuchiwać zdarzenia w Web3 (opens in a new tab), jak pokazano po prawej stronie, ale wymaga to obsłużenia całkiem sporej liczby przypadków.
GameContract.events.BetPlaced({
fromBlock: 0
}, function(error, event) { console.log(event); })
.on('data', function(event) {
// zdarzenie wywołane
})
.on('changed', function(event) {
// zdarzenie zostało ponownie usunięte
})
.on('error', function(error, receipt) {
// transakcja odrzucona
});
To wciąż jest w miarę w porządku dla naszego prostego przykładu. Ale powiedzmy, że chcemy teraz wyświetlać kwoty przegranych/wygranych zakładów tylko dla obecnego gracza. Cóż, mamy pecha, 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 – sprawy mogą się szybko skomplikować.
Widać, że nie jest to optymalne:
- 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 z 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 Facebook. Być może znasz tradycyjny model REST API. Wyobraź sobie teraz, że zamiast tego możesz napisać zapytanie o dokładnie te dane, których potrzebujesz:
Te dwa obrazy w zasadzie oddają istotę GraphQL. Za pomocą zapytania po prawej stronie możemy dokładnie zdefiniować, jakich danych chcemy, więc otrzymujemy wszystko w jednym żądaniu i nic więcej poza tym, czego dokładnie potrzebujemy. Serwer GraphQL zajmuje się pobieraniem wszystkich wymaganych danych, więc jest to niezwykle łatwe w użyciu dla strony konsumenckiej (frontendu). Oto fajne wyjaśnienie (opens in a new tab) tego, jak dokładnie serwer obsługuje zapytanie, jeśli jesteś zainteresowany.
Mając tę wiedzę, przejdźmy wreszcie do przestrzeni blockchain i The Graph.
Czym jest The Graph?
Blockchain to zdecentralizowana baza danych, ale w przeciwieństwie do tego, z czym zazwyczaj mamy do czynienia, nie mamy języka zapytań dla tej bazy danych. Rozwiązania do pobierania danych są uciążliwe lub całkowicie niemożliwe. The Graph to zdecentralizowany protokół do indeksowania i odpytywania danych z blockchaina. I jak można się domyślić, używa GraphQL jako języka zapytań.
Przykłady są zawsze najlepsze do zrozumienia czegoś, więc użyjmy The Graph dla naszego przykładu GameContract.
Jak utworzyć podgraf
Definicja sposobu indeksowania danych nazywa się podgrafem. Wymaga on trzech komponentów:
- Manifest (
subgraph.yaml) - Schemat (
schema.graphql) - Mapowanie (
mapping.ts)
Manifest (subgraph.yaml)
Manifest to nasz plik konfiguracyjny, który definiuje:
- które inteligentne kontrakty indeksować (adres, sieć, ABI...)
- jakich zdarzeń nasłuchiwać
- inne rzeczy do nasłuchiwania, takie jak wywołania funkcji lub bloki
- wywoływane funkcje mapujące (zobacz
mapping.tsponiżej)
Możesz tu zdefiniować wiele kontraktów i handlerów. Typowa konfiguracja miałaby folder podgrafu wewnątrz projektu Hardhat z własnym repozytorium. Wtedy możesz łatwo odwołać się do ABI.
Dla wygody możesz również chcieć użyć narzędzia do szablonów, takiego jak mustache. Wtedy tworzysz subgraph.template.yaml i wstawiasz adresy na podstawie najnowszych wdrożeń. Bardziej zaawansowany przykład konfiguracji można znaleźć na przykład w repozytorium podgrafu Aave (opens in a new tab).
Pełną dokumentację można znaleźć tutaj (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
Schemat (schema.graphql)
Schemat to definicja danych GraphQL. Pozwoli Ci zdefiniować, jakie encje istnieją i jakie są ich typy. Obsługiwane typy przez The Graph to:
- Bytes
- ID
- String
- Boolean
- Int
- BigInt
- BigDecimal
Możesz również używać encji jako typu do definiowania relacji. W naszym przykładzie definiujemy relację jeden-do-wielu od gracza do zakładów. Znak ! oznacza, że wartość nie może być pusta. Pełną dokumentację można znaleźć tutaj (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]!
}
Mapowanie (mapping.ts)
Plik mapowania w The Graph definiuje nasze funkcje, które przekształcają przychodzące zdarzenia w encje. Jest napisany w AssemblyScript, podzbiorze TypeScript. Oznacza to, że może zostać skompilowany do WASM (WebAssembly) w celu bardziej wydajnego i przenośnego wykonywania mapowania.
Będziesz musiał zdefiniować każdą funkcję nazwaną w pliku subgraph.yaml, więc w naszym przypadku potrzebujemy tylko jednej: handleNewBet. Najpierw próbujemy załadować encję Player z adresu nadawcy jako id. Jeśli nie istnieje, tworzymy nową encję i wypełniamy ją wartościami początkowymi.
Następnie tworzymy nową encję Bet. Identyfikatorem dla niej będzie event.transaction.hash.toHex() + "-" + event.logIndex.toString(), co zapewnia zawsze unikalną wartość. Użycie samego hasha nie wystarczy, ponieważ ktoś może wywołać funkcję placeBet kilka razy w jednej transakcji za pośrednictwem inteligentnego kontraktu.
Na koniec możemy zaktualizować encję Player wszystkimi danymi. Tablice nie mogą być bezpośrednio modyfikowane przez push, ale muszą być aktualizowane w sposób pokazany tutaj. Używamy id, aby odwołać się do zakładu. A .save() jest wymagane na końcu, aby zapisać encję.
Pełną dokumentację można znaleźć tutaj: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings (opens in a new tab). Możesz również dodać logowanie do pliku mapowania, zobacz tutaj (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) {
// utwórz, jeśli jeszcze nie istnieje
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++
}
// zaktualizuj tablicę w ten sposób
let bets = player.bets
bets.push(bet.id)
player.bets = bets
player.save()
}
Użycie we frontendzie
Używając czegoś takiego jak Apollo Boost, możesz łatwo zintegrować The Graph w swojej zdecentralizowanej aplikacji (dapp) w React (lub Apollo-Vue). Zwłaszcza przy użyciu hooków React i Apollo, pobieranie danych jest tak proste, jak napisanie pojedynczego zapytania GraphQL w komponencie. Typowa konfiguracja może wyglądać tak:
// Zobacz wszystkie podgrafy: https://thegraph.com/explorer/
const client = new ApolloClient({
uri: "{{ subgraphUrl }}",
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
)
A teraz możemy napisać na przykład takie zapytanie. Pobierze ono dla nas:
- ile razy obecny użytkownik wygrał
- ile razy obecny użytkownik przegrał
- listę znaczników czasu ze wszystkimi jego poprzednimi zakładami
Wszystko to w jednym żądaniu do serwera 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])
Brakuje nam jednak ostatniego elementu układanki, czyli serwera. Możesz go uruchomić 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ć podgraf. Dla wielu projektów można znaleźć już istniejące podgrafy w eksploratorze (opens in a new tab).
Uruchomienie własnego węzła
Alternatywnie możesz uruchomić własny węzeł. Dokumentacja znajduje się tutaj (opens in a new tab). Jednym z powodów, dla których warto to zrobić, może być korzystanie z sieci, która nie jest obsługiwana przez usługę hostowaną. Obecnie obsługiwane sieci można znaleźć tutaj (opens in a new tab).
Zdecentralizowana przyszłość
GraphQL obsługuje również strumienie dla nowo przychodzących zdarzeń. Są one obsługiwane w The Graph poprzez Substreams (opens in a new tab), które są obecnie w fazie otwartej bety.
W 2021 roku (opens in a new tab) The Graph rozpoczął przejście na zdecentralizowaną sieć indeksującą. Więcej o architekturze tej zdecentralizowanej sieci indeksującej możesz przeczytać tutaj (opens in a new tab).
Dwa kluczowe aspekty to:
- Użytkownicy płacą indeksatorom za zapytania.
- Indeksatorzy stakują tokeny Graph (GRT).






