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,13 @@
package keymanager
import "github.com/tech/sendico/pkg/model"
// Driver identifies the key management backend implementation.
type Driver string
const (
DriverVault Driver = "vault"
)
// Config represents a configured key manager driver with arbitrary settings.
type Config = model.DriverConfig[Driver]

View File

@@ -0,0 +1,26 @@
package keymanager
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/core/types"
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
)
// ManagedWalletKey captures information returned after provisioning a managed wallet key.
type ManagedWalletKey struct {
KeyID string
Address string
PublicKey string
}
// Manager defines the contract for managing managed wallet keys.
type Manager interface {
// CreateManagedWalletKey provisions a new managed wallet key for the provided wallet reference and network.
CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*ManagedWalletKey, error)
// SignTransaction signs the provided transaction using the identified key material.
SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
// SignTronTransaction signs a native TRON transaction using the identified key material.
SignTronTransaction(ctx context.Context, keyID string, tx *core.Transaction) (*core.Transaction, error)
}

View File

@@ -0,0 +1,341 @@
package vault
import (
"context"
stdecdsa "crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"math/big"
"os"
"path"
"strings"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
"github.com/hashicorp/vault/api"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"github.com/tech/sendico/gateway/tron/internal/keymanager"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
)
// Config describes how to connect to Vault for managed wallet keys.
type Config struct {
Address string `mapstructure:"address"`
TokenEnv string `mapstructure:"token_env"`
Namespace string `mapstructure:"namespace"`
MountPath string `mapstructure:"mount_path"`
KeyPrefix string `mapstructure:"key_prefix"`
}
// Manager implements the keymanager.Manager contract backed by HashiCorp Vault.
type Manager struct {
logger mlogger.Logger
client *api.Client
store *api.KVv2
keyPrefix string
}
// New constructs a Vault-backed key manager.
func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
if logger == nil {
return nil, merrors.InvalidArgument("vault key manager: logger is required")
}
address := strings.TrimSpace(cfg.Address)
if address == "" {
logger.Error("Vault address missing")
return nil, merrors.InvalidArgument("vault key manager: address is required")
}
tokenEnv := strings.TrimSpace(cfg.TokenEnv)
if tokenEnv == "" {
logger.Error("Vault token env missing")
return nil, merrors.InvalidArgument("vault key manager: token_env is required")
}
token := strings.TrimSpace(os.Getenv(tokenEnv))
if token == "" {
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
}
mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/")
if mountPath == "" {
logger.Error("Vault mount path missing")
return nil, merrors.InvalidArgument("vault key manager: mount_path is required")
}
keyPrefix := strings.Trim(strings.TrimSpace(cfg.KeyPrefix), "/")
if keyPrefix == "" {
keyPrefix = "gateway/chain/wallets"
}
clientCfg := api.DefaultConfig()
clientCfg.Address = address
client, err := api.NewClient(clientCfg)
if err != nil {
logger.Error("Failed to create vault client", zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to create client: " + err.Error())
}
client.SetToken(token)
if ns := strings.TrimSpace(cfg.Namespace); ns != "" {
client.SetNamespace(ns)
}
kv := client.KVv2(mountPath)
return &Manager{
logger: logger.Named("vault"),
client: client,
store: kv,
keyPrefix: keyPrefix,
}, nil
}
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
if strings.TrimSpace(walletRef) == "" {
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", network))
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
}
if strings.TrimSpace(network) == "" {
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
return nil, merrors.InvalidArgument("vault key manager: network is required")
}
privateKey, err := stdecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
if err != nil {
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
}
privateKeyBytes := crypto.FromECDSA(privateKey)
publicKey := privateKey.PublicKey
publicKeyBytes := crypto.FromECDSAPub(&publicKey)
publicKeyHex := hex.EncodeToString(publicKeyBytes)
address := crypto.PubkeyToAddress(publicKey).Hex()
err = m.persistKey(ctx, walletRef, network, privateKeyBytes, publicKeyBytes, address)
if err != nil {
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
zeroBytes(privateKeyBytes)
zeroBytes(publicKeyBytes)
return nil, err
}
zeroBytes(privateKeyBytes)
zeroBytes(publicKeyBytes)
m.logger.Info("Managed wallet key created",
zap.String("wallet_ref", walletRef),
zap.String("network", network),
zap.String("address", strings.ToLower(address)),
)
return &keymanager.ManagedWalletKey{
KeyID: m.buildKeyID(network, walletRef),
Address: strings.ToLower(address),
PublicKey: publicKeyHex,
}, nil
}
func (m *Manager) persistKey(ctx context.Context, walletRef, network string, privateKey, publicKey []byte, address string) error {
secretPath := m.buildKeyID(network, walletRef)
payload := map[string]interface{}{
"private_key": hex.EncodeToString(privateKey),
"public_key": hex.EncodeToString(publicKey),
"address": strings.ToLower(address),
"network": strings.ToLower(network),
}
if _, err := m.store.Put(ctx, secretPath, payload); err != nil {
return merrors.Internal("vault key manager: failed to write secret at " + secretPath + ": " + err.Error())
}
return nil
}
func (m *Manager) buildKeyID(network, walletRef string) string {
net := strings.Trim(strings.ToLower(network), "/")
return path.Join(m.keyPrefix, net, walletRef)
}
// SignTransaction loads the key material from Vault and signs the transaction.
func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
if strings.TrimSpace(keyID) == "" {
m.logger.Warn("Signing failed: empty key id")
return nil, merrors.InvalidArgument("vault key manager: keyID is required")
}
if tx == nil {
m.logger.Warn("Signing failed: nil transaction", zap.String("key_id", keyID))
return nil, merrors.InvalidArgument("vault key manager: transaction is nil")
}
if chainID == nil {
m.logger.Warn("Signing failed: nil chain id", zap.String("key_id", keyID))
return nil, merrors.InvalidArgument("vault key manager: chainID is nil")
}
material, err := m.loadKey(ctx, keyID)
if err != nil {
m.logger.Warn("Failed to load key material", zap.String("key_id", keyID), zap.Error(err))
return nil, err
}
keyBytes, err := hex.DecodeString(material.PrivateKey)
if err != nil {
m.logger.Warn("Invalid key material", zap.String("key_id", keyID), zap.Error(err))
return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error())
}
defer zeroBytes(keyBytes)
privateKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
m.logger.Warn("Failed to construct private key", zap.String("key_id", keyID), zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to construct private key: " + err.Error())
}
signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey)
if err != nil {
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to sign transaction: " + err.Error())
}
m.logger.Info("Transaction signed with managed key",
zap.String("key_id", keyID),
zap.String("network", material.Network),
zap.String("tx_hash", signed.Hash().Hex()),
)
return signed, nil
}
// SignTronTransaction signs a native TRON transaction using the identified key material.
func (m *Manager) SignTronTransaction(ctx context.Context, keyID string, tx *troncore.Transaction) (*troncore.Transaction, error) {
if strings.TrimSpace(keyID) == "" {
m.logger.Warn("TRON signing failed: empty key id")
return nil, merrors.InvalidArgument("vault key manager: keyID is required")
}
if tx == nil {
m.logger.Warn("TRON signing failed: nil transaction", zap.String("key_id", keyID))
return nil, merrors.InvalidArgument("vault key manager: transaction is nil")
}
if tx.GetRawData() == nil {
m.logger.Warn("TRON signing failed: nil raw data", zap.String("key_id", keyID))
return nil, merrors.InvalidArgument("vault key manager: transaction raw_data is nil")
}
material, err := m.loadKey(ctx, keyID)
if err != nil {
m.logger.Warn("Failed to load key material for TRON signing", zap.String("key_id", keyID), zap.Error(err))
return nil, err
}
keyBytes, err := hex.DecodeString(material.PrivateKey)
if err != nil {
m.logger.Warn("Invalid key material for TRON signing", zap.String("key_id", keyID), zap.Error(err))
return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error())
}
defer zeroBytes(keyBytes)
// Marshal the raw_data to bytes for hashing
rawBytes, err := proto.Marshal(tx.GetRawData())
if err != nil {
m.logger.Warn("Failed to marshal TRON transaction raw_data", zap.String("key_id", keyID), zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to marshal transaction: " + err.Error())
}
// SHA256 hash of the raw_data
hash := sha256.Sum256(rawBytes)
// Create secp256k1 private key
privKey := secp256k1.PrivKeyFromBytes(keyBytes)
// Sign using compact signature format (65 bytes: r[32] || s[32] || recovery_id[1])
signature := ecdsa.SignCompact(privKey, hash[:], false)
// TRON expects signature in format: r[32] || s[32] || v[1]
// SignCompact returns: recovery_id[1] || r[32] || s[32]
// We need to rearrange to: r[32] || s[32] || recovery_id[1]
if len(signature) != 65 {
m.logger.Warn("Unexpected signature length", zap.String("key_id", keyID), zap.Int("length", len(signature)))
return nil, merrors.Internal("vault key manager: unexpected signature length")
}
tronSig := make([]byte, 65)
copy(tronSig[0:32], signature[1:33]) // r
copy(tronSig[32:64], signature[33:65]) // s
tronSig[64] = signature[0] // recovery id (v)
// Append signature to transaction
tx.Signature = append(tx.Signature, tronSig)
m.logger.Info("TRON transaction signed with managed key",
zap.String("key_id", keyID),
zap.String("network", material.Network),
)
return tx, nil
}
type keyMaterial struct {
PrivateKey string
PublicKey string
Address string
Network string
}
func (m *Manager) loadKey(ctx context.Context, keyID string) (*keyMaterial, error) {
secretPath := strings.Trim(strings.TrimPrefix(keyID, "/"), "/")
secret, err := m.store.Get(ctx, secretPath)
if err != nil {
m.logger.Warn("Failed to read secret", zap.String("path", secretPath), zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to read secret at " + secretPath + ": " + err.Error())
}
if secret == nil || secret.Data == nil {
m.logger.Warn("Secret not found", zap.String("path", secretPath))
return nil, merrors.NoData("vault key manager: secret " + secretPath + " not found")
}
getString := func(key string) (string, error) {
val, ok := secret.Data[key]
if !ok {
m.logger.Warn("Secret missing field", zap.String("path", secretPath), zap.String("field", key))
return "", merrors.Internal("vault key manager: secret " + secretPath + " missing " + key)
}
str, ok := val.(string)
if !ok || strings.TrimSpace(str) == "" {
m.logger.Warn("Secret field invalid", zap.String("path", secretPath), zap.String("field", key))
return "", merrors.Internal("vault key manager: secret " + secretPath + " invalid " + key)
}
return str, nil
}
privateKey, err := getString("private_key")
if err != nil {
return nil, err
}
publicKey, err := getString("public_key")
if err != nil {
return nil, err
}
address, err := getString("address")
if err != nil {
return nil, err
}
network, err := getString("network")
if err != nil {
return nil, err
}
return &keyMaterial{
PrivateKey: privateKey,
PublicKey: publicKey,
Address: address,
Network: network,
}, nil
}
func zeroBytes(data []byte) {
for i := range data {
data[i] = 0
}
}
var _ keymanager.Manager = (*Manager)(nil)