C#以太坊基礎入門

在這一部分,我們將使用C#開發一個最簡單的.Net控制檯應用,來接入以太坊節點,並打印 所連接節點旳版本信息。通過這一部分的學習,你將掌握以下技能:

  1. 如何使用節點仿真器
  2. 如何在命令行訪問以太坊節點
  3. 如何在C#代碼中訪問以太坊節點

我們將使用ganache來模擬以太坊節點。ganache雖然不是一個真正的以太坊節點軟件, 但它完整實現了以太坊的JSON RPC接口,非常適合以太坊智能合約與去中心化應用開發的 學習與快速驗證:

ganache啓動後將在8545端口監聽http請求,因此,我們會將JSON RPC調用請求 使用http協議發送到節點旳8545端口。不同的節點軟件可能會使用不同的監聽端口,但 大部分節點軟件通常默認使用8545端口。

以太坊規定了節點必須實現web3_clientVersion 調用來返回節點軟件的版本信息,因此我們可以用這個命令來測試與 節點旳鏈接是否成功。

ganache-cli是以太坊節點仿真器軟件ganache的命令行版本,可以方便開發者快速進行 以太坊DApp的開發與測試。在windows下你也可以使用其GUI版本。啓動ganache很簡單,只需要在命令行執行ganache-cli即可:ganache-cli是一個完整的詞,-兩邊是沒有空格的。一切順利的話,你會看到與下圖類似的屏幕輸出:

默認情況下,ganache會隨機創建10個賬戶,每個賬戶中都有100ETH的餘額。你可以在 命令行中指定一些參數來調整這一默認行爲。例如使用-a--acounts參數來指定 要創建的賬戶數量爲20:

ganache-cli -a 20

 

 

使用curl獲取節點版本信息

以太坊規定了節點必須實現web3_clientVersion 接口來向外部應用提供節點旳版本信息。接口協議的交互流程如下:

這是一個典型的請求/應答模型,請求包和響應包都是標準的JSON格式。其中,jsonrpc字段用來 標識協議版本,id則用來幫助建立響應包與請求包的對應關係。

在請求包中,使用method字段來聲明接口方法,例如web3_clientVersion,使用params 字段來聲明接口方法的參數數組。 在響應包中,result字段中保存了命令執行的返回結果。

以太坊JSON RPC並沒有規定傳輸層的實現,不過大部分節點都會實現HTTP和IPC的訪問。因此 我們可以使用命令行工具curl來測試這個接口:

curl http://localhost:8545 -X POST -d '{"jsonrpc": "2.0","method": "web3_clientVersion","params": [], "id": 123}'

 使用C#獲取節點版本信息

就像前一節看到的,我們只要在C#代碼中按照以太坊RPC接口要求發送http請求包就可以了。 你可以使用任何一個你喜歡的http庫,甚至直接使用socket來調用以太坊的JSON RPC API。例如,下面的代碼使用.Net內置的HttpClient類來訪問以太坊節點,注意代碼中的註釋:

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace GetVersionByHttpDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("cuiyw-test");
            GetVersion().Wait();
            GetAccounts().Wait();
            Console.ReadLine();
        }
        static async Task GetVersion()
        {
            HttpClient httpClient = new HttpClient();

            string url = "http://localhost:7545";
            string payload = "{\"jsonrpc\":\"2.0\",\"method\":\"web3_clientVersion\",\"params\":[],\"id\":7878}";
            Console.WriteLine("<= " + payload);
            StringContent content = new StringContent(payload, Encoding.UTF8, "application/json");
            HttpResponseMessage rsp = await httpClient.PostAsync(url, content);
            string ret = await rsp.Content.ReadAsStringAsync();
            Console.WriteLine("=> " + ret);
        }

        static async Task GetAccounts()
        {
            HttpClient httpClient = new HttpClient();

            string url = "http://localhost:7545";
            string payload = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_accounts\",\"params\":[],\"id\":5777}";
            Console.WriteLine("<= " + payload);
            StringContent content = new StringContent(payload, Encoding.UTF8, "application/json");
            HttpResponseMessage rsp = await httpClient.PostAsync(url, content);
            string ret = await rsp.Content.ReadAsStringAsync();
            Console.WriteLine("=> " + ret);
        }
    }
}

序列化與反序列化

在應用邏輯裏直接拼接RPC請求字符串,或者直接解析RPC響應字符串,都不是 令人舒心的事情。

更乾淨的辦法是使用數據傳輸對象(Data Transfer Object)層來 隔離這個問題,在DTO層將C#的對象序列化爲Json字符串,或者從Json字符串 反序列化爲C#的對象,應用代碼只需要操作C#對象即可。

我們首先定義出JSON請求與響應所對應的C#類。例如:

現在我們獲取節點版本的代碼可以不用直接操作字符串了:

如下圖,在SerializeDemo中定義了請求與響應的model。

RpcRequestMessage

using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
namespace SerializeDemo
{
    class RpcRequestMessage
    {
        public RpcRequestMessage(string method, params object[] parameters)
        {
            Id = Environment.TickCount;
            Method = method;
            Parameters = parameters;
        }

        [JsonProperty("id")]
        public int Id;

        [JsonProperty("jsonrpc")]
        public string JsonRpc = "2.0";

        [JsonProperty("method")]
        public string Method;

        [JsonProperty("params")]
        public object Parameters;
    }
}

RpcResponseMessage

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace SerializeDemo
{
    class RpcResponseMessage
    {
        [JsonProperty("id")]
        public int Id { get; set; }

        [JsonProperty("jsonrpc")]
        public string JsonRpc { get; set; }

        [JsonProperty("result")]
        public object Result { get; set; }
    }
}

RpcHttpDto

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace SerializeDemo
{
    class RpcHttpDto
    {
        public async Task Run()
        {
            var version = await Rpc("web3_clientVersion");
            Console.WriteLine("version => " + version + " type => " + version.GetType().Name);
            var accounts = await Rpc("eth_accounts");
            Console.WriteLine("accounts => " + accounts + " type => " + accounts.GetType().Name);
        }

        public async Task<object> Rpc(string method)
        {
            HttpClient httpClient = new HttpClient();

            string url = "http://localhost:7545";

            RpcRequestMessage rpcReqMsg = new RpcRequestMessage(method);
            string payload = JsonConvert.SerializeObject(rpcReqMsg);
            Console.WriteLine("<= " + payload);

            StringContent content = new StringContent(payload, Encoding.UTF8, "application/json");
            HttpResponseMessage rsp = await httpClient.PostAsync(url, content);

            string ret = await rsp.Content.ReadAsStringAsync();
            Console.WriteLine("=> " + ret);
            RpcResponseMessage rpcRspMsg = JsonConvert.DeserializeObject<RpcResponseMessage>(ret);
            return rpcRspMsg.Result;
        }
    }
}

Program

using System;

namespace SerializeDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("cuiyw-test");
            Console.WriteLine("Call Ethereum RPC Api with HttpClient");
            RpcHttpDto demo = new RpcHttpDto();
            demo.Run().Wait();
            Console.ReadLine();
        }
    }
}

 使用現成的輪子

儘管可行,但我還是建議你儘量避免自己去封裝這些rpc接口,畢竟 這個事已經做過好幾次了,而且rpc接口封裝僅僅是整個故事的一部分。

Nethereum是以太坊官方推薦的.Net下的rpc接口封裝庫,因此我們優先 選擇它。

下面是使用Nethereum獲取節點版本信息的代碼:

                Web3 web3 = new Web3("http://localhost:7545");
                string version = await web3.Client.SendRequestAsync<string>("web3_clientVersion");
                Console.WriteLine("version => " + version);

Web3是Nethereum的入口類,我們與以太坊的交互,基本上是通過 這個入口來完成的,實例化Web3需要指定要鏈接的節點地址,例如本地ganache節點,就 可以使用http://localhost:7545這個地址。

Web3實例的Client屬性是一個IClient接口的實現對象,這個接口抽象了與 節點的RPC接口交互的方法,因此與具體的通信傳輸機制無關:

從上圖容易看出,Nethereum目前支持通過四種不同的通信機制來訪問以太坊: Http、WebSocket、命名管道和Unix套接字。

容易理解,當我們提供一個節點url作爲Web3實例化的參數時,Web3將自動創建 一個基於Http的IClient實現實例,即RpcClient實例。

一旦獲得了Iclient的實現實例,就可以調用其SendRequestAsync<T>()方法來向節點 提交請求了,例如,下面的代碼提交一個web3_clientVersion調用請求:

string version = await web3.Client.SendRequestAsync<string>("web3_clientVersion");

SendRequestAsync()是一個泛型方法,其泛型參數T用來聲明返回值的類型。例如, 對於web3_clientVersion調用,其RPC響應的result字段是一個字符串,因此我們使用 string作爲泛型參數。

需要指出的是,SendRequestAsync()不需要我們傳入完整的請求報文,其返回的結果 也不是完整的響應報文,只是其中result字段的內容。

對於需要傳入參數的RPC調用,例如用來計算字符串keccak哈希值的 web3_sha3調用, 可以在SendRequestAsync()方法自第3個參數開始依次寫入。例如,下面的代碼 計算hello,ethereum的keccak哈希:

                HexUTF8String hexstr = new HexUTF8String("hello,ethereum");
                Console.WriteLine("hello,ethereum => " + hexstr.HexValue);
                string hash = await web3.Client.SendRequestAsync<string>("web3_sha3", null, hexstr);
                Console.WriteLine("keccak hash => " + hash);

SendRequestAsync()方法的第2個參數表示路由名稱,可以用來攔截RPC請求,滿足 一些特殊的應用需求,我們通常將其設置爲null即可。由於web3_sha3調用要求傳入 的參數爲16進制字符串格式,例如,hello,ethereum應當表示爲0x68656c6c6f2c657468657265756d, 因此我們使用HexUtf8String類進行轉換:

 使用RPC接口封裝類

如果你傾向於薄薄一層的封裝,那麼使用IClient的SendRequestAsync()接口, 已經可以滿足大部分訪問以太坊的需求了,而且基本上只需要參考RPC API的手冊, 就可以完成工作了。不過Nethereum走的更遠。

Nethereum爲每一個RPC接口都封裝了單獨的類。

例如,對於web3_clientVersion調用,其對應的實現類爲Web3ClientVersion; 而對於web3_sha3調用,其對應的實現類爲Web3Sha3:

有一點有助於我們的開發:容易根據RPC調用的名字猜測出封裝類的名稱 —— 去掉 下劃線,然後轉換爲單詞首字母大寫的Pascal風格的命名。

由於每一個RPC接口的封裝類都依賴於一個IClient接口的實現,因此我們可以直接 在接口封裝類實例上調用SendRequestAsync()方法,而無須再顯式地使用一個IClient 實現對象來承載請求 —— 當然在創建封裝類實例時需要傳入IClient的實現對象。

例如,下面的代碼使用類Web3ClientVersion來獲取節點版本信息:

                Web3ClientVersion w3cv = new Web3ClientVersion(web3.Client);
                string version = await w3cv.SendRequestAsync();
                Console.WriteLine("version => " + version);

容易注意到封裝類的SendRequestAsync()方法不再需要使用泛型參數聲明返回值的 類型,這是因爲特定RPC接口的對應封裝類在定義時已經確定了調用返回值的類型。例如:

namespace Nethereum.RPC.Web3
{
    public class Web3ClientVersion : GenericRpcRequestResponseHandlerNoParam<string>
    {
        public Web3ClientVersion(IClient client);
    }
}

如果RPC接口需要額外的參數,例如web3_sha3,那麼在SendRequestAsync() 方法中依次傳入即可。例如,下面的代碼使用Web3Sha3類來計算一個字符串 的keccak哈希值:

                HexUTF8String hexstr = new HexUTF8String("hello,ethereum");
                Web3Sha3 w3s = new Web3Sha3(web3.Client);
                string hash = await w3s.SendRequestAsync(hexstr);
                Console.WriteLine("keccak hash => " + hash);

接口封裝類比直接使用IClient提供了更多的類型檢查能力,但同時也 帶來了額外的負擔 —— 需要同時查閱RPC API接口文檔和Nethereum的接口 封裝類文檔,才能順利地完成任務。

using Nethereum.Hex.HexTypes;
using Nethereum.RPC.Web3;
using Nethereum.Web3;
using System;
using System.Threading.Tasks;

namespace Web3HeavyDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("cuiyw-test");
            Console.WriteLine("Access Ethereum with Nethereum");
            Task.Run(async () => {
                Web3 web3 = new Web3("http://localhost:7545");

                Web3ClientVersion w3cv = new Web3ClientVersion(web3.Client);
                string version = await w3cv.SendRequestAsync();
                Console.WriteLine("version => " + version);

                HexUTF8String hexstr = new HexUTF8String("hello,ethereum");
                Web3Sha3 w3s = new Web3Sha3(web3.Client);
                string hash = await w3s.SendRequestAsync(hexstr);
                Console.WriteLine("keccak hash => " + hash);
            }).Wait();
            Console.ReadLine();
        }
    }
}

理解Nethereum的命名規則

大多數情況下,我們容易從以太坊的RPC接口名稱,推測出Nethereum的封裝類名稱。但是別忘了,在C#中,還有個命名空間的問題。

Nethereum根據不同的RPC接口系列,在不同的命名空間定義接口實現類。 例如對於web3_*這一族的接口,其封裝類的命名空間爲Nethereum.RPC.Web3:

但是,對於eth_*系列的接口,並不是所有的封裝類都定義在Nethereum.RPC.Eth 命名空間,Nethereum又任性地做了一些額外的工作 —— 根據接口的功能劃分了一些 子命名空間!例如,和交易有關的接口封裝類,被歸入Nethereum.RPC.Eth.Transactions命名 空間,而和塊有關的接口封裝類,則被歸入Nethereum.RPC.Eth.Blocks命名空間。

顯然,如果你從一個RPC調用出發,嘗試推測出它在Nethereum中正確的命名空間和 封裝類名稱,這種設計並不友好 —— 雖然方便了Nethereume的開發者維護代碼, 但會讓Nethereum的使用者感到崩潰 —— 不可預測的API只會傷害開發效率。

 

using Nethereum.Hex.HexTypes;
using Nethereum.RPC.Eth;
using Nethereum.Web3;
using System;
using System.Threading.Tasks;

namespace Web3Namerules
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("cuiyw-test");
            Console.WriteLine("Access Ethereum with Nethereum");
            Task.Run(async () => {
                Web3 web3 = new Web3("http://localhost:7545");

                EthAccounts ea = new EthAccounts(web3.Client);
                string[] accounts = await ea.SendRequestAsync();
                Console.WriteLine("accounts => \n" + string.Join("\n", accounts));

                EthGasPrice egp = new EthGasPrice(web3.Client);
                HexBigInteger price = await egp.SendRequestAsync();
                Console.WriteLine("gas price => " + price.Value);
            }).Wait();
            Console.ReadLine();
        }
    }
}

使用Web3入口類

Netherem推薦通過入口類Web3來使用接口封裝類,這可以在某種程度上減輕 複雜的命名空間設計給使用者帶來的困擾。

例如,我們可以使用web3.Eth.Accounts來直接訪問EthAccounts類的實例, 而無須引入命名空間來實例化:

也就是說,在實例化入口類Web3的時候,Nethereum同時也創建好了所有的接口 封裝類的實例,並掛接在不同的屬性(例如Eth)之下。

我們可以先忽略Eth屬性的具體類型,簡單地將其視爲接口封裝對象的容器。 因此,當我們需要使用EthGetBalance類的時候,通過web3.Eth.GetBalance 即可訪問到其實例對象;同樣,當我們希望使用EthSendTransaction類時, 則可以通過web3.Eth.Transactions.SendTransaction來訪問其實例對象 —— 它在子容器Transactions裏:

例如,下面的代碼調用eth_accounts接口獲取節點賬戶列表,然後調用 eth_getBalance接口獲取第一個賬戶的餘額:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("cuiyw-test");
            Console.WriteLine("Web3 Entry Demo");
            Web3Entry demo = new Web3Entry();
            demo.Run().Wait();
            Console.ReadLine();
        }
    }
using Nethereum.Hex.HexTypes;
using Nethereum.Web3;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Web3EntryDemo
{
    class Web3Entry
    {
        public async Task Run()
        {
            Web3 web3 = new Web3("http://localhost:7545");
            string[] accounts = await web3.Eth.Accounts.SendRequestAsync();
            Console.WriteLine("account#0 => " + accounts[0]);
            HexBigInteger balance = await web3.Eth.GetBalance.SendRequestAsync(accounts[0]);
            Console.WriteLine("balance => " + balance.Value);
        }
    }
}

由於eth_getBalance 返回的賬戶餘額採用16進制字符串表示,因此我們需要使用HexBigInteger類型 的變量來接收這個值:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章