Lompat ke konten utama

Komponen server dan agen untuk aplikasi web3

agen
server
offchain
dapp
Pemula
Ori Pomerantz
15 Juli 2024
8 menit baca

Pengantar

Dalam kebanyakan kasus, aplikasi terdesentralisasi (dapp) menggunakan server untuk mendistribusikan perangkat lunak, tetapi semua interaksi aktual terjadi antara klien (biasanya, peramban web) dan rantai blok.

Normal interaction between web server, client, and 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.

The interaction with the addition of a server

Ada beberapa kemungkinan tugas yang dapat dipenuhi oleh server semacam itu.

  • Pemegang state rahasia. Dalam permainan, sering kali berguna untuk tidak membuat semua informasi yang diketahui permainan tersedia bagi para pemain. Namun, tidak ada rahasia di rantai blok, informasi apa pun yang ada di rantai blok mudah diketahui oleh siapa saja. Oleh karena itu, jika sebagian dari state permainan harus dirahasiakan, itu harus disimpan di tempat lain (dan mungkin efek dari state tersebut diverifikasi menggunakan bukti zero-knowledge).

  • Orakel terpusat. Jika taruhannya cukup rendah, server eksternal yang membaca beberapa informasi secara daring dan kemudian mempostingnya ke rantai mungkin cukup baik untuk digunakan sebagai orakel.

  • Agen. Tidak ada yang terjadi di rantai blok 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:

  1. Klon repositori.

    git clone https://github.com/qbzzt/20240715-server-component.git
    cd 20240715-server-component
    
  2. Instal paket-paket yang diperlukan. Jika Anda belum memilikinya, instal Node terlebih dahulu (opens in a new tab).

    npm install
    
  3. Edit .env untuk menentukan kunci privat 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).

    PRIVATE_KEY=0x <private key goes here>
    
  4. Mulai server.

    npm start
    
  5. Buka penjelajah blok (opens in a new tab), dan menggunakan alamat yang berbeda dari yang memiliki kunci privat, 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 dalam src/app.ts (opens in a new tab).

Membuat objek prasyarat
import {
  createPublicClient,
  createWalletClient,
  getContract,
  http,
  Address,
} 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 secara kuat (strongly typed) (opens in a new tab).

import { privateKeyToAccount } from "viem/accounts"

Fungsi ini (opens in a new tab) memungkinkan kita menghasilkan informasi dompet, termasuk alamat, yang sesuai dengan kunci privat.

import { holesky } from "viem/chains"

Untuk menggunakan rantai blok di Viem, Anda perlu mengimpor definisinya. Dalam hal ini, kita ingin terhubung ke rantai blok pengujian Holesky (opens in a new tab).

// Ini adalah cara kita menambahkan definisi di .env ke process.env.
import * as dotenv from "dotenv"
dotenv.config()

Ini adalah cara kita membaca .env ke dalam lingkungan. Kita membutuhkannya untuk kunci privat (lihat nanti).

Untuk menggunakan kontrak, kita memerlukan alamatnya dan untuk kontrak tersebut. 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.

const publicClient = createPublicClient({
  chain: holesky,
  transport: http(),
})

Buat klien publik (opens in a new tab) Viem. Klien publik tidak memiliki kunci privat yang terlampir, dan karenanya tidak dapat mengirim transaksi. Mereka dapat memanggil fungsi view (opens in a new tab), membaca saldo akun, dll.

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

Variabel lingkungan tersedia di process.env (opens in a new tab). Namun, TypeScript diketik secara 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 menjadi tipe tersebut. Jika tidak, kita akan mendapatkan kesalahan runtime.

Fungsi privateKeyToAccount (opens in a new tab) kemudian menggunakan kunci privat ini untuk membuat objek akun lengkap.

const walletClient = createWalletClient({
  account,
  chain: holesky,
  transport: http(),
})

Selanjutnya, kita menggunakan objek akun untuk membuat klien dompet (opens in a new tab). Klien ini memiliki kunci privat dan alamat, sehingga dapat digunakan untuk mengirim transaksi.

const greeter = getContract({
  address: greeterAddress,
  abi: greeterABI,
  client: { public: publicClient, wallet: walletClient },
})

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 rantai blok
console.log(`Current greeting:`, await greeter.read.greet())

Fungsi kontrak yang bersifat 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 utas tunggal (single-threaded), jadi ketika kita menjalankan proses yang berjalan lama, kita perlu menentukan bahwa kita melakukannya secara asinkron (opens in a new tab). Memanggil rantai blok, bahkan untuk operasi hanya-baca, memerlukan perjalanan bolak-balik antara komputer dan node rantai blok. 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 menggunakan await untuk hasil jika Anda memulai operasi yang memakan waktu lama, dan bahwa fungsi apa pun yang melakukan ini harus dideklarasikan sebagai async.

Mengeluarkan transaksi
const 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.

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

Bidang write dari instans kontrak memiliki semua fungsi yang menulis ke state rantai blok (yang memerlukan pengiriman transaksi), seperti setGreeting (opens in a new tab). Parameter, jika ada, disediakan sebagai daftar, dan fungsi tersebut mengembalikan hash dari transaksi.

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

    return txHash
}

Laporkan hash dari transaksi (sebagai bagian dari URL ke penjelajah blok untuk melihatnya) dan kembalikan.

Merespons peristiwa
greeter.watchEvent.SetGreeting({

Fungsi watchEvent (opens in a new tab) memungkinkan Anda menentukan bahwa suatu fungsi akan dijalankan ketika sebuah 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.

    onLogs: logs => {

Fungsi onLogs dipanggil ketika ada entri Log. Di Ethereum, "Log" dan "peristiwa" biasanya dapat dipertukarkan.

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

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.

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

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.

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

Definisi ini menentukan file JavaScript mana yang akan dijalankan.

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

Skrip adalah berbagai tindakan aplikasi. Dalam hal ini, satu-satunya yang kita miliki adalah start, yang mengompilasi dan kemudian menjalankan server. Perintah tsc adalah bagian dari paket typescript dan mengompilasi TypeScript menjadi JavaScript. Jika Anda ingin menjalankannya secara manual, perintah ini terletak di node_modules/.bin. Perintah kedua menjalankan server.

  "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).

  "devDependencies": {
    "@types/node": "^20.14.2",
    "typescript": "^5.4.5"
  },

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.

  "dependencies": {
    "dotenv": "^16.4.5",
    "viem": "2.14.1"
  }
}

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 diverifikasi dengan mudah. Jika server terpusat memiliki informasi state 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 rantai blok. 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).