Files
sonr/types/txns/fees.go
T

398 lines
11 KiB
Go
Raw Normal View History

2025-10-03 14:45:52 -04:00
package txns
import (
"context"
"fmt"
"math/big"
"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/params"
)
// FeeEstimator interface for estimating transaction fees
type FeeEstimator interface {
// EstimateFee estimates the fee for a transaction
EstimateFee(ctx context.Context, params Params) (*FeeEstimation, error)
// EstimateGas estimates the gas required for a transaction
EstimateGas(ctx context.Context, params Params) (uint64, error)
// GetGasPrice retrieves current gas price
GetGasPrice(ctx context.Context) (any, error)
// ValidateFee validates if a fee is sufficient
ValidateFee(fee any, gasUsed uint64) error
}
// CosmosFeeEstimator estimates fees for Cosmos transactions
type CosmosFeeEstimator struct {
clientCtx client.Context
minGasPrice sdk.DecCoin
gasAdjustment float64
}
// NewCosmosFeeEstimator creates a new Cosmos fee estimator
func NewCosmosFeeEstimator(clientCtx client.Context, minGasPrice sdk.DecCoin) *CosmosFeeEstimator {
return &CosmosFeeEstimator{
clientCtx: clientCtx,
minGasPrice: minGasPrice,
gasAdjustment: 1.2, // Default gas adjustment factor
}
}
// SetGasAdjustment sets the gas adjustment factor
func (cfe *CosmosFeeEstimator) SetGasAdjustment(adjustment float64) {
cfe.gasAdjustment = adjustment
}
// EstimateFee implements FeeEstimator interface
func (cfe *CosmosFeeEstimator) EstimateFee(
ctx context.Context,
params Params,
) (*FeeEstimation, error) {
cosmosParams, ok := params.(*CosmosTransactionParams)
if !ok {
return nil, ErrInvalidTransactionParams
}
if err := cosmosParams.Validate(); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
// Estimate gas usage
gasLimit, err := cfe.EstimateGas(ctx, params)
if err != nil {
return nil, fmt.Errorf("failed to estimate gas: %w", err)
}
// Apply gas adjustment
adjustedGasLimit := uint64(float64(gasLimit) * cfe.gasAdjustment)
// Calculate fee
feeAmount := cfe.minGasPrice.Amount.MulInt64(int64(adjustedGasLimit))
fee := sdk.NewCoins(sdk.NewCoin(cfe.minGasPrice.Denom, feeAmount.TruncateInt()))
return &FeeEstimation{
GasLimit: adjustedGasLimit,
GasPrice: cfe.minGasPrice,
Fee: fee,
Total: fee.String(),
}, nil
}
// EstimateGas implements FeeEstimator interface
func (cfe *CosmosFeeEstimator) EstimateGas(ctx context.Context, params Params) (uint64, error) {
cosmosParams, ok := params.(*CosmosTransactionParams)
if !ok {
return 0, ErrInvalidTransactionParams
}
// Base gas estimation by message type
baseGas := uint64(0)
for _, msg := range cosmosParams.Messages {
baseGas += cfe.estimateGasForMessage(msg)
}
// Add overhead for transaction processing
overhead := uint64(10000) // Base transaction overhead
if cosmosParams.Memo != "" {
overhead += uint64(len(cosmosParams.Memo)) * 10 // Memo overhead
}
return baseGas + overhead, nil
}
// GetGasPrice implements FeeEstimator interface
func (cfe *CosmosFeeEstimator) GetGasPrice(ctx context.Context) (any, error) {
// In Cosmos, gas price is typically fixed or queried from chain parameters
// For now, return the configured minimum gas price
return cfe.minGasPrice, nil
}
// ValidateFee implements FeeEstimator interface
func (cfe *CosmosFeeEstimator) ValidateFee(fee any, gasUsed uint64) error {
feeCoins, ok := fee.(sdk.Coins)
if !ok {
return ErrInvalidTransactionParams
}
// Calculate minimum required fee
minFeeAmount := cfe.minGasPrice.Amount.MulInt64(int64(gasUsed))
minFee := sdk.NewCoins(sdk.NewCoin(cfe.minGasPrice.Denom, minFeeAmount.TruncateInt()))
// Check if provided fee is sufficient
if !feeCoins.IsAllGTE(minFee) {
return fmt.Errorf("insufficient fee: got %s, need at least %s", feeCoins, minFee)
}
return nil
}
// estimateGasForMessage estimates gas usage for a specific message type
func (cfe *CosmosFeeEstimator) estimateGasForMessage(msg sdk.Msg) uint64 {
switch msg.(type) {
case *banktypes.MsgSend:
return 80000 // Base gas for bank send
case *banktypes.MsgMultiSend:
return 120000 // Higher gas for multi-send
default:
return 100000 // Default gas estimate
}
}
// EVMFeeEstimator estimates fees for EVM transactions
type EVMFeeEstimator struct {
client *ethclient.Client
chainID *big.Int
gasAdjustment float64
}
// NewEVMFeeEstimator creates a new EVM fee estimator
func NewEVMFeeEstimator(client *ethclient.Client, chainID *big.Int) *EVMFeeEstimator {
return &EVMFeeEstimator{
client: client,
chainID: chainID,
gasAdjustment: 1.1, // Default gas adjustment factor
}
}
// SetGasAdjustment sets the gas adjustment factor
func (efe *EVMFeeEstimator) SetGasAdjustment(adjustment float64) {
efe.gasAdjustment = adjustment
}
// EstimateFee implements FeeEstimator interface
func (efe *EVMFeeEstimator) EstimateFee(
ctx context.Context,
params Params,
) (*FeeEstimation, error) {
evmParams, ok := params.(*EVMTransactionParams)
if !ok {
return nil, ErrInvalidTransactionParams
}
if err := evmParams.Validate(); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
// Estimate gas usage
gasLimit, err := efe.EstimateGas(ctx, params)
if err != nil {
return nil, fmt.Errorf("failed to estimate gas: %w", err)
}
// Get current gas price
gasPrice, err := efe.GetGasPrice(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get gas price: %w", err)
}
// Apply gas adjustment
adjustedGasLimit := uint64(float64(gasLimit) * efe.gasAdjustment)
// Calculate fee
var totalFee *big.Int
var feeData any
if evmParams.MaxFeePerGas != nil && evmParams.MaxPriorityFeePerGas != nil {
// EIP-1559 transaction
totalFee = new(big.Int).Mul(evmParams.MaxFeePerGas, big.NewInt(int64(adjustedGasLimit)))
feeData = map[string]*big.Int{
"maxFeePerGas": evmParams.MaxFeePerGas,
"maxPriorityFeePerGas": evmParams.MaxPriorityFeePerGas,
}
} else {
// Legacy transaction
gasPriceBig := gasPrice.(*big.Int)
totalFee = new(big.Int).Mul(gasPriceBig, big.NewInt(int64(adjustedGasLimit)))
feeData = gasPriceBig
}
return &FeeEstimation{
GasLimit: adjustedGasLimit,
GasPrice: feeData,
Fee: totalFee,
Total: totalFee.String(),
}, nil
}
// EstimateGas implements FeeEstimator interface
func (efe *EVMFeeEstimator) EstimateGas(ctx context.Context, params Params) (uint64, error) {
evmParams, ok := params.(*EVMTransactionParams)
if !ok {
return 0, ErrInvalidTransactionParams
}
if efe.client == nil {
// Fallback estimation without client
return efe.estimateGasOffline(evmParams), nil
}
// Create a call message for gas estimation
callMsg := ethereum.CallMsg{
To: evmParams.To,
Value: evmParams.Value,
Data: evmParams.Data,
}
// Estimate gas using the client
gasLimit, err := efe.client.EstimateGas(ctx, callMsg)
if err != nil {
// Fallback to offline estimation
return efe.estimateGasOffline(evmParams), nil
}
return gasLimit, nil
}
// GetGasPrice implements FeeEstimator interface
func (efe *EVMFeeEstimator) GetGasPrice(ctx context.Context) (any, error) {
if efe.client == nil {
// Return default gas price
return big.NewInt(params.GWei * 20), nil // 20 Gwei
}
gasPrice, err := efe.client.SuggestGasPrice(ctx)
if err != nil {
// Fallback to default
return big.NewInt(params.GWei * 20), nil
}
return gasPrice, nil
}
// ValidateFee implements FeeEstimator interface
func (efe *EVMFeeEstimator) ValidateFee(fee any, gasUsed uint64) error {
switch f := fee.(type) {
case *big.Int:
// Legacy transaction
minFee := new(big.Int).Mul(big.NewInt(params.GWei), big.NewInt(int64(gasUsed)))
if f.Cmp(minFee) < 0 {
return fmt.Errorf("insufficient fee: got %s, need at least %s", f, minFee)
}
case map[string]*big.Int:
// EIP-1559 transaction
maxFeePerGas, ok := f["maxFeePerGas"]
if !ok {
return fmt.Errorf("missing maxFeePerGas in fee data")
}
minFee := new(big.Int).Mul(big.NewInt(params.GWei), big.NewInt(int64(gasUsed)))
totalMaxFee := new(big.Int).Mul(maxFeePerGas, big.NewInt(int64(gasUsed)))
if totalMaxFee.Cmp(minFee) < 0 {
return fmt.Errorf("insufficient max fee: got %s, need at least %s", totalMaxFee, minFee)
}
default:
return fmt.Errorf("unsupported fee type: %T", fee)
}
return nil
}
// estimateGasOffline provides offline gas estimation
func (efe *EVMFeeEstimator) estimateGasOffline(params *EVMTransactionParams) uint64 {
baseGas := uint64(21000) // Base transaction gas
if params.Data != nil && len(params.Data) > 0 {
// Contract interaction
baseGas += uint64(len(params.Data)) * 16 // Rough estimate for data
if params.To == nil {
// Contract deployment
baseGas += 200000
} else {
// Contract call
baseGas += 100000
}
}
return baseGas
}
// FeeManager manages fee estimation for multiple transaction types
type FeeManager struct {
cosmosEstimator *CosmosFeeEstimator
evmEstimator *EVMFeeEstimator
}
// NewFeeManager creates a new fee manager
func NewFeeManager(cosmosEstimator *CosmosFeeEstimator, evmEstimator *EVMFeeEstimator) *FeeManager {
return &FeeManager{
cosmosEstimator: cosmosEstimator,
evmEstimator: evmEstimator,
}
}
// EstimateFee estimates fee for any transaction type
func (fm *FeeManager) EstimateFee(
ctx context.Context,
txType TransactionType,
params Params,
) (*FeeEstimation, error) {
switch txType {
case TransactionTypeCosmos:
if fm.cosmosEstimator == nil {
return nil, fmt.Errorf("cosmos fee estimator not configured")
}
return fm.cosmosEstimator.EstimateFee(ctx, params)
case TransactionTypeEVM:
if fm.evmEstimator == nil {
return nil, fmt.Errorf("EVM fee estimator not configured")
}
return fm.evmEstimator.EstimateFee(ctx, params)
default:
return nil, ErrUnsupportedChainType
}
}
// GetEstimator returns the appropriate fee estimator for a transaction type
func (fm *FeeManager) GetEstimator(txType TransactionType) (FeeEstimator, error) {
switch txType {
case TransactionTypeCosmos:
if fm.cosmosEstimator == nil {
return nil, fmt.Errorf("cosmos fee estimator not configured")
}
return fm.cosmosEstimator, nil
case TransactionTypeEVM:
if fm.evmEstimator == nil {
return nil, fmt.Errorf("EVM fee estimator not configured")
}
return fm.evmEstimator, nil
default:
return nil, ErrUnsupportedChainType
}
}
// DefaultFeeConfig holds default fee configuration
type DefaultFeeConfig struct {
CosmosMinGasPrice sdk.DecCoin
EVMGasPrice *big.Int
GasAdjustment float64
}
// GetDefaultFeeConfig returns default fee configuration
func GetDefaultFeeConfig() *DefaultFeeConfig {
return &DefaultFeeConfig{
CosmosMinGasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
EVMGasPrice: big.NewInt(params.GWei * 20), // 20 Gwei
GasAdjustment: 1.2,
}
}
// CreateDefaultFeeManager creates a fee manager with default configuration
func CreateDefaultFeeManager(
clientCtx client.Context,
evmClient *ethclient.Client,
chainID *big.Int,
) *FeeManager {
config := GetDefaultFeeConfig()
cosmosEstimator := NewCosmosFeeEstimator(clientCtx, config.CosmosMinGasPrice)
cosmosEstimator.SetGasAdjustment(config.GasAdjustment)
evmEstimator := NewEVMFeeEstimator(evmClient, chainID)
evmEstimator.SetGasAdjustment(config.GasAdjustment)
return NewFeeManager(cosmosEstimator, evmEstimator)
}