377 lines
12 KiB
Go
377 lines
12 KiB
Go
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
|
|
service *gatewayservice.Service
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
if i.service != nil {
|
|
i.service.Shutdown()
|
|
}
|
|
|
|
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),
|
|
}
|
|
svc := gatewayservice.NewService(logger, repo, producer, opts...)
|
|
i.service = svc
|
|
return svc, 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
|
|
}
|
|
}
|