跳至主要內容

Web3 應用程式的伺服器元件與代理

代理
伺服器
鏈下
dapp
初階
奧里·波梅蘭茨
2024年7月15日
13 分鐘閱讀

簡介

在大多數情況下,去中心化應用程式 (dapp) 使用伺服器來散佈軟體,但所有實際的互動都發生在客戶端(通常是網頁瀏覽器)與區塊鏈之間。

Normal interaction between web server, client, and blockchain

然而,在某些情況下,應用程式會受益於擁有一個獨立運作的伺服器元件。這樣的伺服器將能夠透過發送交易來回應事件,以及來自其他來源(例如 API)的請求。

The interaction with the addition of a server

這樣的伺服器可以完成幾種可能的任務。

  • 秘密狀態的持有者。在遊戲中,不讓玩家獲得遊戲所知的所有資訊通常是有用的。然而,區塊鏈上沒有秘密,區塊鏈上的任何資訊都很容易被任何人找出來。因此,如果要將部分遊戲狀態保密,就必須將其儲存在其他地方(並且可能使用 零知識證明 來驗證該狀態的影響)。

  • 中心化預言機。如果風險夠低,一個在線上讀取某些資訊然後將其發佈到鏈上的外部伺服器,可能就足以作為 預言機 使用。

  • 代理。如果沒有交易來觸發,區塊鏈上就不會發生任何事情。當機會出現時,伺服器可以代表使用者執行諸如 套利 等操作。

範例程式

你可以在 GitHub 上 (opens in a new tab) 看到一個範例伺服器。這個伺服器會監聽來自 這個合約 (opens in a new tab)(Hardhat 的 Greeter 修改版)的事件。當問候語被更改時,它會將其改回來。

執行方式:

  1. 複製儲存庫。

    git clone https://github.com/qbzzt/20240715-server-component.git
    cd 20240715-server-component
    
  2. 安裝必要的套件。如果你還沒有安裝,請 先安裝 Node.js (opens in a new tab)

    npm install
    
  3. 編輯 .env 以指定一個在 Holesky 測試網上擁有 ETH 的帳戶私鑰。如果你在 Holesky 上沒有 ETH,你可以 使用這個水龍頭 (opens in a new tab)

    PRIVATE_KEY=0x <private key goes here>
    
  4. 啟動伺服器。

    npm start
    
  5. 前往 區塊鏈瀏覽器 (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 中使用區塊鏈,你需要匯入其定義。在這個例子中,我們想要連接到 Holesky (opens in a new tab) 測試區塊鏈。

// 這是我們將 .env 中的定義加入 process.env 的方式。
import * as dotenv from "dotenv"
dotenv.config()

這是我們將 .env 讀取到環境中的方式。我們需要它來取得私鑰(詳見後文)。

要使用合約,我們需要它的地址和它的 。我們在這裡提供了這兩者。

在 JavaScript(以及 TypeScript)中,你不能將新值指派給常數,但你_可以_修改儲存在其中的物件。透過使用後綴 as const,我們告訴 TypeScript 該列表本身是常數,且不可被更改。

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

建立一個 Viem 公共客戶端 (public client) (opens in a new tab)。公共客戶端沒有附加的私鑰,因此無法發送交易。它們可以呼叫 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(),
})

接下來,我們使用帳戶物件來建立一個 錢包客戶端 (wallet client) (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 是事件的參數,在這個例子中是 sendergreeting

        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",

JavaScript Node 應用程式有多種類型。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) 繼續運作並願意花費燃料的人,都可以使用自己的地址來執行一個新的伺服器實例。

然而,這只有在中心化伺服器的操作可以輕易被驗證時才有效。如果中心化伺服器擁有任何秘密狀態資訊,或者執行困難的計算,它就是一個你需要信任才能使用該應用程式的中心化實體,這正是區塊鏈試圖避免的。在未來的文章中,我計畫展示如何使用 零知識證明 來解決這個問題。

點擊這裡查看我更多的作品 (opens in a new tab)