Přeskočit na hlavní obsah

Serverové komponenty a agenti pro web3 aplikace

agent
server
offchain
Začátečník
Ori Pomerantz
15. července 2024
8 minuta čtení

Úvod

Ve většině případů decentralizovaná aplikace používá server k distribuci softwaru, ale veškerá skutečná interakce probíhá mezi klientem (obvykle webovým prohlížečem) a blockchainem.

Normální interakce mezi webovým serverem, klientem a blockchainem

Existují však případy, kdy by aplikaci prospěla serverová komponenta, která běží nezávisle. Takový server by byl schopen reagovat na události a na požadavky, které přicházejí z jiných zdrojů, jako je API, vydáváním transakcí.

Interakce s přidaným serverem

Existuje několik možných úkolů, které by takový server mohl plnit.

  • Držitel tajného stavu. Při hraní je často užitečné, aby hráči neměli k dispozici všechny informace, které hra zná. Nicméně, na blockchainu neexistují žádná tajemství, jakoukoli informaci, která je v blockchainu, může kdokoli snadno zjistit. Proto, pokud má být část stavu hry utajena, musí být uložena jinde (a případně nechat účinky tohoto stavu ověřit pomocí důkazů s nulovou znalostí).

  • Centralizované orákulum. Pokud jsou sázky dostatečně nízké, externí server, který čte některé informace online a poté je zveřejňuje na řetězci, může být dostatečně dobrý na to, aby byl použit jako orákulum.

  • Agent. Na blockchainu se nic nestane bez transakce, která by to aktivovala. Server může jednat jménem uživatele a provádět akce, jako je arbitráž, když se naskytne příležitost.

Ukázkový program

Ukázkový server si můžete prohlédnout na GitHubu (opens in a new tab). Tento server naslouchá událostem pocházejícím z tohoto kontraktu (opens in a new tab), upravené verze Hardhat Greeteru. Když se pozdrav změní, změní ho zpět.

Spustíte ho takto:

  1. Naklonujte repozitář.

    1git clone https://github.com/qbzzt/20240715-server-component.git
    2cd 20240715-server-component
  2. Nainstalujte potřebné balíčky. Pokud jej ještě nemáte, nainstalujte nejprve Node (opens in a new tab).

    1npm install
  3. Upravte soubor .env a zadejte soukromý klíč účtu, který má ETH na testnetu Holesky. Pokud nemáte ETH na Holesky, můžete použít tento faucet (opens in a new tab).

    1PRIVATE_KEY=0x <zde vložte soukromý klíč>
  4. Spusťte server.

    1npm start
  5. Přejděte do prohlížeče bloků (opens in a new tab) a pomocí jiné adresy, než je ta, ke které máte soukromý klíč, upravte pozdrav. Uvidíte, že pozdrav je automaticky změněn zpět.

Jak to funguje? Jak to funguje

Nejjednodušší způsob, jak pochopit, jak napsat serverovou komponentu, je projít si ukázku řádek po řádku.

src/app.ts

Drtivá většina programu je obsažena v src/app.ts (opens in a new tab).

Vytvoření nezbytných objektů
1import {
2 createPublicClient,
3 createWalletClient,
4 getContract,
5 http,
6 Address,
7} from "viem"

Toto jsou entity Viem (opens in a new tab), které potřebujeme, funkce a typ Address (opens in a new tab). Tento server je napsán v TypeScriptu (opens in a new tab), což je rozšíření JavaScriptu, které ho činí silně typovaným (opens in a new tab).

1import { privateKeyToAccount } from "viem/accounts"

Tato funkce (opens in a new tab) nám umožňuje generovat informace o peněžence, včetně adresy, odpovídající soukromému klíči.

1import { holesky } from "viem/chains"

Abyste mohli v Viem používat blockchain, musíte importovat jeho definici. V tomto případě se chceme připojit k testovacímu blockchainu Holesky (opens in a new tab).

1// Takto přidáváme definice z .env do process.env.
2import * as dotenv from "dotenv"
3dotenv.config()

Takto načítáme .env do prostředí. Potřebujeme to pro soukromý klíč (viz dále).

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
Zobrazit vše

Pro použití kontraktu potřebujeme jeho adresu a . Obojí zde uvádíme.

V JavaScriptu (a tedy i v TypeScriptu) nemůžete konstantě přiřadit novou hodnotu, ale můžete upravit objekt, který je v ní uložen. Použitím přípony as const říkáme TypeScriptu, že seznam samotný je konstantní a nesmí být změněn.

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

Vytvořte Viem veřejného klienta (opens in a new tab). Veřejní klienti nemají připojený soukromý klíč, a proto nemohou odesílat transakce. Mohou volat view funkce (opens in a new tab), číst zůstatky na účtech atd.

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

Proměnné prostředí jsou k dispozici v process.env (opens in a new tab). TypeScript je však silně typovaný. Proměnná prostředí může být jakýkoli řetězec, nebo prázdná, takže typem pro proměnnou prostředí je string | undefined. Klíč je však v Viem definován jako 0x${string} (0x následované řetězcem). Zde říkáme TypeScriptu, že proměnná prostředí PRIVATE_KEY bude tohoto typu. Pokud ne, dojde k chybě za běhu.

Funkce privateKeyToAccount (opens in a new tab) pak použije tento soukromý klíč k vytvoření úplného objektu účtu.

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

Dále použijeme objekt účtu k vytvoření klienta peněženky (opens in a new tab). Tento klient má soukromý klíč a adresu, takže jej lze použít k odesílání transakcí.

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

Nyní, když máme všechny předpoklady, můžeme konečně vytvořit instanci kontraktu (opens in a new tab). Tuto instanci kontraktu použijeme ke komunikaci s on-chain kontraktem.

Čtení z blockchainu
1console.log(`Current greeting:`, await greeter.read.greet())

Funkce kontraktu, které jsou pouze pro čtení (view (opens in a new tab) a pure (opens in a new tab)), jsou dostupné pod read. V tomto případě ji používáme pro přístup k funkci greet (opens in a new tab), která vrací pozdrav.

JavaScript je jednovláknový, takže když spouštíme dlouhotrvající proces, musíme specifikovat, že to děláme asynchronně (opens in a new tab). Volání blockchainu, i pro operaci pouze pro čtení, vyžaduje obousměrnou komunikaci mezi počítačem a uzlem blockchainu. To je důvod, proč zde specifikujeme, že kód musí na výsledek await (počkat).

Pokud vás zajímá, jak to funguje, můžete si o tom přečíst zde (opens in a new tab), ale v praxi vše, co potřebujete vědět, je, že await (čekáte na) výsledky, pokud spustíte operaci, která trvá dlouho, a že jakákoli funkce, která to dělá, musí být deklarována jako async.

Vydávání transakcí
1const setGreeting = async (greeting: string): Promise<any> => {

Toto je funkce, kterou voláte pro vydání transakce, která mění pozdrav. Jelikož se jedná o dlouhou operaci, funkce je deklarována jako async. Kvůli interní implementaci musí každá async funkce vracet objekt Promise. V tomto případě Promise<any> znamená, že nespecifikujeme, co přesně bude v Promise vráceno.

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

Pole write instance kontraktu obsahuje všechny funkce, které zapisují do stavu blockchainu (ty, které vyžadují odeslání transakce), jako je setGreeting (opens in a new tab). Parametry, pokud nějaké jsou, jsou poskytnuty jako seznam a funkce vrací haš transakce.

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

Nahlaste haš transakce (jako součást adresy URL do prohlížeče bloků pro její zobrazení) a vraťte jej.

Reakce na události
1greeter.watchEvent.SetGreeting({

Funkce watchEvent (opens in a new tab) umožňuje specifikovat, že se má funkce spustit, když je emitována událost. Pokud vás zajímá pouze jeden typ události (v tomto případě SetGreeting), můžete použít tuto syntaxi k omezení se na tento typ události.

1 onLogs: logs => {

Funkce onLogs je volána, když existují záznamy protokolu (logy). V Ethereu jsou pojmy „log“ a „událost“ obvykle zaměnitelné.

1console.log(
2 `Adresa ${logs[0].args.sender} změnila pozdrav na ${logs[0].args.greeting}`
3)

Mohlo by zde být více událostí, ale pro jednoduchost nás zajímá pouze ta první. logs[0].args jsou argumenty události, v tomto případě sender a greeting.

1 if (logs[0].args.sender != account.address)
2 setGreeting(`${account.address} trvá na tom, aby to bylo Hello!`)
3 }
4})

Pokud odesílatel není tento server, použijte setGreeting ke změně pozdravu.

package.json

Tento soubor (opens in a new tab) řídí konfiguraci Node.js (opens in a new tab). Tento článek vysvětluje pouze důležité definice.

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

Tato definice určuje, který soubor JavaScriptu se má spustit.

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

Skripty jsou různé akce aplikace. V tomto případě máme pouze jeden, start, který kompiluje a poté spouští server. Příkaz tsc je součástí balíčku typescript a kompiluje TypeScript do JavaScriptu. Pokud jej chcete spustit ručně, nachází se v node_modules/.bin. Druhý příkaz spouští server.

1 "type": "module",

Existuje více typů JavaScriptových node aplikací. Typ module nám umožňuje mít await v kódu nejvyšší úrovně, což je důležité, když provádíte pomalé (a tedy asynchronní) operace.

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

Toto jsou balíčky, které jsou vyžadovány pouze pro vývoj. Zde potřebujeme typescript a protože ho používáme s Node.js, získáváme také typy pro node proměnné a objekty, jako je process. Zápis ^<verze> (opens in a new tab) znamená danou verzi nebo vyšší verzi, která neobsahuje zásadní změny. Více informací o významu čísel verzí naleznete zde (opens in a new tab).

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

Toto jsou balíčky, které jsou vyžadovány za běhu, při spouštění dist/app.js.

Závěr

Centralizovaný server, který jsme zde vytvořili, plní svůj úkol, kterým je jednat jako agent pro uživatele. Kdokoli jiný, kdo chce, aby dapp nadále fungoval a je ochoten utratit palivo, může spustit novou instanci serveru s vlastní adresou.

To však funguje pouze tehdy, když lze akce centralizovaného serveru snadno ověřit. Pokud má centralizovaný server nějaké tajné stavové informace nebo provádí složité výpočty, je to centralizovaná entita, které musíte důvěřovat, abyste mohli aplikaci používat, což je přesně to, čemu se blockchainy snaží vyhnout. V budoucím článku plánuji ukázat, jak tento problém obejít pomocí důkazů s nulovou znalostí.

Více z mé práce najdete zde (opens in a new tab).

Stránka naposledy aktualizována: 25. února 2026

Byl tento tutoriál užitečný?