2026-07-14 17:30:11 +08:00

666 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package binance
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/config"
"hyapp/services/wallet-service/internal/service/wallet/ports"
)
const (
defaultDepositLookback = 90 * 24 * time.Hour
depositHistoryLimit = 1000
defaultPayLookback = 90 * 24 * time.Hour
payOrderTimeRadius = time.Hour
payHistoryLimit = 100
binancePaySuccessCode = "000000"
c2cTradeTypeBuy = "BUY"
c2cOrderStatusComplete = "COMPLETED"
)
// Client 是 Binance 只读查单适配器;它只使用 signed read API不保存、不生成任何链上私钥。
type Client struct {
httpClient *http.Client
apiBaseURL string
recvWindow int64
accounts map[string]account
}
type account struct {
apiKey string
apiSecretKey string
}
// New 从 wallet-service 配置创建 Binance client具体 App 是否有账号在请求时按 appCode 决定。
func New(cfg config.BinanceConfig) *Client {
timeout := cfg.HTTPTimeout
if timeout <= 0 {
timeout = 10 * time.Second
}
return &Client{
httpClient: &http.Client{Timeout: timeout},
apiBaseURL: strings.TrimRight(strings.TrimSpace(firstNonEmpty(cfg.APIBaseURL, "https://api.binance.com")), "/"),
recvWindow: firstPositiveInt64(cfg.RecvWindow, 10000),
accounts: map[string]account{
"lalu": newAccount(cfg.Lalu),
"aslan": newAccount(cfg.Aslan),
"yumi": newAccount(cfg.Yumi),
},
}
}
func newAccount(cfg config.BinanceAccountConfig) account {
return account{
apiKey: strings.TrimSpace(cfg.APIKey),
apiSecretKey: strings.TrimSpace(cfg.APISecretKey),
}
}
// FindUSDTTransfer 先查链上 Deposit History再在未命中时查 Funding Account 的 Pay C2C History。
// 两个事实源的成功语义不同链上入金必须校验网络、地址和状态Pay C2C 必须校验正金额,证明是当前 API Key 账户收款而不是付款。
func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransferLookupRequest) (ports.BinanceTransferRecord, error) {
if c == nil || c.apiBaseURL == "" {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Unavailable, "binance client is not configured")
}
account, ok := c.accountForApp(req.AppCode)
if !ok {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Unavailable, "binance account is not configured")
}
req.ExternalID = strings.TrimSpace(req.ExternalID)
req.Coin = strings.ToUpper(strings.TrimSpace(req.Coin))
req.Network = strings.ToUpper(strings.TrimSpace(req.Network))
req.ToAddress = strings.TrimSpace(req.ToAddress)
if req.ExternalID == "" || req.Coin == "" || req.Network == "" || req.AmountMinor <= 0 {
return ports.BinanceTransferRecord{}, xerr.New(xerr.InvalidArgument, "binance transfer verification request is incomplete")
}
serverTimeMS, err := c.serverTime(ctx)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
if req.Network == "C2C" {
// C2C/P2P 与 Binance Pay 是两套独立账单;显式 C2C 只查法币交易历史,禁止再回退到 Pay 或链上入金。
startTimeMS, endTimeMS := payHistoryRange(req.OrderTimeMS, serverTimeMS)
records, err := c.c2cHistory(ctx, account, startTimeMS, endTimeMS, serverTimeMS)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
return matchC2CRecord(req, records)
}
// Binance 的 deposit history 查询窗口有限;后台凭证校验只做近期有界查询,避免一次手工校验变成无界扫描。
records, err := c.depositHistory(ctx, account, req.Coin, serverTimeMS-int64(defaultDepositLookback/time.Millisecond), serverTimeMS)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
matched, err := matchDepositRecord(req, records)
if err == nil {
return matched, nil
}
if !xerr.IsCode(err, xerr.NotFound) {
// 同一编号已经命中链上事实但字段冲突时禁止切换事实源,避免用 Pay 记录绕过地址、网络或状态校验。
return ports.BinanceTransferRecord{}, err
}
startTimeMS, endTimeMS := payHistoryRange(req.OrderTimeMS, serverTimeMS)
payRecords, err := c.payHistory(ctx, account, startTimeMS, endTimeMS, serverTimeMS)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
return matchPayRecord(req, payRecords)
}
// c2cHistory 查询当前 API Key 账户作为买方完成的 C2C 订单BUY 表示账户收到 assetSELL 则是付出 asset。
func (c *Client) c2cHistory(ctx context.Context, account account, startTimeMS int64, endTimeMS int64, serverTimeMS int64) ([]c2cRecord, error) {
query := url.Values{}
query.Set("tradeType", c2cTradeTypeBuy)
query.Set("startTimestamp", strconv.FormatInt(startTimeMS, 10))
query.Set("endTimestamp", strconv.FormatInt(endTimeMS, 10))
query.Set("page", "1")
query.Set("rows", strconv.Itoa(payHistoryLimit))
body, err := c.signedGET(ctx, account, "/sapi/v1/c2c/orderMatch/listUserOrderHistory", query, serverTimeMS)
if err != nil {
return nil, err
}
var response c2cHistoryResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, err
}
if !strings.EqualFold(strings.TrimSpace(response.Code), "000000") || !response.Success {
return nil, xerr.New(xerr.Unavailable, "binance c2c history returned non-success")
}
records := make([]c2cRecord, 0, len(response.Data))
for _, raw := range response.Data {
var record c2cRecord
if err := json.Unmarshal(raw, &record); err != nil {
return nil, err
}
record.OrderNumber = strings.TrimSpace(record.OrderNumber)
record.TradeType = strings.ToUpper(strings.TrimSpace(record.TradeType))
record.Asset = strings.ToUpper(strings.TrimSpace(record.Asset))
record.Amount = strings.TrimSpace(record.Amount)
record.OrderStatus = strings.ToUpper(strings.TrimSpace(record.OrderStatus))
record.RawJSON = safeC2CRecordJSON(record)
records = append(records, record)
}
return records, nil
}
func matchC2CRecord(req ports.BinanceTransferLookupRequest, records []c2cRecord) (ports.BinanceTransferRecord, error) {
for _, record := range records {
if !strings.EqualFold(record.OrderNumber, req.ExternalID) {
continue
}
if record.TradeType != c2cTradeTypeBuy {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c trade is not income")
}
if !strings.EqualFold(record.Asset, req.Coin) {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c asset mismatch")
}
if record.OrderStatus != c2cOrderStatusComplete {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c order is not completed")
}
amountMinor, err := parseUSDTAmountMinor(record.Amount)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
if amountMinor != req.AmountMinor {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c amount mismatch")
}
return ports.BinanceTransferRecord{Source: "c2c_history", ID: record.OrderNumber, TxID: record.OrderNumber, Coin: record.Asset, Status: 1, Amount: record.Amount, AmountMinor: amountMinor, InsertTimeMS: record.CreateTimeMS, CompleteTimeMS: record.CreateTimeMS, RawJSON: record.RawJSON}, nil
}
return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance c2c transfer not found")
}
func (c *Client) accountForApp(appCode string) (account, bool) {
value, ok := c.accounts[strings.ToLower(strings.TrimSpace(appCode))]
if !ok || strings.TrimSpace(value.apiKey) == "" || strings.TrimSpace(value.apiSecretKey) == "" {
return account{}, false
}
return value, true
}
func (c *Client) serverTime(ctx context.Context) (int64, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiBaseURL+"/api/v3/time", nil)
if err != nil {
return 0, err
}
body, err := c.do(req)
if err != nil {
return 0, err
}
var parsed struct {
ServerTime int64 `json:"serverTime"`
}
if err := json.Unmarshal(body, &parsed); err != nil {
return 0, err
}
if parsed.ServerTime <= 0 {
return 0, xerr.New(xerr.Unavailable, "binance server time is invalid")
}
return parsed.ServerTime, nil
}
func (c *Client) depositHistory(ctx context.Context, account account, coin string, startTimeMS int64, endTimeMS int64) ([]depositRecord, error) {
query := url.Values{}
query.Set("coin", strings.ToUpper(strings.TrimSpace(coin)))
query.Set("startTime", strconv.FormatInt(startTimeMS, 10))
query.Set("endTime", strconv.FormatInt(endTimeMS, 10))
query.Set("limit", strconv.Itoa(depositHistoryLimit))
body, err := c.signedGET(ctx, account, "/sapi/v1/capital/deposit/hisrec", query, endTimeMS)
if err != nil {
return nil, err
}
var raws []json.RawMessage
if err := json.Unmarshal(body, &raws); err != nil {
return nil, err
}
records := make([]depositRecord, 0, len(raws))
for _, raw := range raws {
var wire depositRecordWire
if err := json.Unmarshal(raw, &wire); err != nil {
return nil, err
}
record := wire.toRecord()
record.RawJSON = string(raw)
records = append(records, record)
}
return records, nil
}
// payHistory 查询 Binance Pay 用户账单。接口单次最多返回 100 条,调用方提供订单时间时使用窄窗口降低高频账户漏单概率。
func (c *Client) payHistory(ctx context.Context, account account, startTimeMS int64, endTimeMS int64, serverTimeMS int64) ([]payRecord, error) {
query := url.Values{}
query.Set("startTime", strconv.FormatInt(startTimeMS, 10))
query.Set("endTime", strconv.FormatInt(endTimeMS, 10))
query.Set("limit", strconv.Itoa(payHistoryLimit))
// 查询区间可以是历史日期,但 signed API 的 timestamp 必须始终使用当前 Binance server time否则旧订单会被 -1021 拒绝。
body, err := c.signedGET(ctx, account, "/sapi/v1/pay/transactions", query, serverTimeMS)
if err != nil {
return nil, err
}
var response payHistoryResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, err
}
if strings.TrimSpace(response.Code) != binancePaySuccessCode || !response.Success {
return nil, xerr.New(xerr.Unavailable, "binance pay history returned non-success")
}
records := make([]payRecord, 0, len(response.Data))
for _, raw := range response.Data {
var wire payRecordWire
if err := json.Unmarshal(raw, &wire); err != nil {
return nil, err
}
record := wire.toRecord()
// Pay 回包含付款人与收款人的账户资料;持久审计只保留完成资金校验所需字段,避免把无关 PII 写入充值凭证。
record.RawJSON = safePayRecordJSON(record)
records = append(records, record)
}
return records, nil
}
func payHistoryRange(orderTimeMS int64, serverTimeMS int64) (int64, int64) {
lookbackMS := int64(defaultPayLookback / time.Millisecond)
if orderTimeMS <= 0 {
return maxInt64(0, serverTimeMS-lookbackMS), serverTimeMS
}
radiusMS := int64(payOrderTimeRadius / time.Millisecond)
startTimeMS := maxInt64(0, orderTimeMS-radiusMS)
endTimeMS := minInt64(serverTimeMS, orderTimeMS+radiusMS)
if endTimeMS <= startTimeMS {
// 未来订单时间不应发生;这里返回最小合法区间,让 Binance 给出空结果而不是发出反向时间范围。
endTimeMS = startTimeMS + 1
}
return startTimeMS, endTimeMS
}
func (c *Client) signedGET(ctx context.Context, account account, path string, values url.Values, timestampMS int64) ([]byte, error) {
values.Set("recvWindow", strconv.FormatInt(c.recvWindow, 10))
values.Set("timestamp", strconv.FormatInt(timestampMS, 10))
queryString := values.Encode()
signature := signQueryString(queryString, account.apiSecretKey)
endpoint := c.apiBaseURL + path + "?" + queryString + "&signature=" + signature
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-MBX-APIKEY", account.apiKey)
return c.do(req)
}
func (c *Client) do(req *http.Request) ([]byte, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, xerr.New(xerr.Unavailable, "binance request failed")
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, xerr.New(xerr.Unavailable, "binance returned non-success")
}
return body, nil
}
func matchDepositRecord(req ports.BinanceTransferLookupRequest, records []depositRecord) (ports.BinanceTransferRecord, error) {
var candidate *depositRecord
for index := range records {
if depositTxIDMatches(records[index].TxID, req.ExternalID) {
candidate = &records[index]
break
}
}
if candidate == nil {
return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance deposit not found")
}
if !strings.EqualFold(candidate.Coin, req.Coin) {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit coin mismatch")
}
if !strings.EqualFold(candidate.Network, req.Network) {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit network mismatch")
}
if candidate.Status != 1 {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit status is not successful")
}
// Deposit History 属于具体链上入账,必须有预先配置的收款地址;允许空地址仅服务于随后独立校验的 Pay C2C 事实源。
if strings.TrimSpace(req.ToAddress) == "" || !strings.EqualFold(strings.TrimSpace(candidate.Address), strings.TrimSpace(req.ToAddress)) {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit address mismatch")
}
amountMinor, err := parseUSDTAmountMinor(candidate.Amount)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
if amountMinor != req.AmountMinor {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit amount mismatch")
}
return ports.BinanceTransferRecord{
Source: "deposit_history",
ID: candidate.ID,
TxID: candidate.TxID,
Coin: candidate.Coin,
Network: candidate.Network,
Status: candidate.Status,
Address: candidate.Address,
Amount: candidate.Amount,
AmountMinor: amountMinor,
TransferType: candidate.TransferType,
InsertTimeMS: candidate.InsertTimeMS,
CompleteTimeMS: candidate.CompleteTimeMS,
ConfirmTimes: candidate.ConfirmTimes,
UnlockConfirm: candidate.UnlockConfirm,
WalletType: candidate.WalletType,
RawJSON: strings.TrimSpace(candidate.RawJSON),
}, nil
}
func matchPayRecord(req ports.BinanceTransferLookupRequest, records []payRecord) (ports.BinanceTransferRecord, error) {
var candidate *payRecord
for index := range records {
if payExternalIDMatches(records[index], req.ExternalID) {
candidate = &records[index]
break
}
}
if candidate == nil {
return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance pay transfer not found")
}
if !strings.EqualFold(candidate.OrderType, "C2C") {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay order type is not c2c")
}
if !strings.EqualFold(candidate.Currency, req.Coin) {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay currency mismatch")
}
// Binance Pay 官方语义中正数是当前 API Key 账户收入、负数是支出;只允许正数,防止付款截图被当成平台收款。
if strings.HasPrefix(strings.TrimSpace(candidate.Amount), "-") {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay transfer is not income")
}
amountMinor, err := parseUSDTAmountMinor(candidate.Amount)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
if amountMinor <= 0 {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay transfer is not income")
}
if amountMinor != req.AmountMinor {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay amount mismatch")
}
return ports.BinanceTransferRecord{
Source: "pay_history",
ID: candidate.OrderID,
TxID: firstNonEmpty(candidate.TransactionID, candidate.OrderID),
Coin: candidate.Currency,
Status: 1,
Amount: candidate.Amount,
AmountMinor: amountMinor,
InsertTimeMS: candidate.TransactionTimeMS,
CompleteTimeMS: candidate.TransactionTimeMS,
WalletType: candidate.WalletType,
RawJSON: strings.TrimSpace(candidate.RawJSON),
}, nil
}
func payExternalIDMatches(record payRecord, externalID string) bool {
externalID = strings.TrimSpace(externalID)
if externalID == "" {
return false
}
return strings.EqualFold(strings.TrimSpace(record.OrderID), externalID) ||
strings.EqualFold(strings.TrimSpace(record.TransactionID), externalID)
}
func depositTxIDMatches(txID string, externalID string) bool {
normalizedExternalID := normalizeOffchainExternalID(externalID)
if normalizedExternalID == "" {
return false
}
normalizedTxID := strings.ToLower(strings.TrimSpace(txID))
return strings.Contains(normalizedTxID, normalizedExternalID) || normalizeOffchainExternalID(txID) == normalizedExternalID
}
func normalizeOffchainExternalID(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
value = strings.TrimPrefix(value, "off-chain transfer")
value = strings.TrimSpace(strings.Trim(value, ":#"))
return value
}
func parseUSDTAmountMinor(value string) (int64, error) {
value = strings.TrimSpace(value)
if value == "" || strings.HasPrefix(value, "-") {
return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
}
parts := strings.Split(value, ".")
if len(parts) > 2 {
return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
}
whole, err := strconv.ParseInt(firstNonEmpty(parts[0], "0"), 10, 64)
if err != nil {
return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
}
frac := ""
if len(parts) == 2 {
frac = parts[1]
}
if len(frac) > 2 && strings.Trim(frac[2:], "0") != "" {
return 0, xerr.New(xerr.Conflict, "binance deposit amount has sub-cent precision")
}
if len(frac) > 2 {
frac = frac[:2]
}
for len(frac) < 2 {
frac += "0"
}
minorFrac, err := strconv.ParseInt(firstNonEmpty(frac, "0"), 10, 64)
if err != nil {
return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
}
return whole*100 + minorFrac, nil
}
func signQueryString(queryString string, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(queryString))
return hex.EncodeToString(mac.Sum(nil))
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func firstPositiveInt64(values ...int64) int64 {
for _, value := range values {
if value > 0 {
return value
}
}
return 0
}
type depositRecord struct {
ID string
Amount string
Coin string
Network string
Status int64
Address string
TxID string
TransferType int64
InsertTimeMS int64
CompleteTimeMS int64
ConfirmTimes string
UnlockConfirm int64
WalletType int64
RawJSON string
}
type depositRecordWire struct {
ID json.RawMessage `json:"id"`
Amount string `json:"amount"`
Coin string `json:"coin"`
Network string `json:"network"`
Status int64 `json:"status"`
Address string `json:"address"`
TxID string `json:"txId"`
TransferType int64 `json:"transferType"`
InsertTimeMS int64 `json:"insertTime"`
CompleteTimeMS int64 `json:"completeTime"`
ConfirmTimes string `json:"confirmTimes"`
UnlockConfirm int64 `json:"unlockConfirm"`
WalletType int64 `json:"walletType"`
}
type payHistoryResponse struct {
Code string `json:"code"`
Message string `json:"message"`
Success bool `json:"success"`
Data []json.RawMessage `json:"data"`
}
type c2cHistoryResponse struct {
Code string `json:"code"`
Success bool `json:"success"`
Data []json.RawMessage `json:"data"`
}
type c2cRecord struct {
OrderNumber string `json:"orderNumber"`
TradeType string `json:"tradeType"`
Asset string `json:"asset"`
Amount string `json:"amount"`
OrderStatus string `json:"orderStatus"`
CreateTimeMS int64 `json:"createTime"`
RawJSON string `json:"-"`
}
func safeC2CRecordJSON(record c2cRecord) string {
body, err := json.Marshal(struct {
OrderNumber string `json:"orderNumber"`
TradeType string `json:"tradeType"`
Asset string `json:"asset"`
Amount string `json:"amount"`
OrderStatus string `json:"orderStatus"`
CreateTimeMS int64 `json:"createTime"`
}{record.OrderNumber, record.TradeType, record.Asset, record.Amount, record.OrderStatus, record.CreateTimeMS})
if err != nil {
return ""
}
return string(body)
}
type payRecord struct {
OrderType string
OrderID string
TransactionID string
TransactionTimeMS int64
Amount string
Currency string
WalletType int64
RawJSON string
}
type payRecordWire struct {
OrderType string `json:"orderType"`
OrderID json.RawMessage `json:"orderId"`
TransactionID string `json:"transactionId"`
TransactionTimeMS int64 `json:"transactionTime"`
Amount string `json:"amount"`
Currency string `json:"currency"`
WalletType int64 `json:"walletType"`
}
func (wire payRecordWire) toRecord() payRecord {
return payRecord{
OrderType: strings.ToUpper(strings.TrimSpace(wire.OrderType)),
OrderID: rawScalarString(wire.OrderID),
TransactionID: strings.TrimSpace(wire.TransactionID),
TransactionTimeMS: wire.TransactionTimeMS,
Amount: strings.TrimSpace(wire.Amount),
Currency: strings.ToUpper(strings.TrimSpace(wire.Currency)),
WalletType: wire.WalletType,
}
}
func safePayRecordJSON(record payRecord) string {
payload := struct {
OrderType string `json:"orderType"`
OrderID string `json:"orderId,omitempty"`
TransactionID string `json:"transactionId"`
TransactionTimeMS int64 `json:"transactionTime"`
Amount string `json:"amount"`
Currency string `json:"currency"`
WalletType int64 `json:"walletType"`
}{
OrderType: record.OrderType,
OrderID: record.OrderID,
TransactionID: record.TransactionID,
TransactionTimeMS: record.TransactionTimeMS,
Amount: record.Amount,
Currency: record.Currency,
WalletType: record.WalletType,
}
body, err := json.Marshal(payload)
if err != nil {
return ""
}
return string(body)
}
func (wire depositRecordWire) toRecord() depositRecord {
return depositRecord{
ID: rawScalarString(wire.ID),
Amount: strings.TrimSpace(wire.Amount),
Coin: strings.ToUpper(strings.TrimSpace(wire.Coin)),
Network: strings.ToUpper(strings.TrimSpace(wire.Network)),
Status: wire.Status,
Address: strings.TrimSpace(wire.Address),
TxID: strings.TrimSpace(wire.TxID),
TransferType: wire.TransferType,
InsertTimeMS: wire.InsertTimeMS,
CompleteTimeMS: wire.CompleteTimeMS,
ConfirmTimes: strings.TrimSpace(wire.ConfirmTimes),
UnlockConfirm: wire.UnlockConfirm,
WalletType: wire.WalletType,
}
}
func rawScalarString(raw json.RawMessage) string {
if len(raw) == 0 || string(raw) == "null" {
return ""
}
var text string
if err := json.Unmarshal(raw, &text); err == nil {
return strings.TrimSpace(text)
}
return strings.TrimSpace(string(raw))
}
func minInt64(left int64, right int64) int64 {
if left < right {
return left
}
return right
}
func maxInt64(left int64, right int64) int64 {
if left > right {
return left
}
return right
}