Серверні компоненти та агенти для web3-застосунків
Вступ
У більшості випадків децентралізований застосунок (dapp) використовує сервер для розповсюдження програмного забезпечення, але вся фактична взаємодія відбувається між клієнтом (зазвичай веббраузером) та блокчейном.
Однак існують випадки, коли застосунку було б корисно мати серверний компонент, який працює незалежно. Такий сервер міг би реагувати на події та на запити, що надходять з інших джерел, таких як API, шляхом надсилання транзакцій.
Існує кілька можливих завдань, які міг би виконувати такий сервер.
-
Зберігач секретного стану. В іграх часто корисно, щоб не вся інформація, відома грі, була доступна гравцям. Однак у блокчейні немає секретів, будь-яку інформацію, що знаходиться в блокчейні, легко дізнатися будь-кому. Тому, якщо частину стану гри потрібно тримати в таємниці, її слід зберігати в іншому місці (і, можливо, перевіряти наслідки цього стану за допомогою доведень з нульовим розголошенням).
-
Централізований оракул. Якщо ставки достатньо низькі, зовнішній сервер, який зчитує певну інформацію в інтернеті, а потім публікує її в ланцюзі, може бути цілком придатним для використання як оракул.
-
Агент. У блокчейні нічого не відбувається без транзакції, яка б це активувала. Сервер може діяти від імені користувача для виконання таких дій, як арбітраж, коли з'являється така можливість.
Приклад програми
Ви можете переглянути приклад сервера на GitHub (opens in a new tab). Цей сервер прослуховує події, що надходять від цього контракту (opens in a new tab), модифікованої версії Greeter від Hardhat. Коли привітання змінюється, він змінює його назад.
Щоб запустити його:
-
Клонуйте репозиторій.
git clone https://github.com/qbzzt/20240715-server-component.git cd 20240715-server-component -
Встановіть необхідні пакети. Якщо у вас його ще немає, спочатку встановіть Node.js (opens in a new tab).
npm install -
Відредагуйте
.env, щоб вказати приватний ключ акаунта, який має ETH у тестовій мережі Голескі. Якщо у вас немає ETH у Голескі, ви можете скористатися цим краном (opens in a new tab).PRIVATE_KEY=0x <private key goes here> -
Запустіть сервер.
npm start -
Перейдіть до оглядача блоків (opens in a new tab) і, використовуючи іншу адресу (не ту, від якої у вас є приватний ключ), змініть привітання. Переконайтеся, що привітання автоматично змінюється назад.
Як це працює?
Найпростіший спосіб зрозуміти, як написати серверний компонент — це розібрати приклад рядок за рядком.
src/app.ts
Переважна більшість програми міститься у src/app.ts (opens in a new tab).
Створення необхідних об'єктів
import {
createPublicClient,
createWalletClient,
getContract,
http,
Address,
} from "viem"
Це сутності Viem (opens in a new tab), які нам потрібні: функції та тип Address (opens in a new tab). Цей сервер написаний на TypeScript (opens in a new tab), який є розширенням JavaScript, що робить його строго типізованим (opens in a new tab).
import { privateKeyToAccount } from "viem/accounts"
Ця функція (opens in a new tab) дозволяє нам генерувати інформацію про гаманець, включно з адресою, що відповідає приватному ключу.
import { holesky } from "viem/chains"
Щоб використовувати блокчейн у Viem, потрібно імпортувати його визначення. У цьому випадку ми хочемо підключитися до тестового блокчейну Голескі (opens in a new tab).
// Ось як ми додаємо визначення з .env до process.env.
import * as dotenv from "dotenv"
dotenv.config()
Ось як ми зчитуємо .env у середовище. Це потрібно нам для приватного ключа (див. далі).
const greeterAddress : Address = "0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6"
const greeterABI = [
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
.
.
.
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
] as const
Щоб використовувати контракт, нам потрібна його адреса та для нього. Ми надаємо і те, і інше тут.
У JavaScript (а отже, і в TypeScript) ви не можете призначити нове значення константі, але ви можете змінити об'єкт, який у ній зберігається. Використовуючи суфікс as const, ми повідомляємо TypeScript, що сам список є константою і не може бути змінений.
const publicClient = createPublicClient({
chain: holesky,
transport: http(),
})
Створіть публічний клієнт (opens in a new tab) Viem. Публічні клієнти не мають прив'язаного приватного ключа, а тому не можуть надсилати транзакції. Вони можуть викликати функції view (opens in a new tab), зчитувати баланси акаунтів тощо.
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
Змінні середовища доступні в process.env (opens in a new tab). Однак TypeScript є строго типізованим. Змінна середовища може бути будь-яким рядком або порожньою, тому тип для змінної середовища — string | undefined. Проте ключ у Viem визначається як 0x${string} (0x з наступним рядком). Тут ми повідомляємо TypeScript, що змінна середовища PRIVATE_KEY матиме саме цей тип. Якщо це не так, ми отримаємо помилку під час виконання.
Потім функція privateKeyToAccount (opens in a new tab) використовує цей приватний ключ для створення повноцінного об'єкта акаунта.
const walletClient = createWalletClient({
account,
chain: holesky,
transport: http(),
})
Далі ми використовуємо об'єкт акаунта для створення клієнта гаманця (opens in a new tab). Цей клієнт має приватний ключ та адресу, тому його можна використовувати для надсилання транзакцій.
const greeter = getContract({
address: greeterAddress,
abi: greeterABI,
client: { public: publicClient, wallet: walletClient },
})
Тепер, коли у нас є всі необхідні компоненти, ми нарешті можемо створити екземпляр контракту (opens in a new tab). Ми будемо використовувати цей екземпляр контракту для зв'язку з ончейн-контрактом.
Зчитування з блокчейну
console.log(`Current greeting:`, await greeter.read.greet())
Функції контракту, які призначені лише для зчитування (view (opens in a new tab) та pure (opens in a new tab)), доступні в read. У цьому випадку ми використовуємо його для доступу до функції greet (opens in a new tab), яка повертає привітання.
JavaScript є однопотоковим, тому, коли ми запускаємо тривалий процес, нам потрібно вказати, що ми робимо це асинхронно (opens in a new tab). Звернення до блокчейну, навіть для операції зчитування, вимагає двостороннього зв'язку між комп'ютером і вузлом блокчейну. Саме тому ми вказуємо тут, що код повинен очікувати (await) на результат.
Якщо вам цікаво, як це працює, ви можете прочитати про це тут (opens in a new tab), але на практиці все, що вам потрібно знати, це те, що ви використовуєте await для результатів, якщо починаєте операцію, яка займає багато часу, і що будь-яка функція, яка це робить, має бути оголошена як async.
Надсилання транзакцій
const setGreeting = async (greeting: string): Promise<any> => {
Це функція, яку ви викликаєте для надсилання транзакції, що змінює привітання. Оскільки це тривала операція, функція оголошена як async. Через внутрішню реалізацію будь-яка функція async повинна повертати об'єкт Promise. У цьому випадку Promise<any> означає, що ми не вказуємо, що саме буде повернуто в Promise.
const txHash = await greeter.write.setGreeting([greeting])
Поле write екземпляра контракту містить усі функції, які записують у стан блокчейну (ті, що вимагають надсилання транзакції), наприклад setGreeting (opens in a new tab). Параметри, якщо такі є, надаються у вигляді списку, а функція повертає хеш транзакції.
console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)
return txHash
}
Повідомте хеш транзакції (як частину URL-адреси до оглядача блоків для її перегляду) та поверніть його.
Реагування на події
greeter.watchEvent.SetGreeting({
Функція watchEvent (opens in a new tab) дозволяє вказати, що функція має виконуватися під час випромінювання події. Якщо вас цікавить лише один тип події (у цьому випадку SetGreeting), ви можете використати цей синтаксис, щоб обмежитися цим типом події.
onLogs: logs => {
Функція onLogs викликається, коли є записи логів. В Етеріумі «лог» та «подія» зазвичай є взаємозамінними поняттями.
console.log(
`Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`
)
Може бути кілька подій, але для простоти нас цікавить лише перша. logs[0].args — це аргументи події, у цьому випадку sender та greeting.
if (logs[0].args.sender != account.address)
setGreeting(`${account.address} insists on it being Hello!`)
}
})
Якщо відправником є не цей сервер, використовуйте setGreeting для зміни привітання.
package.json
Цей файл (opens in a new tab) керує конфігурацією Node.js (opens in a new tab). У цій статті пояснюються лише важливі визначення.
{
"main": "dist/index.js",
Це визначення вказує, який файл JavaScript потрібно запустити.
"scripts": {
"start": "tsc && node dist/app.js",
},
Скрипти — це різні дії застосунку. У цьому випадку єдиний скрипт, який у нас є, це start, який компілює, а потім запускає сервер. Команда tsc є частиною пакета typescript і компілює TypeScript у JavaScript. Якщо ви хочете запустити її вручну, вона знаходиться в node_modules/.bin. Друга команда запускає сервер.
"type": "module",
Існує кілька типів застосунків Node.js на JavaScript. Тип module дозволяє нам використовувати await у коді верхнього рівня, що важливо, коли ви виконуєте повільні (і тому асинхронні) операції.
"devDependencies": {
"@types/node": "^20.14.2",
"typescript": "^5.4.5"
},
Це пакети, які потрібні лише для розробки. Тут нам потрібен typescript, і оскільки ми використовуємо його з Node.js, ми також отримуємо типи для змінних та об'єктів Node, таких як process. Нотація ^<version> (opens in a new tab) означає цю версію або вищу версію, яка не має критичних змін. Дивіться тут (opens in a new tab) для отримання додаткової інформації про значення номерів версій.
"dependencies": {
"dotenv": "^16.4.5",
"viem": "2.14.1"
}
}
Це пакети, які потрібні під час виконання, коли запускається dist/app.js.
Висновок
Централізований сервер, який ми тут створили, виконує свою роботу, яка полягає в тому, щоб діяти як агент для користувача. Будь-хто інший, хто хоче, щоб децентралізований застосунок (dapp) продовжував функціонувати, і готовий витрачати газ, може запустити новий екземпляр сервера зі своєю власною адресою.
Однак це працює лише тоді, коли дії централізованого сервера можна легко перевірити. Якщо централізований сервер має будь-яку секретну інформацію про стан або виконує складні обчислення, він є централізованою сутністю, якій потрібно довіряти для використання застосунку, а це саме те, чого блокчейни намагаються уникнути. У майбутній статті я планую показати, як використовувати доведення з нульовим розголошенням, щоб обійти цю проблему.