区块链中心化钱包开发
16 年底开发了一款区块链轻钱包,实现比特币、以太坊上代币的转账功能。其愿景是让数字货币便捷地在线下支付。这篇文章介绍整体架构以及实现细节。
这篇文章是笔者第一次接触区块链开发总结而来,现在回过头来看当时一些知识点的认识不是最优解、甚至是错误的,希望读者能够甄别
架构
钱包后端采用 Rails5 实现 Restful API 给客户端提供接口。通过 JSON-RPC 操作 Bitcoin Core, 完成钱包后端对比特币服务节点的操作;同样也是通过 JSON RPC client 操作 go-ethereum, 完成钱包后端对以太坊节点的操作。同时,用 RabbitMQ 实现消息队列,运行的 bitcoin-core 和 go-ethereum 两个区块链节点钱包中的账户有消息回调时推送消息到轻钱包服务中进行相关业务逻辑操作(部署区块链节点的服务器有脚本监听钱包中账户地址入账)。另外还有定时任务获取汇率且刷新 Redis 中数字货币汇率以及法币的汇率,加速了轻钱包汇率接口的访问速度。
实现细节
轻钱包的接口包含三个模块
- 用户信息模块
- 区块链账户模块
- 区块链数字货币交易模块
轻钱包的系统中多处用了状态机(用户开通的币种是否添加了提现地址、转账的状态和状态推移),我使用了 aasm 这个 Gem ;使用 bitcoin-client 客户端和 ethereum-ruby 客户端通过 JSON-RPC 分别操作 bitcoin-core 和 go-ethereum 钱包节点。还有一点就是获取法币的汇率为使用了 currency_switcher 这个 Gem 。最后需要注意的是钱包中与钱相关的字段一定要使用 BigDecimal 类型,具体可以参考这篇文章看看 Handling money in ruby 。
用户信息模块
使用 clearance 和 ruby-jwt 这两个 Gem 实现用户授权验证;在 ApplicationController 中
before_action :authenticate_request!
....
def authenticate_request!
begin
fail NotAuthenticatedError unless user_id_included_in_auth_token?
@current_user = User.find(decoded_auth_token[:user_id])
rescue JWT::ExpiredSignature
raise AuthenticationTimeoutError
rescue JWT::VerificationError, JWT::DecodeError
raise NotAuthenticatedError
end
end
结合这篇文章 Authentication with JSON Web Tokens using Rails and React / Flux 和 clearance 文档实现起来应该不难,在此就不深入讨论,读者感兴趣可自行探索。用户头像上传的实现使用了 paperclip,其 model 和 controller 大概如下,感兴趣的读者可阅读这个 Gem 的 readme 了解详细使用方法。
user model
has_attached_file :avatar, url: "/avatar/:hash.:extension", hash_secret: "longSecretString", styles: { medium: "300x300>", thumb: "100x100>" }, default_url: "/missing.png" do_not_validate_attachment_file_type :avatar
AttachmentController
class AttachmentController < ApplicationController def create requires! :avatar user = @current_user user.avatar = params[:avatar] path = BlockChainWallet::Tools.avator_path user, request render json: { path: path }, :status => 201 if user.save! end end # curl --header "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo4LCJleHAiOjE0NzgzMzQ4NTcsImp0aSI6ImJmMGI3YTU2ZDhjMWFkMzE0ZmU5NmQ4Y2U5NGQ1MjQwIn0.P9qe_FbeVRTn649n_QVWOuwgTIo2tHB4rUDXsQHJxe4" -i -X POST -H "Content-Type: multipart/form-data" -F "avatar=@/Users/hww/Desktop/block.jpeg" api.block.com/attachment
短信使用了云片网的服务,我还做了一个 sms 使用方式大概如下
response, msm_send_result = Sms::SmsService.deliver(params[:mobile], "【区块链科技】您的验证码是")
if msm_send_result
user = @current_user
if user.phone
user.phone.update_attributes({ "mobile": response["mobile"], "code": response["random_code"], "sid": response["sid"] })
else
phone = Phone.new(mobile: response["mobile"], sid: response["sid"], code: response["random_code"])
phone.user = user
phone.save!
end
render json: {message: "短信验证码发送成功"}, status: 201
else
render json: {message: response["detail"]}, status: 201
end
区块链账户模块
轻钱包中区块链账户分为比特币账户和以太坊中代币账户。这个模块大致情况为:account model 是一个基类,每种数字货币通过“单表继承”的实现方法继承了 account 这个基类。每个用户开通的币种都对应一个区块链地址,所以当 account 记录生成之后,在 after_create 回调中生成一个 address 记录。为什么用户开通的币种账户所对应的区块链地址不作为 account 表某个字段存在呢。原因是我们后续还有有个“提现地址”的类型。系统给用户开通的币种分配了一个“充值地址”,同时用户也可为开通的币种添加多个提现地址,把轻钱包中的资产体现到绑定的提现地址中。所以我设计了一张 address 数据库表,同样也有一个 address 模型 ,通过单表继承实现“充值地址”和“提现地址”两个模型,同时 address 和 account 做关联关系对应起来。
定时任务获取汇率
用户开通了区块链账户之后,在个人账户列表中有对应币种的汇率。汇率的结果生成的过程分为两部:第一步是从交易所中获取数字货币换成某种法币的汇率,由于最后的结果统一现实为指定的法币,所以有第二部,那就是把从交易所得到的汇率换成指定法币的汇率。两步去第三方获取汇率,如果事先不把这些数据缓存到本地而事实去获取的话,轻钱包的接口简直没法用。所以我使用了定时任务,每一分钟刷新 redis 中的汇率数据,轻钱包直接读缓存中的数据而不是去请求第三方获取。实现只要用了 sidekiq-cron 和 Active Job ,具体使用教程可以参考我之前写的一篇文章 Sidekiq 和 ActiveJob 实现定时循环任务
区块链数字货币交易模块
交易模块是钱包的核心功能。细分为以下功能:
- 充值
- 提现
- 钱包之前转账
充值和提现需要区块链确认,bitcoin-core 自带 wallet-notify 回调和 另外一个脚本实现 go-ethereum 钱包中账户发生充值事件产生的回调通过消息队列,在轻钱包中生成交易记录,并记录该交易的交易状态(是否被确认)并把这个交易产生的 txid 放到 redis 中,有一个定时任务去区块链中查询发生在钱包中的充值或者提现交易是否得到指定的确认数,若满足确认数则更改交易的状态,用户在轻钱包的交易记录中可以看到交易状态的更改。transitions 记录状态推进如下:
aasm :column => :state do
state :canceled
state :launch
state :checked
state :pendding
state :confirmed
event :cancel do
transitions :from => :launch, :to => :canceled
end
event :check do
transitions :from => :launch, :to => :checked
end
event :pend do
transitions :form => [:launch, :checked], :to => :pendding
end
event :confirm do
transitions :form => [:launch, :pendding], :to => :confirmed
end
end
- topup_launch 发起充值
- topup_confirmed 充值达到确认要求
- withdraw_launch 发起提现申请
- withdraw_cancel 取消提现申请
- withdraw_checked 提现被审核通过
- wallet 钱包用户之间转账(秒到账)
写在最后
实现轻钱包的接口过程中以资源的角度看到业务需求,尽量遵循 Restful 的规范。