feat(finance): verify Binance off-chain receipts

This commit is contained in:
zhx 2026-07-15 13:09:53 +08:00
parent 51b1ee7481
commit d8ff290392
19 changed files with 372 additions and 12 deletions

View File

@ -16146,8 +16146,11 @@ type VerifyCoinSellerRechargeReceiptRequest struct {
ProviderCurrencyCode string `protobuf:"bytes,9,opt,name=provider_currency_code,json=providerCurrencyCode,proto3" json:"provider_currency_code,omitempty"` ProviderCurrencyCode string `protobuf:"bytes,9,opt,name=provider_currency_code,json=providerCurrencyCode,proto3" json:"provider_currency_code,omitempty"`
ProviderPayType string `protobuf:"bytes,10,opt,name=provider_pay_type,json=providerPayType,proto3" json:"provider_pay_type,omitempty"` ProviderPayType string `protobuf:"bytes,10,opt,name=provider_pay_type,json=providerPayType,proto3" json:"provider_pay_type,omitempty"`
ProviderAmountMinor int64 `protobuf:"varint,11,opt,name=provider_amount_minor,json=providerAmountMinor,proto3" json:"provider_amount_minor,omitempty"` ProviderAmountMinor int64 `protobuf:"varint,11,opt,name=provider_amount_minor,json=providerAmountMinor,proto3" json:"provider_amount_minor,omitempty"`
unknownFields protoimpl.UnknownFields // receive_address 由可信后台从 app-scoped SQL 配置读取;非空时覆盖 wallet YAML
sizeCache protoimpl.SizeCache // 让运营修改收款地址后立即按新地址校验,同时避免 wallet-service 反向直连 admin 数据库。
ReceiveAddress string `protobuf:"bytes,12,opt,name=receive_address,json=receiveAddress,proto3" json:"receive_address,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *VerifyCoinSellerRechargeReceiptRequest) Reset() { func (x *VerifyCoinSellerRechargeReceiptRequest) Reset() {
@ -16257,6 +16260,13 @@ func (x *VerifyCoinSellerRechargeReceiptRequest) GetProviderAmountMinor() int64
return 0 return 0
} }
func (x *VerifyCoinSellerRechargeReceiptRequest) GetReceiveAddress() string {
if x != nil {
return x.ReceiveAddress
}
return ""
}
type VerifyCoinSellerRechargeReceiptResponse struct { type VerifyCoinSellerRechargeReceiptResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Verified bool `protobuf:"varint,1,opt,name=verified,proto3" json:"verified,omitempty"` Verified bool `protobuf:"varint,1,opt,name=verified,proto3" json:"verified,omitempty"`
@ -27435,7 +27445,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\border_id\x18\x03 \x01(\tR\aorderId\x12$\n" + "\border_id\x18\x03 \x01(\tR\aorderId\x12$\n" +
"\x0etarget_user_id\x18\x04 \x01(\x03R\ftargetUserId\"W\n" + "\x0etarget_user_id\x18\x04 \x01(\x03R\ftargetUserId\"W\n" +
"\x17H5RechargeOrderResponse\x12<\n" + "\x17H5RechargeOrderResponse\x12<\n" +
"\x05order\x18\x01 \x01(\v2&.hyapp.wallet.v1.ExternalRechargeOrderR\x05order\"\xe1\x03\n" + "\x05order\x18\x01 \x01(\v2&.hyapp.wallet.v1.ExternalRechargeOrderR\x05order\"\x8a\x04\n" +
"&VerifyCoinSellerRechargeReceiptRequest\x12\x1d\n" + "&VerifyCoinSellerRechargeReceiptRequest\x12\x1d\n" +
"\n" + "\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
@ -27449,7 +27459,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x16provider_currency_code\x18\t \x01(\tR\x14providerCurrencyCode\x12*\n" + "\x16provider_currency_code\x18\t \x01(\tR\x14providerCurrencyCode\x12*\n" +
"\x11provider_pay_type\x18\n" + "\x11provider_pay_type\x18\n" +
" \x01(\tR\x0fproviderPayType\x122\n" + " \x01(\tR\x0fproviderPayType\x122\n" +
"\x15provider_amount_minor\x18\v \x01(\x03R\x13providerAmountMinor\"\xda\x03\n" + "\x15provider_amount_minor\x18\v \x01(\x03R\x13providerAmountMinor\x12'\n" +
"\x0freceive_address\x18\f \x01(\tR\x0ereceiveAddress\"\xda\x03\n" +
"'VerifyCoinSellerRechargeReceiptResponse\x12\x1a\n" + "'VerifyCoinSellerRechargeReceiptResponse\x12\x1a\n" +
"\bverified\x18\x01 \x01(\bR\bverified\x12#\n" + "\bverified\x18\x01 \x01(\bR\bverified\x12#\n" +
"\rprovider_code\x18\x02 \x01(\tR\fproviderCode\x12*\n" + "\rprovider_code\x18\x02 \x01(\tR\fproviderCode\x12*\n" +

View File

@ -1861,6 +1861,9 @@ message VerifyCoinSellerRechargeReceiptRequest {
string provider_currency_code = 9; string provider_currency_code = 9;
string provider_pay_type = 10; string provider_pay_type = 10;
int64 provider_amount_minor = 11; int64 provider_amount_minor = 11;
// receive_address app-scoped SQL wallet YAML
// wallet-service admin
string receive_address = 12;
} }
message VerifyCoinSellerRechargeReceiptResponse { message VerifyCoinSellerRechargeReceiptResponse {

View File

@ -110,6 +110,22 @@ func (h *Handler) GetUSDTAddresses(c *gin.Context) {
response.OK(c, item) response.OK(c, item)
} }
func (h *Handler) UpsertUSDTAddress(c *gin.Context) {
var req financeUSDTAddressUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "USDT 收款地址参数不正确")
return
}
item, err := h.service.UpsertUSDTAddress(shared.ActorFromContext(c), c.Param("app_code"), c.Param("chain"), req)
if err != nil {
writeServiceError(c, err, "保存 USDT 收款地址失败")
return
}
// 收款地址决定真实资金凭证归属,审计资源 ID 同时包含 App 与链,便于后续定位误改来源。
shared.OperationLogWithResourceID(c, h.audit, "upsert-finance-usdt-address", "admin_app_configs", item.AppCode+":"+item.Chain, "success", "")
response.OK(c, item)
}
func (h *Handler) ReplaceCoinSellerRechargeExchangeRate(c *gin.Context) { func (h *Handler) ReplaceCoinSellerRechargeExchangeRate(c *gin.Context) {
var req coinSellerRechargeExchangeRatePolicyRequest var req coinSellerRechargeExchangeRatePolicyRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -18,6 +18,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/verify", middleware.RequirePermission(permissionCoinSellerRechargeVerify), h.VerifyCoinSellerRechargeOrder) protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/verify", middleware.RequirePermission(permissionCoinSellerRechargeVerify), h.VerifyCoinSellerRechargeOrder)
protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/grant", middleware.RequirePermission(permissionCoinSellerRechargeGrant), h.GrantCoinSellerRechargeOrder) protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/grant", middleware.RequirePermission(permissionCoinSellerRechargeGrant), h.GrantCoinSellerRechargeOrder)
protected.GET("/admin/finance/usdt-addresses/:app_code", middleware.RequirePermission(permissionCoinSellerRechargeView), h.GetUSDTAddresses) protected.GET("/admin/finance/usdt-addresses/:app_code", middleware.RequirePermission(permissionCoinSellerRechargeView), h.GetUSDTAddresses)
protected.PUT("/admin/finance/usdt-addresses/:app_code/:chain", middleware.RequirePermission(permissionUSDTAddressUpdate), h.UpsertUSDTAddress)
protected.GET("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.GetCoinSellerRechargeExchangeRate) protected.GET("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.GetCoinSellerRechargeExchangeRate)
protected.PUT("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.ReplaceCoinSellerRechargeExchangeRate) protected.PUT("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.ReplaceCoinSellerRechargeExchangeRate)
} }

View File

@ -29,6 +29,7 @@ const (
permissionCoinSellerRechargeCreate = "finance-order:coin-seller-recharge:create" permissionCoinSellerRechargeCreate = "finance-order:coin-seller-recharge:create"
permissionCoinSellerRechargeVerify = "finance-order:coin-seller-recharge:verify" permissionCoinSellerRechargeVerify = "finance-order:coin-seller-recharge:verify"
permissionCoinSellerRechargeGrant = "finance-order:coin-seller-recharge:grant" permissionCoinSellerRechargeGrant = "finance-order:coin-seller-recharge:grant"
permissionUSDTAddressUpdate = "finance-order:usdt-address:update"
permissionCoinSellerExchangeRate = "coin-seller:exchange-rate" permissionCoinSellerExchangeRate = "coin-seller:exchange-rate"
coinSellerRechargeAssetType = "COIN_SELLER_COIN" coinSellerRechargeAssetType = "COIN_SELLER_COIN"
@ -261,6 +262,10 @@ func (s *Service) VerifyOrder(ctx context.Context, actor shared.Actor, id uint,
if order.GrantStatus == model.CoinSellerRechargeGrantStatusGranted { if order.GrantStatus == model.CoinSellerRechargeGrantStatusGranted {
return nil, repository.ErrCoinSellerRechargeOrderGranted return nil, repository.ErrCoinSellerRechargeOrderGranted
} }
receiveAddress, err := s.financeUSDTReceiveAddress(order.AppCode, order.Chain)
if err != nil {
return nil, err
}
resp, err := s.wallet.VerifyCoinSellerRechargeReceipt(appctx.WithContext(ctx, order.AppCode), &walletv1.VerifyCoinSellerRechargeReceiptRequest{ resp, err := s.wallet.VerifyCoinSellerRechargeReceipt(appctx.WithContext(ctx, order.AppCode), &walletv1.VerifyCoinSellerRechargeReceiptRequest{
RequestId: strings.TrimSpace(requestID), RequestId: strings.TrimSpace(requestID),
AppCode: order.AppCode, AppCode: order.AppCode,
@ -273,6 +278,7 @@ func (s *Service) VerifyOrder(ctx context.Context, actor shared.Actor, id uint,
ProviderCurrencyCode: order.ProviderCurrencyCode, ProviderCurrencyCode: order.ProviderCurrencyCode,
ProviderAmountMinor: order.ProviderAmountMinor, ProviderAmountMinor: order.ProviderAmountMinor,
ProviderPayType: order.ProviderPayType, ProviderPayType: order.ProviderPayType,
ReceiveAddress: receiveAddress,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -543,7 +549,12 @@ func (s *Service) verifyReceiptInput(ctx context.Context, input normalizedReceip
if s == nil || s.wallet == nil { if s == nil || s.wallet == nil {
return coinSellerRechargeReceiptVerificationDTO{}, errors.New("finance order wallet verifier is not configured") return coinSellerRechargeReceiptVerificationDTO{}, errors.New("finance order wallet verifier is not configured")
} }
// admin-server 不直连三方支付和链节点wallet-service 统一持有 provider 配置、收款地址和查单适配器。 receiveAddress, err := s.financeUSDTReceiveAddress(input.AppCode, input.Chain)
if err != nil {
return coinSellerRechargeReceiptVerificationDTO{}, err
}
// admin-server 不直连三方支付和链节点;它只把当前 App 的 SQL 地址作为可信配置快照传给 wallet-service
// wallet-service 继续统一持有 provider 凭证和查单适配器,避免任一服务反向读取对方数据库。
resp, err := s.wallet.VerifyCoinSellerRechargeReceipt(appctx.WithContext(ctx, input.AppCode), &walletv1.VerifyCoinSellerRechargeReceiptRequest{ resp, err := s.wallet.VerifyCoinSellerRechargeReceipt(appctx.WithContext(ctx, input.AppCode), &walletv1.VerifyCoinSellerRechargeReceiptRequest{
RequestId: strings.TrimSpace(requestID), RequestId: strings.TrimSpace(requestID),
AppCode: input.AppCode, AppCode: input.AppCode,
@ -556,6 +567,7 @@ func (s *Service) verifyReceiptInput(ctx context.Context, input normalizedReceip
ProviderCurrencyCode: input.ProviderCurrencyCode, ProviderCurrencyCode: input.ProviderCurrencyCode,
ProviderAmountMinor: input.ProviderAmountMinor, ProviderAmountMinor: input.ProviderAmountMinor,
ProviderPayType: input.ProviderPayType, ProviderPayType: input.ProviderPayType,
ReceiveAddress: receiveAddress,
}) })
if err != nil { if err != nil {
return coinSellerRechargeReceiptVerificationDTO{}, err return coinSellerRechargeReceiptVerificationDTO{}, err
@ -889,6 +901,8 @@ func normalizeRechargeProvider(providerCode string, chain string) (string, strin
return "usdt", "BSC", nil return "usdt", "BSC", nil
case "usdt_c2c", "c2c": case "usdt_c2c", "c2c":
return "usdt", "C2C", nil return "usdt", "C2C", nil
case "usdt_offchain", "offchain", "binance_offchain":
return "usdt", "OFFCHAIN", nil
default: default:
return "", "", errors.New("充值方式不正确") return "", "", errors.New("充值方式不正确")
} }
@ -906,6 +920,8 @@ func normalizeRechargeChain(chain string) string {
return "BSC" return "BSC"
case "c2c", "usdt_c2c": case "c2c", "usdt_c2c":
return "C2C" return "C2C"
case "offchain", "off_chain", "usdt_offchain", "binance_offchain":
return "OFFCHAIN"
default: default:
return "" return ""
} }
@ -922,6 +938,8 @@ func receiptProviderCode(order model.CoinSellerRechargeOrder) string {
return "usdt_bep20" return "usdt_bep20"
case "C2C": case "C2C":
return "usdt_c2c" return "usdt_c2c"
case "OFFCHAIN":
return "usdt_offchain"
default: default:
return "usdt" return "usdt"
} }
@ -948,6 +966,8 @@ func receiptChain(order model.CoinSellerRechargeOrder) string {
return "bep20" return "bep20"
case "C2C": case "C2C":
return "c2c" return "c2c"
case "OFFCHAIN":
return "offchain"
default: default:
return "" return ""
} }

View File

@ -38,6 +38,23 @@ func TestNormalizeCreateOrderRequiresExplicitUSDTChain(t *testing.T) {
} }
} }
func TestNormalizeCreateOrderSupportsExplicitBinanceOffchain(t *testing.T) {
input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
AppCode: "aslan",
TargetUserID: "4337389",
USDAmount: 300.14,
ProviderCode: "usdt",
Chain: "binance_offchain",
ExternalOrderNo: "391252658053",
})
if err != nil {
t.Fatalf("normalize explicit Binance Off-chain failed: %v", err)
}
if input.ProviderCode != "usdt" || input.Chain != "OFFCHAIN" || input.USDMinorAmount != 30014 || receiptVerificationProviderCode(normalizedReceiptVerificationInput{ProviderCode: input.ProviderCode, Chain: input.Chain}) != "usdt_offchain" {
t.Fatalf("explicit Binance Off-chain mapping mismatch: %+v", input)
}
}
func TestNormalizeCreateOrderRequiresProviderCurrencyAmount(t *testing.T) { func TestNormalizeCreateOrderRequiresProviderCurrencyAmount(t *testing.T) {
for _, providerCode := range []string{"mifapay", "v5pay"} { for _, providerCode := range []string{"mifapay", "v5pay"} {
_, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{ _, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{

View File

@ -9,6 +9,8 @@ import (
"hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/model" "hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/modules/shared"
"gorm.io/gorm"
) )
const financeUSDTAddressGroup = "finance-usdt-address" const financeUSDTAddressGroup = "finance-usdt-address"
@ -29,6 +31,17 @@ type financeUSDTAddressesDTO struct {
Items []financeUSDTAddressDTO `json:"items"` Items []financeUSDTAddressDTO `json:"items"`
} }
type financeUSDTAddressUpsertRequest struct {
Address string `json:"address"`
}
type financeUSDTAddressUpsertDTO struct {
AppCode string `json:"appCode"`
Chain string `json:"chain"`
Address string `json:"address"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
// GetUSDTAddresses 只读取当前 App 自己持有的配置行,不继承空 app_code 的历史基线。 // GetUSDTAddresses 只读取当前 App 自己持有的配置行,不继承空 app_code 的历史基线。
// 收款地址一旦跨 App 回退,运营会拿到另一账套的地址并让链上订单校验必然失败。 // 收款地址一旦跨 App 回退,运营会拿到另一账套的地址并让链上订单校验必然失败。
func (s *Service) GetUSDTAddresses(actor shared.Actor, appCode string) (*financeUSDTAddressesDTO, error) { func (s *Service) GetUSDTAddresses(actor shared.Actor, appCode string) (*financeUSDTAddressesDTO, error) {
@ -61,6 +74,79 @@ func (s *Service) GetUSDTAddresses(actor shared.Actor, appCode string) (*finance
return &financeUSDTAddressesDTO{AppCode: appCode, Items: items}, nil return &financeUSDTAddressesDTO{AppCode: appCode, Items: items}, nil
} }
// UpsertUSDTAddress 把收款地址写入现有 app-scoped 配置表;唯一键确保更新是单行 upsert
// 不新增地址历史副本,也不会覆盖其他 App。生产查单会在每次校验前重新读取该行。
func (s *Service) UpsertUSDTAddress(actor shared.Actor, appCode string, chain string, req financeUSDTAddressUpsertRequest) (*financeUSDTAddressUpsertDTO, error) {
if s == nil || s.store == nil {
return nil, errors.New("admin store is not configured")
}
if actor.UserID == 0 || !hasPermission(actor.Permissions, permissionUSDTAddressUpdate) {
return nil, errCoinSellerRechargeForbidden
}
if strings.TrimSpace(appCode) == "" {
return nil, errInvalidCoinSellerRechargeOrderInput
}
appCode = appctx.Normalize(appCode)
chain = strings.ToUpper(strings.TrimSpace(chain))
address := strings.TrimSpace(req.Address)
if _, supported := financeUSDTChainOrder[chain]; !supported {
return nil, errors.New("USDT 链不正确")
}
if !validFinanceUSDTAddress(chain, address) {
return nil, errors.New("USDT 收款地址不正确")
}
access, err := s.store.AppAccessForUser(actor.UserID)
if err != nil {
return nil, err
}
if !access.Allows(appCode) {
return nil, errCoinSellerRechargeForbidden
}
if err := s.store.UpsertScopedAppConfigs(appCode, []model.AppConfig{{
Group: financeUSDTAddressGroup,
Key: chain,
Value: address,
Description: "财务 USDT 收款地址",
}}); err != nil {
return nil, err
}
item, err := s.store.GetOwnedAppConfig(appCode, financeUSDTAddressGroup, chain)
if err != nil {
return nil, err
}
return &financeUSDTAddressUpsertDTO{AppCode: appCode, Chain: chain, Address: item.Value, UpdatedAtMS: item.UpdatedAtMS}, nil
}
// financeUSDTReceiveAddress 把订单校验链映射到当前 SQL 地址。Off-chain 虽没有公链 hash
// 但 Binance Deposit History 仍记录实际 TRX 收款地址,因此必须复用 TRON 配置C2C Pay 则没有地址。
func (s *Service) financeUSDTReceiveAddress(appCode string, receiptChain string) (string, error) {
addressChain := ""
switch strings.ToUpper(strings.TrimSpace(receiptChain)) {
case "TRON":
addressChain = "TRON"
case "BSC":
addressChain = "BSC"
case "OFFCHAIN":
addressChain = "TRON"
case "", "C2C":
return "", nil
default:
return "", errors.New("USDT 链不正确")
}
item, err := s.store.GetOwnedAppConfig(appctx.Normalize(appCode), financeUSDTAddressGroup, addressChain)
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", fmt.Errorf("当前 APP 未配置 %s USDT 收款地址", addressChain)
}
if err != nil {
return "", err
}
address := strings.TrimSpace(item.Value)
if !validFinanceUSDTAddress(addressChain, address) {
return "", fmt.Errorf("%s USDT 收款地址配置不正确", addressChain)
}
return address, nil
}
func financeUSDTAddressesFromConfigs(configs []model.AppConfig) ([]financeUSDTAddressDTO, error) { func financeUSDTAddressesFromConfigs(configs []model.AppConfig) ([]financeUSDTAddressDTO, error) {
items := make([]financeUSDTAddressDTO, 0, len(configs)) items := make([]financeUSDTAddressDTO, 0, len(configs))
for _, config := range configs { for _, config := range configs {

View File

@ -4,6 +4,11 @@ import (
"testing" "testing"
"hyapp-admin-server/internal/model" "hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/mysql"
"gorm.io/gorm"
) )
func TestFinanceUSDTAddressesFromConfigsKeepsSupportedValidChainsInBusinessOrder(t *testing.T) { func TestFinanceUSDTAddressesFromConfigsKeepsSupportedValidChainsInBusinessOrder(t *testing.T) {
@ -26,3 +31,32 @@ func TestFinanceUSDTAddressesFromConfigsRejectsMalformedStoredAddress(t *testing
t.Fatal("malformed SQL address must fail closed instead of being copied into a recharge order") t.Fatal("malformed SQL address must fail closed instead of being copied into a recharge order")
} }
} }
func TestFinanceUSDTReceiveAddressMapsOffchainToCurrentTRONSQLRow(t *testing.T) {
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sql mock failed: %v", err)
}
defer func() { _ = sqlDB.Close() }()
gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{})
if err != nil {
t.Fatalf("create gorm db failed: %v", err)
}
const address = "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J"
mock.ExpectQuery("SELECT \\* FROM `admin_app_configs` WHERE app_code = \\? AND `group` = \\? AND `key` = \\? AND is_deleted = FALSE").
WithArgs("aslan", financeUSDTAddressGroup, "TRON", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "app_code", "group", "key", "value", "description", "is_deleted", "created_at_ms", "updated_at_ms"}).
AddRow(9, "aslan", financeUSDTAddressGroup, "TRON", address, "财务 USDT 收款地址", false, int64(100), int64(200)))
service := NewService(repository.New(gormDB), nil, nil)
got, err := service.financeUSDTReceiveAddress(" ASLAN ", "OFFCHAIN")
if err != nil {
t.Fatalf("load current Off-chain TRON address failed: %v", err)
}
if got != address {
t.Fatalf("Off-chain must use current app-scoped TRON SQL address, got %q", got)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("sql expectations mismatch: %v", err)
}
}

View File

@ -31,6 +31,7 @@ var (
"finance-order:coin-seller-recharge:create", "finance-order:coin-seller-recharge:create",
"finance-order:coin-seller-recharge:verify", "finance-order:coin-seller-recharge:verify",
"finance-order:coin-seller-recharge:grant", "finance-order:coin-seller-recharge:grant",
"finance-order:usdt-address:update",
"finance-withdrawal:view", "finance-withdrawal:view",
"finance-withdrawal:audit", "finance-withdrawal:audit",
} }

View File

@ -142,6 +142,7 @@ var defaultPermissions = []model.Permission{
{Name: "币商充值订单创建", Code: "finance-order:coin-seller-recharge:create", Kind: "button"}, {Name: "币商充值订单创建", Code: "finance-order:coin-seller-recharge:create", Kind: "button"},
{Name: "币商充值订单校验", Code: "finance-order:coin-seller-recharge:verify", Kind: "button"}, {Name: "币商充值订单校验", Code: "finance-order:coin-seller-recharge:verify", Kind: "button"},
{Name: "币商充值订单发放", Code: "finance-order:coin-seller-recharge:grant", Kind: "button"}, {Name: "币商充值订单发放", Code: "finance-order:coin-seller-recharge:grant", Kind: "button"},
{Name: "USDT 收款地址编辑", Code: "finance-order:usdt-address:update", Kind: "button", Description: "允许按 APP 新增或修改 USDT 收款地址"},
{Name: "用户提现申请查看", Code: "finance-withdrawal:view", Kind: "menu"}, {Name: "用户提现申请查看", Code: "finance-withdrawal:view", Kind: "menu"},
{Name: "用户提现申请审核", Code: "finance-withdrawal:audit", Kind: "button"}, {Name: "用户提现申请审核", Code: "finance-withdrawal:audit", Kind: "button"},
{Name: "内购配置查看", Code: "payment-product:view", Kind: "menu"}, {Name: "内购配置查看", Code: "payment-product:view", Kind: "menu"},

View File

@ -95,7 +95,7 @@ JOIN admin_permissions permission
WHERE role.code = 'ops-admin' WHERE role.code = 'ops-admin'
AND permission.code IN ( AND permission.code IN (
'overview:view', 'overview:view',
'finance-order:coin-seller-recharge:view', 'finance-order:coin-seller-recharge:create', 'finance-order:coin-seller-recharge:verify', 'finance-order:coin-seller-recharge:grant', 'finance-order:coin-seller-recharge:view', 'finance-order:coin-seller-recharge:create', 'finance-order:coin-seller-recharge:verify', 'finance-order:coin-seller-recharge:grant', 'finance-order:usdt-address:update',
'finance-withdrawal:view', 'finance-withdrawal:audit', 'finance-withdrawal:view', 'finance-withdrawal:audit',
'app-user:view', 'app-user:export', 'app-user:update', 'app-user:level', 'app-user:status', 'app-user:password', 'app-user:view', 'app-user:export', 'app-user:update', 'app-user:level', 'app-user:status', 'app-user:password',
'level-config:view', 'level-config:update', 'pretty-id:view', 'pretty-id:update', 'pretty-id:grant', 'level-config:view', 'level-config:update', 'pretty-id:view', 'pretty-id:update', 'pretty-id:grant',
@ -146,7 +146,7 @@ JOIN admin_permissions permission
WHERE role.code = 'operations-specialist' WHERE role.code = 'operations-specialist'
AND permission.code IN ( AND permission.code IN (
'overview:view', 'overview:view',
'finance-order:coin-seller-recharge:view', 'finance-order:coin-seller-recharge:create', 'finance-order:coin-seller-recharge:verify', 'finance-order:coin-seller-recharge:grant', 'finance-order:coin-seller-recharge:view', 'finance-order:coin-seller-recharge:create', 'finance-order:coin-seller-recharge:verify', 'finance-order:coin-seller-recharge:grant', 'finance-order:usdt-address:update',
'finance-withdrawal:view', 'finance-withdrawal:audit', 'finance-withdrawal:view', 'finance-withdrawal:audit',
'app-user:view', 'app-user:export', 'app-user:update', 'app-user:level', 'app-user:status', 'app-user:password', 'app-user:view', 'app-user:export', 'app-user:update', 'app-user:level', 'app-user:status', 'app-user:password',
'level-config:view', 'level-config:update', 'pretty-id:view', 'pretty-id:update', 'pretty-id:grant', 'level-config:view', 'level-config:update', 'pretty-id:view', 'pretty-id:update', 'pretty-id:grant',

View File

@ -0,0 +1,19 @@
-- 收款地址是按 APP 隔离的财务敏感配置;独立写权限避免仅有订单查看权限的账号修改校验口径。
-- 迁移只通过角色 code、权限 code 唯一索引写少量元数据,不扫描订单或配置业务表。
SET @now_ms = CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms)
VALUES ('USDT 收款地址编辑', 'finance-order:usdt-address:update', 'button', '允许按 APP 新增或修改 USDT 收款地址', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
-- 平台管理员、运营负责人和运营专员原本都能创建并校验币商充值订单;地址维护沿用同一岗位边界。
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT role.id, permission.id
FROM admin_roles role
JOIN admin_permissions permission
WHERE role.code IN ('platform-admin', 'ops-admin', 'operations-specialist')
AND permission.code = 'finance-order:usdt-address:update';

View File

@ -67,8 +67,8 @@ func newAccount(cfg config.BinanceAccountConfig) account {
} }
} }
// FindUSDTTransfer 先查链上 Deposit History再在未命中时查 Funding Account 的 Pay C2C History。 // FindUSDTTransfer 按显式来源查 Binance 收款事实C2C 只查 PayOFFCHAIN 只查 Deposit History
// 两个事实源的成功语义不同链上入金必须校验网络、地址和状态Pay C2C 必须校验正金额,证明是当前 API Key 账户收款而不是付款。 // 历史 TRX/BSC 调用仍保留 Deposit 未命中后的 Pay 兼容路径。链上/站内入金必须校验网络、地址和状态Pay C2C 必须证明是当前 API Key 账户正向收款。
func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransferLookupRequest) (ports.BinanceTransferRecord, error) { func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransferLookupRequest) (ports.BinanceTransferRecord, error) {
if c == nil || c.apiBaseURL == "" { if c == nil || c.apiBaseURL == "" {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Unavailable, "binance client is not configured") return ports.BinanceTransferRecord{}, xerr.New(xerr.Unavailable, "binance client is not configured")
@ -104,6 +104,11 @@ func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransfer
if err != nil { if err != nil {
return ports.BinanceTransferRecord{}, err return ports.BinanceTransferRecord{}, err
} }
if req.Network == "OFFCHAIN" {
// 显式 Off-chain 只接受 Deposit History 中带该前缀的 TRX 站内转账;不能回退到 Pay
// 否则同一个纯数字编号可能跨事实源误绑定另一笔 C2C 收款。
return matchOffchainDepositRecord(req, records)
}
matched, err := matchDepositRecord(req, records) matched, err := matchDepositRecord(req, records)
if err == nil { if err == nil {
return matched, nil return matched, nil
@ -120,6 +125,22 @@ func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransfer
return matchPayRecord(req, payRecords) return matchPayRecord(req, payRecords)
} }
func matchOffchainDepositRecord(req ports.BinanceTransferLookupRequest, records []depositRecord) (ports.BinanceTransferRecord, error) {
for _, record := range records {
if !depositTxIDMatches(record.TxID, req.ExternalID) {
continue
}
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(record.TxID)), "off-chain transfer") {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit is not off-chain transfer")
}
// Binance 仍把站内入账记录标成实际充值网络 TRX复用强校验时必须恢复真实网络
// 地址、成功状态和金额校验均与普通 Deposit History 保持一致。
req.Network = "TRX"
return matchDepositRecord(req, []depositRecord{record})
}
return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance off-chain deposit not found")
}
func matchExplicitC2CPayRecord(req ports.BinanceTransferLookupRequest, records []payRecord) (ports.BinanceTransferRecord, error) { func matchExplicitC2CPayRecord(req ports.BinanceTransferLookupRequest, records []payRecord) (ports.BinanceTransferRecord, error) {
for _, record := range records { for _, record := range records {
if !payExternalIDMatches(record, req.ExternalID) { if !payExternalIDMatches(record, req.ExternalID) {

View File

@ -88,6 +88,55 @@ func TestFindUSDTTransferMatchesDepositAndSignedRequest(t *testing.T) {
} }
} }
func TestFindUSDTTransferOffchainUsesDepositHistoryOnly(t *testing.T) {
const address = "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J"
var depositCalls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v3/time":
_, _ = w.Write([]byte(`{"serverTime":1784034000000}`))
case "/sapi/v1/capital/deposit/hisrec":
depositCalls++
_, _ = w.Write([]byte(`[{
"id":"deposit-aslan-offchain",
"amount":"300.14000000",
"coin":"USDT",
"network":"TRX",
"status":1,
"address":"` + address + `",
"txId":"Off-chain transfer 391252658053",
"insertTime":1784033439000
}]`))
case "/sapi/v1/pay/transactions":
t.Fatalf("explicit Off-chain verification must not query Pay History")
default:
http.NotFound(w, r)
}
}))
defer server.Close()
client := New(config.BinanceConfig{
Enabled: true,
APIBaseURL: server.URL,
HTTPTimeout: time.Second,
Aslan: config.BinanceAccountConfig{APIKey: "api-aslan", APISecretKey: "secret-aslan"},
})
record, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{
AppCode: "aslan",
ExternalID: "391252658053",
Coin: "USDT",
Network: "OFFCHAIN",
ToAddress: address,
AmountMinor: 30014,
})
if err != nil {
t.Fatalf("explicit Off-chain lookup failed: %v", err)
}
if depositCalls != 1 || record.Source != "deposit_history" || record.TxID != "Off-chain transfer 391252658053" || record.Network != "TRX" || record.AmountMinor != 30014 || record.Address != address {
t.Fatalf("Off-chain deposit mismatch: calls=%d record=%+v", depositCalls, record)
}
}
func TestFindUSDTTransferRejectsDepositMismatches(t *testing.T) { func TestFindUSDTTransferRejectsDepositMismatches(t *testing.T) {
baseReq := ports.BinanceTransferLookupRequest{ baseReq := ports.BinanceTransferLookupRequest{
AppCode: "lalu", AppCode: "lalu",

View File

@ -70,6 +70,9 @@ const (
PaymentProviderUSDTBEP20 = "usdt_bep20" PaymentProviderUSDTBEP20 = "usdt_bep20"
// PaymentProviderUSDTC2C 是 Binance Pay C2C 收款订单;它不绑定公链,也不要求链上收款地址。 // PaymentProviderUSDTC2C 是 Binance Pay C2C 收款订单;它不绑定公链,也不要求链上收款地址。
PaymentProviderUSDTC2C = "usdt_c2c" PaymentProviderUSDTC2C = "usdt_c2c"
// PaymentProviderUSDTOffchain 是 Binance Deposit History 中的站内 Off-chain transfer
// 它使用 TRX 收款地址核对入账事实,但没有可提交给 TronGrid 的链上交易哈希。
PaymentProviderUSDTOffchain = "usdt_offchain"
// ThirdPartyPaymentStatusActive 表示渠道或支付方式可用于 H5 下单。 // ThirdPartyPaymentStatusActive 表示渠道或支付方式可用于 H5 下单。
ThirdPartyPaymentStatusActive = "active" ThirdPartyPaymentStatusActive = "active"
// ThirdPartyPaymentStatusDisabled 表示渠道或支付方式已被后台关闭。 // ThirdPartyPaymentStatusDisabled 表示渠道或支付方式已被后台关闭。

View File

@ -238,6 +238,8 @@ type VerifyCoinSellerRechargeReceiptCommand struct {
ProviderCurrencyCode string ProviderCurrencyCode string
ProviderAmountMinor int64 ProviderAmountMinor int64
ProviderPayType string ProviderPayType string
// ReceiveAddress 只接受内部 admin-server 传入的当前 SQL 配置;为空时保留 H5/YAML 兼容路径。
ReceiveAddress string
} }
// CoinSellerRechargeReceiptVerification 是三方或链上返回的只读凭证快照。 // CoinSellerRechargeReceiptVerification 是三方或链上返回的只读凭证快照。
@ -418,6 +420,8 @@ func NormalizePaymentProvider(provider string) string {
return PaymentProviderUSDTBEP20 return PaymentProviderUSDTBEP20
case PaymentProviderUSDTC2C: case PaymentProviderUSDTC2C:
return PaymentProviderUSDTC2C return PaymentProviderUSDTC2C
case PaymentProviderUSDTOffchain:
return PaymentProviderUSDTOffchain
case PaymentProviderGooglePlay: case PaymentProviderGooglePlay:
return PaymentProviderGooglePlay return PaymentProviderGooglePlay
default: default:

View File

@ -30,12 +30,16 @@ func (s *Service) VerifyCoinSellerRechargeReceipt(ctx context.Context, command l
case ledger.PaymentProviderV5Pay: case ledger.PaymentProviderV5Pay:
return s.verifyV5PayRechargeReceipt(ctx, command) return s.verifyV5PayRechargeReceipt(ctx, command)
case ledger.PaymentProviderUSDTTRC20: case ledger.PaymentProviderUSDTTRC20:
return s.verifyUSDTRechargeReceipt(ctx, command, "trc20", s.externalRecharge.USDTTRC20Enabled, s.externalRecharge.usdtTRC20AddressForApp(command.AppCode), s.tronUSDT) return s.verifyUSDTRechargeReceipt(ctx, command, "trc20", s.externalRecharge.USDTTRC20Enabled, firstNonEmpty(command.ReceiveAddress, s.externalRecharge.usdtTRC20AddressForApp(command.AppCode)), s.tronUSDT)
case ledger.PaymentProviderUSDTBEP20: case ledger.PaymentProviderUSDTBEP20:
return s.verifyUSDTRechargeReceipt(ctx, command, "bep20", s.externalRecharge.USDTBEP20Enabled, s.externalRecharge.usdtBEP20AddressForApp(command.AppCode), s.bscUSDT) return s.verifyUSDTRechargeReceipt(ctx, command, "bep20", s.externalRecharge.USDTBEP20Enabled, firstNonEmpty(command.ReceiveAddress, s.externalRecharge.usdtBEP20AddressForApp(command.AppCode)), s.bscUSDT)
case ledger.PaymentProviderUSDTC2C: case ledger.PaymentProviderUSDTC2C:
// C2C 是 Binance 账户内收款事实,不属于 TRON/BSC空地址和关闭的链上开关不影响只读查单。 // C2C 是 Binance 账户内收款事实,不属于 TRON/BSC空地址和关闭的链上开关不影响只读查单。
return s.verifyUSDTRechargeReceipt(ctx, command, "c2c", false, "", nil) return s.verifyUSDTRechargeReceipt(ctx, command, "c2c", false, "", nil)
case ledger.PaymentProviderUSDTOffchain:
// Binance Off-chain 是 Deposit History 中的 TRX 入账事实;后台 SQL 地址优先于 YAML
// 但仍要求 TRC20 功能开启,避免已停用的收款链被人工入口绕过。
return s.verifyUSDTRechargeReceipt(ctx, command, "offchain", s.externalRecharge.USDTTRC20Enabled, firstNonEmpty(command.ReceiveAddress, s.externalRecharge.usdtTRC20AddressForApp(command.AppCode)), nil)
default: default:
return ledger.CoinSellerRechargeReceiptVerification{}, xerr.New(xerr.InvalidArgument, "receipt provider is invalid") return ledger.CoinSellerRechargeReceiptVerification{}, xerr.New(xerr.InvalidArgument, "receipt provider is invalid")
} }
@ -50,6 +54,7 @@ func normalizeVerifyCoinSellerRechargeReceiptCommand(command ledger.VerifyCoinSe
command.ProviderCountryCode = strings.ToUpper(strings.TrimSpace(command.ProviderCountryCode)) command.ProviderCountryCode = strings.ToUpper(strings.TrimSpace(command.ProviderCountryCode))
command.ProviderCurrencyCode = strings.ToUpper(strings.TrimSpace(command.ProviderCurrencyCode)) command.ProviderCurrencyCode = strings.ToUpper(strings.TrimSpace(command.ProviderCurrencyCode))
command.ProviderPayType = strings.TrimSpace(command.ProviderPayType) command.ProviderPayType = strings.TrimSpace(command.ProviderPayType)
command.ReceiveAddress = strings.TrimSpace(command.ReceiveAddress)
return command return command
} }
@ -61,7 +66,7 @@ func validateVerifyCoinSellerRechargeReceiptCommand(command ledger.VerifyCoinSel
return xerr.New(xerr.InvalidArgument, "external_order_no is too long") return xerr.New(xerr.InvalidArgument, "external_order_no is too long")
} }
switch command.ProviderCode { switch command.ProviderCode {
case ledger.PaymentProviderUSDTTRC20, ledger.PaymentProviderUSDTBEP20: case ledger.PaymentProviderUSDTTRC20, ledger.PaymentProviderUSDTBEP20, ledger.PaymentProviderUSDTOffchain:
if command.USDMinorAmount <= 0 { if command.USDMinorAmount <= 0 {
return xerr.New(xerr.InvalidArgument, "usdt receipt amount is invalid") return xerr.New(xerr.InvalidArgument, "usdt receipt amount is invalid")
} }
@ -349,6 +354,10 @@ func (s *Service) verifyUSDTRechargeReceipt(ctx context.Context, command ledger.
if !enabled || snapshot.ReceiveAddress == "" { if !enabled || snapshot.ReceiveAddress == "" {
return ledger.CoinSellerRechargeReceiptVerification{}, xerr.New(xerr.Unavailable, "usdt "+chain+" receipt verification is not configured") return ledger.CoinSellerRechargeReceiptVerification{}, xerr.New(xerr.Unavailable, "usdt "+chain+" receipt verification is not configured")
} }
if chain == "offchain" {
// Off-chain 编号不是公链 hash只能走 Binance Deposit History地址和金额仍必须与当前 App 配置精确一致。
return s.verifyBinanceTransferReceipt(ctx, command, snapshot, binanceNetworkForReceiptChain(chain))
}
if !isLikelyChainTransactionHash(command.ExternalOrderNo) { if !isLikelyChainTransactionHash(command.ExternalOrderNo) {
// TRON/BSC 只接受标准交易哈希;非链上编号必须由运营明确改选 C2C避免跨事实源误校验。 // TRON/BSC 只接受标准交易哈希;非链上编号必须由运营明确改选 C2C避免跨事实源误校验。
return ledger.CoinSellerRechargeReceiptVerification{}, xerr.New(xerr.InvalidArgument, "usdt "+chain+" receipt requires transaction hash") return ledger.CoinSellerRechargeReceiptVerification{}, xerr.New(xerr.InvalidArgument, "usdt "+chain+" receipt requires transaction hash")
@ -412,6 +421,8 @@ func binanceNetworkForReceiptChain(chain string) string {
return "BSC" return "BSC"
case "c2c": case "c2c":
return "C2C" return "C2C"
case "offchain":
return "OFFCHAIN"
default: default:
return "TRX" return "TRX"
} }

View File

@ -568,6 +568,30 @@ func TestVerifyCoinSellerRechargeReceiptUSDTTRC20HashUsesTronGrid(t *testing.T)
} }
} }
func TestVerifyCoinSellerRechargeReceiptUSDTTRC20PrefersSQLAddress(t *testing.T) {
tron := &fakeTronUSDTClient{rawJSON: `{"chain":true}`}
svc := walletservice.New(nil)
svc.SetTronUSDTClient(tron)
svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{
USDTTRC20Enabled: true,
USDTTRC20Address: "TYAMLAddressMustNotWin",
})
txHash := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
AppCode: "aslan",
ProviderCode: ledger.PaymentProviderUSDTTRC20,
ExternalOrderNo: txHash,
USDMinorAmount: 30014,
ReceiveAddress: "TSQLAddressMustWin",
})
if err != nil {
t.Fatalf("TRC20 SQL address override failed: %v", err)
}
if !receipt.Verified || receipt.ReceiveAddress != "TSQLAddressMustWin" || tron.lastToAddress != "TSQLAddressMustWin" {
t.Fatalf("current SQL address must drive verification immediately: receipt=%+v client=%+v", receipt, tron)
}
}
func TestVerifyCoinSellerRechargeReceiptUSDTTRC20RejectsC2COrderNumber(t *testing.T) { func TestVerifyCoinSellerRechargeReceiptUSDTTRC20RejectsC2COrderNumber(t *testing.T) {
svc := walletservice.New(nil) svc := walletservice.New(nil)
svc.SetTronUSDTClient(&fakeTronUSDTClient{}) svc.SetTronUSDTClient(&fakeTronUSDTClient{})
@ -629,6 +653,44 @@ func TestVerifyCoinSellerRechargeReceiptUSDTC2CUsesBinance(t *testing.T) {
} }
} }
func TestVerifyCoinSellerRechargeReceiptUSDTOffchainUsesSQLTRONAddress(t *testing.T) {
const address = "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J"
binance := &fakeBinanceClient{record: walletservice.BinanceTransferRecord{
Source: "deposit_history",
TxID: "Off-chain transfer 391252658053",
Coin: ledger.PaidCurrencyUSDT,
Network: "TRX",
Status: 1,
Address: address,
Amount: "300.14000000",
AmountMinor: 30014,
RawJSON: `{"txId":"Off-chain transfer 391252658053","status":1}`,
}}
svc := walletservice.New(nil)
svc.SetBinanceClient(binance)
svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{
USDTTRC20Enabled: true,
USDTTRC20Address: "TYAMLAddressMustNotWin",
})
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
AppCode: "aslan",
ProviderCode: ledger.PaymentProviderUSDTOffchain,
ExternalOrderNo: "391252658053",
USDMinorAmount: 30014,
OrderDateMS: 1784033439000,
ReceiveAddress: address,
})
if err != nil {
t.Fatalf("explicit Off-chain receipt failed: %v", err)
}
if !receipt.Verified || receipt.Chain != "offchain" || receipt.ReceiveAddress != address || receipt.ProviderOrderID != "Off-chain transfer 391252658053" {
t.Fatalf("Off-chain receipt mismatch: %+v", receipt)
}
if binance.calls != 1 || binance.req.Network != "OFFCHAIN" || binance.req.ToAddress != address || binance.req.AmountMinor != 30014 {
t.Fatalf("Off-chain Binance request mismatch: calls=%d req=%+v", binance.calls, binance.req)
}
}
func TestVerifyCoinSellerRechargeReceiptUSDTC2CPayKeepsNoChainAddress(t *testing.T) { func TestVerifyCoinSellerRechargeReceiptUSDTC2CPayKeepsNoChainAddress(t *testing.T) {
binance := &fakeBinanceClient{record: walletservice.BinanceTransferRecord{ binance := &fakeBinanceClient{record: walletservice.BinanceTransferRecord{
Source: "pay_history", Source: "pay_history",

View File

@ -235,6 +235,7 @@ func (s *Server) VerifyCoinSellerRechargeReceipt(ctx context.Context, req *walle
ProviderCurrencyCode: req.GetProviderCurrencyCode(), ProviderCurrencyCode: req.GetProviderCurrencyCode(),
ProviderAmountMinor: req.GetProviderAmountMinor(), ProviderAmountMinor: req.GetProviderAmountMinor(),
ProviderPayType: req.GetProviderPayType(), ProviderPayType: req.GetProviderPayType(),
ReceiveAddress: req.GetReceiveAddress(),
}) })
if err != nil { if err != nil {
return nil, xerr.ToGRPCError(err) return nil, xerr.ToGRPCError(err)