|
|
|
|
@@ -0,0 +1,281 @@
|
|
|
|
|
package transfer
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
|
|
|
|
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/tron"
|
|
|
|
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
|
|
|
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
|
|
|
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
|
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
|
|
|
"github.com/tech/sendico/pkg/mlogger"
|
|
|
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
|
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
|
|
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type computeGasTopUpCommand struct {
|
|
|
|
|
deps Deps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewComputeGasTopUp(deps Deps) *computeGasTopUpCommand {
|
|
|
|
|
return &computeGasTopUpCommand{deps: deps}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *computeGasTopUpCommand) Execute(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) gsresponse.Responder[chainv1.ComputeGasTopUpResponse] {
|
|
|
|
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
|
|
|
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
|
|
|
|
return gsresponse.Unavailable[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
|
|
|
}
|
|
|
|
|
if req == nil {
|
|
|
|
|
c.deps.Logger.Warn("nil request")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
walletRef := strings.TrimSpace(req.GetWalletRef())
|
|
|
|
|
if walletRef == "" {
|
|
|
|
|
c.deps.Logger.Warn("wallet ref missing")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
|
|
|
|
}
|
|
|
|
|
estimatedFee := req.GetEstimatedTotalFee()
|
|
|
|
|
if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
|
|
|
|
|
c.deps.Logger.Warn("estimated fee missing")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
topUp, capHit, decision, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, walletRef, estimatedFee)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return gsresponse.Auto[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logDecision(c.deps.Logger, walletRef, estimatedFee, nativeBalance, topUp, capHit, decision, walletModel)
|
|
|
|
|
|
|
|
|
|
return gsresponse.Success(&chainv1.ComputeGasTopUpResponse{
|
|
|
|
|
TopupAmount: topUp,
|
|
|
|
|
CapHit: capHit,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ensureGasTopUpCommand struct {
|
|
|
|
|
deps Deps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewEnsureGasTopUp(deps Deps) *ensureGasTopUpCommand {
|
|
|
|
|
return &ensureGasTopUpCommand{deps: deps}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) gsresponse.Responder[chainv1.EnsureGasTopUpResponse] {
|
|
|
|
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
|
|
|
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
|
|
|
|
return gsresponse.Unavailable[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
|
|
|
}
|
|
|
|
|
if req == nil {
|
|
|
|
|
c.deps.Logger.Warn("nil request")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
|
|
|
|
if idempotencyKey == "" {
|
|
|
|
|
c.deps.Logger.Warn("idempotency key missing")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
|
|
|
|
}
|
|
|
|
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
|
|
|
|
if organizationRef == "" {
|
|
|
|
|
c.deps.Logger.Warn("organization ref missing")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
|
|
|
|
}
|
|
|
|
|
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
|
|
|
|
if sourceWalletRef == "" {
|
|
|
|
|
c.deps.Logger.Warn("source wallet ref missing")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
|
|
|
|
}
|
|
|
|
|
targetWalletRef := strings.TrimSpace(req.GetTargetWalletRef())
|
|
|
|
|
if targetWalletRef == "" {
|
|
|
|
|
c.deps.Logger.Warn("target wallet ref missing")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("target_wallet_ref is required"))
|
|
|
|
|
}
|
|
|
|
|
estimatedFee := req.GetEstimatedTotalFee()
|
|
|
|
|
if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
|
|
|
|
|
c.deps.Logger.Warn("estimated fee missing")
|
|
|
|
|
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
topUp, capHit, decision, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, targetWalletRef, estimatedFee)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return gsresponse.Auto[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logDecision(c.deps.Logger, targetWalletRef, estimatedFee, nativeBalance, topUp, capHit, decision, walletModel)
|
|
|
|
|
|
|
|
|
|
if topUp == nil || strings.TrimSpace(topUp.GetAmount()) == "" {
|
|
|
|
|
return gsresponse.Success(&chainv1.EnsureGasTopUpResponse{
|
|
|
|
|
TopupAmount: nil,
|
|
|
|
|
CapHit: capHit,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
submitReq := &chainv1.SubmitTransferRequest{
|
|
|
|
|
IdempotencyKey: idempotencyKey,
|
|
|
|
|
OrganizationRef: organizationRef,
|
|
|
|
|
SourceWalletRef: sourceWalletRef,
|
|
|
|
|
Destination: &chainv1.TransferDestination{
|
|
|
|
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
|
|
|
|
|
},
|
|
|
|
|
Amount: topUp,
|
|
|
|
|
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
|
|
|
|
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)
|
|
|
|
|
return func(ctx context.Context) (*chainv1.EnsureGasTopUpResponse, error) {
|
|
|
|
|
submitResp, err := submitResponder(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &chainv1.EnsureGasTopUpResponse{
|
|
|
|
|
TopupAmount: topUp,
|
|
|
|
|
CapHit: capHit,
|
|
|
|
|
Transfer: submitResp.GetTransfer(),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func computeGasTopUp(ctx context.Context, deps Deps, walletRef string, estimatedFee *moneyv1.Money) (*moneyv1.Money, bool, *tron.GasTopUpDecision, *moneyv1.Money, *model.ManagedWallet, error) {
|
|
|
|
|
walletRef = strings.TrimSpace(walletRef)
|
|
|
|
|
estimatedFee = shared.CloneMoney(estimatedFee)
|
|
|
|
|
walletModel, err := deps.Storage.Wallets().Get(ctx, walletRef)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, nil, nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
networkKey := strings.ToLower(strings.TrimSpace(walletModel.Network))
|
|
|
|
|
networkCfg, ok := deps.Networks.Network(networkKey)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, false, nil, nil, nil, merrors.InvalidArgument("unsupported chain for wallet")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nativeBalance, err := nativeBalanceForWallet(ctx, deps, walletModel)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, nil, nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.HasPrefix(networkKey, "tron") {
|
|
|
|
|
topUp, decision, err := tron.ComputeGasTopUp(networkCfg, walletModel, estimatedFee, nativeBalance)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, nil, nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
return topUp, decision.CapHit, &decision, nativeBalance, walletModel, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
topUp, err := defaultGasTopUp(estimatedFee, nativeBalance)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, nil, nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
return topUp, false, nil, nativeBalance, walletModel, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nativeBalanceForWallet(ctx context.Context, deps Deps, walletModel *model.ManagedWallet) (*moneyv1.Money, error) {
|
|
|
|
|
if walletModel == nil {
|
|
|
|
|
return nil, merrors.InvalidArgument("wallet is required")
|
|
|
|
|
}
|
|
|
|
|
walletDeps := wallet.Deps{
|
|
|
|
|
Logger: deps.Logger.Named("wallet"),
|
|
|
|
|
Drivers: deps.Drivers,
|
|
|
|
|
Networks: deps.Networks,
|
|
|
|
|
KeyManager: nil,
|
|
|
|
|
Storage: deps.Storage,
|
|
|
|
|
Clock: deps.Clock,
|
|
|
|
|
BalanceCacheTTL: 0,
|
|
|
|
|
RPCTimeout: deps.RPCTimeout,
|
|
|
|
|
EnsureRepository: deps.EnsureRepository,
|
|
|
|
|
}
|
|
|
|
|
_, nativeBalance, err := wallet.OnChainWalletBalances(ctx, walletDeps, walletModel)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if nativeBalance == nil || strings.TrimSpace(nativeBalance.GetAmount()) == "" || strings.TrimSpace(nativeBalance.GetCurrency()) == "" {
|
|
|
|
|
return nil, merrors.InvalidArgument("native balance is unavailable")
|
|
|
|
|
}
|
|
|
|
|
return nativeBalance, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func defaultGasTopUp(estimatedFee *moneyv1.Money, currentBalance *moneyv1.Money) (*moneyv1.Money, error) {
|
|
|
|
|
if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
|
|
|
|
|
return nil, merrors.InvalidArgument("estimated fee is required")
|
|
|
|
|
}
|
|
|
|
|
if currentBalance == nil || strings.TrimSpace(currentBalance.GetAmount()) == "" || strings.TrimSpace(currentBalance.GetCurrency()) == "" {
|
|
|
|
|
return nil, merrors.InvalidArgument("native balance is required")
|
|
|
|
|
}
|
|
|
|
|
if !strings.EqualFold(estimatedFee.GetCurrency(), currentBalance.GetCurrency()) {
|
|
|
|
|
return nil, merrors.InvalidArgument("native balance currency mismatch")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
estimated, err := decimal.NewFromString(strings.TrimSpace(estimatedFee.GetAmount()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
current, err := decimal.NewFromString(strings.TrimSpace(currentBalance.GetAmount()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
required := estimated.Sub(current)
|
|
|
|
|
if !required.IsPositive() {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return &moneyv1.Money{
|
|
|
|
|
Currency: strings.ToUpper(strings.TrimSpace(estimatedFee.GetCurrency())),
|
|
|
|
|
Amount: required.String(),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1.Money, nativeBalance *moneyv1.Money, topUp *moneyv1.Money, capHit bool, decision *tron.GasTopUpDecision, walletModel *model.ManagedWallet) {
|
|
|
|
|
if logger == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fields := []zap.Field{
|
|
|
|
|
zap.String("wallet_ref", walletRef),
|
|
|
|
|
zap.String("estimated_total_fee", amountString(estimatedFee)),
|
|
|
|
|
zap.String("current_native_balance", amountString(nativeBalance)),
|
|
|
|
|
zap.String("topup_amount", amountString(topUp)),
|
|
|
|
|
zap.Bool("cap_hit", capHit),
|
|
|
|
|
}
|
|
|
|
|
if walletModel != nil {
|
|
|
|
|
fields = append(fields, zap.String("network", strings.TrimSpace(walletModel.Network)))
|
|
|
|
|
}
|
|
|
|
|
if decision != nil {
|
|
|
|
|
fields = append(fields,
|
|
|
|
|
zap.String("estimated_total_fee_trx", decision.EstimatedFeeTRX.String()),
|
|
|
|
|
zap.String("current_native_balance_trx", decision.CurrentBalanceTRX.String()),
|
|
|
|
|
zap.String("required_trx", decision.RequiredTRX.String()),
|
|
|
|
|
zap.String("buffered_required_trx", decision.BufferedRequiredTRX.String()),
|
|
|
|
|
zap.String("min_balance_topup_trx", decision.MinBalanceTopUpTRX.String()),
|
|
|
|
|
zap.String("raw_topup_trx", decision.RawTopUpTRX.String()),
|
|
|
|
|
zap.String("rounded_topup_trx", decision.RoundedTopUpTRX.String()),
|
|
|
|
|
zap.String("topup_trx", decision.TopUpTRX.String()),
|
|
|
|
|
zap.String("operation_type", decision.OperationType),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
logger.Info("gas top-up decision", fields...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func amountString(m *moneyv1.Money) string {
|
|
|
|
|
if m == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
amount := strings.TrimSpace(m.GetAmount())
|
|
|
|
|
currency := strings.TrimSpace(m.GetCurrency())
|
|
|
|
|
if amount == "" && currency == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if currency == "" {
|
|
|
|
|
return amount
|
|
|
|
|
}
|
|
|
|
|
if amount == "" {
|
|
|
|
|
return currency
|
|
|
|
|
}
|
|
|
|
|
return amount + " " + currency
|
|
|
|
|
}
|