Gas topup limits
This commit is contained in:
@@ -117,79 +117,58 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
|
||||
}
|
||||
}
|
||||
|
||||
requiredGas, gasCurrency, err := sumNetworkFees(fundingFee, feeTransferFee)
|
||||
totalFee, gasCurrency, err := sumNetworkFees(fundingFee, feeTransferFee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
balanceResp, err := s.deps.gateway.client.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{
|
||||
WalletRef: sourceWalletRef,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("card funding balance check failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
|
||||
return err
|
||||
}
|
||||
if balanceResp == nil {
|
||||
return merrors.Internal("card funding: balance unavailable")
|
||||
}
|
||||
var nativeAvailable *moneyv1.Money
|
||||
if balance := balanceResp.GetBalance(); balance != nil {
|
||||
nativeAvailable = balance.GetNativeAvailable()
|
||||
}
|
||||
available := decimal.Zero
|
||||
availableCurrency := ""
|
||||
if nativeAvailable != nil && strings.TrimSpace(nativeAvailable.GetAmount()) != "" {
|
||||
if strings.TrimSpace(nativeAvailable.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card funding: native balance currency is required")
|
||||
}
|
||||
available, err = decimalFromMoney(nativeAvailable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
availableCurrency = strings.TrimSpace(nativeAvailable.GetCurrency())
|
||||
}
|
||||
if requiredGas.IsPositive() {
|
||||
if availableCurrency == "" {
|
||||
availableCurrency = gasCurrency
|
||||
}
|
||||
if gasCurrency != "" && availableCurrency != "" && !strings.EqualFold(gasCurrency, availableCurrency) {
|
||||
return merrors.InvalidArgument("card funding: native balance currency mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
topUpAmount := decimal.Zero
|
||||
if requiredGas.IsPositive() {
|
||||
topUpAmount = requiredGas.Sub(available)
|
||||
if topUpAmount.IsNegative() {
|
||||
topUpAmount = decimal.Zero
|
||||
}
|
||||
var estimatedTotalFee *moneyv1.Money
|
||||
if gasCurrency != "" && !totalFee.IsNegative() {
|
||||
estimatedTotalFee = makeMoney(gasCurrency, totalFee)
|
||||
}
|
||||
|
||||
var topUpMoney *moneyv1.Money
|
||||
var topUpFee *moneyv1.Money
|
||||
if topUpAmount.IsPositive() {
|
||||
if feeWalletRef == "" {
|
||||
return merrors.InvalidArgument("card funding: fee wallet ref is required for gas top-up")
|
||||
}
|
||||
if gasCurrency == "" {
|
||||
gasCurrency = availableCurrency
|
||||
}
|
||||
if gasCurrency == "" {
|
||||
return merrors.InvalidArgument("card funding: native currency is required for gas top-up")
|
||||
}
|
||||
topUpMoney = makeMoney(gasCurrency, topUpAmount)
|
||||
topUpDest := &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
|
||||
}
|
||||
topUpFee, err = s.estimateTransferNetworkFee(ctx, feeWalletRef, topUpDest, topUpMoney)
|
||||
topUpPositive := false
|
||||
if estimatedTotalFee != nil {
|
||||
computeResp, err := s.deps.gateway.client.ComputeGasTopUp(ctx, &chainv1.ComputeGasTopUpRequest{
|
||||
WalletRef: sourceWalletRef,
|
||||
EstimatedTotalFee: estimatedTotalFee,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("card funding gas top-up compute failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
|
||||
return err
|
||||
}
|
||||
if computeResp != nil {
|
||||
topUpMoney = computeResp.GetTopupAmount()
|
||||
}
|
||||
if topUpMoney != nil && strings.TrimSpace(topUpMoney.GetAmount()) != "" {
|
||||
amountDec, err := decimalFromMoney(topUpMoney)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
topUpPositive = amountDec.IsPositive()
|
||||
}
|
||||
if topUpMoney != nil && topUpPositive {
|
||||
if strings.TrimSpace(topUpMoney.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card funding: gas top-up currency is required")
|
||||
}
|
||||
if feeWalletRef == "" {
|
||||
return merrors.InvalidArgument("card funding: fee wallet ref is required for gas top-up")
|
||||
}
|
||||
topUpDest := &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
|
||||
}
|
||||
topUpFee, err = s.estimateTransferNetworkFee(ctx, feeWalletRef, topUpDest, topUpMoney)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan := ensureExecutionPlan(payment)
|
||||
var gasStep *model.ExecutionStep
|
||||
if topUpMoney != nil && topUpAmount.IsPositive() {
|
||||
if topUpMoney != nil && topUpPositive {
|
||||
gasStep = ensureExecutionStep(plan, stepCodeGasTopUp)
|
||||
gasStep.Description = "Top up native gas from fee wallet"
|
||||
gasStep.Amount = cloneMoney(topUpMoney)
|
||||
@@ -230,27 +209,58 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
|
||||
exec = &model.ExecutionRefs{}
|
||||
}
|
||||
|
||||
if topUpMoney != nil && topUpAmount.IsPositive() {
|
||||
gasReq := &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: payment.IdempotencyKey + ":card:gas",
|
||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||
SourceWalletRef: feeWalletRef,
|
||||
Destination: &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
|
||||
},
|
||||
Amount: topUpMoney,
|
||||
Metadata: cloneMetadata(payment.Metadata),
|
||||
ClientReference: payment.PaymentRef,
|
||||
}
|
||||
gasResp, gasErr := s.deps.gateway.client.SubmitTransfer(ctx, gasReq)
|
||||
if topUpMoney != nil && topUpPositive {
|
||||
ensureResp, gasErr := s.deps.gateway.client.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
||||
IdempotencyKey: payment.IdempotencyKey + ":card:gas",
|
||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||
SourceWalletRef: feeWalletRef,
|
||||
TargetWalletRef: sourceWalletRef,
|
||||
EstimatedTotalFee: estimatedTotalFee,
|
||||
Metadata: cloneMetadata(payment.Metadata),
|
||||
ClientReference: payment.PaymentRef,
|
||||
})
|
||||
if gasErr != nil {
|
||||
s.logger.Warn("card gas top-up transfer failed", zap.Error(gasErr), zap.String("payment_ref", payment.PaymentRef))
|
||||
return gasErr
|
||||
}
|
||||
if gasResp != nil && gasResp.GetTransfer() != nil {
|
||||
gasStep.TransferRef = strings.TrimSpace(gasResp.GetTransfer().GetTransferRef())
|
||||
if gasStep != nil {
|
||||
actual := (*moneyv1.Money)(nil)
|
||||
if ensureResp != nil {
|
||||
actual = ensureResp.GetTopupAmount()
|
||||
if transfer := ensureResp.GetTransfer(); transfer != nil {
|
||||
gasStep.TransferRef = strings.TrimSpace(transfer.GetTransferRef())
|
||||
}
|
||||
}
|
||||
actualPositive := false
|
||||
if actual != nil && strings.TrimSpace(actual.GetAmount()) != "" {
|
||||
actualDec, err := decimalFromMoney(actual)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualPositive = actualDec.IsPositive()
|
||||
}
|
||||
if actual != nil && actualPositive {
|
||||
gasStep.Amount = cloneMoney(actual)
|
||||
if strings.TrimSpace(actual.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card funding: gas top-up currency is required")
|
||||
}
|
||||
topUpDest := &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
|
||||
}
|
||||
topUpFee, err = s.estimateTransferNetworkFee(ctx, feeWalletRef, topUpDest, actual)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gasStep.NetworkFee = cloneMoney(topUpFee)
|
||||
} else {
|
||||
gasStep.Amount = nil
|
||||
gasStep.NetworkFee = nil
|
||||
}
|
||||
}
|
||||
s.logger.Info("card gas top-up transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", gasStep.TransferRef))
|
||||
if gasStep != nil {
|
||||
s.logger.Info("card gas top-up transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", gasStep.TransferRef))
|
||||
}
|
||||
updateExecutionPlanTotalNetworkFee(plan)
|
||||
}
|
||||
|
||||
// Transfer payout amount to funding wallet.
|
||||
|
||||
@@ -27,6 +27,8 @@ func TestSubmitCardFundingTransfers_PlansTopUpAndFunding(t *testing.T) {
|
||||
)
|
||||
|
||||
var estimateCalls []*chainv1.EstimateTransferFeeRequest
|
||||
var computeCalls []*chainv1.ComputeGasTopUpRequest
|
||||
var ensureCalls []*chainv1.EnsureGasTopUpRequest
|
||||
var submitCalls []*chainv1.SubmitTransferRequest
|
||||
|
||||
gateway := &chainclient.Fake{
|
||||
@@ -47,11 +49,17 @@ func TestSubmitCardFundingTransfers_PlansTopUpAndFunding(t *testing.T) {
|
||||
NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.02"},
|
||||
}, nil
|
||||
},
|
||||
GetWalletBalanceFn: func(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||
return &chainv1.GetWalletBalanceResponse{
|
||||
Balance: &chainv1.WalletBalance{
|
||||
NativeAvailable: &moneyv1.Money{Currency: "ETH", Amount: "0.005"},
|
||||
},
|
||||
ComputeGasTopUpFn: func(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
|
||||
computeCalls = append(computeCalls, req)
|
||||
return &chainv1.ComputeGasTopUpResponse{
|
||||
TopupAmount: &moneyv1.Money{Currency: "ETH", Amount: "0.025"},
|
||||
}, nil
|
||||
},
|
||||
EnsureGasTopUpFn: func(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
|
||||
ensureCalls = append(ensureCalls, req)
|
||||
return &chainv1.EnsureGasTopUpResponse{
|
||||
TopupAmount: &moneyv1.Money{Currency: "ETH", Amount: "0.025"},
|
||||
Transfer: &chainv1.Transfer{TransferRef: req.GetIdempotencyKey()},
|
||||
}, nil
|
||||
},
|
||||
SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
@@ -105,22 +113,36 @@ func TestSubmitCardFundingTransfers_PlansTopUpAndFunding(t *testing.T) {
|
||||
t.Fatalf("submitCardFundingTransfers error: %v", err)
|
||||
}
|
||||
|
||||
if len(estimateCalls) != 3 {
|
||||
t.Fatalf("expected 3 fee estimates, got %d", len(estimateCalls))
|
||||
if len(estimateCalls) != 4 {
|
||||
t.Fatalf("expected 4 fee estimates, got %d", len(estimateCalls))
|
||||
}
|
||||
if len(submitCalls) != 2 {
|
||||
t.Fatalf("expected 2 transfer submissions, got %d", len(submitCalls))
|
||||
if len(computeCalls) != 1 {
|
||||
t.Fatalf("expected 1 gas top-up compute call, got %d", len(computeCalls))
|
||||
}
|
||||
if len(ensureCalls) != 1 {
|
||||
t.Fatalf("expected 1 gas top-up ensure call, got %d", len(ensureCalls))
|
||||
}
|
||||
if len(submitCalls) != 1 {
|
||||
t.Fatalf("expected 1 transfer submission, got %d", len(submitCalls))
|
||||
}
|
||||
|
||||
gasCall := findSubmitCall(t, submitCalls, "pay-1:card:gas")
|
||||
if gasCall.GetSourceWalletRef() != feeWalletRef {
|
||||
t.Fatalf("gas top-up source wallet mismatch: %s", gasCall.GetSourceWalletRef())
|
||||
computeCall := computeCalls[0]
|
||||
if computeCall.GetWalletRef() != sourceWalletRef {
|
||||
t.Fatalf("gas top-up compute wallet mismatch: %s", computeCall.GetWalletRef())
|
||||
}
|
||||
if gasCall.GetDestination().GetManagedWalletRef() != sourceWalletRef {
|
||||
t.Fatalf("gas top-up destination mismatch: %s", gasCall.GetDestination().GetManagedWalletRef())
|
||||
if computeCall.GetEstimatedTotalFee().GetCurrency() != "ETH" || computeCall.GetEstimatedTotalFee().GetAmount() != "0.03" {
|
||||
t.Fatalf("gas top-up compute fee mismatch: %s %s", computeCall.GetEstimatedTotalFee().GetCurrency(), computeCall.GetEstimatedTotalFee().GetAmount())
|
||||
}
|
||||
if gasCall.GetAmount().GetCurrency() != "ETH" || gasCall.GetAmount().GetAmount() != "0.025" {
|
||||
t.Fatalf("gas top-up amount mismatch: %s %s", gasCall.GetAmount().GetCurrency(), gasCall.GetAmount().GetAmount())
|
||||
|
||||
ensureCall := ensureCalls[0]
|
||||
if ensureCall.GetSourceWalletRef() != feeWalletRef {
|
||||
t.Fatalf("gas top-up source wallet mismatch: %s", ensureCall.GetSourceWalletRef())
|
||||
}
|
||||
if ensureCall.GetTargetWalletRef() != sourceWalletRef {
|
||||
t.Fatalf("gas top-up destination mismatch: %s", ensureCall.GetTargetWalletRef())
|
||||
}
|
||||
if ensureCall.GetEstimatedTotalFee().GetCurrency() != "ETH" || ensureCall.GetEstimatedTotalFee().GetAmount() != "0.03" {
|
||||
t.Fatalf("gas top-up ensure fee mismatch: %s %s", ensureCall.GetEstimatedTotalFee().GetCurrency(), ensureCall.GetEstimatedTotalFee().GetAmount())
|
||||
}
|
||||
|
||||
fundCall := findSubmitCall(t, submitCalls, "pay-1:card:fund")
|
||||
@@ -146,8 +168,8 @@ func TestSubmitCardFundingTransfers_PlansTopUpAndFunding(t *testing.T) {
|
||||
if gasStep.NetworkFee.GetAmount() != "0.005" || gasStep.NetworkFee.GetCurrency() != "ETH" {
|
||||
t.Fatalf("gas step fee mismatch: %s %s", gasStep.NetworkFee.GetCurrency(), gasStep.NetworkFee.GetAmount())
|
||||
}
|
||||
if gasStep.TransferRef == "" {
|
||||
t.Fatalf("expected gas step transfer ref to be set")
|
||||
if gasStep.TransferRef != "pay-1:card:gas" {
|
||||
t.Fatalf("expected gas step transfer ref to be set, got %s", gasStep.TransferRef)
|
||||
}
|
||||
|
||||
fundStep := findExecutionStep(t, plan, stepCodeFundingTransfer)
|
||||
|
||||
Reference in New Issue
Block a user