package transfer import ( "context" "errors" "strings" "github.com/shopspring/decimal" "github.com/tech/sendico/chain/gateway/internal/service/gateway/shared" "github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mservice" gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/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 *gatewayv1.SubmitTransferRequest) gsresponse.Responder[gatewayv1.SubmitTransferResponse] { if err := c.deps.EnsureRepository(ctx); err != nil { c.deps.Logger.Warn("repository unavailable", zap.Error(err)) return gsresponse.Unavailable[gatewayv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err) } if req == nil { c.deps.Logger.Warn("nil request") return gsresponse.InvalidArgument[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err) } c.deps.Logger.Warn("invalid destination", zap.Error(err)) return gsresponse.InvalidArgument[gatewayv1.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[gatewayv1.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[gatewayv1.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[gatewayv1.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(&gatewayv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)}) } c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef)) return gsresponse.Auto[gatewayv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err) } if c.deps.LaunchExecution != nil { c.deps.LaunchExecution(saved.TransferRef, sourceWalletRef, networkCfg) } return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)}) }