separated quotation and payments
This commit is contained in:
@@ -144,6 +144,8 @@ func CreateAPI(logger mlogger.Logger, config *api.Config, db db.Factory, router
|
||||
}
|
||||
p.logger.Info("Middleware installed", zap.Bool("debug_mode", debug))
|
||||
|
||||
p.resolveServiceAddressesFromDiscovery()
|
||||
|
||||
p.logger.Info("Installing microservices...")
|
||||
if err := p.installServices(); err != nil {
|
||||
p.logger.Error("Failed to install a microservice", zap.Error(err))
|
||||
|
||||
466
api/server/internal/api/discovery_resolver.go
Normal file
466
api/server/internal/api/discovery_resolver.go
Normal file
@@ -0,0 +1,466 @@
|
||||
package apiimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
eapi "github.com/tech/sendico/server/interface/api"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
discoveryBootstrapTimeout = 3 * time.Second
|
||||
discoveryBootstrapSender = "server_bootstrap"
|
||||
discoveryGatewayRailCrypto = "CRYPTO"
|
||||
defaultClientDialTimeoutSecs = 5
|
||||
defaultClientCallTimeoutSecs = 5
|
||||
paymentQuoteOperation = "payment.quote"
|
||||
paymentInitiateOperation = "payment.initiate"
|
||||
ledgerDebitOperation = "ledger.debit"
|
||||
ledgerCreditOperation = "ledger.credit"
|
||||
gatewayReadBalanceOperation = "balance.read"
|
||||
)
|
||||
|
||||
var (
|
||||
ledgerDiscoveryServiceNames = []string{
|
||||
"LEDGER",
|
||||
string(mservice.Ledger),
|
||||
}
|
||||
paymentOrchestratorDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_ORCHESTRATOR",
|
||||
string(mservice.PaymentOrchestrator),
|
||||
}
|
||||
paymentQuotationDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_QUOTATION",
|
||||
"PAYMENTS_QUOTE",
|
||||
"PAYMENT_QUOTATION",
|
||||
"payment_quotation",
|
||||
}
|
||||
)
|
||||
|
||||
type discoveryEndpoint struct {
|
||||
address string
|
||||
insecure bool
|
||||
raw string
|
||||
}
|
||||
|
||||
type serviceSelection struct {
|
||||
service discovery.ServiceSummary
|
||||
endpoint discoveryEndpoint
|
||||
opMatch bool
|
||||
nameRank int
|
||||
}
|
||||
|
||||
type gatewaySelection struct {
|
||||
gateway discovery.GatewaySummary
|
||||
endpoint discoveryEndpoint
|
||||
networkMatch bool
|
||||
opMatch bool
|
||||
}
|
||||
|
||||
// resolveServiceAddressesFromDiscovery looks up downstream service addresses once
|
||||
// during startup and applies them to the runtime config.
|
||||
func (a *APIImp) resolveServiceAddressesFromDiscovery() {
|
||||
if a == nil || a.config == nil || a.config.Mw == nil {
|
||||
return
|
||||
}
|
||||
|
||||
msgCfg := a.config.Mw.Messaging
|
||||
if msgCfg.Driver == "" {
|
||||
return
|
||||
}
|
||||
|
||||
logger := a.logger.Named("discovery_bootstrap")
|
||||
broker, err := msg.CreateMessagingBroker(logger.Named("bus"), &msgCfg)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create discovery bootstrap broker", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := discovery.NewClient(logger, broker, nil, discoveryBootstrapSender)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create discovery bootstrap client", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), discoveryBootstrapTimeout)
|
||||
defer cancel()
|
||||
|
||||
lookup, err := client.Lookup(ctx)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to fetch discovery registry during startup", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
a.resolveChainGatewayAddress(lookup.Gateways)
|
||||
orchestratorFound, orchestratorEndpoint := a.resolvePaymentOrchestratorAddress(lookup.Services)
|
||||
a.resolveLedgerAddress(lookup.Services)
|
||||
a.resolvePaymentQuotationAddress(lookup.Services, orchestratorFound, orchestratorEndpoint)
|
||||
}
|
||||
|
||||
func (a *APIImp) resolveChainGatewayAddress(gateways []discovery.GatewaySummary) {
|
||||
cfg := a.config.ChainGateway
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, selected, ok := selectGatewayEndpoint(
|
||||
gateways,
|
||||
cfg.DefaultAsset.Chain,
|
||||
[]string{gatewayReadBalanceOperation},
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsChainGateway(cfg)
|
||||
|
||||
a.logger.Info("Resolved chain gateway address from discovery",
|
||||
zap.String("rail", selected.Rail),
|
||||
zap.String("gateway_id", selected.ID),
|
||||
zap.String("network", selected.Network),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
}
|
||||
|
||||
func (a *APIImp) resolveLedgerAddress(services []discovery.ServiceSummary) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
ledgerDiscoveryServiceNames,
|
||||
[]string{ledgerDebitOperation, ledgerCreditOperation},
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := ensureLedgerConfig(a.config)
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsLedger(cfg)
|
||||
|
||||
a.logger.Info("Resolved ledger address from discovery",
|
||||
zap.String("service", selected.Service),
|
||||
zap.String("service_id", selected.ID),
|
||||
zap.String("instance_id", selected.InstanceID),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
}
|
||||
|
||||
func (a *APIImp) resolvePaymentOrchestratorAddress(services []discovery.ServiceSummary) (bool, discoveryEndpoint) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentOrchestratorDiscoveryServiceNames,
|
||||
[]string{paymentInitiateOperation},
|
||||
)
|
||||
if !ok {
|
||||
return false, discoveryEndpoint{}
|
||||
}
|
||||
|
||||
cfg := ensurePaymentOrchestratorConfig(a.config)
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsPayment(cfg)
|
||||
|
||||
a.logger.Info("Resolved payment orchestrator address from discovery",
|
||||
zap.String("service", selected.Service),
|
||||
zap.String("service_id", selected.ID),
|
||||
zap.String("instance_id", selected.InstanceID),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
|
||||
return true, endpoint
|
||||
}
|
||||
|
||||
func (a *APIImp) resolvePaymentQuotationAddress(services []discovery.ServiceSummary, orchestratorFound bool, orchestratorEndpoint discoveryEndpoint) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentQuotationDiscoveryServiceNames,
|
||||
[]string{paymentQuoteOperation},
|
||||
)
|
||||
if !ok {
|
||||
cfg := a.config.PaymentQuotation
|
||||
if cfg != nil && strings.TrimSpace(cfg.Address) != "" {
|
||||
return
|
||||
}
|
||||
if !orchestratorFound {
|
||||
return
|
||||
}
|
||||
// Fall back to orchestrator endpoint when quotation service is not announced.
|
||||
endpoint = orchestratorEndpoint
|
||||
selected = discovery.ServiceSummary{Service: "PAYMENTS_ORCHESTRATOR"}
|
||||
}
|
||||
|
||||
cfg := ensurePaymentQuotationConfig(a.config)
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsPayment(cfg)
|
||||
|
||||
a.logger.Info("Resolved payment quotation address from discovery",
|
||||
zap.String("service", selected.Service),
|
||||
zap.String("service_id", selected.ID),
|
||||
zap.String("instance_id", selected.InstanceID),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
}
|
||||
|
||||
func selectServiceEndpoint(services []discovery.ServiceSummary, serviceNames []string, requiredOps []string) (discoveryEndpoint, discovery.ServiceSummary, bool) {
|
||||
selections := make([]serviceSelection, 0)
|
||||
for _, svc := range services {
|
||||
if !svc.Healthy {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(svc.InvokeURI) == "" {
|
||||
continue
|
||||
}
|
||||
nameRank, ok := serviceRank(svc.Service, serviceNames)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
endpoint, err := parseDiscoveryInvokeURI(svc.InvokeURI)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
selections = append(selections, serviceSelection{
|
||||
service: svc,
|
||||
endpoint: endpoint,
|
||||
opMatch: hasAnyOperation(svc.Ops, requiredOps),
|
||||
nameRank: nameRank,
|
||||
})
|
||||
}
|
||||
if len(selections) == 0 {
|
||||
return discoveryEndpoint{}, discovery.ServiceSummary{}, false
|
||||
}
|
||||
|
||||
sort.Slice(selections, func(i, j int) bool {
|
||||
if selections[i].opMatch != selections[j].opMatch {
|
||||
return selections[i].opMatch
|
||||
}
|
||||
if selections[i].nameRank != selections[j].nameRank {
|
||||
return selections[i].nameRank < selections[j].nameRank
|
||||
}
|
||||
if selections[i].service.ID != selections[j].service.ID {
|
||||
return selections[i].service.ID < selections[j].service.ID
|
||||
}
|
||||
return selections[i].service.InstanceID < selections[j].service.InstanceID
|
||||
})
|
||||
|
||||
selected := selections[0]
|
||||
return selected.endpoint, selected.service, true
|
||||
}
|
||||
|
||||
func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork string, requiredOps []string) (discoveryEndpoint, discovery.GatewaySummary, bool) {
|
||||
preferredNetwork = strings.TrimSpace(preferredNetwork)
|
||||
selections := make([]gatewaySelection, 0)
|
||||
|
||||
for _, gateway := range gateways {
|
||||
if !gateway.Healthy {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(gateway.Rail), discoveryGatewayRailCrypto) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(gateway.InvokeURI) == "" {
|
||||
continue
|
||||
}
|
||||
endpoint, err := parseDiscoveryInvokeURI(gateway.InvokeURI)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
selections = append(selections, gatewaySelection{
|
||||
gateway: gateway,
|
||||
endpoint: endpoint,
|
||||
networkMatch: preferredNetwork != "" && strings.EqualFold(strings.TrimSpace(gateway.Network), preferredNetwork),
|
||||
opMatch: hasAnyOperation(gateway.Ops, requiredOps),
|
||||
})
|
||||
}
|
||||
if len(selections) == 0 {
|
||||
return discoveryEndpoint{}, discovery.GatewaySummary{}, false
|
||||
}
|
||||
|
||||
sort.Slice(selections, func(i, j int) bool {
|
||||
if selections[i].networkMatch != selections[j].networkMatch {
|
||||
return selections[i].networkMatch
|
||||
}
|
||||
if selections[i].opMatch != selections[j].opMatch {
|
||||
return selections[i].opMatch
|
||||
}
|
||||
if selections[i].gateway.RoutingPriority != selections[j].gateway.RoutingPriority {
|
||||
return selections[i].gateway.RoutingPriority > selections[j].gateway.RoutingPriority
|
||||
}
|
||||
if selections[i].gateway.ID != selections[j].gateway.ID {
|
||||
return selections[i].gateway.ID < selections[j].gateway.ID
|
||||
}
|
||||
return selections[i].gateway.InstanceID < selections[j].gateway.InstanceID
|
||||
})
|
||||
|
||||
selected := selections[0]
|
||||
return selected.endpoint, selected.gateway, true
|
||||
}
|
||||
|
||||
func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return discoveryEndpoint{}, fmt.Errorf("invoke uri is empty")
|
||||
}
|
||||
|
||||
// Without a scheme we expect a plain host:port target.
|
||||
if !strings.Contains(raw, "://") {
|
||||
if _, _, err := net.SplitHostPort(raw); err != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("invoke uri must include host:port: %w", err)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: raw,
|
||||
insecure: true,
|
||||
raw: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return discoveryEndpoint{}, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
|
||||
case "grpc":
|
||||
address := strings.TrimSpace(parsed.Host)
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("grpc invoke uri must include host:port: %w", splitErr)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: address,
|
||||
insecure: true,
|
||||
raw: raw,
|
||||
}, nil
|
||||
case "grpcs":
|
||||
address := strings.TrimSpace(parsed.Host)
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("grpcs invoke uri must include host:port: %w", splitErr)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: address,
|
||||
insecure: false,
|
||||
raw: raw,
|
||||
}, nil
|
||||
case "dns", "passthrough":
|
||||
// gRPC resolver targets such as dns:///service:port.
|
||||
return discoveryEndpoint{
|
||||
address: raw,
|
||||
insecure: true,
|
||||
raw: raw,
|
||||
}, nil
|
||||
default:
|
||||
return discoveryEndpoint{}, fmt.Errorf("unsupported invoke uri scheme: %s", parsed.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceRank(service string, names []string) (int, bool) {
|
||||
service = strings.TrimSpace(service)
|
||||
if service == "" {
|
||||
return 0, false
|
||||
}
|
||||
for i, name := range names {
|
||||
if strings.EqualFold(service, strings.TrimSpace(name)) {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func hasAnyOperation(ops []string, required []string) bool {
|
||||
if len(required) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, op := range ops {
|
||||
normalized := strings.TrimSpace(op)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
for _, target := range required {
|
||||
if strings.EqualFold(normalized, strings.TrimSpace(target)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureLedgerConfig(cfg *eapi.Config) *eapi.LedgerConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.Ledger == nil {
|
||||
cfg.Ledger = &eapi.LedgerConfig{}
|
||||
}
|
||||
return cfg.Ledger
|
||||
}
|
||||
|
||||
func ensurePaymentOrchestratorConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.PaymentOrchestrator == nil {
|
||||
cfg.PaymentOrchestrator = &eapi.PaymentOrchestratorConfig{}
|
||||
}
|
||||
return cfg.PaymentOrchestrator
|
||||
}
|
||||
|
||||
func ensurePaymentQuotationConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.PaymentQuotation == nil {
|
||||
cfg.PaymentQuotation = &eapi.PaymentOrchestratorConfig{}
|
||||
}
|
||||
return cfg.PaymentQuotation
|
||||
}
|
||||
|
||||
func ensureTimeoutsLedger(cfg *eapi.LedgerConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.DialTimeoutSeconds <= 0 {
|
||||
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
||||
}
|
||||
if cfg.CallTimeoutSeconds <= 0 {
|
||||
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTimeoutsChainGateway(cfg *eapi.ChainGatewayConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.DialTimeoutSeconds <= 0 {
|
||||
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
||||
}
|
||||
if cfg.CallTimeoutSeconds <= 0 {
|
||||
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTimeoutsPayment(cfg *eapi.PaymentOrchestratorConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.DialTimeoutSeconds <= 0 {
|
||||
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
||||
}
|
||||
if cfg.CallTimeoutSeconds <= 0 {
|
||||
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
||||
}
|
||||
}
|
||||
140
api/server/internal/api/discovery_resolver_test.go
Normal file
140
api/server/internal/api/discovery_resolver_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package apiimp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
)
|
||||
|
||||
func TestParseDiscoveryInvokeURI(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
raw string
|
||||
address string
|
||||
insecure bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "host_port",
|
||||
raw: "ledger:50052",
|
||||
address: "ledger:50052",
|
||||
insecure: true,
|
||||
},
|
||||
{
|
||||
name: "grpc_scheme",
|
||||
raw: "grpc://payments-orchestrator:50062",
|
||||
address: "payments-orchestrator:50062",
|
||||
insecure: true,
|
||||
},
|
||||
{
|
||||
name: "grpcs_scheme",
|
||||
raw: "grpcs://payments-orchestrator:50062",
|
||||
address: "payments-orchestrator:50062",
|
||||
insecure: false,
|
||||
},
|
||||
{
|
||||
name: "dns_scheme",
|
||||
raw: "dns:///ledger:50052",
|
||||
address: "dns:///ledger:50052",
|
||||
insecure: true,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
raw: "ledger",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
endpoint, err := parseDiscoveryInvokeURI(tc.raw)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for %q", tc.raw)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("parseDiscoveryInvokeURI(%q) failed: %v", tc.raw, err)
|
||||
}
|
||||
if endpoint.address != tc.address {
|
||||
t.Fatalf("expected address %q, got %q", tc.address, endpoint.address)
|
||||
}
|
||||
if endpoint.insecure != tc.insecure {
|
||||
t.Fatalf("expected insecure %t, got %t", tc.insecure, endpoint.insecure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectServiceEndpointPrefersRequiredOperation(t *testing.T) {
|
||||
services := []discovery.ServiceSummary{
|
||||
{
|
||||
ID: "candidate-without-op",
|
||||
Service: "LEDGER",
|
||||
Healthy: true,
|
||||
InvokeURI: "ledger-2:50052",
|
||||
Ops: []string{"balance.read"},
|
||||
},
|
||||
{
|
||||
ID: "candidate-with-op",
|
||||
Service: "LEDGER",
|
||||
Healthy: true,
|
||||
InvokeURI: "ledger-1:50052",
|
||||
Ops: []string{"ledger.debit"},
|
||||
},
|
||||
}
|
||||
|
||||
endpoint, selected, ok := selectServiceEndpoint(services, []string{"LEDGER"}, []string{"ledger.debit"})
|
||||
if !ok {
|
||||
t.Fatal("expected service endpoint to be selected")
|
||||
}
|
||||
if selected.ID != "candidate-with-op" {
|
||||
t.Fatalf("expected candidate-with-op, got %s", selected.ID)
|
||||
}
|
||||
if endpoint.address != "ledger-1:50052" {
|
||||
t.Fatalf("expected address ledger-1:50052, got %s", endpoint.address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectGatewayEndpointPrefersNetworkAndOperation(t *testing.T) {
|
||||
gateways := []discovery.GatewaySummary{
|
||||
{
|
||||
ID: "high-priority-no-op",
|
||||
Rail: "CRYPTO",
|
||||
Network: "TRON_NILE",
|
||||
Healthy: true,
|
||||
InvokeURI: "gw-high:50053",
|
||||
RoutingPriority: 10,
|
||||
},
|
||||
{
|
||||
ID: "low-priority-with-op",
|
||||
Rail: "CRYPTO",
|
||||
Network: "TRON_NILE",
|
||||
Healthy: true,
|
||||
InvokeURI: "gw-low:50053",
|
||||
Ops: []string{"balance.read"},
|
||||
RoutingPriority: 1,
|
||||
},
|
||||
{
|
||||
ID: "different-network",
|
||||
Rail: "CRYPTO",
|
||||
Network: "ARBITRUM_ONE",
|
||||
Healthy: true,
|
||||
InvokeURI: "gw-other:50053",
|
||||
Ops: []string{"balance.read"},
|
||||
RoutingPriority: 100,
|
||||
},
|
||||
}
|
||||
|
||||
endpoint, selected, ok := selectGatewayEndpoint(gateways, "TRON_NILE", []string{"balance.read"})
|
||||
if !ok {
|
||||
t.Fatal("expected gateway endpoint to be selected")
|
||||
}
|
||||
if selected.ID != "low-priority-with-op" {
|
||||
t.Fatalf("expected low-priority-with-op, got %s", selected.ID)
|
||||
}
|
||||
if endpoint.address != "gw-low:50053" {
|
||||
t.Fatalf("expected address gw-low:50053, got %s", endpoint.address)
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
|
||||
}
|
||||
p.permissionRef = desc.ID
|
||||
|
||||
if err := p.initPaymentClient(apiCtx.Config().PaymentOrchestrator); err != nil {
|
||||
if err := p.initPaymentClient(apiCtx.Config().PaymentOrchestrator, apiCtx.Config().PaymentQuotation); err != nil {
|
||||
p.logger.Error("Failed to initialize payment orchestrator client", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) initPaymentClient(cfg *eapi.PaymentOrchestratorConfig) error {
|
||||
func (a *PaymentAPI) initPaymentClient(cfg *eapi.PaymentOrchestratorConfig, quoteCfg *eapi.PaymentOrchestratorConfig) error {
|
||||
if cfg == nil {
|
||||
return merrors.InvalidArgument("payment orchestrator configuration is not provided")
|
||||
}
|
||||
@@ -111,11 +111,23 @@ func (a *PaymentAPI) initPaymentClient(cfg *eapi.PaymentOrchestratorConfig) erro
|
||||
return merrors.InvalidArgument(fmt.Sprintf("payment orchestrator address is not specified and address env %s is empty", cfg.AddressEnv))
|
||||
}
|
||||
|
||||
quoteAddress := address
|
||||
if quoteCfg != nil {
|
||||
if addr := strings.TrimSpace(quoteCfg.Address); addr != "" {
|
||||
quoteAddress = addr
|
||||
} else if env := strings.TrimSpace(quoteCfg.AddressEnv); env != "" {
|
||||
if resolved := strings.TrimSpace(os.Getenv(env)); resolved != "" {
|
||||
quoteAddress = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clientCfg := orchestratorclient.Config{
|
||||
Address: address,
|
||||
DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
|
||||
CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
|
||||
Insecure: cfg.Insecure,
|
||||
Address: address,
|
||||
QuoteAddress: quoteAddress,
|
||||
DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
|
||||
CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
|
||||
Insecure: cfg.Insecure,
|
||||
}
|
||||
|
||||
client, err := orchestratorclient.New(context.Background(), clientCfg)
|
||||
|
||||
@@ -30,7 +30,7 @@ func (a *VerificationAPI) requestCode(r *http.Request, account *model.Account, t
|
||||
return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
|
||||
}
|
||||
|
||||
target := a.resolveTarget(req.Destination, account)
|
||||
target := a.resolveTarget(req.Target, account)
|
||||
if target == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func (a *VerificationAPI) requestCode(r *http.Request, account *model.Account, t
|
||||
return response.Accepted(a.logger, verificationResponse{
|
||||
TTLSeconds: int(vReq.Ttl.Seconds()),
|
||||
CooldownSeconds: int(a.config.Cooldown.Seconds()),
|
||||
Destination: mask.Email(target),
|
||||
Target: mask.Email(target),
|
||||
IdempotencyKey: req.IdempotencyKey,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,11 +21,7 @@ func (s *ConfirmationStore) Create(
|
||||
ctx context.Context,
|
||||
request *verification.Request,
|
||||
) (verificationCode string, err error) {
|
||||
code, err := s.db.Create(ctx, request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return code, nil
|
||||
return s.db.Create(ctx, request)
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) Verify(
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
type verificationCodeRequest struct {
|
||||
Purpose string `json:"purpose"`
|
||||
Destination string `json:"destination,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ type verificationResponse struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
TTLSeconds int `json:"ttl_seconds"`
|
||||
CooldownSeconds int `json:"cooldown_seconds"`
|
||||
Destination string `json:"destination"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
@@ -27,17 +27,20 @@ func (a *VerificationAPI) verifyCode(r *http.Request, account *model.Account, to
|
||||
return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Code) == "" {
|
||||
code := strings.TrimSpace(req.Code)
|
||||
if code == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_code", "confirmation code is required")
|
||||
}
|
||||
|
||||
target := a.resolveTarget(req.Destination, account)
|
||||
target := a.resolveTarget(req.Target, account)
|
||||
if target == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
|
||||
}
|
||||
dst, err := a.store.Verify(r.Context(), account.ID, purpose, req.Code)
|
||||
dst, err := a.store.Verify(r.Context(), account.ID, purpose, code)
|
||||
if err != nil {
|
||||
a.logger.Debug("Code verification failed", zap.Error(err))
|
||||
a.logger.Debug("Code verification failed", zap.Error(err),
|
||||
mzap.AccRef(account.ID), zap.String("purpose", req.Purpose),
|
||||
)
|
||||
return mutil.MapTokenErrorToResponse(a.logger, a.Name(), err)
|
||||
}
|
||||
if dst != target {
|
||||
|
||||
Reference in New Issue
Block a user