Komponen server dan agen untuk aplikasi web3
Pengantar
Dalam kebanyakan kasus, aplikasi terdesentralisasi menggunakan server untuk mendistribusikan perangkat lunak, tetapi semua interaksi aktual terjadi antara klien (biasanya, peramban web) dan blockchain.
Namun, ada beberapa kasus di mana sebuah aplikasi akan mendapat manfaat dari memiliki komponen server yang berjalan secara independen. Server semacam itu akan dapat merespons peristiwa, dan permintaan yang datang dari sumber lain, seperti API, dengan mengeluarkan transaksi.
Ada beberapa kemungkinan tugas yang dapat dipenuhi oleh server semacam itu.
-
Pemegang status rahasia. Dalam permainan, sering kali berguna untuk tidak membuat semua informasi yang diketahui permainan tersedia bagi para pemain. Namun, tidak ada rahasia di blockchain, informasi apa pun yang ada di blockchain mudah diketahui oleh siapa saja. Oleh karena itu, jika sebagian dari status permainan harus dirahasiakan, itu harus disimpan di tempat lain (dan mungkin efek dari status tersebut diverifikasi menggunakan bukti zero-knowledge).
-
Oracle terpusat. Jika taruhannya cukup rendah, server eksternal yang membaca beberapa informasi secara online dan kemudian mempostingnya ke rantai mungkin cukup baik untuk digunakan sebagai oracle.
-
Agen. Tidak ada yang terjadi di blockchain tanpa transaksi untuk mengaktifkannya. Sebuah server dapat bertindak atas nama pengguna untuk melakukan tindakan seperti arbitrase ketika ada kesempatan.
Contoh program
Anda dapat melihat contoh server di github (opens in a new tab). Server ini mendengarkan peristiwa yang datang dari kontrak ini (opens in a new tab), versi modifikasi dari Greeter milik Hardhat. Ketika sapaan diubah, server akan mengubahnya kembali.
Untuk menjalankannya:
-
Klon repositori.
1git clone https://github.com/qbzzt/20240715-server-component.git2cd 20240715-server-component
122. Instal paket yang diperlukan. Jika Anda belum memilikinya, [instal Node terlebih dahulu](https://nodejs.org/en/download/package-manager).34 ```sh copy5 npm install-
Edit
.envuntuk menentukan kunci pribadi dari akun yang memiliki ETH di testnet Holesky. Jika Anda tidak memiliki ETH di Holesky, Anda dapat menggunakan faucet ini (opens in a new tab).1PRIVATE_KEY=0x <private key goes here>
124. Mulai server.34 ```sh copy5 npm start- Buka penjelajah blok (opens in a new tab), dan menggunakan alamat yang berbeda dari yang memiliki kunci pribadi, ubah sapaannya. Lihat bahwa sapaan tersebut secara otomatis diubah kembali.
Bagaimana cara kerjanya?
Cara termudah untuk memahami cara menulis komponen server adalah dengan memeriksa contohnya baris demi baris.
src/app.ts
Sebagian besar program terdapat di src/app.ts (opens in a new tab).
Membuat objek prasyarat
1import {2 createPublicClient,3 createWalletClient,4 getContract,5 http,6 Address,7} from "viem"Ini adalah entitas Viem (opens in a new tab) yang kita butuhkan, fungsi, dan tipe Address (opens in a new tab). Server ini ditulis dalam TypeScript (opens in a new tab), yang merupakan ekstensi dari JavaScript yang membuatnya diketik dengan kuat (strongly typed) (opens in a new tab).
1import { privateKeyToAccount } from "viem/accounts"Fungsi ini (opens in a new tab) memungkinkan kita menghasilkan informasi dompet, termasuk alamat, yang sesuai dengan kunci pribadi.
1import { holesky } from "viem/chains"Untuk menggunakan blockchain di Viem, Anda perlu mengimpor definisinya. Dalam hal ini, kita ingin terhubung ke blockchain pengujian Holesky (opens in a new tab).
1// This is how we add the definitions in .env to process.env. // Ini adalah cara kita menambahkan definisi di .env ke process.env.2import * as dotenv from "dotenv"3dotenv.config()Ini adalah cara kita membaca .env ke dalam lingkungan. Kita membutuhkannya untuk kunci pribadi (lihat nanti).
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 constTampilkan semuaUntuk menggunakan kontrak, kita memerlukan alamatnya dan untuknya. Kita menyediakan keduanya di sini.
Dalam JavaScript (dan karenanya TypeScript) Anda tidak dapat menetapkan nilai baru ke sebuah konstanta, tetapi Anda dapat memodifikasi objek yang disimpan di dalamnya. Dengan menggunakan akhiran as const, kita memberi tahu TypeScript bahwa daftar itu sendiri adalah konstan dan tidak boleh diubah.
1const publicClient = createPublicClient({2 chain: holesky,3 transport: http(),4})Buat klien publik (opens in a new tab) Viem. Klien publik tidak memiliki kunci pribadi yang terlampir, dan karenanya tidak dapat mengirim transaksi. Mereka dapat memanggil fungsi view (opens in a new tab), membaca saldo akun, dll.
1const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)Variabel lingkungan tersedia di process.env (opens in a new tab). Namun, TypeScript diketik dengan kuat. Variabel lingkungan dapat berupa string apa pun, atau kosong, sehingga tipe untuk variabel lingkungan adalah string | undefined. Namun, sebuah kunci didefinisikan di Viem sebagai 0x${string} (0x diikuti oleh sebuah string). Di sini kita memberi tahu TypeScript bahwa variabel lingkungan PRIVATE_KEY akan bertipe tersebut. Jika tidak, kita akan mendapatkan kesalahan runtime.
Fungsi privateKeyToAccount (opens in a new tab) kemudian menggunakan kunci pribadi ini untuk membuat objek akun lengkap.
1const walletClient = createWalletClient({2 account,3 chain: holesky,4 transport: http(),5})Selanjutnya, kita menggunakan objek akun untuk membuat klien dompet (opens in a new tab). Klien ini memiliki kunci pribadi dan alamat, sehingga dapat digunakan untuk mengirim transaksi.
1const greeter = getContract({2 address: greeterAddress,3 abi: greeterABI,4 client: { public: publicClient, wallet: walletClient },5})Sekarang setelah kita memiliki semua prasyarat, kita akhirnya dapat membuat instans kontrak (opens in a new tab). Kita akan menggunakan instans kontrak ini untuk berkomunikasi dengan kontrak onchain.
Membaca dari blockchain
1console.log(`Current greeting:`, await greeter.read.greet())Fungsi kontrak yang hanya-baca (view (opens in a new tab) dan pure (opens in a new tab)) tersedia di bawah read. Dalam hal ini, kita menggunakannya untuk mengakses fungsi greet (opens in a new tab), yang mengembalikan sapaan.
JavaScript bersifat single-threaded, jadi ketika kita menjalankan proses yang berjalan lama, kita perlu menentukan bahwa kita melakukannya secara asinkron (opens in a new tab). Memanggil blockchain, bahkan untuk operasi hanya-baca, memerlukan perjalanan bolak-balik antara komputer dan node blockchain. Itulah alasan kita menentukan di sini bahwa kode perlu await (menunggu) hasilnya.
Jika Anda tertarik dengan cara kerjanya, Anda dapat membacanya di sini (opens in a new tab), tetapi secara praktis yang perlu Anda ketahui adalah bahwa Anda melakukan await pada hasil jika Anda memulai operasi yang memakan waktu lama, dan bahwa fungsi apa pun yang melakukan ini harus dideklarasikan sebagai async.
Mengeluarkan transaksi
1const setGreeting = async (greeting: string): Promise<any> => {Ini adalah fungsi yang Anda panggil untuk mengeluarkan transaksi yang mengubah sapaan. Karena ini adalah operasi yang panjang, fungsi ini dideklarasikan sebagai async. Karena implementasi internal, setiap fungsi async perlu mengembalikan objek Promise. Dalam hal ini, Promise<any> berarti kita tidak menentukan apa tepatnya yang akan dikembalikan dalam Promise.
1const txHash = await greeter.write.setGreeting([greeting])Bidang write dari instans kontrak memiliki semua fungsi yang menulis ke status blockchain (yang memerlukan pengiriman transaksi), seperti setGreeting (opens in a new tab). Parameter, jika ada, disediakan sebagai daftar, dan fungsi tersebut mengembalikan hash dari transaksi.
1 console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)23 return txHash4}Laporkan hash dari transaksi (sebagai bagian dari URL ke penjelajah blok untuk melihatnya) dan kembalikan.
Merespons peristiwa
1greeter.watchEvent.SetGreeting({Fungsi watchEvent (opens in a new tab) memungkinkan Anda menentukan bahwa sebuah fungsi akan dijalankan ketika suatu peristiwa dipancarkan. Jika Anda hanya peduli pada satu jenis peristiwa (dalam hal ini, SetGreeting), Anda dapat menggunakan sintaks ini untuk membatasi diri Anda pada jenis peristiwa tersebut.
1 onLogs: logs => {Fungsi onLogs dipanggil ketika ada entri log. Di Ethereum, "log" dan "peristiwa" biasanya dapat dipertukarkan.
1console.log(2 `Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`3)Bisa jadi ada beberapa peristiwa, tetapi untuk kesederhanaan kita hanya peduli pada yang pertama. logs[0].args adalah argumen dari peristiwa tersebut, dalam hal ini sender dan greeting.
1 if (logs[0].args.sender != account.address)2 setGreeting(`${account.address} insists on it being Hello!`)3 }4})Jika pengirimnya bukan server ini, gunakan setGreeting untuk mengubah sapaan.
package.json
File ini (opens in a new tab) mengontrol konfigurasi Node.js (opens in a new tab). Artikel ini hanya menjelaskan definisi-definisi penting.
1{2 "main": "dist/index.js",Definisi ini menentukan file JavaScript mana yang akan dijalankan.
1 "scripts": {2 "start": "tsc && node dist/app.js",3 },Skrip adalah berbagai tindakan aplikasi. Dalam hal ini, satu-satunya yang kita miliki adalah start, yang mengkompilasi dan kemudian menjalankan server. Perintah tsc adalah bagian dari paket typescript dan mengkompilasi TypeScript menjadi JavaScript. Jika Anda ingin menjalankannya secara manual, perintah ini terletak di node_modules/.bin. Perintah kedua menjalankan server.
1 "type": "module",Ada beberapa jenis aplikasi node JavaScript. Tipe module memungkinkan kita memiliki await di kode tingkat atas, yang penting ketika Anda melakukan operasi yang lambat (dan karenanya asinkron).
1 "devDependencies": {2 "@types/node": "^20.14.2",3 "typescript": "^5.4.5"4 },Ini adalah paket-paket yang hanya diperlukan untuk pengembangan. Di sini kita membutuhkan typescript dan karena kita menggunakannya dengan Node.js, kita juga mendapatkan tipe untuk variabel dan objek node, seperti process. Notasi ^<version> (opens in a new tab) berarti versi tersebut atau versi yang lebih tinggi yang tidak memiliki perubahan yang merusak (breaking changes). Lihat di sini (opens in a new tab) untuk informasi lebih lanjut tentang arti nomor versi.
1 "dependencies": {2 "dotenv": "^16.4.5",3 "viem": "2.14.1"4 }5}Ini adalah paket-paket yang diperlukan saat runtime, ketika menjalankan dist/app.js.
Kesimpulan
Server terpusat yang kita buat di sini melakukan tugasnya, yaitu bertindak sebagai agen untuk pengguna. Siapa pun yang ingin dapp terus berfungsi dan bersedia menghabiskan gas dapat menjalankan instans baru dari server dengan alamat mereka sendiri.
Namun, ini hanya berfungsi ketika tindakan server terpusat dapat dengan mudah diverifikasi. Jika server terpusat memiliki informasi status rahasia, atau menjalankan perhitungan yang sulit, itu adalah entitas terpusat yang perlu Anda percayai untuk menggunakan aplikasi, yang mana hal ini adalah tepat apa yang coba dihindari oleh blockchain. Dalam artikel mendatang, saya berencana untuk menunjukkan cara menggunakan bukti zero-knowledge untuk mengatasi masalah ini.
Lihat di sini untuk karya saya yang lain (opens in a new tab).
Pembaruan terakhir halaman: 3 Maret 2026