refactored payment orchestration

This commit is contained in:
Stephan D
2026-02-03 00:40:46 +01:00
parent 05d998e0f7
commit 5e87e2f2f9
184 changed files with 3920 additions and 2219 deletions

View File

@@ -126,9 +126,11 @@ func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Ensure
Destination: &chainv1.TransferDestination{
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
},
Amount: topUp,
Metadata: shared.CloneMetadata(req.GetMetadata()),
ClientReference: strings.TrimSpace(req.GetClientReference()),
Amount: topUp,
Metadata: shared.CloneMetadata(req.GetMetadata()),
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
IntentRef: strings.TrimSpace(req.GetIntentRef()),
OperationRef: strings.TrimSpace(req.GetOperationRef()),
}
submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)

View File

@@ -38,11 +38,12 @@ func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
TransferRef: transfer.TransferRef,
IdempotencyKey: transfer.IdempotencyKey,
OrganizationRef: transfer.OrganizationRef,
IntentRef: transfer.IntentRef,
SourceWalletRef: transfer.SourceWalletRef,
Destination: destination,
Asset: asset,
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
NetAmount: shared.CloneMoney(transfer.NetAmount),
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
NetAmount: shared.MonenyToProto(transfer.NetAmount),
Fees: protoFees,
Status: shared.TransferStatusToProto(transfer.Status),
TransactionHash: transfer.TxHash,

View File

@@ -38,6 +38,17 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
c.deps.Logger.Warn("Missing idempotency key")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
}
intentRef := strings.TrimSpace(req.GetIntentRef())
if intentRef == "" {
c.deps.Logger.Warn("Missing intent reference")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("intentRef is required"))
}
operationRef := strings.TrimSpace(req.GetOperationRef())
if operationRef == "" {
c.deps.Logger.Warn("Missing operation reference")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("operationRef is required"))
}
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
if organizationRef == "" {
c.deps.Logger.Warn("Missing organization ref")
@@ -63,6 +74,11 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
c.deps.Logger.Warn("Missing amount value")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
}
paymentRef := strings.TrimSpace(req.GetPaymentRef())
if paymentRef == "" {
c.deps.Logger.Warn("Missing payment reference")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("payment reference is required", "paymentRef"))
}
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
if err != nil {
@@ -123,6 +139,8 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
transfer := &model.Transfer{
IdempotencyKey: idempotencyKey,
OperationRef: operationRef,
IntentRef: intentRef,
TransferRef: shared.GenerateTransferRef(),
OrganizationRef: organizationRef,
SourceWalletRef: sourceWalletRef,
@@ -130,11 +148,11 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
Network: sourceWallet.Network,
TokenSymbol: effectiveTokenSymbol,
ContractAddress: effectiveContractAddress,
RequestedAmount: shared.CloneMoney(amount),
NetAmount: netAmount,
RequestedAmount: shared.ProtoToMoney(amount),
NetAmount: shared.ProtoToMoney(netAmount),
PaymentRef: paymentRef,
Fees: fees,
Status: model.TransferStatusPending,
ClientReference: strings.TrimSpace(req.GetClientReference()),
Status: model.TransferStatusCreated,
LastStatusAt: c.deps.Clock.Now().UTC(),
}

View File

@@ -169,7 +169,9 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
Amount: amount,
Fees: parseChainFees(reader),
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
ClientReference: strings.TrimSpace(reader.String("client_reference")),
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
IntentRef: strings.TrimSpace(op.GetIntentRef()),
OperationRef: strings.TrimSpace(op.GetOperationRef()),
})
if err != nil {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
@@ -208,7 +210,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
return &connectorv1.SubmitOperationResponse{
Receipt: &connectorv1.OperationReceipt{
OperationId: opID,
Status: connectorv1.OperationStatus_CONFIRMED,
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
Result: result,
},
}, nil
@@ -238,7 +240,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
return &connectorv1.SubmitOperationResponse{
Receipt: &connectorv1.OperationReceipt{
OperationId: opID,
Status: connectorv1.OperationStatus_CONFIRMED,
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
},
}, nil
@@ -256,12 +258,14 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
}
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
IntentRef: strings.TrimSpace(op.GetIntentRef()),
OperationRef: strings.TrimSpace(op.GetOperationRef()),
OrganizationRef: orgRef,
SourceWalletRef: source,
TargetWalletRef: target,
EstimatedTotalFee: fee,
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
ClientReference: strings.TrimSpace(reader.String("client_reference")),
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
})
if err != nil {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
@@ -273,7 +277,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
return &connectorv1.SubmitOperationResponse{
Receipt: &connectorv1.OperationReceipt{
OperationId: opID,
Status: connectorv1.OperationStatus_CONFIRMED,
Status: shared.ChainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
},
}, nil
@@ -544,25 +548,51 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
return connectorv1.OperationStatus_CONFIRMED
case chainv1.TransferStatus_TRANSFER_CREATED:
return connectorv1.OperationStatus_OPERATION_CREATED
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return connectorv1.OperationStatus_OPERATION_PROCESSING
case chainv1.TransferStatus_TRANSFER_WAITING:
return connectorv1.OperationStatus_OPERATION_WAITING
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return connectorv1.OperationStatus_OPERATION_SUCCESS
case chainv1.TransferStatus_TRANSFER_FAILED:
return connectorv1.OperationStatus_FAILED
return connectorv1.OperationStatus_OPERATION_FAILED
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return connectorv1.OperationStatus_CANCELED
return connectorv1.OperationStatus_OPERATION_CANCELLED
default:
return connectorv1.OperationStatus_PENDING
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
}
}
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
switch status {
case connectorv1.OperationStatus_CONFIRMED:
return chainv1.TransferStatus_TRANSFER_CONFIRMED
case connectorv1.OperationStatus_FAILED:
case connectorv1.OperationStatus_OPERATION_CREATED:
return chainv1.TransferStatus_TRANSFER_CREATED
case connectorv1.OperationStatus_OPERATION_PROCESSING:
return chainv1.TransferStatus_TRANSFER_PROCESSING
case connectorv1.OperationStatus_OPERATION_WAITING:
return chainv1.TransferStatus_TRANSFER_WAITING
case connectorv1.OperationStatus_OPERATION_SUCCESS:
return chainv1.TransferStatus_TRANSFER_SUCCESS
case connectorv1.OperationStatus_OPERATION_FAILED:
return chainv1.TransferStatus_TRANSFER_FAILED
case connectorv1.OperationStatus_CANCELED:
case connectorv1.OperationStatus_OPERATION_CANCELLED:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}

View File

@@ -3,7 +3,6 @@ package tron
import (
"context"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/core/types"
@@ -157,9 +156,3 @@ func GetTransactionStatus(
}
return 0, nil
}
// isTronNetwork checks if the network name indicates a TRON network.
func isTronNetwork(networkName string) bool {
name := strings.ToLower(strings.TrimSpace(networkName))
return strings.HasPrefix(name, "tron")
}

View File

@@ -162,6 +162,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
IdempotencyKey: "transfer-1",
OrganizationRef: "org-1",
SourceWalletRef: srcRef,
PaymentRef: "ref-1",
Destination: &ichainv1.TransferDestination{
Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
},
@@ -172,6 +173,8 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
},
},
OperationRef: "oper-1",
IntentRef: "intent-1",
})
require.NoError(t, err)
require.NotNil(t, transferResp.GetTransfer())
@@ -179,7 +182,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
require.NotNil(t, stored)
require.Equal(t, model.TransferStatusPending, stored.Status)
require.Equal(t, model.TransferStatusCreated, stored.Status)
// GetTransfer
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})

View File

@@ -6,7 +6,9 @@ import (
"github.com/tech/sendico/gateway/tron/storage/model"
chainasset "github.com/tech/sendico/pkg/chain"
pmodel "github.com/tech/sendico/pkg/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.mongodb.org/mongo-driver/v2/bson"
)
@@ -77,42 +79,82 @@ func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.Manage
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_PENDING:
return model.TransferStatusPending
case chainv1.TransferStatus_TRANSFER_SIGNING:
return model.TransferStatusSigning
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
return model.TransferStatusSubmitted
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
return model.TransferStatusConfirmed
case chainv1.TransferStatus_TRANSFER_CREATED:
return model.TransferStatusCreated
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return model.TransferStatusProcessing
case chainv1.TransferStatus_TRANSFER_WAITING:
return model.TransferStatusWaiting
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return model.TransferStatusSuccess
case chainv1.TransferStatus_TRANSFER_FAILED:
return model.TransferStatusFailed
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return model.TransferStatusCancelled
default:
return ""
return model.TransferStatus("")
}
}
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
switch status {
case model.TransferStatusPending:
return chainv1.TransferStatus_TRANSFER_PENDING
case model.TransferStatusSigning:
return chainv1.TransferStatus_TRANSFER_SIGNING
case model.TransferStatusSubmitted:
return chainv1.TransferStatus_TRANSFER_SUBMITTED
case model.TransferStatusConfirmed:
return chainv1.TransferStatus_TRANSFER_CONFIRMED
case model.TransferStatusCreated:
return chainv1.TransferStatus_TRANSFER_CREATED
case model.TransferStatusProcessing:
return chainv1.TransferStatus_TRANSFER_PROCESSING
case model.TransferStatusWaiting:
return chainv1.TransferStatus_TRANSFER_WAITING
case model.TransferStatusSuccess:
return chainv1.TransferStatus_TRANSFER_SUCCESS
case model.TransferStatusFailed:
return chainv1.TransferStatus_TRANSFER_FAILED
case model.TransferStatusCancelled:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}
}
func ChainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_CREATED:
return connectorv1.OperationStatus_OPERATION_CREATED
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return connectorv1.OperationStatus_OPERATION_PROCESSING
case chainv1.TransferStatus_TRANSFER_WAITING:
return connectorv1.OperationStatus_OPERATION_WAITING
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return connectorv1.OperationStatus_OPERATION_SUCCESS
case chainv1.TransferStatus_TRANSFER_FAILED:
return connectorv1.OperationStatus_OPERATION_FAILED
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return connectorv1.OperationStatus_OPERATION_CANCELLED
default:
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
}
}
// NativeCurrency returns the canonical native token symbol for a network.
func NativeCurrency(network Network) string {
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
@@ -146,3 +188,23 @@ type ServiceWallet struct {
Address string
PrivateKey string
}
func ProtoToMoney(money *moneyv1.Money) *paymenttypes.Money {
if money == nil {
return &paymenttypes.Money{}
}
return &paymenttypes.Money{
Amount: money.GetAmount(),
Currency: money.GetCurrency(),
}
}
func MonenyToProto(money *paymenttypes.Money) *moneyv1.Money {
if money == nil {
return &moneyv1.Money{}
}
return &moneyv1.Money{
Amount: money.Amount,
Currency: money.Currency,
}
}

View File

@@ -40,26 +40,26 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
return err
}
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSigning, "", ""); err != nil {
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil {
s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
}
driverDeps := s.driverDeps()
chainDriver, err := s.driverForNetwork(network.Name.String())
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
return err
}
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
return err
}
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
return err
}
if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
@@ -68,7 +68,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
zap.String("wallet_ref", sourceWalletRef),
zap.String("network", network.Name.String()),
)
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", ""); err != nil {
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil {
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
}
return nil
@@ -76,11 +76,14 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
s.logger.Warn("Failed to submit transfer", zap.String("transfer_ref", transferRef), zap.Error(err))
if _, e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil {
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(e))
}
return err
}
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSubmitted, "", txHash); err != nil {
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil {
s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
}
@@ -94,15 +97,15 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
return err
}
if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
}
return nil
failureReason := ""
pStatus := model.TransferStatusSuccess
if receipt != nil && receipt.Status != types.ReceiptStatusSuccessful {
failureReason = "transaction reverted"
pStatus = model.TransferStatusFailed
}
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil {
s.logger.Warn("Failed to update transfer status", zap.Error(err),
zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus)))
}
return nil
}

View File

@@ -0,0 +1,77 @@
package gateway
import (
"context"
"fmt"
"github.com/tech/sendico/gateway/tron/storage/model"
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/payments/rail"
"go.uber.org/zap"
)
func isFinalStatus(t *model.Transfer) bool {
switch t.Status {
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
return true
default:
return false
}
}
func toOpStatus(t *model.Transfer) rail.OperationResult {
switch t.Status {
case model.TransferStatusFailed:
return rail.OperationResultFailed
case model.TransferStatusSuccess:
return rail.OperationResultSuccess
case model.TransferStatusCancelled:
return rail.OperationResultCancelled
default:
panic(fmt.Sprintf("toOpStatus: unexpected transfer status: %s", t.Status))
}
}
func toError(t *model.Transfer) string {
if t == nil {
return ""
}
if t.Status == model.TransferStatusSuccess {
return ""
}
return t.FailureReason
}
func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) {
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
if err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
}
if isFinalStatus(transfer) {
s.emitTransferStatusEvent(transfer)
}
return transfer, err
}
func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
if s == nil || s.producer == nil || transfer == nil {
return
}
exec := pmodel.PaymentGatewayExecution{
PaymentIntentID: transfer.IntentRef,
IdempotencyKey: transfer.IdempotencyKey,
ExecutedMoney: transfer.NetAmount,
PaymentRef: transfer.PaymentRef,
Status: toOpStatus(transfer),
OperationRef: transfer.OperationRef,
Error: toError(transfer),
TransferRef: transfer.TransferRef,
}
env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec)
if err := s.producer.SendMessage(env); err != nil {
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
}
}