package serverimp import ( "context" "fmt" "os" "strings" "time" "github.com/mitchellh/mapstructure" "github.com/shopspring/decimal" "github.com/tech/sendico/gateway/chain/internal/keymanager" vaultmanager "github.com/tech/sendico/gateway/chain/internal/keymanager/vault" gatewayservice "github.com/tech/sendico/gateway/chain/internal/service/gateway" "github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers" "github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient" gatewayshared "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" "github.com/tech/sendico/gateway/chain/storage" gatewaymongo "github.com/tech/sendico/gateway/chain/storage/mongo" "github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/db" "github.com/tech/sendico/pkg/merrors" msg "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/server/grpcapp" "go.uber.org/zap" "gopkg.in/yaml.v3" ) type Imp struct { logger mlogger.Logger file string debug bool config *config app *grpcapp.App[storage.Repository] rpcClients *rpcclient.Clients } type config struct { *grpcapp.Config `yaml:",inline"` Chains []chainConfig `yaml:"chains"` ServiceWallet serviceWalletConfig `yaml:"service_wallet"` KeyManagement keymanager.Config `yaml:"key_management"` Settings gatewayservice.CacheSettings `yaml:"cache"` } type chainConfig struct { Name string `yaml:"name"` RPCURLEnv string `yaml:"rpc_url_env"` ChainID uint64 `yaml:"chain_id"` NativeToken string `yaml:"native_token"` Tokens []tokenConfig `yaml:"tokens"` GasTopUpPolicy *gasTopUpPolicyConfig `yaml:"gas_topup_policy"` } type serviceWalletConfig struct { Chain string `yaml:"chain"` Address string `yaml:"address"` AddressEnv string `yaml:"address_env"` PrivateKeyEnv string `yaml:"private_key_env"` } type tokenConfig struct { Symbol string `yaml:"symbol"` Contract string `yaml:"contract"` ContractEnv string `yaml:"contract_env"` } type gasTopUpPolicyConfig struct { gasTopUpRuleConfig `yaml:",inline"` Native *gasTopUpRuleConfig `yaml:"native"` Contract *gasTopUpRuleConfig `yaml:"contract"` } type gasTopUpRuleConfig struct { BufferPercent float64 `yaml:"buffer_percent"` MinNativeBalanceTRX float64 `yaml:"min_native_balance_trx"` RoundingUnitTRX float64 `yaml:"rounding_unit_trx"` MaxTopUpTRX float64 `yaml:"max_topup_trx"` } // Create initialises the chain gateway server implementation. func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { return &Imp{ logger: logger.Named("server"), file: file, debug: debug, }, nil } func (i *Imp) Shutdown() { if i.app == nil { return } timeout := 15 * time.Second if i.config != nil && i.config.Runtime != nil { timeout = i.config.Runtime.ShutdownTimeout() } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() i.app.Shutdown(ctx) if i.rpcClients != nil { i.rpcClients.Close() } } func (i *Imp) Start() error { cfg, err := i.loadConfig() if err != nil { return err } i.config = cfg repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { return gatewaymongo.New(logger, conn) } cl := i.logger.Named("config") networkConfigs, err := resolveNetworkConfigs(cl.Named("network"), cfg.Chains) if err != nil { i.logger.Error("Invalid chain network configuration", zap.Error(err)) return err } rpcClients, err := rpcclient.Prepare(context.Background(), i.logger.Named("rpc"), networkConfigs) if err != nil { i.logger.Error("Failed to prepare rpc clients", zap.Error(err)) return err } i.rpcClients = rpcClients walletConfig := resolveServiceWallet(cl.Named("wallet"), cfg.ServiceWallet) keyManager, err := resolveKeyManager(i.logger.Named("key_manager"), cfg.KeyManagement) if err != nil { return err } driverRegistry, err := drivers.NewRegistry(i.logger.Named("drivers"), networkConfigs) if err != nil { return err } serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { opts := []gatewayservice.Option{ gatewayservice.WithNetworks(networkConfigs), gatewayservice.WithServiceWallet(walletConfig), gatewayservice.WithKeyManager(keyManager), gatewayservice.WithRPCClients(rpcClients), gatewayservice.WithDriverRegistry(driverRegistry), gatewayservice.WithSettings(cfg.Settings), } return gatewayservice.NewService(logger, repo, producer, opts...), nil } app, err := grpcapp.NewApp(i.logger, "chain", cfg.Config, i.debug, repoFactory, serviceFactory) if err != nil { return err } i.app = app return i.app.Start() } func (i *Imp) loadConfig() (*config, error) { data, err := os.ReadFile(i.file) if err != nil { i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) return nil, err } cfg := &config{ Config: &grpcapp.Config{}, } if err := yaml.Unmarshal(data, cfg); err != nil { i.logger.Error("Failed to parse configuration", zap.Error(err)) return nil, err } if cfg.Runtime == nil { cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} } if cfg.GRPC == nil { cfg.GRPC = &routers.GRPCConfig{ Network: "tcp", Address: ":50070", EnableReflection: true, EnableHealth: true, } } return cfg, nil } func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatewayshared.Network, error) { result := make([]gatewayshared.Network, 0, len(chains)) for _, chain := range chains { if strings.TrimSpace(chain.Name) == "" { logger.Warn("Skipping unnamed chain configuration") continue } rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv)) if rpcURL == "" { logger.Error("RPC url not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv)) return nil, merrors.InvalidArgument(fmt.Sprintf("chain RPC endpoint not configured (chain=%s env=%s)", chain.Name, chain.RPCURLEnv)) } contracts := make([]gatewayshared.TokenContract, 0, len(chain.Tokens)) for _, token := range chain.Tokens { symbol := strings.TrimSpace(token.Symbol) if symbol == "" { logger.Warn("Skipping token with empty symbol", zap.String("chain", chain.Name)) continue } addr := strings.TrimSpace(token.Contract) env := strings.TrimSpace(token.ContractEnv) if addr == "" && env != "" { addr = strings.TrimSpace(os.Getenv(env)) } if addr == "" { if env != "" { logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", chain.Name)) } else { logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("chain", chain.Name)) } continue } contracts = append(contracts, gatewayshared.TokenContract{ Symbol: symbol, ContractAddress: addr, }) } gasPolicy, err := buildGasTopUpPolicy(chain.Name, chain.GasTopUpPolicy) if err != nil { logger.Error("Invalid gas top-up policy", zap.String("chain", chain.Name), zap.Error(err)) return nil, err } result = append(result, gatewayshared.Network{ Name: chain.Name, RPCURL: rpcURL, ChainID: chain.ChainID, NativeToken: chain.NativeToken, TokenConfigs: contracts, GasTopUpPolicy: gasPolicy, }) } return result, nil } func buildGasTopUpPolicy(chainName string, cfg *gasTopUpPolicyConfig) (*gatewayshared.GasTopUpPolicy, error) { if cfg == nil { return nil, nil } defaultRule, defaultSet, err := parseGasTopUpRule(chainName, "default", cfg.gasTopUpRuleConfig) if err != nil { return nil, err } if !defaultSet { return nil, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy is required", chainName)) } policy := &gatewayshared.GasTopUpPolicy{ Default: defaultRule, } if cfg.Native != nil { rule, set, err := parseGasTopUpRule(chainName, "native", *cfg.Native) if err != nil { return nil, err } if set { policy.Native = &rule } } if cfg.Contract != nil { rule, set, err := parseGasTopUpRule(chainName, "contract", *cfg.Contract) if err != nil { return nil, err } if set { policy.Contract = &rule } } return policy, nil } func parseGasTopUpRule(chainName, label string, cfg gasTopUpRuleConfig) (gatewayshared.GasTopUpRule, bool, error) { if cfg.BufferPercent == 0 && cfg.MinNativeBalanceTRX == 0 && cfg.RoundingUnitTRX == 0 && cfg.MaxTopUpTRX == 0 { return gatewayshared.GasTopUpRule{}, false, nil } if cfg.BufferPercent < 0 { return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s buffer_percent must be >= 0", chainName, label)) } if cfg.MinNativeBalanceTRX < 0 { return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s min_native_balance_trx must be >= 0", chainName, label)) } if cfg.RoundingUnitTRX <= 0 { return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s rounding_unit_trx must be > 0", chainName, label)) } if cfg.MaxTopUpTRX <= 0 { return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s max_topup_trx must be > 0", chainName, label)) } return gatewayshared.GasTopUpRule{ BufferPercent: decimal.NewFromFloat(cfg.BufferPercent), MinNativeBalance: decimal.NewFromFloat(cfg.MinNativeBalanceTRX), RoundingUnit: decimal.NewFromFloat(cfg.RoundingUnitTRX), MaxTopUp: decimal.NewFromFloat(cfg.MaxTopUpTRX), }, true, nil } func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayshared.ServiceWallet { address := strings.TrimSpace(cfg.Address) if address == "" && cfg.AddressEnv != "" { address = strings.TrimSpace(os.Getenv(cfg.AddressEnv)) } privateKey := strings.TrimSpace(os.Getenv(cfg.PrivateKeyEnv)) if address == "" { if cfg.AddressEnv != "" { logger.Warn("Service wallet address not configured", zap.String("env", cfg.AddressEnv)) } else { logger.Warn("Service wallet address not configured", zap.String("chain", cfg.Chain)) } } if privateKey == "" { logger.Warn("Service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv)) } return gatewayshared.ServiceWallet{ Network: cfg.Chain, Address: address, PrivateKey: privateKey, } } func resolveKeyManager(logger mlogger.Logger, cfg keymanager.Config) (keymanager.Manager, error) { driver := strings.ToLower(strings.TrimSpace(string(cfg.Driver))) if driver == "" { err := merrors.InvalidArgument("key management driver is not configured") logger.Error("Key management driver missing") return nil, err } switch keymanager.Driver(driver) { case keymanager.DriverVault: settings := vaultmanager.Config{} if len(cfg.Settings) > 0 { if err := mapstructure.Decode(cfg.Settings, &settings); err != nil { logger.Error("Failed to decode vault key manager settings", zap.Error(err), zap.Any("settings", cfg.Settings)) return nil, merrors.InvalidArgument("invalid vault key manager settings: " + err.Error()) } } manager, err := vaultmanager.New(logger, settings) if err != nil { logger.Error("Failed to initialise vault key manager", zap.Error(err)) return nil, err } return manager, nil default: err := merrors.InvalidArgument("unsupported key management driver: " + driver) logger.Error("Unsupported key management driver", zap.String("driver", driver)) return nil, err } }