298 lines
8.0 KiB
Go
298 lines
8.0 KiB
Go
package quotation
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
|
"github.com/tech/sendico/payments/storage/model"
|
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
|
"github.com/tech/sendico/pkg/discovery"
|
|
"github.com/tech/sendico/pkg/mlogger"
|
|
"github.com/tech/sendico/pkg/payments/rail"
|
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
|
)
|
|
|
|
// Option configures service dependencies.
|
|
type Option func(*Service)
|
|
|
|
// GatewayInvokeResolver resolves gateway invoke URIs into chain gateway clients.
|
|
type GatewayInvokeResolver interface {
|
|
Resolve(ctx context.Context, invokeURI string) (chainclient.Client, error)
|
|
}
|
|
|
|
// ChainGatewayResolver resolves chain gateway clients by network.
|
|
type ChainGatewayResolver interface {
|
|
Resolve(ctx context.Context, network string) (chainclient.Client, error)
|
|
}
|
|
|
|
// GatewayRegistry exposes gateway instances for capability-based selection.
|
|
type GatewayRegistry interface {
|
|
List(ctx context.Context) ([]*model.GatewayInstanceDescriptor, error)
|
|
}
|
|
|
|
// CardGatewayRoute maps a gateway to its funding and fee destinations.
|
|
type CardGatewayRoute struct {
|
|
FundingAddress string
|
|
FeeAddress string
|
|
FeeWalletRef string
|
|
}
|
|
|
|
type feesDependency struct {
|
|
client feesv1.FeeEngineClient
|
|
timeout time.Duration
|
|
}
|
|
|
|
func (f feesDependency) available() bool {
|
|
if f.client == nil {
|
|
return false
|
|
}
|
|
if checker, ok := f.client.(interface{ Available() bool }); ok {
|
|
return checker.Available()
|
|
}
|
|
return true
|
|
}
|
|
|
|
type gatewayDependency struct {
|
|
resolver ChainGatewayResolver
|
|
}
|
|
|
|
type oracleDependency struct {
|
|
client oracleclient.Client
|
|
}
|
|
|
|
func (o oracleDependency) available() bool {
|
|
if o.client == nil {
|
|
return false
|
|
}
|
|
if checker, ok := o.client.(interface{ Available() bool }); ok {
|
|
return checker.Available()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// WithFeeEngine wires the fee engine client.
|
|
func WithFeeEngine(client feesv1.FeeEngineClient, timeout time.Duration) Option {
|
|
return func(s *Service) {
|
|
s.deps.fees = feesDependency{client: client, timeout: timeout}
|
|
}
|
|
}
|
|
|
|
// WithOracleClient wires the FX oracle client.
|
|
func WithOracleClient(client oracleclient.Client) Option {
|
|
return func(s *Service) {
|
|
s.deps.oracle = oracleDependency{client: client}
|
|
}
|
|
}
|
|
|
|
// WithChainGatewayResolver wires a resolver for chain gateway clients.
|
|
func WithChainGatewayResolver(resolver ChainGatewayResolver) Option {
|
|
return func(s *Service) {
|
|
if resolver != nil {
|
|
s.deps.gateway = gatewayDependency{resolver: resolver}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithGatewayInvokeResolver wires a resolver for invoke URIs.
|
|
func WithGatewayInvokeResolver(resolver GatewayInvokeResolver) Option {
|
|
return func(s *Service) {
|
|
if resolver != nil {
|
|
s.deps.gatewayInvokeResolver = resolver
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithGatewayRegistry wires gateway descriptors used by quote computation/gateway selection.
|
|
func WithGatewayRegistry(registry GatewayRegistry) Option {
|
|
return func(s *Service) {
|
|
if registry != nil {
|
|
s.deps.gatewayRegistry = registry
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithCardGatewayRoutes configures funding/fee wallet routing per gateway.
|
|
func WithCardGatewayRoutes(routes map[string]CardGatewayRoute) Option {
|
|
return func(s *Service) {
|
|
if len(routes) == 0 {
|
|
return
|
|
}
|
|
s.deps.cardRoutes = make(map[string]CardGatewayRoute, len(routes))
|
|
for key, route := range routes {
|
|
normalized := strings.ToLower(strings.TrimSpace(key))
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
s.deps.cardRoutes[normalized] = route
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithFeeLedgerAccounts maps gateway IDs to fee ledger accounts.
|
|
func WithFeeLedgerAccounts(accounts map[string]string) Option {
|
|
return func(s *Service) {
|
|
if len(accounts) == 0 {
|
|
return
|
|
}
|
|
s.deps.feeLedgerAccounts = make(map[string]string, len(accounts))
|
|
for key, account := range accounts {
|
|
normalized := strings.ToLower(strings.TrimSpace(key))
|
|
value := strings.TrimSpace(account)
|
|
if normalized == "" || value == "" {
|
|
continue
|
|
}
|
|
s.deps.feeLedgerAccounts[normalized] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithClock overrides the default clock.
|
|
func WithClock(clock clockpkg.Clock) Option {
|
|
return func(s *Service) {
|
|
if clock != nil {
|
|
s.clock = clock
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithLedgerClient is retained for backward compatibility and is currently a no-op.
|
|
func WithLedgerClient(_ ledgerclient.Client) Option {
|
|
return func(*Service) {}
|
|
}
|
|
|
|
// WithProviderSettlementGatewayClient is retained for backward compatibility and is currently a no-op.
|
|
func WithProviderSettlementGatewayClient(_ chainclient.Client) Option {
|
|
return func(*Service) {}
|
|
}
|
|
|
|
// WithProviderSettlementGatewayResolver is retained for backward compatibility and is currently a no-op.
|
|
func WithProviderSettlementGatewayResolver(_ ChainGatewayResolver) Option {
|
|
return func(*Service) {}
|
|
}
|
|
|
|
// WithRailGateways is retained for backward compatibility and is currently a no-op.
|
|
func WithRailGateways(_ map[string]rail.RailGateway) Option {
|
|
return func(*Service) {}
|
|
}
|
|
|
|
type discoveryGatewayRegistry struct {
|
|
registry *discovery.Registry
|
|
}
|
|
|
|
// NewDiscoveryGatewayRegistry adapts discovery entries into gateway descriptors.
|
|
func NewDiscoveryGatewayRegistry(_ mlogger.Logger, registry *discovery.Registry) GatewayRegistry {
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return &discoveryGatewayRegistry{registry: registry}
|
|
}
|
|
|
|
func (r *discoveryGatewayRegistry) List(_ context.Context) ([]*model.GatewayInstanceDescriptor, error) {
|
|
if r == nil || r.registry == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
entries := r.registry.List(time.Now(), true)
|
|
items := make([]*model.GatewayInstanceDescriptor, 0, len(entries))
|
|
for _, entry := range entries {
|
|
railID := railFromDiscovery(entry.Rail)
|
|
if railID == model.RailUnspecified {
|
|
continue
|
|
}
|
|
operations := operationsFromDiscovery(entry.Operations)
|
|
items = append(items, &model.GatewayInstanceDescriptor{
|
|
ID: strings.TrimSpace(entry.ID),
|
|
InstanceID: strings.TrimSpace(entry.InstanceID),
|
|
Rail: railID,
|
|
Network: strings.ToUpper(strings.TrimSpace(entry.Network)),
|
|
InvokeURI: strings.TrimSpace(entry.InvokeURI),
|
|
Currencies: currenciesFromDiscovery(entry.Currencies),
|
|
Operations: operations,
|
|
Capabilities: model.RailCapabilitiesFromOperations(operations),
|
|
Limits: limitsFromDiscovery(entry.Limits),
|
|
IsEnabled: entry.Healthy,
|
|
})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func railFromDiscovery(value string) model.Rail {
|
|
switch discovery.NormalizeRail(value) {
|
|
case discovery.RailCrypto:
|
|
return model.RailCrypto
|
|
case discovery.RailProviderSettlement:
|
|
return model.RailProviderSettlement
|
|
case discovery.RailLedger:
|
|
return model.RailLedger
|
|
case discovery.RailCardPayout:
|
|
return model.RailCardPayout
|
|
case discovery.RailFiatOnRamp:
|
|
return model.RailFiatOnRamp
|
|
default:
|
|
return model.RailUnspecified
|
|
}
|
|
}
|
|
|
|
func operationsFromDiscovery(values []string) []model.RailOperation {
|
|
return model.NormalizeRailOperationStrings(discovery.NormalizeRailOperations(values))
|
|
}
|
|
|
|
func currenciesFromDiscovery(values []string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]string, 0, len(values))
|
|
seen := map[string]bool{}
|
|
for _, value := range values {
|
|
currency := strings.ToUpper(strings.TrimSpace(value))
|
|
if currency == "" || seen[currency] {
|
|
continue
|
|
}
|
|
seen[currency] = true
|
|
result = append(result, currency)
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func limitsFromDiscovery(src *discovery.Limits) model.Limits {
|
|
limits := model.Limits{}
|
|
if src == nil {
|
|
return limits
|
|
}
|
|
|
|
limits.MinAmount = strings.TrimSpace(src.MinAmount)
|
|
limits.MaxAmount = strings.TrimSpace(src.MaxAmount)
|
|
|
|
if len(src.VolumeLimit) > 0 {
|
|
limits.VolumeLimit = map[string]string{}
|
|
for bucket, value := range src.VolumeLimit {
|
|
key := strings.TrimSpace(bucket)
|
|
amount := strings.TrimSpace(value)
|
|
if key == "" || amount == "" {
|
|
continue
|
|
}
|
|
limits.VolumeLimit[key] = amount
|
|
}
|
|
}
|
|
|
|
if len(src.VelocityLimit) > 0 {
|
|
limits.VelocityLimit = map[string]int{}
|
|
for bucket, value := range src.VelocityLimit {
|
|
key := strings.TrimSpace(bucket)
|
|
if key == "" || value <= 0 {
|
|
continue
|
|
}
|
|
limits.VelocityLimit[key] = value
|
|
}
|
|
}
|
|
|
|
return limits
|
|
}
|