All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
149 lines
7.0 KiB
Go
149 lines
7.0 KiB
Go
package transfer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"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/mservice"
|
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type submitTransferCommand struct {
|
|
deps Deps
|
|
}
|
|
|
|
func NewSubmitTransfer(deps Deps) *submitTransferCommand {
|
|
return &submitTransferCommand{deps: deps}
|
|
}
|
|
|
|
func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.SubmitTransferRequest) gsresponse.Responder[chainv1.SubmitTransferResponse] {
|
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
|
return gsresponse.Unavailable[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
}
|
|
if req == nil {
|
|
c.deps.Logger.Warn("nil request")
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
}
|
|
|
|
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
|
if idempotencyKey == "" {
|
|
c.deps.Logger.Warn("missing idempotency key")
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
|
}
|
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
|
if organizationRef == "" {
|
|
c.deps.Logger.Warn("missing organization ref")
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
|
}
|
|
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
|
if sourceWalletRef == "" {
|
|
c.deps.Logger.Warn("missing source wallet ref")
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
|
}
|
|
amount := req.GetAmount()
|
|
if amount == nil {
|
|
c.deps.Logger.Warn("missing amount")
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
|
}
|
|
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
|
|
if amountCurrency == "" {
|
|
c.deps.Logger.Warn("missing amount currency")
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
|
|
}
|
|
amountValue := strings.TrimSpace(amount.GetAmount())
|
|
if amountValue == "" {
|
|
c.deps.Logger.Warn("missing amount value")
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
|
|
}
|
|
|
|
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
|
|
if err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
c.deps.Logger.Warn("source wallet not found", zap.String("source_wallet_ref", sourceWalletRef))
|
|
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
}
|
|
c.deps.Logger.Warn("storage get wallet failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
|
|
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
}
|
|
if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
|
|
c.deps.Logger.Warn("organization mismatch", zap.String("wallet_org", sourceWallet.OrganizationRef), zap.String("req_org", organizationRef))
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
|
|
}
|
|
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
|
networkCfg, ok := c.deps.Networks[networkKey]
|
|
if !ok {
|
|
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
|
}
|
|
|
|
destination, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
|
|
if err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
c.deps.Logger.Warn("destination not found", zap.String("destination_wallet_ref", req.GetDestination().GetManagedWalletRef()))
|
|
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
}
|
|
c.deps.Logger.Warn("invalid destination", zap.Error(err))
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
}
|
|
|
|
fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
|
|
if err != nil {
|
|
c.deps.Logger.Warn("fee conversion failed", zap.Error(err))
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
}
|
|
amountDec, err := decimal.NewFromString(amountValue)
|
|
if err != nil {
|
|
c.deps.Logger.Warn("invalid amount", zap.Error(err))
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
|
|
}
|
|
netDec := amountDec.Sub(feeSum)
|
|
if netDec.IsNegative() {
|
|
c.deps.Logger.Warn("fees exceed amount", zap.String("amount", amountValue), zap.String("fee_sum", feeSum.String()))
|
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
|
|
}
|
|
|
|
netAmount := shared.CloneMoney(amount)
|
|
netAmount.Amount = netDec.String()
|
|
|
|
transfer := &model.Transfer{
|
|
IdempotencyKey: idempotencyKey,
|
|
TransferRef: shared.GenerateTransferRef(),
|
|
OrganizationRef: organizationRef,
|
|
SourceWalletRef: sourceWalletRef,
|
|
Destination: destination,
|
|
Network: sourceWallet.Network,
|
|
TokenSymbol: sourceWallet.TokenSymbol,
|
|
ContractAddress: sourceWallet.ContractAddress,
|
|
RequestedAmount: shared.CloneMoney(amount),
|
|
NetAmount: netAmount,
|
|
Fees: fees,
|
|
Status: model.TransferStatusPending,
|
|
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
|
LastStatusAt: c.deps.Clock.Now().UTC(),
|
|
}
|
|
|
|
saved, err := c.deps.Storage.Transfers().Create(ctx, transfer)
|
|
if err != nil {
|
|
if errors.Is(err, merrors.ErrDataConflict) {
|
|
c.deps.Logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
|
|
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
|
|
}
|
|
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
|
|
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
|
}
|
|
|
|
if c.deps.LaunchExecution != nil {
|
|
c.deps.LaunchExecution(saved.TransferRef, sourceWalletRef, networkCfg)
|
|
}
|
|
|
|
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
|
|
}
|