new tron gateway
This commit is contained in:
431
api/gateway/tron/internal/server/internal/serverimp.go
Normal file
431
api/gateway/tron/internal/server/internal/serverimp.go
Normal file
@@ -0,0 +1,431 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/gateway/tron/internal/keymanager"
|
||||
vaultmanager "github.com/tech/sendico/gateway/tron/internal/keymanager/vault"
|
||||
gatewayservice "github.com/tech/sendico/gateway/tron/internal/service/gateway"
|
||||
"github.com/tech/sendico/gateway/tron/internal/service/gateway/drivers"
|
||||
"github.com/tech/sendico/gateway/tron/internal/service/gateway/rpcclient"
|
||||
gatewayshared "github.com/tech/sendico/gateway/tron/internal/service/gateway/shared"
|
||||
"github.com/tech/sendico/gateway/tron/internal/service/gateway/tronclient"
|
||||
"github.com/tech/sendico/gateway/tron/storage"
|
||||
gatewaymongo "github.com/tech/sendico/gateway/tron/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"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"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
|
||||
tronClients *tronclient.Registry
|
||||
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"`
|
||||
GRPCURLEnv string `yaml:"grpc_url_env"` // Native TRON gRPC endpoint
|
||||
GRPCTokenEnv string `yaml:"grpc_token_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()
|
||||
}
|
||||
if i.tronClients != nil {
|
||||
i.tronClients.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
|
||||
}
|
||||
|
||||
// Prepare native TRON gRPC clients (optional - fallback to EVM if not configured)
|
||||
tronClients, err := tronclient.Prepare(context.Background(), i.logger.Named("tron"), networkConfigs)
|
||||
if err != nil {
|
||||
i.logger.Warn("TRON gRPC clients not available, falling back to EVM", zap.Error(err))
|
||||
// Continue without TRON clients - will fallback to EVM
|
||||
} else {
|
||||
i.tronClients = tronClients
|
||||
}
|
||||
|
||||
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
|
||||
invokeURI := ""
|
||||
if cfg.GRPC != nil {
|
||||
invokeURI = cfg.GRPC.DiscoveryInvokeURI()
|
||||
}
|
||||
opts := []gatewayservice.Option{
|
||||
gatewayservice.WithDiscoveryInvokeURI(invokeURI),
|
||||
gatewayservice.WithNetworks(networkConfigs),
|
||||
gatewayservice.WithServiceWallet(walletConfig),
|
||||
gatewayservice.WithKeyManager(keyManager),
|
||||
gatewayservice.WithRPCClients(rpcClients),
|
||||
gatewayservice.WithTronClients(i.tronClients),
|
||||
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: ":50071",
|
||||
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
|
||||
}
|
||||
network, ok := pmodel.ParseChainNetwork(chain.Name)
|
||||
if !ok {
|
||||
logger.Error("Unknown chain network", zap.String("chain", chain.Name))
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("unknown chain network: %s", chain.Name))
|
||||
}
|
||||
if !network.IsValid() {
|
||||
logger.Error("Invalid chain network", zap.String("chain", chain.Name))
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("invalid chain network: %s", chain.Name))
|
||||
}
|
||||
rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv))
|
||||
if rpcURL == "" {
|
||||
logger.Error("RPC url not configured", zap.String("chain", network.String()), zap.String("env", chain.RPCURLEnv))
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("chain RPC endpoint not configured (chain=%s env=%s)", network, 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", network.String()))
|
||||
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", network.String()))
|
||||
} else {
|
||||
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("chain", network.String()))
|
||||
}
|
||||
continue
|
||||
}
|
||||
contracts = append(contracts, gatewayshared.TokenContract{
|
||||
Symbol: symbol,
|
||||
ContractAddress: addr,
|
||||
})
|
||||
}
|
||||
|
||||
gasPolicy, err := buildGasTopUpPolicy(network.String(), chain.GasTopUpPolicy)
|
||||
if err != nil {
|
||||
logger.Error("Invalid gas top-up policy", zap.String("chain", network.String()), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve optional TRON gRPC URL
|
||||
grpcURL := ""
|
||||
if grpcEnv := strings.TrimSpace(chain.GRPCURLEnv); grpcEnv != "" {
|
||||
grpcURL = strings.TrimSpace(os.Getenv(grpcEnv))
|
||||
if grpcURL != "" {
|
||||
logger.Info("TRON gRPC URL configured", zap.String("chain", network.String()), zap.String("env", grpcEnv))
|
||||
}
|
||||
}
|
||||
grpcToken := ""
|
||||
if grpcTokenEnv := strings.TrimSpace(chain.GRPCTokenEnv); grpcTokenEnv != "" {
|
||||
grpcToken = strings.TrimSpace(os.Getenv(grpcTokenEnv))
|
||||
if grpcToken != "" {
|
||||
logger.Info("TRON gRPC token configured", zap.String("chain", network.String()), zap.String("env", grpcTokenEnv))
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, gatewayshared.Network{
|
||||
Name: network,
|
||||
RPCURL: rpcURL,
|
||||
GRPCUrl: grpcURL,
|
||||
GRPCToken: grpcToken,
|
||||
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))
|
||||
|
||||
network, ok := pmodel.ParseChainNetwork(cfg.Chain)
|
||||
if !ok {
|
||||
logger.Warn("Unknown service wallet chain network", zap.String("chain", cfg.Chain))
|
||||
}
|
||||
|
||||
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", network.String()))
|
||||
}
|
||||
}
|
||||
if privateKey == "" {
|
||||
logger.Warn("Service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
|
||||
}
|
||||
|
||||
return gatewayshared.ServiceWallet{
|
||||
Network: network,
|
||||
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
|
||||
}
|
||||
}
|
||||
12
api/gateway/tron/internal/server/server.go
Normal file
12
api/gateway/tron/internal/server/server.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
serverimp "github.com/tech/sendico/gateway/tron/internal/server/internal"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/server"
|
||||
)
|
||||
|
||||
// Create constructs the chain gateway server implementation.
|
||||
func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
|
||||
return serverimp.Create(logger, file, debug)
|
||||
}
|
||||
Reference in New Issue
Block a user