Серверні компоненти та агенти для web3-додатків
Вступ
У більшості випадків децентралізований додаток використовує сервер для розповсюдження програмного забезпечення, але вся фактична взаємодія відбувається між клієнтом (зазвичай веб-браузером) і блокчейном.
Однак є випадки, коли додатку буде корисно мати серверний компонент, який працює незалежно. Такий сервер зможе реагувати на події та запити з інших джерел, наприклад, API, шляхом випуску транзакцій.
Є кілька можливих завдань, які такий сервер міг би виконувати.
-
Зберігач таємного стану. В іграх часто буває корисно, щоб не вся інформація, якою володіє гра, була доступною гравцям. Однак у блокчейні немає секретів, будь-яку інформацію в блокчейні будь-хто може легко дізнатися. Тому, якщо частина ігрового стану має зберігатися в секреті, її потрібно зберігати деінде (і, можливо, перевіряти наслідки цього стану за допомогою доказів із нульовим розголошенням).
-
Централізований оракул. Якщо ставки досить низькі, зовнішній сервер, який зчитує певну інформацію онлайн, а потім публікує її в ланцюжку, може бути достатньо хорошим для використання як оракул.
-
Агент. У блокчейні нічого не відбувається без транзакції, яка б це активувала. Сервер може діяти від імені користувача, щоб виконувати такі дії, як арбітраж, коли з’являється така можливість.
Приклад програми
Ви можете побачити приклад сервера на Githubopens in a new tab. Цей сервер прослуховує події, що надходять від цього контрактуopens in a new tab, модифікованої версії Hardhat Greeter. Коли привітання змінюється, він змінює його назад.
Щоб запустити її:
-
Клонуйте репозиторій.
1git clone https://github.com/qbzzt/20240715-server-component.git2cd 20240715-server-component -
Встановіть необхідні пакети. Якщо у вас його ще немає, спочатку встановіть Nodeopens in a new tab.
1npm install -
Відредагуйте
.env, щоб вказати приватний ключ облікового запису, який має ETH у тестовій мережі Holesky. Якщо у вас немає ETH у мережі Holesky, ви можете скористатися цим краномopens in a new tab.1PRIVATE_KEY=0x <тут ваш приватний ключ> -
Запустіть сервер.
1npm start -
Перейдіть до оглядача блоківopens in a new tab і, використовуючи іншу адресу, ніж та, що має приватний ключ, змініть привітання. Переконайтеся, що привітання автоматично змінюється назад.
Як це працює?
Найпростіший спосіб зрозуміти, як написати серверний компонент, — це розібрати приклад рядок за рядком.
src/app.ts
Переважна більшість програми міститься в src/app.tsopens in a new tab.
Створення необхідних об'єктів
1import {2 createPublicClient,3 createWalletClient,4 getContract,5 http,6 Address,7} from "viem"Це сутності Viemopens in a new tab, які нам потрібні, функції та тип Addressopens in a new tab. Цей сервер написаний на TypeScriptopens in a new tab, який є розширенням JavaScript, що робить його сильно типізованимopens in a new tab.
1import { privateKeyToAccount } from "viem/accounts"Ця функціяopens in a new tab дозволяє нам генерувати інформацію про гаманець, включно з адресою, що відповідає приватному ключу.
1import { holesky } from "viem/chains"Щоб використовувати блокчейн у Viem, вам потрібно імпортувати його визначення. У цьому випадку ми хочемо підключитися до тестового блокчейну Holeskyopens in a new tab.
1// Так ми додаємо визначення з .env до process.env.2import * as dotenv from "dotenv"3dotenv.config()Так ми зчитуємо .env у середовище. Це потрібно нам для приватного ключа (див. далі).
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Показати всеЩоб використовувати контракт, нам потрібна його адреса та для нього. Ми надаємо обидва тут.
У JavaScript (а отже, і в TypeScript) не можна присвоїти нове значення константі, але можна змінити об'єкт, який у ній зберігається. Використовуючи суфікс as const, ми повідомляємо TypeScript, що сам список є константою і не може бути змінений.
1const publicClient = createPublicClient({2 chain: holesky,3 transport: http(),4})Створюємо публічний клієнтopens in a new tab Viem. Публічні клієнти не мають прикріпленого приватного ключа, а отже, не можуть надсилати транзакції. Вони можуть викликати view функціїopens in a new tab, зчитувати баланси облікових записів тощо.
1const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)Змінні середовища доступні в process.envopens in a new tab. Однак TypeScript є сильно типізованим. Змінна середовища може бути будь-яким рядком або порожньою, тому тип для змінної середовища — string | undefined. Однак ключ у Viem визначається як 0x${string} (0x з подальшим рядком). Тут ми повідомляємо TypeScript, що змінна середовища PRIVATE_KEY буде цього типу. Якщо це не так, ми отримаємо помилку виконання.
Функція privateKeyToAccountopens in a new tab потім використовує цей приватний ключ для створення повного об'єкта облікового запису.
1const walletClient = createWalletClient({2 account,3 chain: holesky,4 transport: http(),5})Далі ми використовуємо об'єкт облікового запису для створення клієнта гаманцяopens in a new tab. Цей клієнт має приватний ключ та адресу, тому його можна використовувати для надсилання транзакцій.
1const greeter = getContract({2 address: greeterAddress,3 abi: greeterABI,4 client: { public: publicClient, wallet: walletClient },5})Тепер, коли ми маємо всі необхідні умови, ми нарешті можемо створити екземпляр контрактуopens in a new tab. Ми будемо використовувати цей екземпляр контракту для зв'язку з контрактом у ланцюжку.
Читання з блокчейну
1console.log(`Current greeting:`, await greeter.read.greet())Функції контракту, які доступні лише для читання (viewopens in a new tab та pureopens in a new tab), доступні під read. У цьому випадку ми використовуємо його для доступу до функції greetopens in a new tab, яка повертає привітання.
JavaScript є однопотоковим, тому, коли ми запускаємо тривалий процес, нам потрібно вказати, що ми робимо це асинхронноopens in a new tab. Виклик блокчейну, навіть для операції лише для читання, вимагає обміну даними між комп'ютером і вузлом блокчейну. Ось чому ми вказуємо тут, що код повинен await (очікувати) на результат.
Якщо вас цікавить, як це працює, ви можете прочитати про це тутopens in a new tab, але з практичної точки зору все, що вам потрібно знати, це те, що ви await (очікуєте) результатів, якщо ви починаєте операцію, яка займає багато часу, і що будь-яка функція, яка це робить, має бути оголошена як async.
Випуск транзакцій
1const setGreeting = async (greeting: string): Promise<any> => {Це функція, яку ви викликаєте, щоб випустити транзакцію, яка змінює привітання. Оскільки це тривала операція, функція оголошена як async. Через внутрішню реалізацію будь-яка async функція повинна повертати об'єкт Promise. У цьому випадку Promise<any> означає, що ми не вказуємо, що саме буде повернуто в Promise.
1const txHash = await greeter.write.setGreeting([greeting])Поле write екземпляра контракту має всі функції, що записують у стан блокчейну (ті, що вимагають надсилання транзакції), наприклад setGreetingopens in a new tab. Параметри, якщо вони є, надаються у вигляді списку, і функція повертає хеш транзакції.
1 console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)23 return txHash4}Повідомте хеш транзакції (як частину URL-адреси до оглядача блоків для її перегляду) і поверніть його.
Реагування на події
1greeter.watchEvent.SetGreeting({Функція watchEventopens in a new tab дозволяє вказати, що функція має виконуватися, коли виникає подія. Якщо вас цікавить лише один тип подій (у цьому випадку SetGreeting), ви можете використовувати цей синтаксис, щоб обмежитися цим типом подій.
1 onLogs: logs => {Функція onLogs викликається, коли є записи в журналі. У Ethereum log (журнал) і event (подія) зазвичай є взаємозамінними.
1console.log(2 `Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`3)Може бути кілька подій, але для простоти нас цікавить лише перша. logs[0].args — це аргументи події, у цьому випадку sender і greeting.
1 if (logs[0].args.sender != account.address)2 setGreeting(`${account.address} insists on it being Hello!`)3 }4})Якщо відправником є не цей сервер, використовуйте setGreeting, щоб змінити привітання.
package.json
Цей файлopens in a new tab керує конфігурацією Node.jsopens in a new tab. У цій статті пояснюються лише важливі визначення.
1{2 "main": "dist/index.js",Це визначення вказує, який файл JavaScript запускати.
1 "scripts": {2 "start": "tsc && node dist/app.js",3 },Скрипти — це різні дії програми. У цьому випадку єдиний, що ми маємо, це start, який компілює, а потім запускає сервер. Команда tsc є частиною пакета typescript і компілює TypeScript у JavaScript. Якщо ви хочете запустити його вручну, він знаходиться в node_modules/.bin. Друга команда запускає сервер.
1 "type": "module",Існує кілька типів додатків вузлів JavaScript. Тип module дозволяє нам мати await у коді верхнього рівня, що важливо, коли ви виконуєте повільні (і тому асинхронні) операції.
1 "devDependencies": {2 "@types/node": "^20.14.2",3 "typescript": "^5.4.5"4 },Це пакети, які потрібні лише для розробки. Тут нам потрібен typescript, і оскільки ми використовуємо його з Node.js, ми також отримуємо типи для змінних і об’єктів вузла, наприклад process. Нотація ^<version>opens in a new tab означає цю версію або вищу версію, яка не має кардинальних змін. Див. тутopens in a new tab для отримання додаткової інформації про значення номерів версій.
1 "dependencies": {2 "dotenv": "^16.4.5",3 "viem": "2.14.1"4 }5}Це пакети, які потрібні під час виконання, під час запуску dist/app.js.
Висновок
Централізований сервер, який ми тут створили, виконує свою роботу, яка полягає в тому, щоб діяти як агент для користувача. Будь-хто інший, хто хоче, щоб dapp продовжував функціонувати, і готовий витрачати газ, може запустити новий екземпляр сервера зі своєю власною адресою.
Однак це працює лише тоді, коли дії централізованого сервера можна легко перевірити. Якщо централізований сервер має будь-яку секретну інформацію про стан або виконує складні обчислення, це централізована сутність, якій потрібно довіряти, щоб використовувати додаток, а саме цього намагаються уникнути блокчейни. У наступній статті я планую показати, як використовувати докази з нульовим розголошенням, щоб обійти цю проблему.
Більше моїх робіт дивіться тутopens in a new tab.
Останні оновлення сторінки: 9 вересня 2025 р.