在交易所的业务中,有充值业务,那自然也会有提现业务。在上一讲中,我们在交易的发现器中处理了充值业务,而在这一讲中,我们将会来处理提现业务。
在交易所的业务中,有充值业务,那自然也会有提现业务。在上一讲中,我们在交易的发现器中处理了充值业务,而在这一讲中,我们将会来处理提现业务。
提现业务简单来说,也是在于交易的 from
地址和 to
地址的不同。对于 from
地址为交易所的热钱包地址、to
地址为外部的地址的交易类型我们可以称之为提现交易。
不像充值业务可以只在交易的发现器中处理。(1. 发现充值交易,此时为进行中。 2. 确认位过后更新充值交易的状态,此时为已完成。) 提现的交易需要在两个地方进行操作:
项目方调用钱包业务层的构建未签名交易接口,调用签名机的签名,调用钱包业务层的构建已签名接口。(这几部我们在前面几讲中已经实现)
钱包业务层启动一个定时任务,负责处理钱包的提现交易,将提现发送到区块链网络上。(这正是我们这一讲中需要实现的内容)
完整项目 github
地址(如果对您有用,请给个小 star
⭐️):
exchange-wallet-service
:钱包业务层服务: <https://github.com/Shawn-Shaw-x/exchange-wallet-service>signature-machine
:离线签名机: <https://github.com/Shawn-Shaw-x/signature-machine>chains-union-rpc
:多链统一 rpc
: <https://github.com/Shawn-Shaw-x/chains-union-rpc>
整体的提现业务流程图如上图所示,我们首先从项目方出发,解析这个提现的流程:
项目方传入 from
、to
、value
,其中 from
为项目方的热钱包地址,到我们钱包业务层的 rpc
服务中,构建一笔未签名的交易,返回 32
字节的 messageHash
和这笔交易的 transactionId
。
项目方持有这个 messageHash
,调用他自己部署的签名机服务,发起一笔离线签名,获得 65
字节的 signature
。
项目方使用 transactionId
和 signature
请求钱包业务层的 rpc
服务去构建一笔已签名交易。这时候,钱包的业务层会将这笔交易的完整的、已签名的交易体存入数据库中。
在钱包的业务层中,会有一个提现任务的处理器,这是一个通过协程启动的定时任务。它会去扫描数据库中的提现交易,如果发现了已签名、未发送的提现交易,则将这笔交易发送到区块链网络中,此时的提现状态为已广播、未完成。(余额信息也需要处理)
在我们之前实现的交易同步器、交易发现器中,会扫描链上区块,解析、筛选出来这笔提现的交易。然后更新这笔提现交易的状态为已完成即可。(当然还会去处理余额信息)
下面我们基于提现业务的泳道图来再次进行一次分析:
用户向业务层(项目方)发起提现,提现交给我们钱包的业务层。
钱包去链上获取必要参数,如 nonce
、gasPrice
等。
钱包构建一笔未签名交易,获得 32
字节的 messageHash
。
钱包调用签名机进行签名。(这里是单个交易所的逻辑,我们这个项目中实际上是由项目方发起签名)
组装签名和原来的交易体,发送到区块链网络中,然后通知项目方说这笔交易已上链。
钱包这里会持续扫链(同步器、发现器所做的事),在链上发现了这笔交易之后,则可以通知项目方说这笔提现交易已成功。
在提现任务中,我们需要做的事情比较简单(因为在上一讲发现器中,我们已经将提现的发现流程处理了) 提现的任务主要分两步:
发送提现交易
from
地址为热钱包地址,to
地址为外部地址。调用之前 RPC
服务写好的构建未签名交易、签名机签名、构造已签名交易(前面在 RPC
服务搭建中,改步骤已实现)因为在构完已签名交易之后,我们会把这笔已签名交易存储到提现表中,其中包含已签名交易的完整的交易内容。所以,在这一步中,我们只需要使用协程启动一个定时任务,在定时任务中,将这笔交易从数据库中查询出来,然后调用接口发送到区块链网络,同时更新余额表和提现表即可。
/*启动定时任务发送提现记录*/
func (w *Withdraw) Start() error {
log.Info("starting withdraw....")
w.tasks.Go(func() error {
for {
select {
case <-w.ticker.C:
/*定时发送提现交易*/
businessList, err := w.db.Business.QueryBusinessList()
if err != nil {
log.Error("failed to query business list", "err", err)
continue
}
for _, business := range businessList {
/*每个项目方处理已签名但未发出的交易*/
unSendTransactionList, err := w.db.Withdraws.UnSendWithdrawsList(business.BusinessUid)
if err != nil {
log.Error("failed to unsend transaction", "err", err)
continue
}
if unSendTransactionList == nil || len(unSendTransactionList) == 0 {
log.Error("no withdraw transaction found", "businessId", business.BusinessUid)
continue
}
var balanceList []*database.Balances
for _, unSendTransaction := range unSendTransactionList {
/*每一笔提现交易发出去*/
txHash, err := w.rpcClient.SendTx(unSendTransaction.TxSignHex)
if err != nil {
log.Error("failed to send transaction", "err", err)
continue
} else {
/*成功更新余额*/
balanceItem := &database.Balances{
TokenAddress: unSendTransaction.TokenAddress,
Address: unSendTransaction.FromAddress,
/*发出提现,balance-,lockBalance+,*/
LockBalance: unSendTransaction.Amount,
}
balanceList = append(balanceList, balanceItem)
unSendTransaction.TxHash = common.HexToHash(txHash)
/*已广播,未确认*/
unSendTransaction.Status = constant.TxStatusBroadcasted
}
}
retryStrategy := &retry.ExponentialStrategy{Min: 1000, Max: 20_000, MaxJitter: 250}
/*数据库重试*/
if _, err := retry.Do[interface{}](w.resourceCtx, 10, retryStrategy, func() (interface{}, error) {
/*事务*/
if err := w.db.Gorm.Transaction(func(tx *gorm.DB) error {
/*更新余额表*/
if len(balanceList) > 0 {
log.Info("update withdraw balance transaction", "totalTx", len(balanceList))
if err := w.db.Balances.UpdateBalanceListByTwoAddress(business.BusinessUid, balanceList); err != nil {
log.Error("failed to update withdraw balance transaction", "err", err)
return err
}
}
/*更新提现表*/
if len(unSendTransactionList) > 0 {
err = w.db.Withdraws.UpdateWithdrawListById(business.BusinessUid, unSendTransactionList)
if err != nil {
log.Error("update withdraw status fail", "err", err)
return err
}
}
return nil
}); err != nil {
return err, nil
}
return nil, nil
}); err != nil {
return err
}
}
case <-w.resourceCtx.Done():
/*提现任务终止*/
log.Info("stopping withdraw in worker")
return nil
}
}
})
return nil
}
(此处图片有笔误误,应该是余额为 0.1 ETH)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!