idempotency key usage fix

This commit is contained in:
Stephan D
2026-01-21 15:23:50 +01:00
parent a15375f18e
commit d2e78356e6
48 changed files with 729 additions and 559 deletions

View File

@@ -3,13 +3,9 @@ package serverimp
import (
"strings"
chainclient "github.com/tech/sendico/gateway/chain/client"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
"github.com/tech/sendico/payments/orchestrator/storage/model"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/payments/rail"
"go.uber.org/zap"
)
func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]orchestrator.CardGatewayRoute {
@@ -62,183 +58,3 @@ func buildGatewayRegistry(logger mlogger.Logger, src []gatewayInstanceConfig, re
}
return orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
}
func buildRailGateways(chainClient chainclient.Client, paymentGatewayClient chainclient.Client, src []gatewayInstanceConfig) map[string]rail.RailGateway {
if len(src) == 0 || (chainClient == nil && paymentGatewayClient == nil) {
return nil
}
instances := buildGatewayInstances(nil, src)
if len(instances) == 0 {
return nil
}
result := map[string]rail.RailGateway{}
for _, inst := range instances {
if inst == nil || !inst.IsEnabled {
continue
}
cfg := chainclient.RailGatewayConfig{
Rail: string(inst.Rail),
Network: inst.Network,
Capabilities: rail.RailCapabilities{
CanPayIn: inst.Capabilities.CanPayIn,
CanPayOut: inst.Capabilities.CanPayOut,
CanReadBalance: inst.Capabilities.CanReadBalance,
CanSendFee: inst.Capabilities.CanSendFee,
RequiresObserveConfirm: inst.Capabilities.RequiresObserveConfirm,
CanBlock: inst.Capabilities.CanBlock,
CanRelease: inst.Capabilities.CanRelease,
},
}
switch inst.Rail {
case model.RailCrypto:
if chainClient == nil {
continue
}
result[inst.ID] = chainclient.NewRailGateway(chainClient, cfg)
case model.RailProviderSettlement:
if paymentGatewayClient == nil {
continue
}
result[inst.ID] = orchestrator.NewProviderSettlementGateway(paymentGatewayClient, cfg)
}
}
if len(result) == 0 {
return nil
}
return result
}
func buildGatewayInstances(logger mlogger.Logger, src []gatewayInstanceConfig) []*model.GatewayInstanceDescriptor {
if len(src) == 0 {
return nil
}
if logger != nil {
logger = logger.Named("gateway_instances")
}
result := make([]*model.GatewayInstanceDescriptor, 0, len(src))
for _, cfg := range src {
id := strings.TrimSpace(cfg.ID)
if id == "" {
if logger != nil {
logger.Warn("Gateway instance skipped: missing id")
}
continue
}
rail := parseRail(cfg.Rail)
if rail == model.RailUnspecified {
if logger != nil {
logger.Warn("Gateway instance skipped: invalid rail", zap.String("id", id), zap.String("rail", cfg.Rail))
}
continue
}
enabled := true
if cfg.IsEnabled != nil {
enabled = *cfg.IsEnabled
}
result = append(result, &model.GatewayInstanceDescriptor{
ID: id,
Rail: rail,
Network: strings.ToUpper(strings.TrimSpace(cfg.Network)),
Currencies: normalizeCurrencies(cfg.Currencies),
Capabilities: model.RailCapabilities{
CanPayIn: cfg.Capabilities.CanPayIn,
CanPayOut: cfg.Capabilities.CanPayOut,
CanReadBalance: cfg.Capabilities.CanReadBalance,
CanSendFee: cfg.Capabilities.CanSendFee,
RequiresObserveConfirm: cfg.Capabilities.RequiresObserveConfirm,
CanBlock: cfg.Capabilities.CanBlock,
CanRelease: cfg.Capabilities.CanRelease,
},
Limits: buildGatewayLimits(cfg.Limits),
Version: strings.TrimSpace(cfg.Version),
IsEnabled: enabled,
})
}
return result
}
func parseRail(value string) model.Rail {
switch strings.ToUpper(strings.TrimSpace(value)) {
case string(model.RailCrypto):
return model.RailCrypto
case string(model.RailProviderSettlement):
return model.RailProviderSettlement
case string(model.RailLedger):
return model.RailLedger
case string(model.RailCardPayout):
return model.RailCardPayout
case string(model.RailFiatOnRamp):
return model.RailFiatOnRamp
default:
return model.RailUnspecified
}
}
func normalizeCurrencies(values []string) []string {
if len(values) == 0 {
return nil
}
seen := map[string]bool{}
result := make([]string, 0, len(values))
for _, value := range values {
clean := strings.ToUpper(strings.TrimSpace(value))
if clean == "" || seen[clean] {
continue
}
seen[clean] = true
result = append(result, clean)
}
return result
}
func buildGatewayLimits(cfg limitsConfig) model.Limits {
limits := model.Limits{
MinAmount: strings.TrimSpace(cfg.MinAmount),
MaxAmount: strings.TrimSpace(cfg.MaxAmount),
PerTxMaxFee: strings.TrimSpace(cfg.PerTxMaxFee),
PerTxMinAmount: strings.TrimSpace(cfg.PerTxMinAmount),
PerTxMaxAmount: strings.TrimSpace(cfg.PerTxMaxAmount),
}
if len(cfg.VolumeLimit) > 0 {
limits.VolumeLimit = map[string]string{}
for key, value := range cfg.VolumeLimit {
bucket := strings.TrimSpace(key)
amount := strings.TrimSpace(value)
if bucket == "" || amount == "" {
continue
}
limits.VolumeLimit[bucket] = amount
}
}
if len(cfg.VelocityLimit) > 0 {
limits.VelocityLimit = map[string]int{}
for key, value := range cfg.VelocityLimit {
bucket := strings.TrimSpace(key)
if bucket == "" {
continue
}
limits.VelocityLimit[bucket] = value
}
}
if len(cfg.CurrencyLimits) > 0 {
limits.CurrencyLimits = map[string]model.LimitsOverride{}
for key, override := range cfg.CurrencyLimits {
currency := strings.ToUpper(strings.TrimSpace(key))
if currency == "" {
continue
}
limits.CurrencyLimits[currency] = model.LimitsOverride{
MaxVolume: strings.TrimSpace(override.MaxVolume),
MinAmount: strings.TrimSpace(override.MinAmount),
MaxAmount: strings.TrimSpace(override.MaxAmount),
MaxFee: strings.TrimSpace(override.MaxFee),
MaxOps: override.MaxOps,
}
}
}
return limits
}

View File

@@ -1,159 +1,5 @@
package serverimp
import (
"context"
"crypto/tls"
oracleclient "github.com/tech/sendico/fx/oracle/client"
chainclient "github.com/tech/sendico/gateway/chain/client"
mntxclient "github.com/tech/sendico/gateway/mntx/client"
ledgerclient "github.com/tech/sendico/ledger/client"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
)
func (i *Imp) initFeesClient(cfg clientConfig) (feesv1.FeeEngineClient, *grpc.ClientConn) {
addr := cfg.address()
if addr == "" {
return nil, nil
}
dialCtx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
defer cancel()
creds := credentials.NewTLS(&tls.Config{})
if cfg.InsecureTransport {
creds = insecure.NewCredentials()
}
conn, err := grpc.DialContext(dialCtx, addr, grpc.WithTransportCredentials(creds))
if err != nil {
i.logger.Warn("Failed to connect to fees service", zap.String("address", addr), zap.Error(err))
return nil, nil
}
i.logger.Info("Connected to fees service", zap.String("address", addr))
return feesv1.NewFeeEngineClient(conn), conn
}
func (i *Imp) initLedgerClient(cfg clientConfig) ledgerclient.Client {
addr := cfg.address()
if addr == "" {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
defer cancel()
client, err := ledgerclient.New(ctx, ledgerclient.Config{
Address: addr,
DialTimeout: cfg.dialTimeout(),
CallTimeout: cfg.callTimeout(),
Insecure: cfg.InsecureTransport,
})
if err != nil {
i.logger.Warn("Failed to connect to ledger service", zap.String("address", addr), zap.Error(err))
return nil
}
i.logger.Info("Connected to ledger service", zap.String("address", addr))
return client
}
func (i *Imp) initGatewayClient(cfg clientConfig) chainclient.Client {
addr := cfg.address()
if addr == "" {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
defer cancel()
client, err := chainclient.New(ctx, chainclient.Config{
Address: addr,
DialTimeout: cfg.dialTimeout(),
CallTimeout: cfg.callTimeout(),
Insecure: cfg.InsecureTransport,
})
if err != nil {
i.logger.Warn("failed to connect to chain gateway service", zap.String("address", addr), zap.Error(err))
return nil
}
i.logger.Info("connected to chain gateway service", zap.String("address", addr))
return client
}
func (i *Imp) initPaymentGatewayClient(cfg clientConfig) chainclient.Client {
addr := cfg.address()
if addr == "" {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
defer cancel()
client, err := chainclient.New(ctx, chainclient.Config{
Address: addr,
DialTimeout: cfg.dialTimeout(),
CallTimeout: cfg.callTimeout(),
Insecure: cfg.InsecureTransport,
})
if err != nil {
i.logger.Warn("failed to connect to payment gateway service", zap.String("address", addr), zap.Error(err))
return nil
}
i.logger.Info("connected to payment gateway service", zap.String("address", addr))
return client
}
func (i *Imp) initMntxClient(cfg clientConfig) mntxclient.Client {
addr := cfg.address()
if addr == "" {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
defer cancel()
client, err := mntxclient.New(ctx, mntxclient.Config{
Address: addr,
DialTimeout: cfg.dialTimeout(),
CallTimeout: cfg.callTimeout(),
Logger: i.logger.Named("client.mntx"),
})
if err != nil {
i.logger.Warn("Failed to connect to mntx gateway service", zap.String("address", addr), zap.Error(err))
return nil
}
i.logger.Info("Connected to mntx gateway service", zap.String("address", addr))
return client
}
func (i *Imp) initOracleClient(cfg clientConfig) oracleclient.Client {
addr := cfg.address()
if addr == "" {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
defer cancel()
client, err := oracleclient.New(ctx, oracleclient.Config{
Address: addr,
DialTimeout: cfg.dialTimeout(),
CallTimeout: cfg.callTimeout(),
Insecure: cfg.InsecureTransport,
})
if err != nil {
i.logger.Warn("Failed to connect to oracle service", zap.String("address", addr), zap.Error(err))
return nil
}
i.logger.Info("Connected to oracle service", zap.String("address", addr))
return client
}
func (i *Imp) closeClients() {
if i.discoveryClients != nil {
i.discoveryClients.Close()

View File

@@ -77,17 +77,6 @@ type limitsOverrideCfg struct {
MaxOps int `yaml:"max_ops"`
}
func (c clientConfig) address() string {
return strings.TrimSpace(c.Address)
}
func (c clientConfig) dialTimeout() time.Duration {
if c.DialTimeoutSecs <= 0 {
return 5 * time.Second
}
return time.Duration(c.DialTimeoutSecs) * time.Second
}
func (c clientConfig) callTimeout() time.Duration {
if c.CallTimeoutSecs <= 0 {
return 3 * time.Second

View File

@@ -16,7 +16,7 @@ type orchestratorDeps struct {
gatewayInvokeResolver orchestrator.GatewayInvokeResolver
}
func (i *Imp) initDependencies(cfg *config) *orchestratorDeps {
func (i *Imp) initDependencies(_ *config) *orchestratorDeps {
deps := &orchestratorDeps{}
if i.discoveryReg == nil {
if i.logger != nil {