钱包业务层 - 4. 实现交易所提现业务

在交易所的业务中,有充值业务,那自然也会有提现业务。在上一讲中,我们在交易的发现器中处理了充值业务,而在这一讲中,我们将会来处理提现业务。

提现业务

在交易所的业务中,有充值业务,那自然也会有提现业务。在上一讲中,我们在交易的发现器中处理了充值业务,而在这一讲中,我们将会来处理提现业务。

提现业务简单来说,也是在于交易的 from 地址和 to 地址的不同。对于 from 地址为交易所的热钱包地址、to 地址为外部的地址的交易类型我们可以称之为提现交易。

不像充值业务可以只在交易的发现器中处理。(1. 发现充值交易,此时为进行中。 2. 确认位过后更新充值交易的状态,此时为已完成。) 提现的交易需要在两个地方进行操作:

  1. 项目方调用钱包业务层的构建未签名交易接口,调用签名机的签名,调用钱包业务层的构建已签名接口。(这几部我们在前面几讲中已经实现)

  2. 钱包业务层启动一个定时任务,负责处理钱包的提现交易,将提现发送到区块链网络上。(这正是我们这一讲中需要实现的内容)

完整项目 github 地址(如果对您有用,请给个小 star ⭐️):

  1. exchange-wallet-service:钱包业务层服务: <https://github.com/Shawn-Shaw-x/exchange-wallet-service>
  2. signature-machine:离线签名机:  <https://github.com/Shawn-Shaw-x/signature-machine>
  3. chains-union-rpc:多链统一 rpc:  <https://github.com/Shawn-Shaw-x/chains-union-rpc>

提现业务流程图

image.png 整体的提现业务流程图如上图所示,我们首先从项目方出发,解析这个提现的流程:

  1. 项目方传入 fromtovalue,其中 from 为项目方的热钱包地址,到我们钱包业务层的 rpc 服务中,构建一笔未签名的交易,返回 32 字节的 messageHash 和这笔交易的 transactionId

  2. 项目方持有这个 messageHash,调用他自己部署的签名机服务,发起一笔离线签名,获得 65 字节的 signature

  3. 项目方使用 transactionIdsignature 请求钱包业务层的 rpc 服务去构建一笔已签名交易。这时候,钱包的业务层会将这笔交易的完整的、已签名的交易体存入数据库中。

  4. 在钱包的业务层中,会有一个提现任务的处理器,这是一个通过协程启动的定时任务。它会去扫描数据库中的提现交易,如果发现了已签名、未发送的提现交易,则将这笔交易发送到区块链网络中,此时的提现状态为已广播、未完成。(余额信息也需要处理)

  5. 在我们之前实现的交易同步器、交易发现器中,会扫描链上区块,解析、筛选出来这笔提现的交易。然后更新这笔提现交易的状态为已完成即可。(当然还会去处理余额信息)

提现业务的泳道图

image.png 下面我们基于提现业务的泳道图来再次进行一次分析:

  1. 用户向业务层(项目方)发起提现,提现交给我们钱包的业务层。

  2. 钱包去链上获取必要参数,如 noncegasPrice 等。

  3. 钱包构建一笔未签名交易,获得 32 字节的 messageHash

  4. 钱包调用签名机进行签名。(这里是单个交易所的逻辑,我们这个项目中实际上是由项目方发起签名)

  5. 组装签名和原来的交易体,发送到区块链网络中,然后通知项目方说这笔交易已上链。

  6. 钱包这里会持续扫链(同步器、发现器所做的事),在链上发现了这笔交易之后,则可以通知项目方说这笔提现交易已成功。

8. 提现业务实现

在提现任务中,我们需要做的事情比较简单(因为在上一讲发现器中,我们已经将提现的发现流程处理了) 提现的任务主要分两步:

  1. 发送提现交易

    1. 首先,我们需要使用热钱包地址构建一笔提现交易,from 地址为热钱包地址,to 地址为外部地址。调用之前 RPC 服务写好的构建未签名交易、签名机签名、构造已签名交易(前面在 RPC 服务搭建中,改步骤已实现)
    2. 因为在构完已签名交易之后,我们会把这笔已签名交易存储到提现表中,其中包含已签名交易的完整的交易内容。所以,在这一步中,我们只需要使用协程启动一个定时任务,在定时任务中,将这笔交易从数据库中查询出来,然后调用接口发送到区块链网络,同时更新余额表和提现表即可。

      /*启动定时任务发送提现记录*/
      func (w *Withdraw) Start() error {
      log.Info("starting withdraw....")
      w.tasks.Go(func() error {
      for {
      select {
      case &lt;-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 &lt;-w.resourceCtx.Done():
                  /*提现任务终止*/
                  log.Info("stopping withdraw in worker")
                  return nil
              }
          }
      })
      return nil
      }
  2. 同步、发现提现交易(这一步已经在前面一讲中的交易发现器中处理完毕,此处无需处理)

提现任务测试

  1. 签名机生成秘钥对 生成一个热钱包地址去使用

image.png

  1. 注册进钱包业务 将这个热钱包地址注册进交易所业务层中

image.png

  1. 转钱给热钱包地址 先给这个热钱包地址一点资金,作为提现所用

image.png

  1. 手动修改数据库余额(模拟归集后热钱包有钱) 因为不是在交易所钱包业务中归集的,所以需要手动改一下库用于测试

image.png

  1. 构建一笔未签名交易 调用交易所钱包业务的构建未签名交易接口

image.png

image.png

  1. 签名这笔交易 将未签名交易的 messageHash 交给签名机离线签名

image.png

  1. 检查余额、提现记录 先检查下交易还未发送之前的热钱包余额和提现记录情况,方便后续发出交易后对比

image.png (此处图片有笔误误,应该是余额为 0.1 ETH)

image.png

  1. 构建已签名交易,等待发起 调用钱包层已经签名交易的接口,钱包层收到后,定时任务会发现这笔交易已签名,调用发送交易发送到区块链 网络上(交易状态为已广播)然后交易同步器、发现器发现这笔提现交易后,即修改交易状态为(完成)

image.png

  1. 等待交易发出、扫块发现 检查数据库中提现记录,发现提现交易已完成。再检查余额记录,发现 0.02 ETH 已被成功扣除。

image.png

image.png

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
shawn_shaw
shawn_shaw
web3潜水员、技术爱好者、web3钱包开发工程师。欢迎闲聊唠嗑、精进技术、交流工作机会。vx:cola_ocean