hold/release + discovery based routing
This commit is contained in:
@@ -48,10 +48,19 @@ func buildFeeLedgerAccounts(src map[string]string) map[string]string {
|
||||
}
|
||||
|
||||
func buildGatewayRegistry(logger mlogger.Logger, src []gatewayInstanceConfig, registry *discovery.Registry) orchestrator.GatewayRegistry {
|
||||
static := buildGatewayInstances(logger, src)
|
||||
staticRegistry := orchestrator.NewGatewayRegistry(logger, static)
|
||||
discoveryRegistry := orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
|
||||
return orchestrator.NewCompositeGatewayRegistry(logger, staticRegistry, discoveryRegistry)
|
||||
if logger != nil {
|
||||
logger = logger.Named("gateway_registry")
|
||||
}
|
||||
if len(src) > 0 && logger != nil {
|
||||
logger.Warn("Static gateway configuration ignored; using discovery registry only")
|
||||
}
|
||||
if registry == nil {
|
||||
if logger != nil {
|
||||
logger.Warn("Discovery registry unavailable; gateway routing disabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
|
||||
}
|
||||
|
||||
func buildRailGateways(chainClient chainclient.Client, paymentGatewayClient chainclient.Client, src []gatewayInstanceConfig) map[string]rail.RailGateway {
|
||||
@@ -76,6 +85,8 @@ func buildRailGateways(chainClient chainclient.Client, paymentGatewayClient chai
|
||||
CanReadBalance: inst.Capabilities.CanReadBalance,
|
||||
CanSendFee: inst.Capabilities.CanSendFee,
|
||||
RequiresObserveConfirm: inst.Capabilities.RequiresObserveConfirm,
|
||||
CanBlock: inst.Capabilities.CanBlock,
|
||||
CanRelease: inst.Capabilities.CanRelease,
|
||||
},
|
||||
}
|
||||
switch inst.Rail {
|
||||
@@ -135,6 +146,8 @@ func buildGatewayInstances(logger mlogger.Logger, src []gatewayInstanceConfig) [
|
||||
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),
|
||||
|
||||
@@ -155,22 +155,8 @@ func (i *Imp) initOracleClient(cfg clientConfig) oracleclient.Client {
|
||||
}
|
||||
|
||||
func (i *Imp) closeClients() {
|
||||
if i.ledgerClient != nil {
|
||||
_ = i.ledgerClient.Close()
|
||||
}
|
||||
if i.gatewayClient != nil {
|
||||
_ = i.gatewayClient.Close()
|
||||
}
|
||||
if i.paymentGatewayClient != nil {
|
||||
_ = i.paymentGatewayClient.Close()
|
||||
}
|
||||
if i.mntxClient != nil {
|
||||
_ = i.mntxClient.Close()
|
||||
}
|
||||
if i.oracleClient != nil {
|
||||
_ = i.oracleClient.Close()
|
||||
}
|
||||
if i.feesConn != nil {
|
||||
_ = i.feesConn.Close()
|
||||
if i.discoveryClients != nil {
|
||||
i.discoveryClients.Close()
|
||||
i.discoveryClients = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ type gatewayCapabilitiesConfig struct {
|
||||
CanReadBalance bool `yaml:"can_read_balance"`
|
||||
CanSendFee bool `yaml:"can_send_fee"`
|
||||
RequiresObserveConfirm bool `yaml:"requires_observe_confirm"`
|
||||
CanBlock bool `yaml:"can_block"`
|
||||
CanRelease bool `yaml:"can_release"`
|
||||
}
|
||||
|
||||
type limitsConfig struct {
|
||||
|
||||
@@ -2,7 +2,6 @@ package serverimp
|
||||
|
||||
import (
|
||||
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"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||
@@ -10,47 +9,28 @@ import (
|
||||
)
|
||||
|
||||
type orchestratorDeps struct {
|
||||
feesClient feesv1.FeeEngineClient
|
||||
ledgerClient ledgerclient.Client
|
||||
gatewayClient chainclient.Client
|
||||
paymentGatewayClient chainclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
feesClient feesv1.FeeEngineClient
|
||||
ledgerClient ledgerclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
gatewayInvokeResolver orchestrator.GatewayInvokeResolver
|
||||
}
|
||||
|
||||
func (i *Imp) initDependencies(cfg *config) *orchestratorDeps {
|
||||
deps := &orchestratorDeps{}
|
||||
if cfg == nil {
|
||||
if i.discoveryReg == nil {
|
||||
if i.logger != nil {
|
||||
i.logger.Warn("Discovery registry unavailable; downstream clients disabled")
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
deps.feesClient, i.feesConn = i.initFeesClient(cfg.Fees)
|
||||
|
||||
deps.ledgerClient = i.initLedgerClient(cfg.Ledger)
|
||||
if deps.ledgerClient != nil {
|
||||
i.ledgerClient = deps.ledgerClient
|
||||
}
|
||||
|
||||
deps.gatewayClient = i.initGatewayClient(cfg.Gateway)
|
||||
if deps.gatewayClient != nil {
|
||||
i.gatewayClient = deps.gatewayClient
|
||||
}
|
||||
|
||||
deps.paymentGatewayClient = i.initPaymentGatewayClient(cfg.PaymentGateway)
|
||||
if deps.paymentGatewayClient != nil {
|
||||
i.paymentGatewayClient = deps.paymentGatewayClient
|
||||
}
|
||||
|
||||
deps.mntxClient = i.initMntxClient(cfg.Mntx)
|
||||
if deps.mntxClient != nil {
|
||||
i.mntxClient = deps.mntxClient
|
||||
}
|
||||
|
||||
deps.oracleClient = i.initOracleClient(cfg.Oracle)
|
||||
if deps.oracleClient != nil {
|
||||
i.oracleClient = deps.oracleClient
|
||||
}
|
||||
|
||||
i.discoveryClients = newDiscoveryClientResolver(i.logger, i.discoveryReg)
|
||||
deps.feesClient = &discoveryFeeClient{resolver: i.discoveryClients}
|
||||
deps.ledgerClient = &discoveryLedgerClient{resolver: i.discoveryClients}
|
||||
deps.oracleClient = &discoveryOracleClient{resolver: i.discoveryClients}
|
||||
deps.mntxClient = &discoveryMntxClient{resolver: i.discoveryClients}
|
||||
deps.gatewayInvokeResolver = discoveryGatewayInvokeResolver{resolver: i.discoveryClients}
|
||||
return deps
|
||||
}
|
||||
|
||||
@@ -65,21 +45,15 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
|
||||
if deps.ledgerClient != nil {
|
||||
opts = append(opts, orchestrator.WithLedgerClient(deps.ledgerClient))
|
||||
}
|
||||
if deps.gatewayClient != nil {
|
||||
opts = append(opts, orchestrator.WithChainGatewayClient(deps.gatewayClient))
|
||||
}
|
||||
if deps.paymentGatewayClient != nil {
|
||||
opts = append(opts, orchestrator.WithProviderSettlementGatewayClient(deps.paymentGatewayClient))
|
||||
}
|
||||
if railGateways := buildRailGateways(deps.gatewayClient, deps.paymentGatewayClient, cfg.GatewayInstances); len(railGateways) > 0 {
|
||||
opts = append(opts, orchestrator.WithRailGateways(railGateways))
|
||||
}
|
||||
if deps.mntxClient != nil {
|
||||
opts = append(opts, orchestrator.WithMntxGateway(deps.mntxClient))
|
||||
}
|
||||
if deps.oracleClient != nil {
|
||||
opts = append(opts, orchestrator.WithOracleClient(deps.oracleClient))
|
||||
}
|
||||
if deps.gatewayInvokeResolver != nil {
|
||||
opts = append(opts, orchestrator.WithGatewayInvokeResolver(deps.gatewayInvokeResolver))
|
||||
}
|
||||
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
|
||||
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
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"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
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"
|
||||
)
|
||||
|
||||
const discoveryLogThrottle = 30 * time.Second
|
||||
|
||||
var (
|
||||
feesServiceNames = []string{"BILLING_FEES", string(mservice.FeePlans)}
|
||||
ledgerServiceNames = []string{"LEDGER", string(mservice.Ledger)}
|
||||
oracleServiceNames = []string{"FX_ORACLE", string(mservice.FXOracle)}
|
||||
mntxServiceNames = []string{"CARD_PAYOUT_RAIL_GATEWAY", string(mservice.MntxGateway)}
|
||||
)
|
||||
|
||||
type discoveryEndpoint struct {
|
||||
address string
|
||||
insecure bool
|
||||
raw string
|
||||
}
|
||||
|
||||
func (e discoveryEndpoint) key() string {
|
||||
return fmt.Sprintf("%s|%t", e.address, e.insecure)
|
||||
}
|
||||
|
||||
type discoveryClientResolver struct {
|
||||
logger mlogger.Logger
|
||||
registry *discovery.Registry
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
feesConn *grpc.ClientConn
|
||||
feesEndpoint discoveryEndpoint
|
||||
|
||||
ledgerClient ledgerclient.Client
|
||||
ledgerEndpoint discoveryEndpoint
|
||||
|
||||
oracleClient oracleclient.Client
|
||||
oracleEndpoint discoveryEndpoint
|
||||
|
||||
mntxClient mntxclient.Client
|
||||
mntxEndpoint discoveryEndpoint
|
||||
|
||||
chainClients map[string]chainclient.Client
|
||||
|
||||
lastSelection map[string]string
|
||||
lastMissing map[string]time.Time
|
||||
}
|
||||
|
||||
func newDiscoveryClientResolver(logger mlogger.Logger, registry *discovery.Registry) *discoveryClientResolver {
|
||||
if logger != nil {
|
||||
logger = logger.Named("discovery_clients")
|
||||
}
|
||||
return &discoveryClientResolver{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
chainClients: map[string]chainclient.Client{},
|
||||
lastSelection: map[string]string{},
|
||||
lastMissing: map[string]time.Time{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) Close() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.feesConn != nil {
|
||||
_ = r.feesConn.Close()
|
||||
r.feesConn = nil
|
||||
}
|
||||
if r.ledgerClient != nil {
|
||||
_ = r.ledgerClient.Close()
|
||||
r.ledgerClient = nil
|
||||
}
|
||||
if r.oracleClient != nil {
|
||||
_ = r.oracleClient.Close()
|
||||
r.oracleClient = nil
|
||||
}
|
||||
if r.mntxClient != nil {
|
||||
_ = r.mntxClient.Close()
|
||||
r.mntxClient = nil
|
||||
}
|
||||
for key, client := range r.chainClients {
|
||||
if client != nil {
|
||||
_ = client.Close()
|
||||
}
|
||||
delete(r.chainClients, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) FeesAvailable() bool {
|
||||
_, ok := r.findEntry("fees", feesServiceNames, "", "")
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) LedgerAvailable() bool {
|
||||
_, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) OracleAvailable() bool {
|
||||
_, ok := r.findEntry("oracle", oracleServiceNames, "", "")
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) MntxAvailable() bool {
|
||||
_, ok := r.findEntry("mntx", mntxServiceNames, "", "")
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEngineClient, error) {
|
||||
entry, ok := r.findEntry("fees", feesServiceNames, "", "")
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: fees service unavailable")
|
||||
}
|
||||
endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
|
||||
if err != nil {
|
||||
r.logMissing("fees", "invalid fees invoke uri", entry.InvokeURI, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.feesConn == nil || r.feesEndpoint.key() != endpoint.key() || r.feesEndpoint.address != endpoint.address {
|
||||
if r.feesConn != nil {
|
||||
_ = r.feesConn.Close()
|
||||
r.feesConn = nil
|
||||
}
|
||||
conn, dialErr := dialGrpc(ctx, endpoint)
|
||||
if dialErr != nil {
|
||||
r.logMissing("fees", "failed to dial fees service", endpoint.raw, dialErr)
|
||||
return nil, dialErr
|
||||
}
|
||||
r.feesConn = conn
|
||||
r.feesEndpoint = endpoint
|
||||
}
|
||||
|
||||
return feesv1.NewFeeEngineClient(r.feesConn), nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclient.Client, error) {
|
||||
entry, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: ledger service unavailable")
|
||||
}
|
||||
endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
|
||||
if err != nil {
|
||||
r.logMissing("ledger", "invalid ledger invoke uri", entry.InvokeURI, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.ledgerClient == nil || r.ledgerEndpoint.key() != endpoint.key() || r.ledgerEndpoint.address != endpoint.address {
|
||||
if r.ledgerClient != nil {
|
||||
_ = r.ledgerClient.Close()
|
||||
r.ledgerClient = nil
|
||||
}
|
||||
client, dialErr := ledgerclient.New(ctx, ledgerclient.Config{
|
||||
Address: endpoint.address,
|
||||
Insecure: endpoint.insecure,
|
||||
})
|
||||
if dialErr != nil {
|
||||
r.logMissing("ledger", "failed to dial ledger service", endpoint.raw, dialErr)
|
||||
return nil, dialErr
|
||||
}
|
||||
r.ledgerClient = client
|
||||
r.ledgerEndpoint = endpoint
|
||||
}
|
||||
|
||||
return r.ledgerClient, nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclient.Client, error) {
|
||||
entry, ok := r.findEntry("oracle", oracleServiceNames, "", "")
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: oracle service unavailable")
|
||||
}
|
||||
endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
|
||||
if err != nil {
|
||||
r.logMissing("oracle", "invalid oracle invoke uri", entry.InvokeURI, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.oracleClient == nil || r.oracleEndpoint.key() != endpoint.key() || r.oracleEndpoint.address != endpoint.address {
|
||||
if r.oracleClient != nil {
|
||||
_ = r.oracleClient.Close()
|
||||
r.oracleClient = nil
|
||||
}
|
||||
client, dialErr := oracleclient.New(ctx, oracleclient.Config{
|
||||
Address: endpoint.address,
|
||||
Insecure: endpoint.insecure,
|
||||
})
|
||||
if dialErr != nil {
|
||||
r.logMissing("oracle", "failed to dial oracle service", endpoint.raw, dialErr)
|
||||
return nil, dialErr
|
||||
}
|
||||
r.oracleClient = client
|
||||
r.oracleEndpoint = endpoint
|
||||
}
|
||||
|
||||
return r.oracleClient, nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) MntxClient(ctx context.Context) (mntxclient.Client, error) {
|
||||
entry, ok := r.findEntry("mntx", mntxServiceNames, "", "")
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: mntx service unavailable")
|
||||
}
|
||||
endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
|
||||
if err != nil {
|
||||
r.logMissing("mntx", "invalid mntx invoke uri", entry.InvokeURI, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.mntxClient == nil || r.mntxEndpoint.key() != endpoint.key() || r.mntxEndpoint.address != endpoint.address {
|
||||
if r.mntxClient != nil {
|
||||
_ = r.mntxClient.Close()
|
||||
r.mntxClient = nil
|
||||
}
|
||||
if !endpoint.insecure && r.logger != nil {
|
||||
r.logger.Warn("Mntx gateway does not support TLS, falling back to insecure transport",
|
||||
zap.String("invoke_uri", endpoint.raw))
|
||||
}
|
||||
client, dialErr := mntxclient.New(ctx, mntxclient.Config{
|
||||
Address: endpoint.address,
|
||||
})
|
||||
if dialErr != nil {
|
||||
r.logMissing("mntx", "failed to dial mntx service", endpoint.raw, dialErr)
|
||||
return nil, dialErr
|
||||
}
|
||||
r.mntxClient = client
|
||||
r.mntxEndpoint = endpoint
|
||||
}
|
||||
|
||||
return r.mntxClient, nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) ChainClient(ctx context.Context, invokeURI string) (chainclient.Client, error) {
|
||||
endpoint, err := parseDiscoveryEndpoint(invokeURI)
|
||||
if err != nil {
|
||||
r.logMissing("chain", "invalid chain gateway invoke uri", invokeURI, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if client, ok := r.chainClients[endpoint.key()]; ok && client != nil {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client, dialErr := chainclient.New(ctx, chainclient.Config{
|
||||
Address: endpoint.address,
|
||||
Insecure: endpoint.insecure,
|
||||
})
|
||||
if dialErr != nil {
|
||||
r.logMissing("chain", "failed to dial chain gateway", endpoint.raw, dialErr)
|
||||
return nil, dialErr
|
||||
}
|
||||
r.chainClients[endpoint.key()] = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) PaymentGatewayClient(ctx context.Context, invokeURI string) (chainclient.Client, error) {
|
||||
endpoint, err := parseDiscoveryEndpoint(invokeURI)
|
||||
if err != nil {
|
||||
r.logMissing("payment_gateway", "invalid payment gateway invoke uri", invokeURI, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if client, ok := r.chainClients[endpoint.key()]; ok && client != nil {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client, dialErr := chainclient.New(ctx, chainclient.Config{
|
||||
Address: endpoint.address,
|
||||
Insecure: endpoint.insecure,
|
||||
})
|
||||
if dialErr != nil {
|
||||
r.logMissing("payment_gateway", "failed to dial payment gateway", endpoint.raw, dialErr)
|
||||
return nil, dialErr
|
||||
}
|
||||
r.chainClients[endpoint.key()] = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) findEntry(key string, services []string, rail string, network string) (*discovery.RegistryEntry, bool) {
|
||||
if r == nil || r.registry == nil {
|
||||
r.logMissing(key, "discovery registry unavailable", "", nil)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
entries := r.registry.List(time.Now(), true)
|
||||
matches := make([]discovery.RegistryEntry, 0)
|
||||
for _, entry := range entries {
|
||||
if !matchesService(entry.Service, services) {
|
||||
continue
|
||||
}
|
||||
if rail != "" && !strings.EqualFold(strings.TrimSpace(entry.Rail), rail) {
|
||||
continue
|
||||
}
|
||||
if network != "" && !strings.EqualFold(strings.TrimSpace(entry.Network), network) {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, entry)
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
r.logMissing(key, "discovery entry missing", "", nil)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
if matches[i].RoutingPriority != matches[j].RoutingPriority {
|
||||
return matches[i].RoutingPriority > matches[j].RoutingPriority
|
||||
}
|
||||
if matches[i].ID != matches[j].ID {
|
||||
return matches[i].ID < matches[j].ID
|
||||
}
|
||||
return matches[i].InstanceID < matches[j].InstanceID
|
||||
})
|
||||
|
||||
entry := matches[0]
|
||||
entryKey := discoveryEntryKey(entry)
|
||||
r.logSelection(key, entryKey, entry)
|
||||
return &entry, true
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) logSelection(key, entryKey string, entry discovery.RegistryEntry) {
|
||||
if r.logger == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
last := r.lastSelection[key]
|
||||
if last == entryKey {
|
||||
r.mu.Unlock()
|
||||
return
|
||||
}
|
||||
r.lastSelection[key] = entryKey
|
||||
r.mu.Unlock()
|
||||
r.logger.Info("Discovery endpoint selected",
|
||||
zap.String("service_key", key),
|
||||
zap.String("service", entry.Service),
|
||||
zap.String("rail", entry.Rail),
|
||||
zap.String("network", entry.Network),
|
||||
zap.String("entry_id", entry.ID),
|
||||
zap.String("instance_id", entry.InstanceID),
|
||||
zap.String("invoke_uri", entry.InvokeURI))
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) logMissing(key, message, invokeURI string, err error) {
|
||||
if r.logger == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
r.mu.Lock()
|
||||
last := r.lastMissing[key]
|
||||
if !last.IsZero() && now.Sub(last) < discoveryLogThrottle {
|
||||
r.mu.Unlock()
|
||||
return
|
||||
}
|
||||
r.lastMissing[key] = now
|
||||
r.mu.Unlock()
|
||||
fields := []zap.Field{zap.String("service_key", key)}
|
||||
if invokeURI != "" {
|
||||
fields = append(fields, zap.String("invoke_uri", strings.TrimSpace(invokeURI)))
|
||||
}
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
}
|
||||
r.logger.Warn(message, fields...)
|
||||
}
|
||||
|
||||
func discoveryEntryKey(entry discovery.RegistryEntry) string {
|
||||
return fmt.Sprintf("%s|%s|%s|%s|%s|%s",
|
||||
strings.TrimSpace(entry.Service),
|
||||
strings.TrimSpace(entry.ID),
|
||||
strings.TrimSpace(entry.InstanceID),
|
||||
strings.TrimSpace(entry.Rail),
|
||||
strings.TrimSpace(entry.Network),
|
||||
strings.TrimSpace(entry.InvokeURI))
|
||||
}
|
||||
|
||||
func matchesService(service string, candidates []string) bool {
|
||||
service = strings.TrimSpace(service)
|
||||
if service == "" || len(candidates) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if strings.EqualFold(service, strings.TrimSpace(candidate)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseDiscoveryEndpoint(raw string) (discoveryEndpoint, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return discoveryEndpoint{}, merrors.InvalidArgument("discovery: invoke uri is required")
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Scheme == "" {
|
||||
if _, _, splitErr := net.SplitHostPort(raw); splitErr != nil {
|
||||
if err != nil {
|
||||
return discoveryEndpoint{}, err
|
||||
}
|
||||
return discoveryEndpoint{}, merrors.InvalidArgument("discovery: invoke uri must include host:port")
|
||||
}
|
||||
return discoveryEndpoint{address: raw, insecure: true, raw: raw}, nil
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
|
||||
switch scheme {
|
||||
case "grpc":
|
||||
address := strings.TrimSpace(parsed.Host)
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
return discoveryEndpoint{}, merrors.InvalidArgument("discovery: invoke uri must include host:port")
|
||||
}
|
||||
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{}, merrors.InvalidArgument("discovery: invoke uri must include host:port")
|
||||
}
|
||||
return discoveryEndpoint{address: address, insecure: false, raw: raw}, nil
|
||||
case "dns", "passthrough":
|
||||
return discoveryEndpoint{address: raw, insecure: true, raw: raw}, nil
|
||||
default:
|
||||
return discoveryEndpoint{}, merrors.InvalidArgument("discovery: unsupported invoke uri scheme")
|
||||
}
|
||||
}
|
||||
|
||||
func dialGrpc(ctx context.Context, endpoint discoveryEndpoint) (*grpc.ClientConn, error) {
|
||||
dialOpts := []grpc.DialOption{}
|
||||
if endpoint.insecure {
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
} else {
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(nil)))
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return grpc.DialContext(ctx, endpoint.address, dialOpts...)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
type discoveryGatewayInvokeResolver struct {
|
||||
resolver *discoveryClientResolver
|
||||
}
|
||||
|
||||
func (r discoveryGatewayInvokeResolver) Resolve(ctx context.Context, invokeURI string) (chainclient.Client, error) {
|
||||
if r.resolver == nil {
|
||||
return nil, merrors.Internal("discovery gateway resolver unavailable")
|
||||
}
|
||||
return r.resolver.ChainClient(ctx, invokeURI)
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
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"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/payments/rail"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type discoveryFeeClient struct {
|
||||
resolver *discoveryClientResolver
|
||||
}
|
||||
|
||||
func (c *discoveryFeeClient) Available() bool {
|
||||
if c == nil || c.resolver == nil {
|
||||
return false
|
||||
}
|
||||
return c.resolver.FeesAvailable()
|
||||
}
|
||||
|
||||
func (c *discoveryFeeClient) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest, opts ...grpc.CallOption) (*feesv1.QuoteFeesResponse, error) {
|
||||
client, err := c.resolver.FeesClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.QuoteFees(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (c *discoveryFeeClient) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFeesRequest, opts ...grpc.CallOption) (*feesv1.PrecomputeFeesResponse, error) {
|
||||
client, err := c.resolver.FeesClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PrecomputeFees(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (c *discoveryFeeClient) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeTokenRequest, opts ...grpc.CallOption) (*feesv1.ValidateFeeTokenResponse, error) {
|
||||
client, err := c.resolver.FeesClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ValidateFeeToken(ctx, req, opts...)
|
||||
}
|
||||
|
||||
type discoveryLedgerClient struct {
|
||||
resolver *discoveryClientResolver
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) Available() bool {
|
||||
if c == nil || c.resolver == nil {
|
||||
return false
|
||||
}
|
||||
return c.resolver.LedgerAvailable()
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) ReadBalance(ctx context.Context, accountID string) (*moneyv1.Money, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ReadBalance(ctx, accountID)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) CreateTransaction(ctx context.Context, tx rail.LedgerTx) (string, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return client.CreateTransaction(ctx, tx)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.TransferInternal(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) HoldBalance(ctx context.Context, accountID string, amount string) error {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.HoldBalance(ctx, accountID, amount)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.CreateAccount(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ListAccounts(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PostCreditWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PostDebitWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ApplyFXWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetBalance(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetJournalEntry(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetStatement(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) Close() error {
|
||||
if c == nil || c.resolver == nil {
|
||||
return nil
|
||||
}
|
||||
client, err := c.resolver.LedgerClient(context.Background())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
type discoveryOracleClient struct {
|
||||
resolver *discoveryClientResolver
|
||||
}
|
||||
|
||||
func (c *discoveryOracleClient) Available() bool {
|
||||
if c == nil || c.resolver == nil {
|
||||
return false
|
||||
}
|
||||
return c.resolver.OracleAvailable()
|
||||
}
|
||||
|
||||
func (c *discoveryOracleClient) LatestRate(ctx context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) {
|
||||
client, err := c.resolver.OracleClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.LatestRate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryOracleClient) GetQuote(ctx context.Context, req oracleclient.GetQuoteParams) (*oracleclient.Quote, error) {
|
||||
client, err := c.resolver.OracleClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetQuote(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryOracleClient) Close() error {
|
||||
if c == nil || c.resolver == nil {
|
||||
return nil
|
||||
}
|
||||
client, err := c.resolver.OracleClient(context.Background())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
type discoveryMntxClient struct {
|
||||
resolver *discoveryClientResolver
|
||||
}
|
||||
|
||||
func (c *discoveryMntxClient) Available() bool {
|
||||
if c == nil || c.resolver == nil {
|
||||
return false
|
||||
}
|
||||
return c.resolver.MntxAvailable()
|
||||
}
|
||||
|
||||
func (c *discoveryMntxClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||
client, err := c.resolver.MntxClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.CreateCardPayout(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryMntxClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
||||
client, err := c.resolver.MntxClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.CreateCardTokenPayout(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryMntxClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
||||
client, err := c.resolver.MntxClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetCardPayoutStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryMntxClient) ListGatewayInstances(ctx context.Context, req *mntxv1.ListGatewayInstancesRequest) (*mntxv1.ListGatewayInstancesResponse, error) {
|
||||
client, err := c.resolver.MntxClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ListGatewayInstances(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryMntxClient) Close() error {
|
||||
if c == nil || c.resolver == nil {
|
||||
return nil
|
||||
}
|
||||
client, err := c.resolver.MntxClient(context.Background())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
type discoveryChainClient struct {
|
||||
resolver *discoveryClientResolver
|
||||
invokeURI string
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) Available() bool {
|
||||
return c != nil && c.resolver != nil && c.invokeURI != ""
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) client(ctx context.Context) (chainclient.Client, error) {
|
||||
if c == nil || c.resolver == nil {
|
||||
return nil, merrors.Internal("discovery chain client unavailable")
|
||||
}
|
||||
return c.resolver.ChainClient(ctx, c.invokeURI)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.CreateManagedWallet(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetManagedWallet(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ListManagedWallets(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetWalletBalance(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.SubmitTransfer(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.GetTransfer(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ListTransfers(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.EstimateTransferFee(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ComputeGasTopUp(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
|
||||
client, err := c.client(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.EnsureGasTopUp(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryChainClient) Close() error {
|
||||
if c == nil || c.resolver == nil {
|
||||
return nil
|
||||
}
|
||||
client, err := c.resolver.ChainClient(context.Background(), c.invokeURI)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return client.Close()
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
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"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||
"github.com/tech/sendico/payments/orchestrator/storage"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type Imp struct {
|
||||
@@ -23,11 +18,6 @@ type Imp struct {
|
||||
discoveryWatcher *discovery.RegistryWatcher
|
||||
discoveryReg *discovery.Registry
|
||||
discoveryAnnouncer *discovery.Announcer
|
||||
discoveryClients *discoveryClientResolver
|
||||
service *orchestrator.Service
|
||||
feesConn *grpc.ClientConn
|
||||
ledgerClient ledgerclient.Client
|
||||
gatewayClient chainclient.Client
|
||||
paymentGatewayClient chainclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user