Composants de serveur et agents pour les applications web3
Introduction
Dans la plupart des cas, une application décentralisée (dapp) utilise un serveur pour distribuer le logiciel, mais toute l'interaction réelle se produit entre le client (généralement un navigateur Web) et la blockchain.
Cependant, il existe des cas où une application bénéficierait d'un composant serveur qui s'exécute de manière indépendante. Un tel serveur serait capable de répondre à des événements et à des requêtes provenant d'autres sources, comme une API, en émettant des transactions.
Il y a plusieurs tâches possibles qu'un tel serveur pourrait accomplir.
-
Détenteur d'état secret. Dans les jeux, il est souvent utile de ne pas mettre à la disposition des joueurs toutes les informations que le jeu connaît. Cependant, il n'y a pas de secrets sur la blockchain, toute information qui s'y trouve est facile à découvrir pour n'importe qui. Par conséquent, si une partie de l'état du jeu doit rester secrète, elle doit être stockée ailleurs (et éventuellement faire vérifier les effets de cet état à l'aide de preuves à divulgation nulle de connaissance).
-
Oracle centralisé. Si les enjeux sont suffisamment faibles, un serveur externe qui lit des informations en ligne puis les publie sur la chaîne peut être suffisant pour être utilisé comme un oracle.
-
Agent. Rien ne se passe sur la blockchain sans une transaction pour l'activer. Un serveur peut agir au nom d'un utilisateur pour effectuer des actions telles que l'arbitrage lorsque l'occasion se présente.
Exemple de programme
Vous pouvez voir un exemple de serveur sur github (opens in a new tab). Ce serveur écoute les événements provenant de ce contrat (opens in a new tab), une version modifiée du Greeter de Hardhat. Lorsque le message de bienvenue est modifié, il le rétablit.
Pour l'exécuter :
-
Cloner le dépôt.
1git clone https://github.com/qbzzt/20240715-server-component.git2cd 20240715-server-component -
Installez les paquets nécessaires. Si vous ne l'avez pas déjà, installez d'abord Node (opens in a new tab).
1npm install -
Modifiez
.envpour spécifier la clé privée d'un compte qui possède des ETH sur le réseau de test Holesky. Si vous n'avez pas d'ETH sur Holesky, vous pouvez utiliser ce faucet (opens in a new tab).1PRIVATE_KEY=0x <la clé privée va ici> -
Démarrer le serveur.
1npm start -
Allez sur un explorateur de blocs (opens in a new tab), et en utilisant une adresse différente de celle qui a la clé privée, modifiez le message de bienvenue. Voyez que le message de bienvenue est automatiquement rétabli.
Comment ça marche ?
La manière la plus simple de comprendre comment écrire un composant serveur est de parcourir l'exemple ligne par ligne.
src/app.ts
La grande majorité du programme est contenue dans src/app.ts (opens in a new tab).
Création des objets prérequis
1import {2 createPublicClient,3 createWalletClient,4 getContract,5 http,6 Address,7} from "viem"Ce sont les entités Viem (opens in a new tab) dont nous avons besoin, les fonctions et le type Address (opens in a new tab). Ce serveur est écrit en TypeScript (opens in a new tab), qui est une extension de JavaScript qui le rend fortement typé (opens in a new tab).
1import { privateKeyToAccount } from "viem/accounts"Cette fonction (opens in a new tab) nous permet de générer les informations du portefeuille, y compris l'adresse, correspondant à une clé privée.
1import { holesky } from "viem/chains"Pour utiliser une blockchain dans Viem, vous devez importer sa définition. Dans ce cas, nous voulons nous connecter à la blockchain de test Holesky (opens in a new tab).
1// C'est ainsi que nous ajoutons les définitions dans .env à process.env.2import * as dotenv from "dotenv"3dotenv.config()C'est ainsi que nous lisons .env dans l'environnement. Nous en avons besoin pour la clé privée (voir plus loin).
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 constAfficher toutPour utiliser un contrat, nous avons besoin de son adresse et de son . Nous fournissons les deux ici.
En JavaScript (et donc en TypeScript), vous ne pouvez pas attribuer une nouvelle valeur à une constante, mais vous pouvez modifier l'objet qui y est stocké. En utilisant le suffixe as const, nous indiquons à TypeScript que la liste elle-même est constante et ne peut pas être modifiée.
1const publicClient = createPublicClient({2 chain: holesky,3 transport: http(),4})Créer un client public (opens in a new tab) Viem. Les clients publics n'ont pas de clé privée attachée et ne peuvent donc pas envoyer de transactions. Ils peuvent appeler des fonctions view (opens in a new tab), lire les soldes des comptes, etc.
1const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)Les variables d'environnement sont disponibles dans process.env (opens in a new tab). Cependant, TypeScript est fortement typé. Une variable d'environnement peut être n'importe quelle chaîne de caractères, ou vide, donc le type d'une variable d'environnement est string | undefined. Cependant, une clé est définie dans Viem comme 0x${string} (0x suivi d'une chaîne de caractères). Ici, nous indiquons à TypeScript que la variable d'environnement PRIVATE_KEY sera de ce type. Si ce n'est pas le cas, nous obtiendrons une erreur d'exécution.
La fonction privateKeyToAccount (opens in a new tab) utilise ensuite cette clé privée pour créer un objet de compte complet.
1const walletClient = createWalletClient({2 account,3 chain: holesky,4 transport: http(),5})Ensuite, nous utilisons l'objet de compte pour créer un client de portefeuille (opens in a new tab). Ce client a une clé privée et une adresse, il peut donc être utilisé pour envoyer des transactions.
1const greeter = getContract({2 address: greeterAddress,3 abi: greeterABI,4 client: { public: publicClient, wallet: walletClient },5})Maintenant que nous avons tous les prérequis, nous pouvons enfin créer une instance de contrat (opens in a new tab). Nous utiliserons cette instance de contrat pour communiquer avec le contrat en chaîne.
Lecture depuis la blockchain
1console.log(`Message de bienvenue actuel :`, await greeter.read.greet())Les fonctions de contrat qui sont en lecture seule (view (opens in a new tab) et pure (opens in a new tab)) sont disponibles sous read. Dans ce cas, nous l'utilisons pour accéder à la fonction greet (opens in a new tab), qui renvoie le message de bienvenue.
JavaScript est monothread, donc lorsque nous lançons un processus de longue durée, nous devons spécifier que nous le faisons de manière asynchrone (opens in a new tab). L'appel à la blockchain, même pour une opération en lecture seule, nécessite un aller-retour entre l'ordinateur et un nœud de la blockchain. C'est la raison pour laquelle nous spécifions ici que le code doit await le résultat.
Si vous êtes intéressé par son fonctionnement, vous pouvez en lire plus ici (opens in a new tab), mais en pratique, tout ce que vous devez savoir, c'est que vous devez await les résultats si vous lancez une opération qui prend beaucoup de temps, et que toute fonction qui le fait doit être déclarée async.
Émission de transactions
1const setGreeting = async (greeting: string): Promise<any> => {C'est la fonction que vous appelez pour émettre une transaction qui modifie le message de bienvenue. Comme il s'agit d'une opération longue, la fonction est déclarée comme async. En raison de l'implémentation interne, toute fonction async doit retourner un objet Promise. Dans ce cas, Promise<any> signifie que nous ne spécifions pas ce qui sera exactement retourné dans la Promise.
1const txHash = await greeter.write.setGreeting([greeting])Le champ write de l'instance du contrat contient toutes les fonctions qui écrivent dans l'état de la blockchain (celles qui nécessitent l'envoi d'une transaction), telles que setGreeting (opens in a new tab). Les paramètres, s'il y en a, sont fournis sous forme de liste, et la fonction renvoie le hachage de la transaction.
1 console.log(`Correction en cours, voir https://eth-holesky.blockscout.com/tx/${txHash}`)23 return txHash4}Signaler le hachage de la transaction (dans le cadre d'une URL vers l'explorateur de blocs pour la visualiser) et le renvoyer.
Répondre aux événements
1greeter.watchEvent.SetGreeting({La fonction watchEvent (opens in a new tab) vous permet de spécifier qu'une fonction doit s'exécuter lorsqu'un événement est émis. Si vous ne vous souciez que d'un seul type d'événement (dans ce cas, SetGreeting), vous pouvez utiliser cette syntaxe pour vous limiter à ce type d'événement.
1 onLogs: logs => {La fonction onLogs est appelée lorsqu'il y a des entrées de journal. Dans Ethereum, « log » et « événement » sont généralement interchangeables.
1console.log(2 `L'adresse ${logs[0].args.sender} a changé le message de bienvenue en ${logs[0].args.greeting}`3)Il pourrait y avoir plusieurs événements, mais par souci de simplicité, nous ne nous intéressons qu'au premier. logs[0].args sont les arguments de l'événement, dans ce cas sender et greeting.
1 if (logs[0].args.sender != account.address)2 setGreeting(`${account.address} insiste pour que ce soit Hello!`)3 }4})Si l'expéditeur n'est pas ce serveur, utilisez setGreeting pour changer le message de bienvenue.
package.json
Ce fichier (opens in a new tab) contrôle la configuration de Node.js (opens in a new tab). Cet article n'explique que les définitions importantes.
1{2 "main": "dist/index.js",Cette définition spécifie quel fichier JavaScript exécuter.
1 "scripts": {2 "start": "tsc && node dist/app.js",3 },Les scripts sont diverses actions de l'application. Dans ce cas, le seul que nous ayons est start, qui compile puis exécute le serveur. La commande tsc fait partie du package typescript et compile TypeScript en JavaScript. Si vous voulez l'exécuter manuellement, il se trouve dans node_modules/.bin. La deuxième commande exécute le serveur.
1 "type": "module",Il existe plusieurs types d'applications Node JavaScript. Le type module nous permet d'avoir await dans le code de premier niveau, ce qui est important lorsque vous effectuez des opérations lentes (et donc asynchrones).
1 "devDependencies": {2 "@types/node": "^20.14.2",3 "typescript": "^5.4.5"4 },Ce sont des packages qui ne sont requis que pour le développement. Ici, nous avons besoin de typescript et comme nous l'utilisons avec Node.js, nous obtenons également les types pour les variables et les objets de Node, tels que process. La notation ^<version> (opens in a new tab) signifie cette version ou une version supérieure qui n'apporte pas de changements cassants. Voir ici (opens in a new tab) pour plus d'informations sur la signification des numéros de version.
1 "dependencies": {2 "dotenv": "^16.4.5",3 "viem": "2.14.1"4 }5}Ce sont des packages qui sont requis à l'exécution, lors de l'exécution de dist/app.js.
Conclusion
Le serveur centralisé que nous avons créé ici fait son travail, qui est d'agir en tant qu'agent pour un utilisateur. Toute autre personne qui souhaite que la dapp continue de fonctionner et qui est prête à dépenser le gaz peut exécuter une nouvelle instance du serveur avec sa propre adresse.
Cependant, cela ne fonctionne que lorsque les actions du serveur centralisé peuvent être facilement vérifiées. Si le serveur centralisé possède des informations d'état secrètes ou exécute des calculs difficiles, c'est une entité centralisée à laquelle vous devez faire confiance pour utiliser l'application, ce qui est exactement ce que les blockchains essaient d'éviter. Dans un futur article, je prévois de montrer comment utiliser les preuves à divulgation nulle de connaissance pour contourner ce problème.
Voir ici pour plus de mon travail (opens in a new tab).
Dernière mise à jour de la page : 25 février 2026