使用OmniCore在Unix系统上构建USDT钱包
USDT是由Tether公司发行的基于比特币区块链的一种去中心化数字货币,作为当前数字货币市场的主流锚定货币之一,其官方承诺将严格遵守与美元1:1的比例准备保证金。在技术层面,USDT是基于Omni协议发行的代币,在Omni共识网络上令牌id为31。Omni是一个可以自由发行数字货币的平台,它完全基于比特币协议,并在原有的比特币核心上增加了新的共识网络,类似与HTTP协议基于TCP协议。
OmniCore是Omni协议的C++实现,完全采用与bitcoin的区块数据,所以如果需要同时集成USDT与BTC,实际上只需要使用OmniCore一个核心钱包即可。
本文的主要内容是介绍如何在服务端集成OmniCore实现USDT钱包的基本功能
示例代码仓库 https://github.com/initsysctrl/WalletDe
参数说明:
- 服务端 Ubuntu x86 5.4.0-6
- OmniCore 0.3.1 fork bitcoin 0.13
- 语言:java 、c++
一.安装与配置
1.安装OmniCore客户端
Step1:安装git和pkg-config,已经安装过的可以跳过
sudo apt-get install git
sudo apt-get install pkg-config
Step2:Clone OmniCore
git clone https://github.com/OmniLayer/omnicore.git
Step3:安装依赖项
首先安装必须的构建工具
sudo apt-get install build-essential libtool autotools-dev automake pkg-config libssl-dev libevent-dev bsdmainutils
然后安装boost,为了兼容各个系统版本,建议安装所有的boost开发包
sudo apt-get install libboost-all-dev
最后安装BerkeleyDB。尽管Ubuntu自带libdb-dev,但同样为了钱包的兼容性,建议使用下面的版本
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:bitcoin/bitcoin
sudo apt-get update
sudo apt-get install libdb4.8-dev libdb4.8++-dev
本文中并不需要构建Bitcoin-Qt,所以没有依赖ZMQ和GUI。
Step4:开始构建OmniCore
上一步依赖安装完毕后,进入OmnIcore安装目录
cd omnicore/
执行构建脚本
./autogen.sh
./configure
make
2.bitcoin 基本配置
启动之前,需要配置位于工程目录之下的.bitcoin文件夹中的OmniCore配置文件bitcoin.conf
server=1
txindex=1
rpcuser=你的rpc用户名
rpcpassword=你的rpc密码
rpcallowip=127.0.0.1
rpcport=8332
paytxfee=0.00001
minrelaytxfee=0.00001
datacarriersize=80
logtimestamps=1
omnidebug=tally
omnidebug=packets
omnidebug=pending
-
server=1
代表开启RPC访问 -
txindex=1
代表事务初始索引 -
recuser
和rpcpassword
代表rpc访问的身份验证, -
rpcallowip
和rpcport
代表允许访问钱包的ip地址及端口。 -
paytxfee
和minrelattxfee
控制bitcoin交易的手续费,Omni交易也属于一种特殊的比特币交易,打包与广播也需要向矿工支付费用。手续费设置过低会造成交易确认慢甚至交易失败,手续费过高会造成资源的浪费(以2018.09.13的BTC价格换算,每多消耗0.0001btc需要浪费4rmb),所以设置动态配置交易手续费十分必要。预估比特币交易手续费可以使用下面的网址bitcoinfees.earn,buybitcoinworldwide。假设当前预估的比特币交易费率为0.0000001BTC/Byte,那么需要设置paytxfee=0.00001BTC/kByte
。
3.启动方式
上述构建完成后,进入omnicore/src
目录,开始启动钱包,启动时可以配置启动项以选择不同的网络。
-
./omnicored -testnet
连接test3测试网络,会同步test3网络的区块数据(约20G) -
./omnicored -regtest
单机运行,不需要连接其他网络,区块数据在本地运行。 -
./omnicored
连接比特币主网网络,会同步真实区块数据(约180G)。
钱包初始化完成后,将自动开始同步区块。启动主网或测试网后需要同步一段较长的时间,在这段时间内不要进行任何交易。可以新开一个终端连接钱包所在服务器,通过getinfo
和omni_getinfo
可以查看底层bitcoin信息和上层omni信息。区块浏览器(Test3区块浏览器)可以作为区块同步的参考。同步将作为守护进程在后台执行,如果需要停止,使用指令 ./omnicore-cli stop
。前期开发建议在test3测试网络上进行。
$ ./omnicore-cli getinfo
{
"version": 130200,
"protocolversion": 70015,
"walletversion": 130000,
"balance": 4.85909131, //钱包比特币总余额
"blocks": 1413349,
"timeoffset": 0,
"connections": 0,
"proxy": "",
"difficulty": 56234572.68927951,
"testnet": true, //是否是测试网
"keypoololdest": 1535371434,
"keypoolsize": 100,
"paytxfee": 0.00010000,
"relayfee": 0.00001000,
"errors": ""
}
$ ./omnicore-cli omni_getinfo
{
"omnicoreversion_int": 30001000,
"omnicoreversion": "0.3.1", //omni core 版本
"mastercoreversion": "0.3.1",
"bitcoincoreversion": "0.13.2", //基于比特币版本
"block": 1413349,//区块要举高高
"blocktime": 1536841368,
"blocktransactions": 0,
"totaltrades": 15601,
"totaltransactions": 43731,
"alerts": [
]
}
二.RPC访问
1. json-rpc协议
json-rpc是一种轻量级传输协议,定义一个完整网络请求中请求对象的格式和响应对象的格式。与rest api相比,仅仅只是数据格式的差异而已,网络请求的本身并没有什么差别。
请求对象:
{
"jsonrpc": "2.0",//rpc版本号
"method": "your_method",//方法名
"params": [//参数数组
"var1",
"var2"
],
"id": 9527//请求
}
响应对象(正确):
{"jsonrpc": "2.0", "result": "this is result", "id": 9527}
响应对象(错误):
{
"result": null,
"error": {
"code": -32601,//错误码
"message": "Method not found"//错误原因
},
"id": 9527
}
2. 通过RPC接口连接钱包
请求的协议是http,请求的地址是钱包主机地址。身份验证信息将以Authorzation的形式添加到headers中,方法、参数、id信息将以raw的形式添加到hbody中:
image image返回的结果如下:
image服务端,以java为例:
String RPC_USER = "your_user_name";
String RPC_PASSWORD = "your_password";
RestTemplate client = new RestTemplateBuilder()
.basicAuthorization(RPC_USER, RPC_PASSWORD)
.rootUri(URL)
.build();
client.postForObject(URL, baseRpcReq, BaseRPCresponse.class);
或者采用jsonrpc4j
,这种方式可以捕捉异常便于调试:
<!--JSON-PRP handler-->
<dependency>
<groupId>com.github.briandilley.jsonrpc4j</groupId>
<artifactId>jsonrpc4j</artifactId>
<version>1.5.3</version>
</dependency>
String RPC_USER = "your_user_name";
String RPC_PASSWORD = "your_password";
String cred = Base64.encodeBase64String((RPC_USER + ":" + RPC_PASSWORD).getBytes());
Map<String, String> headers = new HashMap<>(1);
headers.put("Authorization", "Basic " + cred);
try {
this.mClient = new JsonRpcHttpClient(new URL(URL), headers);
} catch (MalformedURLException e) {
e.printStackTrace();
}
rpc仅仅只是一种数据请求的固定格式,username和passwrod并不能保证访问的安全性。钱包需要配置rpcallowip字段来限定运行访问钱包的ip地址,默认情况下为localhost
,在测试节点,可以使用0.0.0.0/0
开启无限制访问。
三.核心指令集
Omnicore的指令集完全兼容bitcoin,除了与omni令牌相关的指令集外,其余的指令集都来全部继承自bitcoin-cli。下面为钱包创建的核心指令集,更详细的内容可以从OmniCore JSON-RPC和BitCoin JSON-RPC进行查询。
1.生成BTC、USDT地址
因为Omnicore底层基于Bitcoin,所以USDT地址实际上就是BTC地址,当前的比特币钱包采用的是deterministic wallet钱包模式,使用如树状层级推导 (hierarchical deterministic) 的推导方式,从一个随机数生成源推导所有地址密钥。所以一个USDT钱包中,所有的地址实际上来自同一个种子源。如果是测试网络,地址一般以"m","n"获取新地址可以指定account名称,如果不指定,那么会分配到默认账户。
$ ./omnicore-cli getnewaddress
mkRj6TFspkyso96LvDTJq77DwoqoFMBEcJ
$ ./omnicore-cli getnewaddress feeaccount
n1WuiWX5zjmz7MVymUtsdSnC325xQ1v4SR
一个账户名可以对应多个地址
$ ./omnicore-cli getaddressesbyaccount feeaccount
[
"mk8cMZBX7v7zzzc9FHBMQbRNVPwRtq9CZ2",
"n1WuiWX5zjmz7MVymUtsdSnC325xQ1v4SR"
]
如果是正式环境,那么必须使用其他地址转账或提现到新地址才能获取BTC和USDT。如果使用regtest本地网络,那么需要通过挖矿获得比特币。如果使用test3测试网络,那么TBTC可以从coinfaucet或者faucet获取。但在测试网络是没有测试usdt的,所有只能用test omni代替usdt进行测试。发送TBTCmoneyqMan7uh8FqdCA2BV5yZ8qVrc9ikLP
即可获得少量TOMNI,汇率为100 TOMNI/1 TBTC,令牌id分别为1和2。
2.查询BTC的未花费列表
比特币实际上没有“余额”这个概念,只有UTXO(Unspent Transaction Outputs)。在传统的交易系统中,从A地址转给B地址100个单位的资产的过程是把A地址下的余额减100,B地址下的余额加100,两步必须满足原子性。但在比特币中A地址下并没有余额,只有一张张零碎的“支票”,记录着每一笔转入资金,转账的过程实际上是把一张或者多张“支票”凑起来花费掉,没有花掉的部分作为“找零”返回给找零地址。所以一般需要把找零地址设置为发送地址,如果没有的话,系统将在钱包中随机挑选一个地址作为“找零地址”。
列出比特币UTXO:
方法: listunspent
参数:
- min_confrim (int,可选) 最小确认
- max_comfrim(int ,可选)最大确认
- address (string[],可选)地址列表
返回:UTXO列表
$ ./omnicore-cli listunspent 0 999999 '["mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC"]'
[
{
"txid": "ef6e77063dff8988b82044286b3d3f022df5a14aae260179a95c2e80c0e47ec4",//来源
"vout": 2,
"address": "mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC",//地址
"account": "account0",//所属账户
"scriptPubKey": "76a914293d87f697ca96ffb00f049b60645e5c8979498488ac",
"amount": 0.00054600,//支票金额
"confirmations": 799,
"spendable": true,//可花费?
"solvable": true
}
]
3.USDT转账
USDT的转账实际上是代号为31的OmniCore令牌转账。Omnicore提供了多套api实现令牌转账功能,v0.3.1版本之前,可以使用omni_send
和omni_sendall
。这种方式必须保证发送地址上不仅需要有令牌余额,还需要有一定数量的比特币用于支付手续费。从v0.3.1版本开始,Omnicore提供了两个新的api omni_funded_send
和omni_funded_sendall
,这种方式的好处在于可以指定手续费的支付方,所有的令牌交易都可以使用统一的地址进行支付比特币手续费,而不需要发送者自身拥有比特币。但这里并未设定手续费的具体数量,系统将根据在配置文件中的关于手续费的配置文件进行动态设定。
方法 omni_funded_send
参数
-
fromaddress (string,必选) 令牌发送者
-
toaddress (string,必选) 令牌接收者
-
propertyid (number,必选) 令牌id
-
amount (string,必选) 发送金额
-
feeaddress (string,可选)用于支付手续费的地址,如果设置此地址,那么此地址上必须拥有比特币
返回:事务 hex
$ ./omnicore-cli omni_funded_send mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC mqrA5Ai8XdKe1ob1L2HwyYr3TXUf9nUeBf 1 5 mpaumxor659PhoJhXp1VCVHVwbFCZSRmuf
a25260a79243a48df21ca2d9fba2209818ea1339026d91b6476d531929c52dad
错误返回:
{
error code: -212
error message:Error choosing inputs for the send transaction
}
发送USDT或其他令牌的过程属于一种比较特殊的比特币交易,交易的打包广播同样需要支付矿工费用,费用太低交易将无法成功。发送令牌的过程可能会出现各种错误,可以检查发送者地址是否是本地钱包地址令牌余额是否充足、feeaddress是否是本机钱包地址、比特币余额是否充足。
除了使用基本的api外,还可以使用 Raw Transaction API 创建并广播事务,但过程相当的繁琐,需要经过七步构建。一般情况下不建议这么做,但如果需要将打包签名的过程与发送的过程进行分离,那么就必须使用这种方式。例如某些情况下,需要在冷钱包中签名,然后在热钱包中广播。
4.查询交易事务
方法:omni_gettransaction
参数:hex(string_64位事务哈希),发送交易后的交易哈希txid
返回 :
- 未确认状态
$ ./omnicore-cli omni_gettransaction a25260a79243a48df21ca2d9fba2209818ea1339026d91b6476d531929c52dad
{
"txid": "a25260a79243a48df21ca2d9fba2209818ea1339026d91b6476d531929c52dad",//交易哈希
"fee": "0.00002765",//手续费金额
"sendingaddress": "mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC",//发送者
"referenceaddress": "mqrA5Ai8XdKe1ob1L2HwyYr3TXUf9nUeBf",//接受者
"ismine": true,//是否本机地址
"version": 0,
"type_int": 0,
"type": "Simple Send",
"propertyid": 1,
"divisible": true,
"amount": "5.00000000",//发送令牌金额
"confirmations": 0//确认数,,默认情况下,>5一般才认为交易有效
}
- 已确认状态
$ ./omnicore-cli omni_gettransaction a25260a79243a48df21ca2d9fba2209818ea1339026d91b6476d531929c52dad
{
"txid": "a25260a79243a48df21ca2d9fba2209818ea1339026d91b6476d531929c52dad",//交易哈希
"fee": "0.00002765",
"sendingaddress": "mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC",//发送address
"referenceaddress": "mqrA5Ai8XdKe1ob1L2HwyYr3TXUf9nUeBf",//接收address
"ismine": true,
"version": 0,
"type_int": 0,
"type": "Simple Send",//交易类型
"propertyid": 1,
"divisible": true,
"amount": "5.00000000",
"valid": true,//已经成功
"blockhash": "00000000000000460219a9fe9761cb92120eb7d67b640d2b643a0a05185fa2a0",
"blocktime": 1536324400,
"positioninblock": 1391,
"block": 1412595,
"confirmations": 1123//通过节点确认
}
5.查询本地事务列表
方法:omni_listtransactions
参数:
addfilt | string | 可选 | 地址过滤 (default: "*" ) |
---|---|---|---|
count |
number | 可选 | 最大数量(default: 10 ) |
skip |
number | 可选 | 跳过第n个事务 (default: 0 ) |
startblock |
number | 可选 | 起始的区块(default: 0 ) |
endblock |
number | 可选 | last block to include in the search (default: 999999 ) |
请求:
$ ./omnicore-cli omni_listtransactions
[
{
"txid": "a25260a79243a48df21ca2d9fba2209818ea1339026d91b6476d531929c52dad",//事务哈希
"fee": "0.00002765",//手续费
"sendingaddress": "mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC",//发送
"referenceaddress": "mqrA5Ai8XdKe1ob1L2HwyYr3TXUf9nUeBf",//接收
"ismine": true,
"version": 0,
"type_int": 0,
"type": "Simple Send",//类型
"propertyid": 1,//令牌id
"divisible": true,
"amount": "5.00000000",
"valid": true,//是否有效的交易事务
"blockhash": "00000000000000460219a9fe9761cb92120eb7d67b640d2b643a0a05185fa2a0",
"blocktime": 1536324400,
"positioninblock": 1391,
"block": 1412595,
"confirmations": 221//确认数,默认大于5猜有效
},
{
"txid": "2e527c3c85b2a9b21252b50efd6cda31022ee5ebcf9fee451255bea61211b799",
"fee": "0.00002570",
"sendingaddress": "mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC",
"referenceaddress": "mq8fRoxRB9M4vstJ9BrEBaysZVUjxPxoK3",
"ismine": true,
"version": 0,
"type_int": 0,
"type": "Simple Send",
"propertyid": 2,
"divisible": true,
"amount": "3.00000000",
"valid": true,
"blockhash": "0000000000000007c957dc0642c39c26b9bb46327620e18e23244936f894c570",
"blocktime": 1536324082,
"positioninblock": 2115,
"block": 1412594,
"confirmations": 222
}
]
如果交易刚刚发送,即没有被验证是否合法,也没有被节点确认,那么该事务将处于pengding 状态使用 omni_listtransactions
不能作为转账的确认状态。使用omni_listpendingtransactions
可以在缓冲区找到这一类型的事务信息,但pengding状态并不稳定,不能用于确认转账结果。
6.查询指定地址的USDT余额
查询USDT的余额即查询第31号令牌的余额。
方法:omni_getbalance
参数:
- address 地址
- id 令牌id
$ ./omnicore-cli omni_getbalance mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC 1
{
"balance": "4.00000000",//余额
"reserved": "0.00000000",
"frozen": "0.00000000"//被冻结,没啥用
}
7.查询钱包内的所有地址的USDT余额列表
方法:omni_getwalletaddressbalances
将返回钱包内所有余额不为0的地址列表,每个地址都可能有不同的令牌余额。令牌id如果为31,那么这个令牌即USDT。
$ ./omnicore-cli omni_getwalletaddressbalances
[
{
"address": "mjH1iB7wt5TC4f8qjvZqtmBXd1aCPSPinC",//地址
"balances": [
{
"propertyid": 1,//令牌id,31=USDT
"name": "Omni",//令牌名称
"balance": "4.00000000",//余额
"reserved": "0.00000000",
"frozen": "0.00000000"
},
{
"propertyid": 2,
"name": "Test Omni",
"balance": "7.00000000",
"reserved": "0.00000000",
"frozen": "0.00000000"
}
]
}
]
8.查询钱包内的USDT总额
方法:omni_getwalletbalances
返回:
$ ./omnicore-cli omni_getwalletbalances
[
{
"propertyid": 1,//令牌id
"name": "Omni",//令牌名称
"balance": "11.00000000",//可用余额
"reserved": "0.00000000",
"frozen": "0.00000000"
},
{
"propertyid": 2,
"name": "Test Omni",
"balance": "8.00000000",
"reserved": "0.00000000",
"frozen": "0.00000000"
}
]
其他的相关指令集会在源代码中示例出来。
四.中心化钱包模式
中心化钱包的本质是代替用户托管资产,钱包保存了所有地址的私钥,对上面的令牌有完全的使用权。对于用户而言,对资产的流动有知情权,但并没有实际控制权。一个完整的中心化钱包可以分为两层,记账层和区块底层,至少需要集成四个基本的业务功能:
1.地址生成
USDT地址即比特币区块链上的地址,借助比特币内核 getnewaddress
可以从同一个种子推导出无数个地址,生成地址的过程类似与把一枚硬币连续抛255次。服务端需要在自己的用户系统中为每个用户生成不同的地址,用户的看到的资产实际上服务端的记账状态,并非真实资产。
2.扫描充值事务
钱包一旦启动,会开启同步区块的守护进程,服务端不需要进行手动的区块同步操作。但服务端需要定期的扫描区块以发现并确认充值事务。通过omni_listtransactions
可以查询当前钱包内的事务列表,根据业务需要,可以定时每小时全量扫描一次,每次最多返回100条事务。遍历每条事务,如果事务已经验证且确认数大于等于6,那么被认为是一条有效的充值记录。然后判断记账层是否已经记录了该事务,如果没有记录则写入充值记录表,同时查询绑定该地址的用户,在余额表中该用户的可用余额加上充值金额。如果已经写入了那么跳过本次事务。单次的事务处理流程如下:
这是最简易的模式,根据业务情景可以适当调整扫描周期和最大事务数。
3.转移充值余额
用户充值后USDT保留在用户绑定的区块地址中,需要及时的转移到中央地址中去。中央地址即保存整个平台资产的一个或者多个地址。可以使用与普通用户相同的“种子”,也可以单独使用一个钱包,或者直接使用冷钱包离线保存。在保证安全和效率的情况下,越少的转账次数越好,可以最大限度的节省手续费。获取钱包地址USDT余额列表有多种方式 ,从v0.3.1开始可以使用omni_getwalletaddressbalances
直接返回所有每个地址的所有令牌列表。一旦检测id=31的令牌余额不为0,且大于最小额度(一般大于预估的手续费)则使用omni_funded_sendall
转移所有的USDT到指定的中央钱包。
但需要注意的是,在Omnicore上从发送者转账转移指定id的令牌到接受者,当交易被创建且被发送成功后,交易验证需要一定时间,发送者的令牌余额不会立即变化。所以如果扫描余额的时间周期太短,会造成一个地址上的余额被多次转移,虽然只会有一次成功但会重复消耗手续费,所以建议2-6hour扫描一次本地钱包余额列表。
4.提现事件
提现是指用户把实际资产从平台钱包中转移出去,只要判断是本人操作而且提现金额小于可用额度就被认为是有效的提现请求。根据提现地址的不同有两种情况:
当提现地址是钱包内的地址时(即平台内的另外一个用户)属于内部转账。这种方式并不需在从中央钱包发送USDT到指定地址,只需要在记账层进行依次对两个账户上的USDT余额进行修改,几乎没有时间延迟。
当提现地址是不是钱包内的地址时(非平台用户)属于外部转账。这种方式需要操作区块链,不会马上进行确认,根据手续费设定和当前比特币主网拥堵状况可能需要几小时到一天的确认时间。
对于外部转账,如果用户绑定的区块地址上还存在余额,那么优先使用该地址进行转账,其次选择中央钱包进行转账。可以使用omni_funded_send
来进行创建USDT交易并广播,交易发送成功后会生成的事务哈希。根据事务哈希,通过omni_gettransaction
可以进行提现进度的跟踪。
参考: