在前面两篇帖子中,我们实现了signature-machine离线签名机项目和chains-union-rpc多链统一rpc服务。在这一篇帖子中,我们正式来开始实现一个钱包的业务层。
在前面两篇帖子中,我们实现了 signature-machine
离线签名机项目和 chains-union-rpc
多链统一 rpc
服务。在这一篇帖子中,我们正式来开始实现一个钱包的业务层。它将和 signature-machine
签名机以及 chians-union-rpc
统一 rpc
进行联通,共同构成一个完整的交易所钱包业务流。
完整项目 github
地址(如果对您有用,请给个小 star
):
exchange-wallet-service
:钱包业务层服务https://github.com/Shawn-Shaw-x/exchange-wallet-service signature-machine
:离线签名机 https://github.com/Shawn-Shaw-x/signature-machinechains-union-rpc
:多链统一 rpc
https://github.com/Shawn-Shaw-x/chains-union-rpcexchange-wallet-service
是一个基于 gRPC
和 PostgreSQL
构建的高性能钱包服务,支持交易所钱包 SaaS
化部署,为多项目方提供账户体系、链上交易扫描、充值提现管理、热冷钱包归集与划转等全功能解决方案。
PS
: 本钱包项目基于交易所钱包业务抽象、简化而来。添加多租户支持,可提供项目方 Sass
化支持
Http
、gRPC
等形式将充值、提现等事件推送给业务方。技术 | 描述 |
---|---|
gRPC |
服务间通信协议,定义清晰的 protobuf 接口 |
GORM |
Go ORM 框架,简化数据库访问 |
PostgreSQL |
持久化存储引擎 |
Protobuf |
用于服务接口定义和数据结构描述 |
Makefile |
标准化开发与部署流程 |
Go Modules |
依赖管理与构建 |
├── cmd 主程序入口、命令行程序框架
├── common 通用工具库
├── config 配置文件管理代码
├── database 数据库代码
├── flags 环境变量管理代码
├── migrations 数据库迁移
├── notifier 回调通知管理
├── protobuf grpc 接口及生成代码
├── rpcclient grpc 连接客户端
├── services grpc 服务管理及接口实现
├── sh shell 命令
├── worker 核心工作代码(充值、提现、归集、热转冷)
├── exchange.go 主程序生命周期管理
├── Makefile shell 命令管理
├── devops.md 开发步骤
├── go.mod 依赖管理
├── README.md
在这个系列文章中,我将逐步搭建完整的钱包业务项目。 大概流程为:
在这一篇中,我们只实现业务的第一步,即搭建项目框架、完成数据库设计和实现服务接口。
在数据库方面,我们提供 10
个基础表,如:
Business(businessId,notifyUrl...)
: 注册商户表Blocks(hash,parentHash,number...)
: 区块信息表ReorgBlocks(hash,parentHash,number)
: 回滚区块表(回滚时处理交易使用)Address(address,addressType,publicKey...)
: 钱包地址表Balance(address,tokenAddress,balance,lockBalance...)
: 地址余额表Deposit(from,to,amount,confirms,blockHash...)
: 充值表Withdraw(from,to,amount,blockHash...)
: 提现表Internals(from,to,amount,blockHash...)
: 内部交易表(归集、热转冷、冷转热)Transactions(from,to,amount,fee,hash...)
: 交易流水表Token(tokenAddress,decimals,collectAmount...)
: token
合约表其中 Blocks
、ReOrgBlocks
、Business
是全局共享的,其他表为每个项目方独立的(项目方注册时,会为其生成独立的表,表名类似 Deposits_{requestId}
)
在本系统中的 rpc
接口设计中,我们提供 5
个接口。分别用于业务方注册、批量导出地址、构建未签名交易、构建已签名交易、设置 token
合约地址。
/*业务方注册*/
rpc businessRegister(BusinessRegisterRequest) returns (BusinessRegisterResponse);
/*地址导出*/
rpc exportAddressByPublicKeys(ExportAddressRequest) returns (ExportAddressResponse);
/*构建未签名交易*/
rpc buildUnSignTransaction(UnSignTransactionRequest) returns (UnSignTransactionResponse);
/*构建已签名交易*/
rpc buildSignedTransaction(SignedTransactionRequest) returns (SignedTransactionResponse);
/*设置 token 地址*/
rpc setTokenAddress(SetTokenAddressRequest) returns (SetTokenAddressResponse);
首先来看这三个项目的统一架构图,以项目方发送一笔交易的流程为例:
在这个流程图中,我们以项目方发起一笔提现交易为例:
32
字节的 messageHash
和交易的 transactionId
messageHash
请求离线签名机签名,返回 signature。transactionId
和 messageHash
调用钱包业务层,构建已签名交易。rpc
节点即可成功发起一笔提现交易。
requestId
进行注册,系统会根据 requestId
为其生成独立的 address
、balance
、transactions
、deposits
、withdraw
、internal
、tokens
表requestId
进行请求,数据独立在其自己的表中。
BusinessRegister(context.Context, *BusinessRegisterRequest) (*BusinessRegisterResponse, error)
signature-machine
” 项目(项目方自己部署,自己掌控私钥和签名流程)批量生成公钥,将公钥传入此接口,批量获取地址。address_{requestId}
表中,并初始化 balances
ExportAddressesByPublicKeys(context.Context, *ExportAddressesRequest) (*ExportAddressesResponse, error)
from
、to
、amount
、chainId
等信息,调用该接口。该接口会调用 “chains-union-rpc
” 项目去获取地址的 nonce
、gasFee
等。EIP-1159
的交易,调用 “chains-union-rpc
” 项目去构建交易,返回 16
进制的未签名交易 messageHash
(32
字节)、将交易信息保存在表中。返回 messageHash
和请求的 transactionId
BuildUnSignTransaction(context.Context, *UnSignTransactionRequest) (*UnSignTransactionResponse, error)
messageHash
,调用 “signature-machine
” 使用该交易对应的 from
地址私钥进行对此 messageHash
签名,返回 signature
(65
字节) 信息signature
、transactionId
。 由 transactionId
从表中查出这笔交易,然后重新构造出来相同交易。调用 “chains-union-rpc
” 的构建已签名接口,使用 signature
和 原交易信息发起调用 BuildSignedTransaction
接口。chains-union-rpc
”中,会将 signature
拆分出 r、s、v
值和原交易组合起来,格式化返回一个已签名的交易(16
进制,base64
编码)16
进制数据后,即可调用 “chains-union-rpc
” 里面的 sendTx
接口,将这笔交易公布到 rpc
网络中即可
BuildSignedTransaction(context.Context, *SignedTransactionRequest) (*SignedTransactionResponse, error)
ERC20
合约地址,作为合约项目白名单,存 tokens_{requestId}
表, 后续接入代币处理用。
SetTokenAddress(context.Context, *SetTokenAddressRequest) (*SetTokenAddressResponse, error)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!