1. 程式人生 > >比特幣入門之使用PRC應用開發介面

比特幣入門之使用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。

&n