一、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。

 

版权声明:本文为5ishare原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/5ishare/p/10933399.html