new tron gateway
This commit is contained in:
219
api/gateway/tron/internal/service/gateway/tronclient/client.go
Normal file
219
api/gateway/tron/internal/service/gateway/tronclient/client.go
Normal 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)
|
||||
}
|
||||
127
api/gateway/tron/internal/service/gateway/tronclient/registry.go
Normal file
127
api/gateway/tron/internal/service/gateway/tronclient/registry.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user