比特币入门之使用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。