fixed fee direction

This commit is contained in:
Stephan D
2026-03-05 13:24:41 +01:00
parent 1e376da719
commit 4a5e26b03a
69 changed files with 8677 additions and 82 deletions

View File

@@ -47,7 +47,7 @@ func (e *gatewayCryptoExecutor) ExecuteCrypto(ctx context.Context, req sexec.Ste
if err != nil {
return nil, err
}
destination, err := e.resolveDestination(req.Payment, action)
destination, err := e.resolveDestination(ctx, client, req.Payment, action)
if err != nil {
return nil, err
}
@@ -223,7 +223,7 @@ func (e *gatewayCryptoExecutor) submitWalletFeeTransfer(
return nil
}
destination, err := e.resolveDestination(req.Payment, discovery.RailOperationFee)
destination, err := e.resolveDestination(ctx, client, req.Payment, discovery.RailOperationFee)
if err != nil {
return err
}
@@ -341,7 +341,12 @@ func isWalletDebitFeeLine(line *paymenttypes.FeeLine) bool {
return strings.EqualFold(strings.TrimSpace(meta["fee_target"]), "wallet")
}
func (e *gatewayCryptoExecutor) resolveDestination(payment *agg.Payment, action model.RailOperation) (*chainv1.TransferDestination, error) {
func (e *gatewayCryptoExecutor) resolveDestination(
ctx context.Context,
client chainclient.Client,
payment *agg.Payment,
action model.RailOperation,
) (*chainv1.TransferDestination, error) {
if payment == nil {
return nil, merrors.InvalidArgument("crypto send: payment is required")
}
@@ -367,7 +372,7 @@ func (e *gatewayCryptoExecutor) resolveDestination(payment *agg.Payment, action
Memo: strings.TrimSpace(destination.ExternalChain.Memo),
}, nil
case model.EndpointTypeCard:
address, err := e.resolveCardFundingAddress(payment, action)
address, err := e.resolveCardFundingAddress(ctx, client, payment, action)
if err != nil {
return nil, err
}
@@ -381,7 +386,12 @@ func (e *gatewayCryptoExecutor) resolveDestination(payment *agg.Payment, action
}
}
func (e *gatewayCryptoExecutor) resolveCardFundingAddress(payment *agg.Payment, action model.RailOperation) (string, error) {
func (e *gatewayCryptoExecutor) resolveCardFundingAddress(
ctx context.Context,
client chainclient.Client,
payment *agg.Payment,
action model.RailOperation,
) (string, error) {
if payment == nil {
return "", merrors.InvalidArgument("crypto send: payment is required")
}
@@ -395,6 +405,13 @@ func (e *gatewayCryptoExecutor) resolveCardFundingAddress(payment *agg.Payment,
}
switch action {
case discovery.RailOperationFee:
if feeWalletRef := strings.TrimSpace(route.FeeWalletRef); feeWalletRef != "" {
address, err := resolveManagedWalletDepositAddress(ctx, client, feeWalletRef)
if err != nil {
return "", err
}
return address, nil
}
if feeAddress := strings.TrimSpace(route.FeeAddress); feeAddress != "" {
return feeAddress, nil
}
@@ -406,6 +423,28 @@ func (e *gatewayCryptoExecutor) resolveCardFundingAddress(payment *agg.Payment,
return address, nil
}
func resolveManagedWalletDepositAddress(ctx context.Context, client chainclient.Client, walletRef string) (string, error) {
if client == nil {
return "", merrors.InvalidArgument("crypto send: gateway client is required to resolve fee wallet")
}
ref := strings.TrimSpace(walletRef)
if ref == "" {
return "", merrors.InvalidArgument("crypto send: fee wallet ref is required")
}
resp, err := client.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: ref})
if err != nil {
return "", err
}
if resp == nil || resp.GetWallet() == nil {
return "", merrors.Internal("crypto send: fee wallet response is missing")
}
address := strings.TrimSpace(resp.GetWallet().GetDepositAddress())
if address == "" {
return "", merrors.InvalidArgument("crypto send: fee wallet deposit address is required")
}
return address, nil
}
func destinationCardGatewayKey(payment *agg.Payment) string {
if payment == nil || payment.QuoteSnapshot == nil || payment.QuoteSnapshot.Route == nil {
return ""

View File

@@ -332,6 +332,135 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsWalletFeeTransferOnSend(t *t
}
}
func TestGatewayCryptoExecutor_ExecuteCrypto_ResolvesFeeAddressFromFeeWalletRef(t *testing.T) {
orgID := bson.NewObjectID()
submitRequests := make([]*chainv1.SubmitTransferRequest, 0, 2)
var managedWalletReq *chainv1.GetManagedWalletRequest
client := &chainclient.Fake{
GetManagedWalletFn: func(_ context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
managedWalletReq = req
return &chainv1.GetManagedWalletResponse{
Wallet: &chainv1.ManagedWallet{
WalletRef: "fee-wallet-ref",
DepositAddress: "TUA_FEE_FROM_WALLET",
},
}, nil
},
SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
submitRequests = append(submitRequests, req)
switch len(submitRequests) {
case 1:
return &chainv1.SubmitTransferResponse{
Transfer: &chainv1.Transfer{
TransferRef: "trf-principal",
OperationRef: "op-principal",
},
}, nil
case 2:
return &chainv1.SubmitTransferResponse{
Transfer: &chainv1.Transfer{
TransferRef: "trf-fee",
OperationRef: "op-fee",
},
}, nil
default:
t.Fatalf("unexpected transfer submission call %d", len(submitRequests))
return nil, nil
}
},
}
resolver := &fakeGatewayInvokeResolver{client: client}
registry := &fakeGatewayRegistry{
items: []*model.GatewayInstanceDescriptor{
{
ID: "crypto_rail_gateway_arbitrum_sepolia",
InstanceID: "crypto_rail_gateway_arbitrum_sepolia",
Rail: discovery.RailCrypto,
InvokeURI: "grpc://crypto-gateway",
IsEnabled: true,
},
},
}
executor := &gatewayCryptoExecutor{
gatewayInvokeResolver: resolver,
gatewayRegistry: registry,
cardGatewayRoutes: map[string]CardGatewayRoute{
paymenttypes.DefaultCardsGatewayID: {FundingAddress: "TUA_DEST", FeeWalletRef: "fee-wallet-ref"},
},
}
req := sexec.StepRequest{
Payment: &agg.Payment{
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
PaymentRef: "payment-1",
IdempotencyKey: "idem-1",
IntentSnapshot: model.PaymentIntent{
Ref: "intent-1",
Source: model.PaymentEndpoint{
Type: model.EndpointTypeManagedWallet,
ManagedWallet: &model.ManagedWalletEndpoint{
ManagedWalletRef: "wallet-src",
},
},
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeCard,
Card: &model.CardEndpoint{Pan: "4111111111111111"},
},
Amount: &paymenttypes.Money{Amount: "10", Currency: "USDT"},
},
QuoteSnapshot: &model.PaymentQuoteSnapshot{
DebitAmount: &paymenttypes.Money{Amount: "10.000000", Currency: "USDT"},
FeeLines: []*paymenttypes.FeeLine{
{
Money: &paymenttypes.Money{Amount: "0.70", Currency: "USDT"},
LineType: paymenttypes.PostingLineTypeFee,
Side: paymenttypes.EntrySideDebit,
Meta: map[string]string{"fee_target": "wallet"},
},
},
Route: &paymenttypes.QuoteRouteSpecification{
Hops: []*paymenttypes.QuoteRouteHop{
{Index: 1, Rail: "CRYPTO", Gateway: "crypto_rail_gateway_arbitrum_sepolia", InstanceID: "crypto_rail_gateway_arbitrum_sepolia", Role: paymenttypes.QuoteRouteHopRoleSource},
{Index: 4, Rail: "CARD", Gateway: paymenttypes.DefaultCardsGatewayID, InstanceID: paymenttypes.DefaultCardsGatewayID, Role: paymenttypes.QuoteRouteHopRoleDestination},
},
},
},
},
Step: xplan.Step{
StepRef: "hop_1_crypto_send",
StepCode: "hop.1.crypto.send",
Action: discovery.RailOperationSend,
Rail: discovery.RailCrypto,
Gateway: "crypto_rail_gateway_arbitrum_sepolia",
InstanceID: "crypto_rail_gateway_arbitrum_sepolia",
},
StepExecution: agg.StepExecution{
StepRef: "hop_1_crypto_send",
StepCode: "hop.1.crypto.send",
Attempt: 1,
},
}
_, err := executor.ExecuteCrypto(context.Background(), req)
if err != nil {
t.Fatalf("ExecuteCrypto returned error: %v", err)
}
if managedWalletReq == nil {
t.Fatal("expected managed wallet lookup request")
}
if got, want := managedWalletReq.GetWalletRef(), "fee-wallet-ref"; got != want {
t.Fatalf("fee wallet ref lookup mismatch: got=%q want=%q", got, want)
}
if got, want := len(submitRequests), 2; got != want {
t.Fatalf("submit transfer calls mismatch: got=%d want=%d", got, want)
}
feeReq := submitRequests[1]
if got, want := feeReq.GetDestination().GetExternalAddress(), "TUA_FEE_FROM_WALLET"; got != want {
t.Fatalf("fee destination mismatch: got=%q want=%q", got, want)
}
}
func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesWalletFeeAmount(t *testing.T) {
orgID := bson.NewObjectID()