修正币安C2C订单查询接口
This commit is contained in:
parent
1fbbe2758f
commit
d66799b312
@ -25,6 +25,8 @@ const (
|
||||
payOrderTimeRadius = time.Hour
|
||||
payHistoryLimit = 100
|
||||
binancePaySuccessCode = "000000"
|
||||
c2cTradeTypeBuy = "BUY"
|
||||
c2cOrderStatusComplete = "COMPLETED"
|
||||
)
|
||||
|
||||
// Client 是 Binance 只读查单适配器;它只使用 signed read API,不保存、不生成任何链上私钥。
|
||||
@ -86,6 +88,15 @@ func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransfer
|
||||
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 {
|
||||
@ -107,6 +118,68 @@ func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransfer
|
||||
return matchPayRecord(req, payRecords)
|
||||
}
|
||||
|
||||
// c2cHistory 查询当前 API Key 账户作为买方完成的 C2C 订单;BUY 表示账户收到 asset,SELL 则是付出 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) == "" {
|
||||
@ -459,6 +532,37 @@ type payHistoryResponse struct {
|
||||
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
|
||||
|
||||
@ -209,6 +209,40 @@ func TestFindUSDTTransferFallsBackToIncomingPayC2C(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindUSDTTransferC2CUsesFiatTradeHistoryOnly(t *testing.T) {
|
||||
const orderID = "440537037787594752"
|
||||
var sawC2C bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v3/time":
|
||||
_, _ = w.Write([]byte(`{"serverTime":1784018000000}`))
|
||||
case "/sapi/v1/c2c/orderMatch/listUserOrderHistory":
|
||||
sawC2C = true
|
||||
values := r.URL.Query()
|
||||
signature := values.Get("signature")
|
||||
values.Del("signature")
|
||||
if signature != signQueryString(values.Encode(), "secret-lalu") {
|
||||
t.Fatalf("C2C signature mismatch: %s", values.Encode())
|
||||
}
|
||||
if values.Get("tradeType") != "BUY" || values.Get("page") != "1" || values.Get("rows") != "100" {
|
||||
t.Fatalf("C2C query mismatch: %s", values.Encode())
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":"000000","success":true,"data":[{"orderNumber":"440537037787594752","tradeType":"BUY","asset":"USDT","amount":"200.00000000","orderStatus":"COMPLETED","createTime":1784017560000}]}`))
|
||||
default:
|
||||
t.Fatalf("C2C lookup must not call another history endpoint: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := New(config.BinanceConfig{Enabled: true, APIBaseURL: server.URL, RecvWindow: 10000, HTTPTimeout: time.Second, Lalu: config.BinanceAccountConfig{APIKey: "api-lalu", APISecretKey: "secret-lalu"}})
|
||||
record, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{AppCode: "lalu", ExternalID: orderID, Coin: "USDT", Network: "C2C", AmountMinor: 20000, OrderTimeMS: 1784017560000})
|
||||
if err != nil {
|
||||
t.Fatalf("C2C lookup failed: %v", err)
|
||||
}
|
||||
if !sawC2C || record.Source != "c2c_history" || record.ID != orderID || record.AmountMinor != 20000 {
|
||||
t.Fatalf("C2C record mismatch: %+v", record)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindUSDTTransferDoesNotBypassDepositConflictWithPay(t *testing.T) {
|
||||
var payCalls int
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user