Zum Hauptinhalt springen

Serverkomponenten und Agenten für Web3-Apps

Agent
Server
Off-Chain
Dapps
Anfänger
Ori Pomerantz
15. Juli 2024
9 Minuten Lesezeit

Einführung

In den meisten Fällen verwendet eine dezentralisierte Anwendung einen Server, um die Software zu verteilen, aber die gesamte eigentliche Interaktion findet zwischen dem Client (typischerweise dem Webbrowser) und der Blockchain statt.

Normale Interaktion zwischen Webserver, Client und Blockchain

Es gibt jedoch einige Fälle, in denen eine Anwendung von einer unabhängig laufenden Serverkomponente profitieren würde. Ein solcher Server wäre in der Lage, auf Ereignisse und auf Anfragen aus anderen Quellen, wie z. B. einer API, durch die Ausgabe von Transaktionen zu reagieren.

Die Interaktion mit der Hinzufügung eines Servers

Es gibt mehrere mögliche Aufgaben, die ein solcher Server erfüllen könnte.

  • Halter eines geheimen Zustands. Beim Gaming ist es oft nützlich, den Spielern nicht alle Informationen zugänglich zu machen, die das Spiel kennt. Jedoch gibt es keine Geheimnisse auf der Blockchain; jede Information, die sich auf der Blockchain befindet, ist für jeden leicht herauszufinden. Wenn also ein Teil des Spielzustands geheim gehalten werden soll, muss er woanders gespeichert werden (und die Auswirkungen dieses Zustands möglicherweise mithilfe von Zero-Knowledge-Beweisen verifiziert werden).

  • Zentralisiertes Orakel. Wenn die Einsätze niedrig genug sind, kann ein externer Server, der einige Informationen online liest und sie dann auf der Chain veröffentlicht, gut genug sein, um als Orakel verwendet zu werden.

  • Agent. Nichts passiert auf der Blockchain ohne eine Transaktion, die es aktiviert. Ein Server kann im Namen eines Benutzers handeln, um Aktionen wie Arbitrage durchzuführen, wenn sich die Gelegenheit dazu bietet.

Beispielprogramm

Du kannst dir einen Beispielserver auf GitHub (opens in a new tab) ansehen. Dieser Server lauscht auf Ereignisse, die von diesem Vertrag (opens in a new tab) kommen, einer modifizierten Version von Hardhats Greeter. Wenn die Begrüßung geändert wird, ändert er sie wieder zurück.

Um ihn auszuführen:

  1. Klone das Repository.

    1git clone https://github.com/qbzzt/20240715-server-component.git
    2cd 20240715-server-component
1
22. Installiere die erforderlichen Pakete. Falls du es noch nicht hast, [installiere zuerst Node](https://nodejs.org/en/download/package-manager).
3
4 ```sh copy
5 npm install
  1. Bearbeite die .env-Datei, um den Private-Key eines Kontos anzugeben, das ETH im Holesky-Testnet hat. Wenn du keine ETH auf Holesky hast, kannst du dieses Faucet verwenden (opens in a new tab).

    1PRIVATE_KEY=0x <private key goes here>
1
24. Starte den Server.
3
4 ```sh copy
5 npm start
  1. Gehe zu einer Blocksuchmaschine (opens in a new tab) und ändere die Begrüßung unter Verwendung einer anderen Adresse als derjenigen, die den Private-Key besitzt. Du wirst sehen, dass die Begrüßung automatisch wieder zurückgeändert wird.

Wie funktioniert das?

Der einfachste Weg zu verstehen, wie man eine Serverkomponente schreibt, ist, das Beispiel Zeile für Zeile durchzugehen.

src/app.ts

Der weitaus größte Teil des Programms ist in src/app.ts (opens in a new tab) enthalten.

Erstellen der vorausgesetzten Objekte
1import {
2 createPublicClient,
3 createWalletClient,
4 getContract,
5 http,
6 Address,
7} from "viem"

Dies sind die Viem (opens in a new tab)-Entitäten, die wir benötigen: Funktionen und der Address-Typ (opens in a new tab). Dieser Server ist in TypeScript (opens in a new tab) geschrieben, einer Erweiterung von JavaScript, die es streng typisiert (opens in a new tab) macht.

1import { privateKeyToAccount } from "viem/accounts"

Diese Funktion (opens in a new tab) ermöglicht es uns, die Wallet-Informationen, einschließlich der Adresse, passend zu einem Private-Key zu generieren.

1import { holesky } from "viem/chains"

Um eine Blockchain in Viem zu verwenden, musst du deren Definition importieren. In diesem Fall möchten wir uns mit der Holesky (opens in a new tab)-Test-Blockchain verbinden.

1// So fügen wir die Definitionen in .env zu process.env hinzu.
2import * as dotenv from "dotenv"
3dotenv.config()

So lesen wir .env in die Umgebung ein. Wir benötigen dies für den Private-Key (siehe später).

1const greeterAddress : Address = "0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6"
2const greeterABI = [
3 {
4 "inputs": [
5 {
6 "internalType": "string",
7 "name": "_greeting",
8 "type": "string"
9 }
10 ],
11 "stateMutability": "nonpayable",
12 "type": "constructor"
13 },
14 .
15 .
16 .
17 {
18 "inputs": [
19 {
20 "internalType": "string",
21 "name": "_greeting",
22 "type": "string"
23 }
24 ],
25 "name": "setGreeting",
26 "outputs": [],
27 "stateMutability": "nonpayable",
28 "type": "function"
29 }
30] as const

Um einen Vertrag zu nutzen, benötigen wir seine Adresse und die dafür. Wir stellen hier beides bereit.

In JavaScript (und somit auch in TypeScript) kann man einer Konstanten keinen neuen Wert zuweisen, aber man kann das darin gespeicherte Objekt modifizieren. Durch die Verwendung des Suffixes as const teilen wir TypeScript mit, dass die Liste selbst konstant ist und nicht geändert werden darf.

1const publicClient = createPublicClient({
2 chain: holesky,
3 transport: http(),
4})

Erstelle einen Public Client (opens in a new tab) von Viem. Public Clients haben keinen angehängten Private-Key und können daher keine Transaktionen senden. Sie können view-Funktionen (opens in a new tab) aufrufen, Kontostände lesen usw.

1const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)

Die Umgebungsvariablen sind in process.env (opens in a new tab) verfügbar. TypeScript ist jedoch streng typisiert. Eine Umgebungsvariable kann ein beliebiger String oder leer sein, daher ist der Typ für eine Umgebungsvariable string | undefined. Ein Schlüssel ist in Viem jedoch als 0x${string} definiert (0x gefolgt von einem String). Hier teilen wir TypeScript mit, dass die Umgebungsvariable PRIVATE_KEY von diesem Typ sein wird. Wenn dies nicht der Fall ist, erhalten wir einen Laufzeitfehler.

Die Funktion privateKeyToAccount (opens in a new tab) verwendet dann diesen Private-Key, um ein vollständiges Konto-Objekt zu erstellen.

1const walletClient = createWalletClient({
2 account,
3 chain: holesky,
4 transport: http(),
5})

Als Nächstes verwenden wir das Konto-Objekt, um einen Wallet Client (opens in a new tab) zu erstellen. Dieser Client verfügt über einen Private-Key und eine Adresse, sodass er zum Senden von Transaktionen verwendet werden kann.

1const greeter = getContract({
2 address: greeterAddress,
3 abi: greeterABI,
4 client: { public: publicClient, wallet: walletClient },
5})

Da wir nun alle Voraussetzungen haben, können wir endlich eine Vertragsinstanz (opens in a new tab) erstellen. Wir werden diese Vertragsinstanz verwenden, um mit dem On-Chain-Vertrag zu kommunizieren.

Lesen von der Blockchain
1console.log(`Current greeting:`, await greeter.read.greet())

Die Vertragsfunktionen, die nur lesend sind (view (opens in a new tab) und pure (opens in a new tab)), sind unter read verfügbar. In diesem Fall verwenden wir es, um auf die Funktion greet (opens in a new tab) zuzugreifen, welche die Begrüßung zurückgibt.

JavaScript ist Single-Threaded. Wenn wir also einen lang andauernden Prozess anstoßen, müssen wir angeben, dass wir dies asynchron tun (opens in a new tab). Der Aufruf der Blockchain, selbst für eine reine Leseoperation, erfordert einen Roundtrip zwischen dem Computer und einem Blockchain-Knoten. Das ist der Grund, warum wir hier angeben, dass der Code auf das Ergebnis warten (await) muss.

Wenn du dich dafür interessierst, wie das funktioniert, kannst du hier darüber lesen (opens in a new tab). In der Praxis musst du jedoch nur wissen, dass du auf die Ergebnisse wartest (await), wenn du eine Operation startest, die lange dauert, und dass jede Funktion, die dies tut, als async deklariert werden muss.

Ausgeben von Transaktionen
1const setGreeting = async (greeting: string): Promise<any> => {

Dies ist die Funktion, die du aufrufst, um eine Transaktion auszugeben, welche die Begrüßung ändert. Da dies eine langwierige Operation ist, wird die Funktion als async deklariert. Aufgrund der internen Implementierung muss jede async-Funktion ein Promise-Objekt zurückgeben. In diesem Fall bedeutet Promise<any>, dass wir nicht genau spezifizieren, was im Promise zurückgegeben wird.

1const txHash = await greeter.write.setGreeting([greeting])

Das write-Feld der Vertragsinstanz enthält alle Funktionen, die in den Blockchain-Zustand schreiben (solche, die das Senden einer Transaktion erfordern), wie z. B. setGreeting (opens in a new tab). Die Parameter, falls vorhanden, werden als Liste bereitgestellt, und die Funktion gibt den Hash der Transaktion zurück.

1 console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)
2
3 return txHash
4}

Melde den Hash der Transaktion (als Teil einer URL zur Blocksuchmaschine, um ihn anzusehen) und gib ihn zurück.

Reagieren auf Ereignisse
1greeter.watchEvent.SetGreeting({

Die Funktion watchEvent (opens in a new tab) ermöglicht es dir anzugeben, dass eine Funktion ausgeführt werden soll, wenn ein Ereignis ausgelöst wird. Wenn du dich nur für einen bestimmten Ereignistyp interessierst (in diesem Fall SetGreeting), kannst du diese Syntax verwenden, um dich auf diesen Ereignistyp zu beschränken.

1 onLogs: logs => {

Die Funktion onLogs wird aufgerufen, wenn es Protokolleinträge (Logs) gibt. In Ethereum sind „Log“ und „Ereignis“ (Event) in der Regel austauschbar.

1console.log(
2 `Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`
3)

Es könnte mehrere Ereignisse geben, aber der Einfachheit halber kümmern wir uns nur um das erste. logs[0].args sind die Argumente des Ereignisses, in diesem Fall sender und greeting.

1 if (logs[0].args.sender != account.address)
2 setGreeting(`${account.address} insists on it being Hello!`)
3 }
4})

Wenn der Absender nicht dieser Server ist, verwende setGreeting, um die Begrüßung zu ändern.

package.json

Diese Datei (opens in a new tab) steuert die Node.js (opens in a new tab)-Konfiguration. Dieser Artikel erklärt nur die wichtigen Definitionen.

1{
2 "main": "dist/index.js",

Diese Definition gibt an, welche JavaScript-Datei ausgeführt werden soll.

1 "scripts": {
2 "start": "tsc && node dist/app.js",
3 },

Die Skripte sind verschiedene Anwendungsaktionen. In diesem Fall haben wir nur start, was den Server kompiliert und dann ausführt. Der Befehl tsc ist Teil des typescript-Pakets und kompiliert TypeScript zu JavaScript. Wenn du ihn manuell ausführen möchtest, befindet er sich in node_modules/.bin. Der zweite Befehl führt den Server aus.

1 "type": "module",

Es gibt mehrere Arten von JavaScript-Node-Anwendungen. Der Typ module ermöglicht es uns, await im Code auf oberster Ebene zu verwenden, was wichtig ist, wenn man langsame (und daher asynchrone) Operationen durchführt.

1 "devDependencies": {
2 "@types/node": "^20.14.2",
3 "typescript": "^5.4.5"
4 },

Dies sind Pakete, die nur für die Entwicklung benötigt werden. Hier brauchen wir typescript, und da wir es mit Node.js verwenden, holen wir uns auch die Typen für Node-Variablen und -Objekte, wie z. B. process. Die Notation ^<version> (opens in a new tab) bedeutet diese Version oder eine höhere Version, die keine Breaking Changes aufweist. Siehe hier (opens in a new tab) für weitere Informationen über die Bedeutung von Versionsnummern.

1 "dependencies": {
2 "dotenv": "^16.4.5",
3 "viem": "2.14.1"
4 }
5}

Dies sind Pakete, die zur Laufzeit benötigt werden, wenn dist/app.js ausgeführt wird.

Fazit

Der zentralisierte Server, den wir hier erstellt haben, erfüllt seine Aufgabe, nämlich als Agent für einen Benutzer zu fungieren. Jeder andere, der möchte, dass die Dapp weiterhin funktioniert, und bereit ist, das Gas auszugeben, kann eine neue Instanz des Servers mit seiner eigenen Adresse ausführen.

Dies funktioniert jedoch nur, wenn die Aktionen des zentralisierten Servers leicht verifiziert werden können. Wenn der zentralisierte Server geheime Zustandsinformationen hat oder schwierige Berechnungen durchführt, ist er eine zentralisierte Entität, der du vertrauen musst, um die Anwendung zu nutzen – was genau das ist, was Blockchains zu vermeiden versuchen. In einem zukünftigen Artikel plane ich zu zeigen, wie man Zero-Knowledge-Beweise verwendet, um dieses Problem zu umgehen.

Siehe hier für weitere meiner Arbeiten (opens in a new tab).

Letzte Aktualisierung der Seite: 3. März 2026

War dieses Tutorial hilfreich?