比特幣入門之使用PRC應用開發接口

一、RPC API概述

比特幣定義了RPC API來允許第三方應用通過節點軟件訪問比特幣網絡。 事實上,bitcoin-cli就是通過這個接口來實現其功能的,也就是說, 我們可以在自己的C#程序中完全實現bitcoin-cli的功能。

JSON RPC採用JSON語法表示一個遠程過程調用(Remote Procedure Call) 的請求與應答消息。例如對於getbalance調用,請求消息與應答消息的格式 示意如下:

在請求消息中使用method字段聲明要調用的遠程方法名,使用params字段 聲明調用參數列表;消息中的jsonrpc字段聲明所採用的JSON RPC版本號, 而可選的id字段則用於建立響應消息與請求消息之間的關聯,以便客戶端 在同時發送多個請求後能正確跟蹤其響應。

響應消息中的result字段記錄了遠程調用的執行結果,而error字段 則記錄了調用執行過程中出現的錯誤,id字段則對應於請求消息中的同名 字段值。

JSON RPC是傳輸協議無關的,但基於HTTP的廣泛應用,節點通常都會提供基於 HTTP協議的實現,也就是說將JSON PRC消息作爲HTTP報文的內容載荷進行傳輸:

bitcoind在不同的運行模式下,會在不同的默認端口監聽HTTP RPC API請求:

  • 主網模式:8332
  • 測試網模式:18332
  • Regtest開發模式:18443

可以在bitcoind的配置文件中使用rpcbind選項和rpcport選項修改監聽端結點, 例如,設置爲本地7878端口:

rpcbind=127.0.0.1
rpcport=7878

二、使用curl測試RPC API

curl是一個支持URL語法的多協議命令行數據傳輸工具,可以從 官網下載:

curl支持HTTP、FTP等多種協議,因此我們可以使用它來驗證節點基於HTTP旳rpc接口 是否正常工作。例如,使用如下的命令訪問節點旳getnetworkinfo接口:

~$ curl -X POST -d '{
> "jsonrpc":"1.0",
> "method":"getnetworkinfo",
> "params":[],
> "id":"123"
> }'  http://user:123456@localhost:18443

curl提供了很多選項用來定製HTTP請求。例如,可以使用-X選項聲明HTTP請求 的方法,對於JSON RPC來說,我們總是使用POST方法;-d選項則用來聲明請求中包含 的數據,對於JSON RPC調用,這部分就是請求消息,例如我們按照getnetworkinfo調用的 要求進行組織即可;命令的最後,也就是RPC調用消息的發送目的地址,即節點RPC API的訪問URL。

默認情況下curl返回的結果是沒有格式化的JSON字符串,對機器友好,但並不適合人類查閱:

如果你希望結果顯示的更友好一些,可以級聯一個命令行的json解析工具例如jq

~$ curl -X POST -s -d '{...}' http://user:123456@localhost:18443 | jq

jq是一個輕量級的命令行JSON處理器,你可以從官網 下載它。

curl -X POST -s -d '{"method":"getnetworkinfo","params":[],"id":123,"jsonrpc":"1.0"}' \
      http://user:123456@localhost:18443 | jq

三、在C#代碼中訪問RPC API

自然,我們也可以在C#代碼中來調用節點旳JSON RPC開發接口,可以藉助於一個 http協議封裝庫來執行這些發生在HTTP之上的遠程調用,例如.NET內置的HttpClient:

例如,下面的代碼使用HttpClient調用比特幣節點的getnetworkinfo接口:

 首先下載bitcoin: https://bitcoin.org/zh_CN/download,如果使用主網絡需要同步240G的數據,這裏在本地以私鏈模式運行。私鏈模式運行也比較容易配置,只需要在bitcoin.conf中配置regtest=1。在windows下,bitcoin.conf的默認路徑爲%APPDATA%\bitcoin\bitcoin.conf。我的電腦在C:\Users\Administrator\AppData\Roaming\Bitcoin目錄下。默認情況下bitcoind並不會自動創建上述路徑下的bitcoin.conf配置文件,因此需要 自行製作一份放入上述目錄。如果你沒有現成的配置文件可用,可以從github拷貝一份:https://github.com/bitcoin/bitcoin/blob/master/share/examples/bitcoin.conf。關於bitcoin.conf的配置可以參考我的另一博客。

這裏regtest=1使用私鏈模式,server=1啓動rpc,rpcuser=usertest、rpcpassword=usertest 設置用戶名、密碼。

#testnet=0
regtest=1
proxy=127.0.0.1:9050
#bind=<addr>
#whitebind=<addr>
#addnode=69.164.218.197
#addnode=10.0.0.2:8333
#connect=69.164.218.197
#listen=1
#maxconnections=
server=1
#rpcbind=<addr>
rpcuser=usertest
rpcpassword=usertest
#rpcclienttimeout=30
#rpcallowip=10.1.1.34/255.255.255.0
#rpcallowip=1.2.3.4/24
#rpcallowip=2001:db8:85a3:0:0:8a2e:370:7334/96
#rpcport=8332
#rpcconnect=127.0.0.1
#txconfirmtarget=n
#paytxfee=0.000x
#keypool=100
#prune=550
#min=1
#minimizetotray=1
View Code

啓動之後如下圖所示:會有一個regtest標記。

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

namespace RPCHttpClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.Run(async () =>
            {
                HttpClient httpClient = new HttpClient();

                byte[] authBytes = Encoding.ASCII.GetBytes("usertest:usertest");
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes));

                string payload = "{\"jsonrpc\":\"1.0\",\"method\":\"getnetworkinfo\",\"params\":[],\"id\":7878}";

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

                string ret = await rsp.Content.ReadAsStringAsync();
                Console.WriteLine(ret);
                Console.ReadLine();
            }).Wait();
        }
    }
}
View Code

在上面的代碼中,我們首先實例化一個HttpClient對象並設置HTTP驗證信息,然後調用該對象 的PostAsync()方法向節點旳RPC端口發送請求消息即可完成調用。

 四、序列化與反序列化

在應用邏輯裏直接拼接RPC請求字符串,或者直接解析RPC響應字符串,都不是件令人舒心的事情, 我們需要改進這一點。

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

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

現在我們獲取比特幣網絡信息的代碼可以不用直接操作字符串了:

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

namespace RPCHttpDTO
{
    class RpcRequestMessage
    {
        [JsonProperty("id")]
        public int Id;

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

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

        [JsonProperty("jsonrpc")]
        public string JsonRPC = "1.0";

        public RpcRequestMessage(string method, params object[] parameters)
        {
            Id = Environment.TickCount;
            Method = method;
            Parameters = parameters;
        }
    }
     

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

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

        [JsonProperty("jsonrpc")]
        public string JsonRPC { get; set; }
    }
    
}
View Code
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace RPCHttpDTO
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.Run(async () =>
            {
                HttpClient httpClient = new HttpClient();

                byte[] authBytes = Encoding.ASCII.GetBytes("usertest:usertest");
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes));

                RpcRequestMessage reqMsg = new RpcRequestMessage("getnetworkinfo");
                Console.WriteLine("=> {0}", reqMsg.Method);

                string payload = JsonConvert.SerializeObject(reqMsg);


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

                string ret = await rsp.Content.ReadAsStringAsync();
                RpcResponseMessage rspMsg = JsonConvert.DeserializeObject<RpcResponseMessage>(ret);
                Console.WriteLine("<= {0}", rspMsg.Result);
                Console.ReadLine();
            }).Wait();
        }
    }
}
 
    
View Code

五、使用JSON RPC封裝庫

 除了直接使用HTTP協議庫來訪問比特幣節點,在開源社區中也有一些直接針對 比特幣RPC協議的封裝,例如MetacoSA的NBitcoin

NBitcoin是.NET平臺上最完整的比特幣開發庫,實現了很多相關的比特幣改進建議(Bitcoin Improvement Proposal)。 與RPC協議封裝相關的類主要在NBitcoin.RPC命名空間下,入口類爲RPCClient, 它代表了對一個的比特幣RPC訪問端結點的協議封裝。

例如,下面的代碼創建一個指向本機的私有鏈節點RPC的RPCClient實例:

//using NBitcon.RPC;
string auth = "user:123456";        //rpc接口的賬號和密碼
string url = "http://localhost:18443"     //本機私有鏈的默認訪問端結點
Network network  = Network.RegTest;      //網絡參數對象
RPCClient client = new RPCClient(auth,url,network);  //實例化

比特幣有三個不同的網絡:主網、測試網和私有鏈,分別有一套對應的網絡參數。 在NBitcoin中,使用Network類來表徵比特幣網絡,它提供了三個靜態屬性分別 返回對應於三個不同網絡的Network實例。在實例化RPCClient時需要傳入與節點 對應的網絡參數對象,例如當連接的節點是主網節點時,需要傳入Network.Main, 而當需要本地私有鏈節點時,就需要傳入Network.RegTest

一旦實例化了RPCClient,就可以使用其SendCommand()SendCommandAsync() 方法調用比特幣節點的RPC接口了。容易理解,這兩個方法分別對應於同步調用 和異步調用,除此之外,兩者是完全一致的。

例如,下面的代碼使用同步方法調用getnetworkinfo接口返回節點軟件版本號:

//using Newtonsoft.Json.Linq;
RPCRequest req = new RPCRequest{           //RPC請求對象
  Method = "getnetworkinfo",
  Params = new object[]{}
};
RPCResponse rsp = client.SendCommand(req); //返回RPC響應對象
Console.WriteLine(rsp.ResultString); //ResultString返回原始的響應字符串

SendCommand/SendCommandAsync的重載

如果你注意到實例化RPCRequest對象最重要的是Method和Params這兩個屬性,就容易 理解應該有更簡單的SendCommand/SendCommandAsync方法了。下面是最常用的一種, 只需要傳入方法名和動態參數列表,不需要自己再定義RPCRequest數據:

public RPCResponse SendCommand(string commandName, params object[] parameters)

例如,下面的代碼分別展示了無參和有參調用的使用方法:

client.SendCommand("getnetworkworkinfo");  //無參調用
client.SendCommand("generate",1);          //有參調用

容易理解,這個重載在內部幫我們構建了RPCRequest對象。

從響應結果中提取數據

RPCResponse的ResultString屬性返回原始的JSON響應字符串,因此從中提取 數據的一個辦法就是將其轉換爲C#的動態對象,這是最簡明直接的方法:

dynamic ret = JsonConvert.DeserializeObject(rsp.ResultString);
Console.WriteLine(ret.networks[0].name);

另一種提取數據的方法是使用RPCResponse的Result屬性,它返回一個JToken對象, 因此可以非常方便地使用JPath表達式來提取指定路徑的數據。

例如,下面的代碼從getnetworkinfo的響應結果中提取並顯示節點啓用的所有網絡 接口名稱:

IEnumerable<JToken> names = rsp.Result.SelectTokens("networks[*].name"/*JPath表達式*/); 
foreach(var name in names) Console.WriteLine(name);

如果你不熟悉JToken和JPath,那麼JToken的使用方法可以訪問其 官網文檔, 關於JPath表達式可以訪問這裏

 首先需要引入NBitcoin。

using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;

namespace RPCNbitcoin
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.Run(async () => {
                RPCClient client = new RPCClient("usertest:usertest", "http://localhost:18443", Network.RegTest);
                RPCRequest req = new RPCRequest
                {
                    Method = "getnetworkinfo",
                    Params = { }
                };
                RPCResponse rsp = await client.SendCommandAsync(req);
                dynamic ret = JsonConvert.DeserializeObject(rsp.ResultString);
                Console.WriteLine("network#0 => {0}", ret.networks[0].name);

                var names = rsp.Result.SelectTokens("networks[*].name");
                foreach (var name in names) Console.WriteLine(name);
                Console.ReadLine();
            }).Wait();
        }
    }
}
View Code

 六、NBitcoin的RPC封裝完成度

在大多數情況下,使用RPCClient的SendCommand或SendCommandAsync方法, 就可以完成比特幣的RPC調用工作了。考慮到比特幣RPC接口本身的不穩定性, 這是萬能的使用方法。

不過看起來NBitcoin似乎是希望在RPCClient中逐一實現RPC接口,雖然 這一任務還沒有完成。例如,對於getbalance調用,其對應的方法爲 GetBalance和GetBalanceAsync,因此我們也可以採用如下的方法獲取錢包餘額:

Money balance = client.GetBalance();
Console.WriteLine("balance: {0} BTC", balance.ToUnit(MoneyUnit.BTC)); //單位:btc
Console.WriteLine("balance: {0} SAT", balance.Satoshi);               //單位:sat

顯然,NBitcoin的預封裝方法進行了額外的數據處理以返回一個Money實例, 這比直接使用SendCommand會更方便一些:

 

因此如果NBitcoin已經實現了你需要的那個RPC接口的直接封裝,建議首選直接封裝方法, 可以在這裏 查看RCPClient的官方完整參考文檔。

下表列出了部分在RPCClient中已經實現的RPC接口及對應的同步方法名,考慮到空間問題, 表中省去了異步方法名,相信這個清單會隨着NBitcoin的開發越來越長:

分類RPC接口RPCClient方法備註
P2P網絡 addnode AddNode 添加/刪除P2P節點地址
  getaddednodeinfo GetAddedNodeInfo 獲取添加的P2P節點的信息
  getpeerinfo GetPeerInfo 獲取已連接節點的信息
區塊鏈 getblockchaininfo GteBlockchainInfo 獲取區塊鏈的當前信息
  getbestblockhash GetBestBlockHash 獲取最優鏈的最近區塊哈希
  getblockcount GetBlockCount 獲取本地最優鏈中的區塊數量
  getblock GetBlock 獲取具有指定塊頭哈希的區塊
  getblockhash GetBlockHash 獲取指定高度區塊的塊頭哈希
  getrawmempool GetRawMemPool 獲取內存池中的交易ID數組
  gettxout GetTxOut 獲取指定的未消費交易輸出的詳細信息
工具類 estimatefee EstimateFee 估算千字節交易費率
  estimatesmartfee EstimateSmartFee  
未公開 invalidateblock InvalidateBlock  
錢包 backupwallet BackupWallet 備份錢包文件
  dumpprivkey DumpPrivateKey 導出指定地址的私鑰
  getaccountaddress GetAccountAddress 返回指定賬戶的當前地址
  importprivkey ImportPrivKey 導入WIF格式的私鑰
  importaddress ImportAddress 導入地址以監聽其相關交易
  listaccounts ListAccounts 獲取賬戶及對應餘額清單
  listaddressgroupings ListAddressGroupings 獲取地址分組清單
  listunspent ListUnspent 獲取錢包內未消費交易輸出清單
  lockunspent LockUnspent 鎖定/解鎖指定的交易輸出
  walletpassphrase WalletPassphrase 解鎖錢包
  getbalance GetBalance 獲取錢包餘額
  getnewaddress GetNewAddress 創建並返回一個新的錢包地址

值得指出的是,NBitcoin採用了PASCAL命名規則來生成RPC接口對應的方法名稱, 即每個單詞的首字母大寫。

 

using NBitcoin;
using NBitcoin.RPC;
using System;

namespace RPCNbitcoinAdvanced
{
    class Program
    {
        static void Main(string[] args)
        {
            RPCClient client = new RPCClient("usertest:usertest", "http://localhost:18443", Network.RegTest);

            Money balance = client.GetBalance();
            Console.WriteLine("balance => {0} btc", balance);

            BitcoinAddress address = client.GetNewAddress();
            Console.WriteLine("address => {0}", address);

            uint256 txid = client.SendToAddress(address, Money.Coins(0.1m));
            Console.WriteLine("sent 0.1 btc to above address.");

            client.Generate(100);
            Console.WriteLine("mined a block.");

            UnspentCoin[] coins = client.ListUnspent(0, 9999, address);
            foreach (var coin in coins)
            {
                Console.WriteLine("unspent coin => {0} btc", coin.Amount);
            }
            Console.ReadLine();
        }
    }
}

 七、利用UTXO計算錢包餘額

我們知道,比特幣都在UTXO上存着,因此容易理解,錢包的餘額 應該就是錢包內所有的地址相關的UTXO的彙總:

首先查看錢包餘額:

Money balance = client.getBalance();

然後使用listunspent接口列出錢包內地址相關的UTXO:

UnspentCoin[] coins = client.ListUnspent(); //listunspent接口封裝方法
long amount = 0;
foreach(var coin in coins){          //累加所有utxo的金額
    amount += coin.Amount.Satoshi;   
}

ListUnspent()方法返回的結果是一個數組,每個成員都是一個UnspentCoin 對象:

最後我們比較一下:

if(balance.Satoshi == amount){ Console.WriteLine("verified!"); }

using NBitcoin;
using NBitcoin.RPC;
using System;
using System.Threading.Tasks;

namespace CalcBalance
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.Run(async () => {
                RPCClient client = new RPCClient("usertest:usertest", "http://localhost:18443", Network.RegTest);
                Money balance = await client.GetBalanceAsync();
                Console.WriteLine("getbalance => {0}", balance.Satoshi);
                UnspentCoin[] coins = await client.ListUnspentAsync();
                long amount = 0;
                foreach (var coin in coins)
                {
                    amount += coin.Amount.Satoshi;
                }
                Console.WriteLine("unspent acc => {0}", amount);

                if (balance.Equals(Money.Satoshis(amount))) Console.WriteLine("verified successfully!");
                else Console.WriteLine("failed to verify balance");
                Console.ReadLine();
            }).Wait();
        }
    }
}
View Code

八、讓網站支持比特幣支付

使用bitcoind,我們可以非常快速地爲網站增加接受比特幣支付的功能:

當用戶選擇採用比特幣支付其訂單時,網站將自動提取該訂單對應的 比特幣地址(如果訂單沒有對應的比特幣地址,則可以使用getnewaddress創建一個), 並在支付網頁中顯示訂單信息、支付地址和比特幣支付金額。爲了方便 使用手機錢包的用戶,可以將支付信息以二維碼的形式在頁面展現出來:

用戶使用比特幣錢包向指定的地址支付指定數量的比特幣後,即可點擊 [已支付]按鈕,提請網站檢查支付結果。網站則開始週期性地調用節點 的getreceivedbyaddress命令來檢查訂單對應地址的收款情況,一旦 收到足量比特幣,即可結束該訂單的支付並啓動用戶產品或服務的交付。 默認情況下,getreceivedbyaddress將至少需要六個確認纔會報告 地址收到的交易。

除了使用getreceivedbyadress命令來輪詢收款交易,另一種檢查 用戶支付的方法是使用bitcoind的walletnotify選項。當bitcoind檢測 到錢包中的地址發生交易時,將會調用walletnotify選項設置的腳本, 並傳入交易id作爲參數,因此可以在腳本中進一步獲取交易詳細信息。 例如在下面的配置文件中,當錢包中的地址發生交易時,將觸發 tx-monitor.sh腳本:

walletnofity=/var/myshop/tx-monitor.sh %s

這是一個相當樸素的方案,但很容易實現。此外,如果你需要實時進行 法幣和比特幣的換算,還可以使用blockchain.info 提供的相關api。

 

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