关键词:以太坊交易所、ETH钱包开发、交易所钱包提币、交易所API 提币、Ethereum 提币接口、OutSerial幂等、地址校验、余额精度校验在以太坊交易所钱包开发系列里,我们已经跑通了 ETH充值地址生成、监听区块到账与后台调度提币。接下来,用户在交易所 App 或 Web 页面中点击「提现」按钮后,平台需要做一次轻量、可靠的前置校验:申请提币。本文将逐行拆解这一过程,助你快速完成安全、幂等、可扩展的提币接口设计。
1. 请求入口与整体时序
提币的整体时序简化为三步:
- 前端携带参数向
/api/v1/withdraw/apply发起 HTTP 请求; - 服务端校验字段、去重、落库,返回流水号;
- 后台轮询任务从数据库取出 已审核 记录再去链上广播交易(见上一篇《提币调度》)。
为了便于测试,我们仍使用基于 Gin 的中间件完成统一鉴权、统一响应封装,核心路径是 POST /api/v1/withdraw/apply。
2. 请求参数结构体
Go 模型简写如下:
var req struct {
Symbol string `json:"symbol" binding:"required" validate:"oneof=eth"` // 仅支持 ETH
OutSerial string `json:"out_serial" binding:"required" validate:"max=40"` // 外部业务流水
Address string `json:"address" binding:"required"` // 提币地址
Balance string `json:"balance" binding:"required"` // 提币数量,string 格式防丢精度
}关键词:OutSerial、幂等、交易所提币接口OutSerial(外部流水号)在用户系统里是提现订单号,数据库加唯一索引保证“同号拒绝”,防止按钮狂点或网络重发导致重复出金。- 数量字段 用 string 而非 float,规避浮点到二进制精度失真。
3. 地址格式与合法性校验
ETH 地址需要:
- 转小写:减少人为大小写混用导致的缓存、Key 重复。
- 正则校验:标准以太坊地址为 42 位十六进制字符串,前缀 0x。
req.Address = strings.ToLower(req.Address)
re := regexp.MustCompile(`^0x[0-9a-f]{40}$`)
if !re.MatchString(req.Address) {
c.JSON(http.StatusOK, gin.H{
"error": hcommon.ErrorAddressWrong,
"err_msg": hcommon.ErrorAddressWrongMsg,
})
return
}有两处细节常被忽略:
- 合规性 与 反向校验:若未来要支持多链,可把正则提升到配置并通过工厂模式选择。
- 黑名单过滤:链上公开标记的诈骗合约、交易所内部 hot wallet 地址可直接拒绝。
4. 提币金额精度校验
ETH 精度 = 10¹⁸ Wei,前端通常以 十进制字符串 方式传回,示例 "0.123"。
使用 shopspring/decimal 包可防范以下三种错误:
- 变成科学计数法;
- 整数位超出最大可转出限额;
- 小数位超过 18 位。
代码片段:
balanceObj, err := decimal.NewFromString(req.Balance)
if err != nil || balanceObj.LessThanOrEqual(decimal.NewFromInt(0)) {
c.JSON(http.StatusOK,
gin.H{"error": hcommon.ErrorBalanceFormat, "err_msg": hcommon.ErrorBalanceFormatMsg})
return
}
if balanceObj.Exponent() < -18 {
c.JSON(http.StatusOK,
gin.H{"error": hcommon.ErrorBalanceFormat, "err_msg": hcommon.ErrorBalanceFormatMsg})
return
}关键词:ETH 精度、decimal 库、交易所钱包余额精度
5. 幂等控制与数据库写入
一旦参数检验全部通过:
- 写入表
withdraw_apply(示例字段:out_serial/address/symbol/amount/status='pending'/created_at)。 - MySQL 给
out_serial加 唯一索引,同时把address+amount+ 时间戳做 MD5 token 作为日志冗余,便于追踪。 - 立即给出可读回执,让用户安心等待区块确认。
apply := model.WithdrawApply{
OutSerial: req.OutSerial,
Address: req.Address,
Symbol: req.Symbol,
Amount: balanceObj.String(),
Status: StatusPending,
}
if err := db.Create(&apply).Error; err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == 1062 {
c.JSON(http.StatusOK, gin.H{"error": "DUPLICATE_OUT_SERIAL"})
return
}
hcommon.Log.Errorf("insert apply err: %v", err)
c.JSON(http.StatusOK, gin.H{"error": "INTERNAL"})
return
}至此,申请阶段任务完成,后续由异步任务做 余额校验 -> 签交易 -> 广播 -> 回写哈希。
6. 场景化示例:如何避免 UX 误区
场景:用户在高峰期,连续点击“提现”2 次,两次 OutSerial 相同。
后台在 50ms 内 完成写入后第二次返回 DUPLICATE_OUT_SERIAL,前端将按钮锁定并提示“已提交”,减少客服工单。
场景:运营要批量驳回异常提现单据,可在后台把状态从 pending 改成 reject,不会影响用户重新发起的新 OutSerial。
7. 常见问题解答(FAQ)
Q1. OutSerial 一旦重复会发生什么?
A:数据库唯一键约束会直接拒绝,接口返回 DUPLICATE_OUT_SERIAL,建议前端随机 uuid 拼接用户 ID 确保全局唯一。
Q2. 为什么用 string 传余额?float64 不香吗?
A:JavaScript 的 0.1+0.2 !== 0.3,交易金额一旦发生 0.00000001 的偏差就可能导致用户投诉或账不平,string/decimal 可彻底避免。
Q3. 如何控制单笔/单日均提币限额?
A:申请阶段可先 读用户缓存(Redis 计数)做一次快速熔断,审核阶段再核对链上实际余额。遇大额度走人工审核流程。
Q4. 支持多链时,怎样扩展校验逻辑?
A:把地址正则、精度、最小提币额封装到 ChainConfig 结构体,按 symbol 从 map 读取即可,服务器零重启。
Q5. 如何实时告诉用户“已发送但还未确认”?
A:后台广播完 tx 后把状态更新为 sent,同时把 tx hash 写回 apply 表;前端轮询或 websocket 推送到页面。
8. 小结
申请提币接口虽“轻”,却肩负安全检查、幂等控制与流量削峰三大任务:
- 通过 正则 + decimal 校验 把低级错误拦在第一关;
- 利用 OutSerial 唯一索引 杜绝重复入金;
- 将繁重的「监管合规 + 区块链转账」延后给异步任务,保障接口 RT < 150ms,可抵挡流量洪峰。
合理拆分“申请”与“执行”既让用户体验流畅,也让开发者在后续升级支持 ERC-20、Layer2 或比特币时更游刃有余。