Vai al contenuto principale

Componenti server e agenti per app web3

agente
server
fuori catena
dApp
Principiante
Ori Pomerantz
15 luglio 2024
9 minuti di lettura

Introduzione

Nella maggior parte dei casi, un'app decentralizzata utilizza un server per distribuire il software, ma tutta l'interazione effettiva avviene tra il client (in genere, il browser web) e la blockchain.

Interazione normale tra server web, client e blockchain

Tuttavia, ci sono alcuni casi in cui un'applicazione trarrebbe vantaggio dall'avere un componente server che viene eseguito in modo indipendente. Un tale server sarebbe in grado di rispondere agli eventi e alle richieste provenienti da altre fonti, come un'API, emettendo transazioni.

L'interazione con l'aggiunta di un server

Ci sono diverse possibili attività che un tale server potrebbe svolgere.

  • Detentore di uno stato segreto. Nel gaming è spesso utile non avere tutte le informazioni note al gioco a disposizione dei giocatori. Tuttavia, non ci sono segreti sulla blockchain, qualsiasi informazione presente nella blockchain è facile da scoprire per chiunque. Pertanto, se parte dello stato del gioco deve essere mantenuta segreta, deve essere archiviata altrove (e possibilmente far verificare gli effetti di tale stato utilizzando prove a conoscenza-zero).

  • Oracolo centralizzato. Se la posta in gioco è sufficientemente bassa, un server esterno che legge alcune informazioni online e poi le pubblica sulla catena potrebbe essere sufficiente per essere utilizzato come oracolo.

  • Agente. Non succede nulla sulla blockchain senza una transazione che lo attivi. Un server può agire per conto di un utente per eseguire azioni come l'arbitraggio quando se ne presenta l'opportunità.

Programma di esempio

Puoi vedere un server di esempio su github (opens in a new tab). Questo server ascolta gli eventi provenienti da questo contratto (opens in a new tab), una versione modificata del Greeter di Hardhat. Quando il saluto viene modificato, lo ripristina.

Per eseguirlo:

  1. Clona il repository.

    1git clone https://github.com/qbzzt/20240715-server-component.git
    2cd 20240715-server-component
1
22. Installa i pacchetti necessari. Se non lo hai già fatto, [installa prima Node](https://nodejs.org/en/download/package-manager).
3
4 ```sh copy
5 npm install
  1. Modifica .env per specificare la chiave privata di un account che possiede ETH sulla rete di test Holesky. Se non hai ETH su Holesky, puoi usare questo rubinetto (opens in a new tab).

    1PRIVATE_KEY=0x <private key goes here>
1
24. Avvia il server.
3
4 ```sh copy
5 npm start
  1. Vai su un esploratore di blocchi (opens in a new tab) e, utilizzando un indirizzo diverso da quello che possiede la chiave privata, modifica il saluto. Vedrai che il saluto viene automaticamente ripristinato.

Come funziona?

Il modo più semplice per capire come scrivere un componente server è esaminare l'esempio riga per riga.

src/app.ts

La stragrande maggioranza del programma è contenuta in src/app.ts (opens in a new tab).

Creazione degli oggetti prerequisiti
1import {
2 createPublicClient,
3 createWalletClient,
4 getContract,
5 http,
6 Address,
7} from "viem"

Queste sono le entità di Viem (opens in a new tab) di cui abbiamo bisogno, le funzioni e il tipo Address (opens in a new tab). Questo server è scritto in TypeScript (opens in a new tab), che è un'estensione di JavaScript che lo rende fortemente tipizzato (opens in a new tab).

1import { privateKeyToAccount } from "viem/accounts"

Questa funzione (opens in a new tab) ci consente di generare le informazioni del portafoglio, incluso l'indirizzo, corrispondenti a una chiave privata.

1import { holesky } from "viem/chains"

Per utilizzare una blockchain in Viem è necessario importarne la definizione. In questo caso, vogliamo connetterci alla blockchain di test Holesky (opens in a new tab).

1// Ecco come aggiungiamo le definizioni in .env a process.env.
2import * as dotenv from "dotenv"
3dotenv.config()

Ecco come leggiamo .env nell'ambiente. Ne abbiamo bisogno per la chiave privata (vedi in seguito).

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
Mostra tutto

Per utilizzare un contratto abbiamo bisogno del suo indirizzo e della sua . Li forniamo entrambi qui.

In JavaScript (e quindi in TypeScript) non puoi assegnare un nuovo valore a una costante, ma puoi modificare l'oggetto in essa memorizzato. Utilizzando il suffisso as const stiamo dicendo a TypeScript che l'elenco stesso è costante e non può essere modificato.

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

Crea un client pubblico (opens in a new tab) Viem. I client pubblici non hanno una chiave privata associata e pertanto non possono inviare transazioni. Possono chiamare funzioni view (opens in a new tab), leggere i saldi degli account, ecc.

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

Le variabili d'ambiente sono disponibili in process.env (opens in a new tab). Tuttavia, TypeScript è fortemente tipizzato. Una variabile d'ambiente può essere qualsiasi stringa, o vuota, quindi il tipo per una variabile d'ambiente è string | undefined. Tuttavia, una chiave è definita in Viem come 0x${string} (0x seguito da una stringa). Qui diciamo a TypeScript che la variabile d'ambiente PRIVATE_KEY sarà di quel tipo. In caso contrario, otterremo un errore di runtime.

La funzione privateKeyToAccount (opens in a new tab) utilizza quindi questa chiave privata per creare un oggetto account completo.

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

Successivamente, utilizziamo l'oggetto account per creare un client portafoglio (opens in a new tab). Questo client ha una chiave privata e un indirizzo, quindi può essere utilizzato per inviare transazioni.

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

Ora che abbiamo tutti i prerequisiti, possiamo finalmente creare un'istanza del contratto (opens in a new tab). Utilizzeremo questa istanza del contratto per comunicare con il contratto on-chain.

Lettura dalla blockchain
1console.log(`Current greeting:`, await greeter.read.greet())

Le funzioni del contratto di sola lettura (view (opens in a new tab) e pure (opens in a new tab)) sono disponibili sotto read. In questo caso, lo utilizziamo per accedere alla funzione greet (opens in a new tab), che restituisce il saluto.

JavaScript è a thread singolo, quindi quando avviamo un processo di lunga durata dobbiamo specificare che lo facciamo in modo asincrono (opens in a new tab). Chiamare la blockchain, anche per un'operazione di sola lettura, richiede un viaggio di andata e ritorno tra il computer e un nodo della blockchain. Questo è il motivo per cui specifichiamo qui che il codice deve attendere (await) il risultato.

Se sei interessato a come funziona, puoi leggerlo qui (opens in a new tab), ma in termini pratici tutto ciò che devi sapere è che devi usare await per i risultati se avvii un'operazione che richiede molto tempo, e che qualsiasi funzione che lo fa deve essere dichiarata come async.

Emissione di transazioni
1const setGreeting = async (greeting: string): Promise<any> => {

Questa è la funzione che chiami per emettere una transazione che modifica il saluto. Poiché si tratta di un'operazione lunga, la funzione è dichiarata come async. A causa dell'implementazione interna, qualsiasi funzione async deve restituire un oggetto Promise. In questo caso, Promise<any> significa che non specifichiamo cosa verrà esattamente restituito nella Promise.

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

Il campo write dell'istanza del contratto contiene tutte le funzioni che scrivono nello stato della blockchain (quelle che richiedono l'invio di una transazione), come setGreeting (opens in a new tab). I parametri, se presenti, vengono forniti come elenco e la funzione restituisce l'hash della transazione.

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

Segnala l'hash della transazione (come parte di un URL all'esploratore di blocchi per visualizzarlo) e restituiscilo.

Rispondere agli eventi
1greeter.watchEvent.SetGreeting({

La funzione watchEvent (opens in a new tab) ti consente di specificare che una funzione deve essere eseguita quando viene emesso un evento. Se ti interessa solo un tipo di evento (in questo caso, SetGreeting), puoi utilizzare questa sintassi per limitarti a quel tipo di evento.

1 onLogs: logs => {

La funzione onLogs viene chiamata quando ci sono voci di registro. In Ethereum "log" ed "evento" sono solitamente intercambiabili.

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

Potrebbero esserci più eventi, ma per semplicità ci interessa solo il primo. logs[0].args sono gli argomenti dell'evento, in questo caso sender e greeting.

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

Se il mittente non è questo server, usa setGreeting per cambiare il saluto.

package.json

Questo file (opens in a new tab) controlla la configurazione di Node.js (opens in a new tab). Questo articolo spiega solo le definizioni importanti.

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

Questa definizione specifica quale file JavaScript eseguire.

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

Gli script sono varie azioni dell'applicazione. In questo caso, l'unico che abbiamo è start, che compila e poi esegue il server. Il comando tsc fa parte del pacchetto typescript e compila TypeScript in JavaScript. Se vuoi eseguirlo manualmente, si trova in node_modules/.bin. Il secondo comando esegue il server.

1 "type": "module",

Esistono diversi tipi di applicazioni node JavaScript. Il tipo module ci consente di avere await nel codice di livello superiore, il che è importante quando si eseguono operazioni lente (e quindi asincrone).

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

Questi sono pacchetti richiesti solo per lo sviluppo. Qui abbiamo bisogno di typescript e, poiché lo stiamo utilizzando con Node.js, stiamo anche ottenendo i tipi per le variabili e gli oggetti di node, come process. La notazione ^<version> (opens in a new tab) indica quella versione o una versione superiore che non presenta modifiche incompatibili. Vedi qui (opens in a new tab) per maggiori informazioni sul significato dei numeri di versione.

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

Questi sono pacchetti richiesti in fase di esecuzione, quando si esegue dist/app.js.

Conclusione

Il server centralizzato che abbiamo creato qui fa il suo lavoro, ovvero agire come agente per un utente. Chiunque altro voglia che la dApp continui a funzionare e sia disposto a spendere il gas può eseguire una nuova istanza del server con il proprio indirizzo.

Tuttavia, questo funziona solo quando le azioni del server centralizzato possono essere facilmente verificate. Se il server centralizzato ha informazioni di stato segrete o esegue calcoli difficili, è un'entità centralizzata di cui devi fidarti per utilizzare l'applicazione, che è esattamente ciò che le blockchain cercano di evitare. In un articolo futuro ho intenzione di mostrare come utilizzare le prove a conoscenza-zero per aggirare questo problema.

Vedi qui per altri miei lavori (opens in a new tab).

Ultimo aggiornamento della pagina: 25 febbraio 2026

Questo tutorial è stato utile?