package vault import ( "context" "crypto/sha256" "encoding/hex" "math/big" "strings" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" "github.com/ethereum/go-ethereum/core/types" troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core" "github.com/tech/sendico/pkg/vault/managedkey" "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 = managedkey.Config // Manager implements the keymanager.Manager contract backed by HashiCorp Vault. type Manager struct { logger mlogger.Logger keys managedkey.Service } // 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") } keys, err := managedkey.New(managedkey.Options{ Logger: logger, Config: managedkey.Config(cfg), Component: "vault key manager", DefaultKeyPrefix: "gateway/tron/wallets", }) if err != nil { logger.Error("Failed to initialise vault key manager", zap.Error(err)) return nil, err } return &Manager{ logger: logger.Named("vault"), keys: keys, }, 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) { created, err := m.keys.CreateManagedWalletKey(ctx, walletRef, network) if err != nil { m.logger.Warn("Failed to create managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err)) return nil, err } return &keymanager.ManagedWalletKey{ KeyID: created.KeyID, Address: created.Address, PublicKey: created.PublicKey, }, nil } // 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) { signed, err := m.keys.SignEVMTransaction(ctx, keyID, tx, chainID) if err != nil { m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err)) return nil, err } 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.keys.LoadKeyMaterial(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 } func zeroBytes(data []byte) { for i := range data { data[i] = 0 } } var _ keymanager.Manager = (*Manager)(nil)