diff --git a/api/server/interface/api/srequest/payment.go b/api/server/interface/api/srequest/payment.go index e9d4ed1..366edc7 100644 --- a/api/server/interface/api/srequest/payment.go +++ b/api/server/interface/api/srequest/payment.go @@ -1,19 +1,17 @@ package srequest -import orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" - type QuotePayment struct { - IdempotencyKey string `json:"idempotencyKey"` - Intent *orchestratorv1.PaymentIntent `json:"intent"` - PreviewOnly bool `json:"previewOnly"` - Metadata map[string]string `json:"metadata,omitempty"` + IdempotencyKey string `json:"idempotencyKey"` + Intent *PaymentIntent `json:"intent"` + PreviewOnly bool `json:"previewOnly"` + Metadata map[string]string `json:"metadata,omitempty"` } type InitiatePayment struct { - IdempotencyKey string `json:"idempotencyKey"` - Intent *orchestratorv1.PaymentIntent `json:"intent"` - Metadata map[string]string `json:"metadata,omitempty"` - FeeQuoteToken string `json:"feeQuoteToken,omitempty"` - FxQuoteRef string `json:"fxQuoteRef,omitempty"` - QuoteRef string `json:"quoteRef,omitempty"` + IdempotencyKey string `json:"idempotencyKey"` + Intent *PaymentIntent `json:"intent"` + Metadata map[string]string `json:"metadata,omitempty"` + FeeQuoteToken string `json:"feeQuoteToken,omitempty"` + FxQuoteRef string `json:"fxQuoteRef,omitempty"` + QuoteRef string `json:"quoteRef,omitempty"` } diff --git a/api/server/interface/api/srequest/payment_types.go b/api/server/interface/api/srequest/payment_types.go new file mode 100644 index 0000000..f82744b --- /dev/null +++ b/api/server/interface/api/srequest/payment_types.go @@ -0,0 +1,103 @@ +package srequest + +// PaymentKind mirrors the orchestrator payment kinds without importing generated proto types. +type PaymentKind int32 + +const ( + PaymentKindUnspecified PaymentKind = 0 + PaymentKindPayout PaymentKind = 1 + PaymentKindInternalTransfer PaymentKind = 2 + PaymentKindFxConversion PaymentKind = 3 +) + +// FXSide mirrors the common FX side enum. +type FXSide int32 + +const ( + FXSideUnspecified FXSide = 0 + FXSideBuyBaseSellQuote FXSide = 1 + FXSideSellBaseBuyQuote FXSide = 2 +) + +// ChainNetwork mirrors the chain network enum used by managed wallets. +type ChainNetwork int32 + +const ( + ChainNetworkUnspecified ChainNetwork = 0 + ChainNetworkEthereumMainnet ChainNetwork = 1 + ChainNetworkArbitrumOne ChainNetwork = 2 + ChainNetworkOtherEVM ChainNetwork = 3 +) + +// InsufficientNetPolicy mirrors the fee engine policy override. +type InsufficientNetPolicy int32 + +const ( + InsufficientNetPolicyUnspecified InsufficientNetPolicy = 0 + InsufficientNetPolicyBlockPosting InsufficientNetPolicy = 1 + InsufficientNetPolicySweepOrgCash InsufficientNetPolicy = 2 + InsufficientNetPolicyInvoiceLater InsufficientNetPolicy = 3 +) + +type PaymentIntent struct { + Kind PaymentKind `json:"kind,omitempty"` + Source *PaymentEndpoint `json:"source,omitempty"` + Destination *PaymentEndpoint `json:"destination,omitempty"` + Amount *Money `json:"amount,omitempty"` + RequiresFX bool `json:"requires_fx,omitempty"` + FX *FXIntent `json:"fx,omitempty"` + FeePolicy *PolicyOverrides `json:"fee_policy,omitempty"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +type PaymentEndpoint struct { + Ledger *LedgerEndpoint `json:"ledger,omitempty"` + ManagedWallet *ManagedWalletEndpoint `json:"managed_wallet,omitempty"` + ExternalChain *ExternalChainEndpoint `json:"external_chain,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +type LedgerEndpoint struct { + LedgerAccountRef string `json:"ledger_account_ref"` + ContraLedgerAccountRef string `json:"contra_ledger_account_ref,omitempty"` +} + +type ManagedWalletEndpoint struct { + ManagedWalletRef string `json:"managed_wallet_ref"` + Asset *Asset `json:"asset,omitempty"` +} + +type ExternalChainEndpoint struct { + Asset *Asset `json:"asset,omitempty"` + Address string `json:"address"` + Memo string `json:"memo,omitempty"` +} + +type Asset struct { + Chain ChainNetwork `json:"chain"` + TokenSymbol string `json:"token_symbol"` + ContractAddress string `json:"contract_address,omitempty"` +} + +type Money struct { + Amount string `json:"amount"` + Currency string `json:"currency"` +} + +type CurrencyPair struct { + Base string `json:"base"` + Quote string `json:"quote"` +} + +type FXIntent struct { + Pair *CurrencyPair `json:"pair,omitempty"` + Side FXSide `json:"side,omitempty"` + Firm bool `json:"firm,omitempty"` + TTLms int64 `json:"ttl_ms,omitempty"` + PreferredProvider string `json:"preferred_provider,omitempty"` + MaxAgeMs int32 `json:"max_age_ms,omitempty"` +} + +type PolicyOverrides struct { + InsufficientNet InsufficientNetPolicy `json:"insufficient_net,omitempty"` +} diff --git a/api/server/internal/server/paymentapiimp/mapper.go b/api/server/internal/server/paymentapiimp/mapper.go new file mode 100644 index 0000000..676e726 --- /dev/null +++ b/api/server/internal/server/paymentapiimp/mapper.go @@ -0,0 +1,175 @@ +package paymentapiimp + +import ( + "github.com/tech/sendico/pkg/merrors" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" + "github.com/tech/sendico/server/interface/api/srequest" +) + +func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIntent, error) { + if intent == nil { + return nil, merrors.InvalidArgument("intent is required") + } + + source, err := mapPaymentEndpoint(intent.Source, "source") + if err != nil { + return nil, err + } + destination, err := mapPaymentEndpoint(intent.Destination, "destination") + if err != nil { + return nil, err + } + + fx, err := mapFXIntent(intent.FX) + if err != nil { + return nil, err + } + + return &orchestratorv1.PaymentIntent{ + Kind: orchestratorv1.PaymentKind(intent.Kind), + Source: source, + Destination: destination, + Amount: mapMoney(intent.Amount), + RequiresFx: intent.RequiresFX, + Fx: fx, + FeePolicy: mapPolicyOverrides(intent.FeePolicy), + Attributes: copyStringMap(intent.Attributes), + }, nil +} + +func mapPaymentEndpoint(endpoint *srequest.PaymentEndpoint, field string) (*orchestratorv1.PaymentEndpoint, error) { + if endpoint == nil { + return nil, nil + } + + var ( + count int + result orchestratorv1.PaymentEndpoint + ) + + if endpoint.Ledger != nil { + count++ + result.Endpoint = &orchestratorv1.PaymentEndpoint_Ledger{ + Ledger: mapLedgerEndpoint(endpoint.Ledger), + } + } + if endpoint.ManagedWallet != nil { + count++ + result.Endpoint = &orchestratorv1.PaymentEndpoint_ManagedWallet{ + ManagedWallet: mapManagedWalletEndpoint(endpoint.ManagedWallet), + } + } + if endpoint.ExternalChain != nil { + count++ + result.Endpoint = &orchestratorv1.PaymentEndpoint_ExternalChain{ + ExternalChain: mapExternalChainEndpoint(endpoint.ExternalChain), + } + } + + if count > 1 { + return nil, merrors.InvalidArgument(field + " endpoint must set only one of ledger, managed_wallet, external_chain") + } + + result.Metadata = copyStringMap(endpoint.Metadata) + return &result, nil +} + +func mapLedgerEndpoint(endpoint *srequest.LedgerEndpoint) *orchestratorv1.LedgerEndpoint { + if endpoint == nil { + return nil + } + return &orchestratorv1.LedgerEndpoint{ + LedgerAccountRef: endpoint.LedgerAccountRef, + ContraLedgerAccountRef: endpoint.ContraLedgerAccountRef, + } +} + +func mapManagedWalletEndpoint(endpoint *srequest.ManagedWalletEndpoint) *orchestratorv1.ManagedWalletEndpoint { + if endpoint == nil { + return nil + } + return &orchestratorv1.ManagedWalletEndpoint{ + ManagedWalletRef: endpoint.ManagedWalletRef, + Asset: mapAsset(endpoint.Asset), + } +} + +func mapExternalChainEndpoint(endpoint *srequest.ExternalChainEndpoint) *orchestratorv1.ExternalChainEndpoint { + if endpoint == nil { + return nil + } + return &orchestratorv1.ExternalChainEndpoint{ + Asset: mapAsset(endpoint.Asset), + Address: endpoint.Address, + Memo: endpoint.Memo, + } +} + +func mapAsset(asset *srequest.Asset) *chainv1.Asset { + if asset == nil { + return nil + } + return &chainv1.Asset{ + Chain: chainv1.ChainNetwork(asset.Chain), + TokenSymbol: asset.TokenSymbol, + ContractAddress: asset.ContractAddress, + } +} + +func mapMoney(m *srequest.Money) *moneyv1.Money { + if m == nil { + return nil + } + return &moneyv1.Money{ + Amount: m.Amount, + Currency: m.Currency, + } +} + +func mapFXIntent(fx *srequest.FXIntent) (*orchestratorv1.FXIntent, error) { + if fx == nil { + return nil, nil + } + return &orchestratorv1.FXIntent{ + Pair: mapCurrencyPair(fx.Pair), + Side: fxv1.Side(fx.Side), + Firm: fx.Firm, + TtlMs: fx.TTLms, + PreferredProvider: fx.PreferredProvider, + MaxAgeMs: fx.MaxAgeMs, + }, nil +} + +func mapCurrencyPair(pair *srequest.CurrencyPair) *fxv1.CurrencyPair { + if pair == nil { + return nil + } + return &fxv1.CurrencyPair{ + Base: pair.Base, + Quote: pair.Quote, + } +} + +func mapPolicyOverrides(policy *srequest.PolicyOverrides) *feesv1.PolicyOverrides { + if policy == nil { + return nil + } + return &feesv1.PolicyOverrides{ + InsufficientNet: feesv1.InsufficientNetPolicy(policy.InsufficientNet), + } +} + +func copyStringMap(src map[string]string) map[string]string { + if len(src) == 0 { + return nil + } + dst := make(map[string]string, len(src)) + for k, v := range src { + dst[k] = v + } + return dst +} diff --git a/api/server/internal/server/paymentapiimp/pay.go b/api/server/internal/server/paymentapiimp/pay.go index 44bf567..c39ae84 100644 --- a/api/server/internal/server/paymentapiimp/pay.go +++ b/api/server/internal/server/paymentapiimp/pay.go @@ -46,12 +46,17 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to payload.QuoteRef = "" } + intent, err := mapPaymentIntent(payload.Intent) + if err != nil { + return response.BadPayload(a.logger, a.Name(), err) + } + req := &orchestratorv1.InitiatePaymentRequest{ Meta: &orchestratorv1.RequestMeta{ OrganizationRef: orgRef.Hex(), }, IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey), - Intent: payload.Intent, + Intent: intent, FeeQuoteToken: strings.TrimSpace(payload.FeeQuoteToken), FxQuoteRef: strings.TrimSpace(payload.FxQuoteRef), QuoteRef: strings.TrimSpace(payload.QuoteRef), diff --git a/api/server/internal/server/paymentapiimp/quote.go b/api/server/internal/server/paymentapiimp/quote.go index 844bf9b..a854eaf 100644 --- a/api/server/internal/server/paymentapiimp/quote.go +++ b/api/server/internal/server/paymentapiimp/quote.go @@ -39,12 +39,17 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token return response.BadPayload(a.logger, a.Name(), err) } + intent, err := mapPaymentIntent(payload.Intent) + if err != nil { + return response.BadPayload(a.logger, a.Name(), err) + } + req := &orchestratorv1.QuotePaymentRequest{ Meta: &orchestratorv1.RequestMeta{ OrganizationRef: orgRef.Hex(), }, IdempotencyKey: payload.IdempotencyKey, - Intent: payload.Intent, + Intent: intent, PreviewOnly: payload.PreviewOnly, }