562 lines
19 KiB
Go
562 lines
19 KiB
Go
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"
|
||
)
|
||
|
||
// 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
|
||
}
|
||
// 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)
|
||
}
|
||
|
||
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 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
|
||
}
|