new tron gateway

This commit is contained in:
Stephan D
2026-01-30 15:51:28 +01:00
parent 51f5b0804a
commit 8788ff67ec
77 changed files with 11050 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
package tronclient
import (
"context"
"crypto/tls"
"encoding/hex"
"fmt"
"math/big"
"net/url"
"strings"
"time"
"github.com/fbsobreira/gotron-sdk/pkg/client"
"github.com/fbsobreira/gotron-sdk/pkg/proto/api"
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
"github.com/tech/sendico/pkg/merrors"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
// Client wraps the gotron-sdk gRPC client with convenience methods.
type Client struct {
grpc *client.GrpcClient
timeout time.Duration
}
// NewClient creates a new TRON gRPC client connected to the given endpoint.
func NewClient(grpcURL string, timeout time.Duration, authToken string) (*Client, error) {
if grpcURL == "" {
return nil, merrors.InvalidArgument("tronclient: grpc url is required")
}
if timeout <= 0 {
timeout = 30 * time.Second
}
address, useTLS, err := normalizeGRPCAddress(grpcURL)
if err != nil {
return nil, err
}
grpcClient := client.NewGrpcClientWithTimeout(address, timeout)
var transportCreds grpc.DialOption
if useTLS {
transportCreds = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}))
} else {
transportCreds = grpc.WithTransportCredentials(insecure.NewCredentials())
}
opts := []grpc.DialOption{transportCreds}
if token := strings.TrimSpace(authToken); token != "" {
opts = append(opts,
grpc.WithUnaryInterceptor(grpcTokenUnaryInterceptor(token)),
grpc.WithStreamInterceptor(grpcTokenStreamInterceptor(token)),
)
}
if err := grpcClient.Start(opts...); err != nil {
return nil, merrors.Internal(fmt.Sprintf("tronclient: failed to connect to %s: %v", grpcURL, err))
}
return &Client{
grpc: grpcClient,
timeout: timeout,
}, nil
}
func normalizeGRPCAddress(grpcURL string) (string, bool, error) {
target := strings.TrimSpace(grpcURL)
useTLS := false
if target == "" {
return "", false, merrors.InvalidArgument("tronclient: grpc url is required")
}
if strings.Contains(target, "://") {
u, err := url.Parse(target)
if err != nil {
return "", false, merrors.InvalidArgument("tronclient: invalid grpc url")
}
if u.Scheme == "https" || u.Scheme == "grpcs" {
useTLS = true
}
host := strings.TrimSpace(u.Host)
if host == "" {
return "", false, merrors.InvalidArgument("tronclient: grpc url missing host")
}
if useTLS && u.Port() == "" {
host = host + ":443"
}
return host, useTLS, nil
}
return target, useTLS, nil
}
func grpcTokenUnaryInterceptor(token string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = metadata.AppendToOutgoingContext(ctx, "x-token", token)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
func grpcTokenStreamInterceptor(token string) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
ctx = metadata.AppendToOutgoingContext(ctx, "x-token", token)
return streamer(ctx, desc, cc, method, opts...)
}
}
// Close closes the gRPC connection.
func (c *Client) Close() {
if c != nil && c.grpc != nil {
c.grpc.Stop()
}
}
// SetAPIKey configures the TRON-PRO-API-KEY for TronGrid requests.
func (c *Client) SetAPIKey(apiKey string) {
if c != nil && c.grpc != nil {
c.grpc.SetAPIKey(apiKey)
}
}
// Transfer creates a native TRX transfer transaction.
// Addresses should be in base58 format.
// Amount is in SUN (1 TRX = 1,000,000 SUN).
func (c *Client) Transfer(from, to string, amountSun int64) (*api.TransactionExtention, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
return c.grpc.Transfer(from, to, amountSun)
}
// TRC20Send creates a TRC20 token transfer transaction.
// Addresses should be in base58 format.
// Amount is in the token's smallest unit.
// FeeLimit is in SUN (recommended: 100_000_000 = 100 TRX).
func (c *Client) TRC20Send(from, to, contract string, amount *big.Int, feeLimit int64) (*api.TransactionExtention, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
return c.grpc.TRC20Send(from, to, contract, amount, feeLimit)
}
// Broadcast broadcasts a signed transaction to the network.
func (c *Client) Broadcast(tx *core.Transaction) (*api.Return, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
return c.grpc.Broadcast(tx)
}
// GetTransactionInfoByID retrieves transaction info by its hash.
// The txID should be a hex string (without 0x prefix).
func (c *Client) GetTransactionInfoByID(txID string) (*core.TransactionInfo, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
return c.grpc.GetTransactionInfoByID(txID)
}
// GetTransactionByID retrieves the full transaction by its hash.
func (c *Client) GetTransactionByID(txID string) (*core.Transaction, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
return c.grpc.GetTransactionByID(txID)
}
// TRC20GetDecimals returns the decimals of a TRC20 token.
func (c *Client) TRC20GetDecimals(contract string) (*big.Int, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
return c.grpc.TRC20GetDecimals(contract)
}
// TRC20ContractBalance returns the balance of an address for a TRC20 token.
func (c *Client) TRC20ContractBalance(addr, contract string) (*big.Int, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
return c.grpc.TRC20ContractBalance(addr, contract)
}
// AwaitConfirmation polls for transaction confirmation until ctx is cancelled.
func (c *Client) AwaitConfirmation(ctx context.Context, txID string, pollInterval time.Duration) (*core.TransactionInfo, error) {
if c == nil || c.grpc == nil {
return nil, merrors.Internal("tronclient: client not initialized")
}
if pollInterval <= 0 {
pollInterval = 3 * time.Second
}
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for {
txInfo, err := c.grpc.GetTransactionInfoByID(txID)
if err == nil && txInfo != nil && txInfo.BlockNumber > 0 {
return txInfo, nil
}
select {
case <-ticker.C:
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// TxIDFromExtention extracts the transaction ID hex string from a TransactionExtention.
func TxIDFromExtention(txExt *api.TransactionExtention) string {
if txExt == nil || len(txExt.Txid) == 0 {
return ""
}
return hex.EncodeToString(txExt.Txid)
}

View File

@@ -0,0 +1,127 @@
package tronclient
import (
"context"
"fmt"
"strings"
"time"
"github.com/tech/sendico/gateway/tron/internal/service/gateway/shared"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
// Registry manages TRON gRPC clients keyed by network name.
type Registry struct {
logger mlogger.Logger
clients map[string]*Client
}
// NewRegistry creates an empty registry.
func NewRegistry(logger mlogger.Logger) *Registry {
return &Registry{
logger: logger.Named("tron_registry"),
clients: make(map[string]*Client),
}
}
// Prepare initializes TRON gRPC clients for all networks with a configured GRPCUrl.
// Networks without GRPCUrl are skipped (they will fallback to EVM).
func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Network) (*Registry, error) {
if logger == nil {
return nil, merrors.InvalidArgument("tronclient: logger is required")
}
registry := NewRegistry(logger)
timeout := 30 * time.Second
for _, network := range networks {
name := network.Name.String()
grpcURL := strings.TrimSpace(network.GRPCUrl)
grpcToken := strings.TrimSpace(network.GRPCToken)
if !network.Name.IsValid() {
continue
}
// Skip networks without TRON gRPC URL configured
if grpcURL == "" {
registry.logger.Debug("Skipping network without TRON gRPC URL",
zap.String("network", name),
)
continue
}
registry.logger.Info("Initializing TRON gRPC client",
zap.String("network", name),
zap.String("grpc_url", grpcURL),
)
client, err := NewClient(grpcURL, timeout, grpcToken)
if err != nil {
registry.Close()
registry.logger.Error("Failed to initialize TRON gRPC client",
zap.String("network", name),
zap.Error(err),
)
return nil, merrors.Internal(fmt.Sprintf("tronclient: failed to connect to %s: %v", name, err))
}
registry.clients[name] = client
registry.logger.Info("TRON gRPC client ready",
zap.String("network", name),
)
}
if len(registry.clients) > 0 {
registry.logger.Info("TRON gRPC clients initialized",
zap.Int("count", len(registry.clients)),
)
} else {
registry.logger.Debug("No TRON gRPC clients were initialized (no networks with grpc_url_env)")
}
return registry, nil
}
// Client returns the TRON gRPC client for the given network.
func (r *Registry) Client(networkName string) (*Client, error) {
if r == nil {
return nil, merrors.Internal("tronclient: registry not initialized")
}
name := strings.ToLower(strings.TrimSpace(networkName))
client, ok := r.clients[name]
if !ok || client == nil {
return nil, merrors.InvalidArgument(fmt.Sprintf("tronclient: no client for network %s", name))
}
return client, nil
}
// HasClient checks if a TRON gRPC client is available for the given network.
func (r *Registry) HasClient(networkName string) bool {
if r == nil || len(r.clients) == 0 {
return false
}
name := strings.ToLower(strings.TrimSpace(networkName))
client, ok := r.clients[name]
return ok && client != nil
}
// Close closes all TRON gRPC connections.
func (r *Registry) Close() {
if r == nil {
return
}
for name, client := range r.clients {
if client != nil {
client.Close()
if r.logger != nil {
r.logger.Info("TRON gRPC client closed", zap.String("network", name))
}
}
}
r.clients = make(map[string]*Client)
}