在區(qū)塊鏈技術(shù)的浪潮中,以太坊(Ethereum)無疑占據(jù)了舉足輕重的地位,它不僅是一個(gè)加密貨幣平臺(tái),更是一個(gè)去中心化的全球計(jì)算機(jī),允許開發(fā)者構(gòu)建和部署智能合約與去中心化應(yīng)用(DApps),而 Node.js,憑借其異步、事件驅(qū)動(dòng)的特性以及龐大的 npm 生態(tài)系統(tǒng),成為了區(qū)塊鏈開發(fā)中,尤其是與以太坊交互的熱門選擇,本文將深入探討如何利用 Node.js 通過 RPC(Remote Procedure Call,遠(yuǎn)程過程調(diào)用)與以太坊節(jié)點(diǎn)進(jìn)行通信,解鎖區(qū)塊鏈應(yīng)用開發(fā)的大門。

理解核心概念

在深入代碼之前,我們有必要清晰理解幾個(gè)核心概念:

  1. 以太坊節(jié)點(diǎn) (Ethereum Node):這是運(yùn)行以太坊協(xié)議的軟件實(shí)例,它維護(hù)著整個(gè)以太坊區(qū)塊鏈的副本,能夠驗(yàn)證交易、執(zhí)行智能合約并與網(wǎng)絡(luò)中的其他節(jié)點(diǎn)同步數(shù)據(jù),常見的以太坊節(jié)點(diǎn)客戶端有 Geth、OpenEthereum(原 Parity)等。
  2. RPC (Remote Procedure Call):這是一種網(wǎng)絡(luò)協(xié)議,允許一臺(tái)程序(客戶端)請(qǐng)求另一臺(tái)程序(服務(wù)器)上的服務(wù),而無需了解底層網(wǎng)絡(luò)的細(xì)節(jié),在以太坊的語境下,節(jié)點(diǎn)通過 RPC 接口暴露其功能,使得外部應(yīng)用(如我們的 Node.js 腳本)可以調(diào)用這些功能,例如查詢賬戶余額、發(fā)送交易、調(diào)用智能合約方法等。
  3. Node.js:一個(gè)基于 Chrome V8 引擎的 JavaScript 運(yùn)行時(shí),它使得開發(fā)者可以使用 JavaScript 來編寫服務(wù)器端應(yīng)用程序,其非阻塞 I/O 模型非常適合處理網(wǎng)絡(luò)請(qǐng)求,如與以太坊節(jié)點(diǎn)的 RPC 通信。

搭建開發(fā)環(huán)境

在開始之前,確保你的開發(fā)環(huán)境已準(zhǔn)備就緒:

  1. 安裝 Node.js 和 npm:訪問 Node.js 官網(wǎng) 下載并安裝適合你操作系統(tǒng)的 LTS 版本。

  2. 安裝以太坊節(jié)點(diǎn)客戶端:這里以 Geth 為例,你可以從 Geth 官方 GitHub 下載對(duì)應(yīng)系統(tǒng)的二進(jìn)制文件,或者通過包管理器安裝(如 brew install geth on macOS)。

  3. 啟動(dòng)以太坊節(jié)點(diǎn)并啟用 RPC 服務(wù): 啟動(dòng) Geth 節(jié)點(diǎn)時(shí),你需要指定一些參數(shù)來啟用 RPC 服務(wù),并設(shè)置訪問權(quán)限,最簡單的方式是啟動(dòng)一個(gè)測(cè)試網(wǎng)(如 Ropsten 或 Goerli)節(jié)點(diǎn),并允許本地連接:

    # 以 Goerli 測(cè)試網(wǎng)為例,啟動(dòng)節(jié)點(diǎn)并啟用 RPC
    geth --goerli --http --http.addr "0.0.0.0" --http.port "8545" --http.api "eth,net,web3,personal"
    • --goerli: 連接到 Goerli 測(cè)試網(wǎng)。
    • --http: 啟用 HTTP-RPC 服務(wù)器。
    • --http.addr "0.0.0.0": 允許任何 IP 地址訪問(開發(fā)環(huán)境,生產(chǎn)環(huán)境請(qǐng)謹(jǐn)慎設(shè)置)。
    • --http.port "8545": RPC 服務(wù)監(jiān)聽的端口號(hào),默認(rèn)是 8545。
    • --http.api "eth,net,web3,personal": 指定通過 RPC 可用的 API 命令集合。

    你的以太坊節(jié)點(diǎn)已經(jīng)在監(jiān)聽 8545 端口的 HTTP-RPC 請(qǐng)求了。

使用 Node.js 連接以太坊節(jié)點(diǎn)

Node.js 通過 HTTP 或 WebSocket 與以太坊節(jié)點(diǎn)的 RPC 接口通信,最常用的庫是 web3.js,它是以太坊官方提供的 JavaScript API 庫,極大地簡化了與以太坊節(jié)點(diǎn)的交互。

  1. 初始化項(xiàng)目并安裝 web3.js

    mkdir eth-rpc-demo
    cd eth-rpc-demo
    npm init -y
    npm install web3
  2. 編寫 Node.js 腳本連接 RPC 并調(diào)用方法

    創(chuàng)建一個(gè)名為 app.js 的文件,并編寫以下代碼:

    const Web3 = require('web3');
    // 1. 連接到以太坊節(jié)點(diǎn)的 RPC 接口
    // 如果節(jié)點(diǎn)在本地運(yùn)行,默認(rèn)地址為 'http://localhost:8545'
    const web3 = new Web3('http://localhost:8545');
    // 2. 檢查連接是否成功
    web3.eth.getBlockNumber()
        .then(blockNumber => {
            console.log('當(dāng)前區(qū)塊號(hào):', blockNumber);
        })
        .catch(err => {
            console.error('連接失敗或獲取區(qū)塊號(hào)出錯(cuò):', err);
        });
    // 3. 獲取節(jié)點(diǎn)信息
    web3.eth.getNodeInfo()
        .then(nodeInfo => {
            console.log('節(jié)點(diǎn)信息:', nodeInfo);
        })
        .catch(err => {
            console.error('獲取節(jié)點(diǎn)信息出錯(cuò):', err);
        });
    // 4. 獲取賬戶列表
    web3.eth.getAccounts()
        .then(accounts => {
            console.log('可用賬戶:', accounts);
            if (accounts.length > 0) {
                // 示例:獲取第一個(gè)賬戶的余額
                const account = accounts[0];
                web3.eth.getBalance(account)
                    .then(balance => {
                        // 余額是 Wei,轉(zhuǎn)換為 Ether
                        console.log(`賬戶 ${account} 的余額: ${web3.utils.fromWei(balance, 'ether')} ETH`);
                    })
                    .catch(err => {
                        console.error('獲取余額出錯(cuò):', err);
                    });
            }
        })
        .catch(err => {
            console.error('獲取賬戶列表出錯(cuò):', err);
        });

    運(yùn)行這個(gè)腳本:

    node app.js

    如果一切正常,你應(yīng)該能看到當(dāng)前區(qū)塊號(hào)、節(jié)點(diǎn)信息、可用賬戶及其余額(測(cè)試網(wǎng)賬戶可能初始沒有 ETH,你需要從測(cè)試網(wǎng)水龍頭獲取一些)。

進(jìn)階應(yīng)用:發(fā)送交易與調(diào)用智能合約

RPC 的強(qiáng)大之處在于它能執(zhí)行幾乎所有的以太坊操作。

  1. 發(fā)送交易: 發(fā)送交易需要解鎖賬戶、指定接收方、金額和 gas 等參數(shù),這通常需要賬戶的密碼(如果節(jié)點(diǎn)是用 --password 參數(shù)啟動(dòng)的,或者使用 personal.unlockAccount RPC 方法解鎖)。

    // 注意:實(shí)際使用時(shí)請(qǐng)妥善處理密碼,不要硬編碼在代碼中
    const password = 'your_account_password'; // 替換為你的賬戶密碼
    const fromAccount = '0xYourAccountAddress'; // 替換為你的發(fā)送賬戶地址
    const toAccount = '0xRecipientAddress';    // 替換為接收賬戶地址
    const amount = web3.utils.toWei('0.01', 'ether'); // 轉(zhuǎn)換為 Wei
    web3.eth.personal.unlockAccount(fromAccount, password)
        .then(unlocked => {
            if (unlocked) {
                console.log('賬戶解鎖成功');
                web3.eth.sendTransaction({
                    from: fromAccount,
                    to: toAccount,
                    value: amount,
                    gas: 21000 // 轉(zhuǎn)賬交易的最小 gas
                })
                .then(receipt => {
                    console.log('交易成功,收據(jù):', receipt);
                })
                .catch(err => {
                    console.error('交易失敗:', err);
                })
                .finally(() => {
                    // 記得鎖定賬戶
                    web3.eth.personal.lockAccount(fromAccount);
                    console.log('賬戶已鎖定');
                });
            }
        })
        .catch(err => {
            console.error('解鎖賬戶失敗:', err);
        });
  2. 調(diào)用智能合約: 與智能合約交互需要合約的 ABI(Application Binary Interface,應(yīng)用程序二進(jìn)制接口)和合約地址。

    // 假設(shè)我們有一個(gè)簡單的存儲(chǔ)合約
    const contractABI = [
        {
            "constant": false,
            "inputs": [{"name": "_x", "type": "uint256"}],
            "name": "set",
            "outputs": [],
            "type": "function"
        },
        {
            "constant": true,
            "inputs": [],
            "name": "get",
            "outputs": [{"name": "retVal", "type": "uint256"}],
            "type": "function"
        }
    ];
    const contractAddress = '0xYourContractAddress'; // 替換為你的合約地址
    const contract = new web3.eth.Contract(contractABI, contractAddress);
    // 調(diào)用 view/pure 函數(shù)(不需要發(fā)送交易)
    contract.methods.get()
        .call()
        .then(result => {
            console.log('合約存儲(chǔ)的值:', result);
        })
        .catch(err => {
            console.error('調(diào)用合約 get 方法出錯(cuò):', err);
        });
    // 發(fā)送交易
    隨機(jī)配圖
    調(diào)用非 constant 函數(shù) contract.methods.set(42) .send({ from: fromAccount, gas: 100000 }) .then(receipt => { console.log('調(diào)用合約 set 方法成功,收據(jù):', receipt);