feat(v2): crypto 孤儿到账发现——OrphanScanner 扫链核对 + orphan_payments 落表告警(对账兜底)

This commit is contained in:
wangjia
2026-07-10 17:52:10 +08:00
parent 04c3f4f31a
commit 727ed74899
12 changed files with 370 additions and 7 deletions
+63
View File
@@ -240,6 +240,69 @@ func (p *Provider) VerifyCallback(_ context.Context, _ provider.CallbackInput) (
return nil, provider.ErrNotSupported
}
// ScanOrphans 扫地址近 Since 的确认到账,金额不在"任一 Known 的期望金额集"内 → 孤儿。
// 期望金额 = known.AmountMinor + tailFromRef(known.ProviderRef);块时须晚于 Since。
func (p *Provider) ScanOrphans(ctx context.Context, req provider.OrphanScanRequest) ([]provider.OrphanTransfer, error) {
addr, err := p.address(req.AccountID)
if err != nil {
return nil, err
}
expected := make(map[int64]struct{}, len(req.Known))
for _, k := range req.Known {
tail, terr := tailFromRef(k.ProviderRef)
if terr != nil {
continue // 无尾数的 ref 跳过(不误判为孤儿依据)
}
expected[k.AmountMinor+tail] = struct{}{}
}
endpoint := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20?only_confirmed=true&contract_address=%s&limit=50",
p.baseURL, url.PathEscape(addr), url.QueryEscape(USDTContract))
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
if k := p.apiKey(req.AccountID); k != "" {
httpReq.Header.Set("TRON-PRO-API-KEY", k)
}
resp, err := p.http.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("crypto: TronGrid HTTP %d: %s", resp.StatusCode, body)
}
var tr trc20Resp
if err := json.Unmarshal(body, &tr); err != nil {
return nil, err
}
sinceUnix := req.Since.Unix()
var out []provider.OrphanTransfer
for _, d := range tr.Data {
if d.To != addr || d.Type != "Transfer" {
continue
}
blockTs := d.BlockMs / 1000
if blockTs < sinceUnix {
continue // 窗外旧款不扫(避免把历史正常单反复报孤儿)
}
val, perr := strconv.ParseInt(d.Value, 10, 64)
if perr != nil {
continue
}
if _, ok := expected[val]; ok {
continue // 金额有主(匹配某 attempt 期望额)→ 非孤儿
}
out = append(out, provider.OrphanTransfer{
TxID: d.TxID, AmountMinor: val, Currency: "USDT", At: time.Unix(blockTs, 0),
})
}
return out, nil
}
// trc20Resp 对应 TronGrid /v1/accounts/{addr}/transactions/trc20 响应
// (canonical tron/client.go 同构;contract_address 查询参数已在服务端过滤合约)。
type trc20Resp struct {