mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 18:31:41 +00:00
@@ -0,0 +1,316 @@
|
||||
// Package tx provides gas estimation and fee calculation utilities for the Sonr client SDK.
|
||||
package tx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
sdktypes "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx"
|
||||
|
||||
"github.com/sonr-io/sonr/client/config"
|
||||
"github.com/sonr-io/sonr/client/errors"
|
||||
)
|
||||
|
||||
// GasEstimator provides an interface for estimating gas costs and calculating fees.
|
||||
type GasEstimator interface {
|
||||
// Gas estimation
|
||||
EstimateGas(ctx context.Context, msgs []sdktypes.Msg) (*GasEstimate, error)
|
||||
EstimateGasForTx(ctx context.Context, unsignedTx *UnsignedTx) (*GasEstimate, error)
|
||||
|
||||
// Fee calculation
|
||||
CalculateFee(gasUsed uint64, gasPrice float64, denom string) sdktypes.Coins
|
||||
CalculateFeeWithAdjustment(gasUsed uint64, gasPrice float64, adjustment float64, denom string) sdktypes.Coins
|
||||
|
||||
// Gas configuration
|
||||
WithGasAdjustment(adjustment float64) GasEstimator
|
||||
WithMinGasPrice(price float64) GasEstimator
|
||||
WithMaxGasLimit(limit uint64) GasEstimator
|
||||
|
||||
// Utility methods
|
||||
GetRecommendedGasPrice(ctx context.Context) (float64, error)
|
||||
GetNetworkGasInfo(ctx context.Context) (*NetworkGasInfo, error)
|
||||
}
|
||||
|
||||
// GasEstimate contains the result of gas estimation.
|
||||
type GasEstimate struct {
|
||||
GasWanted uint64 // Estimated gas needed
|
||||
GasUsed uint64 // Gas used in simulation
|
||||
GasLimit uint64 // Recommended gas limit (with adjustment)
|
||||
Fee sdktypes.Coins // Calculated fee
|
||||
GasPrice float64 // Gas price used
|
||||
GasAdjustment float64 // Adjustment factor applied
|
||||
}
|
||||
|
||||
// NetworkGasInfo contains network-wide gas information.
|
||||
type NetworkGasInfo struct {
|
||||
MinGasPrice float64 // Minimum gas price accepted by validators
|
||||
MedianGasPrice float64 // Median gas price from recent transactions
|
||||
RecommendedGasPrice float64 // Recommended gas price for fast inclusion
|
||||
MaxGasLimit uint64 // Maximum gas limit per transaction
|
||||
}
|
||||
|
||||
// GasConfig holds gas estimation configuration.
|
||||
type GasConfig struct {
|
||||
Adjustment float64 // Gas adjustment factor (default: 1.5)
|
||||
MinGasPrice float64 // Minimum gas price
|
||||
MaxGasLimit uint64 // Maximum gas limit
|
||||
Denom string // Gas fee denomination
|
||||
}
|
||||
|
||||
// gasEstimator implements GasEstimator.
|
||||
type gasEstimator struct {
|
||||
grpcConn *grpc.ClientConn
|
||||
config *config.NetworkConfig
|
||||
txServiceClient tx.ServiceClient
|
||||
gasConfig GasConfig
|
||||
}
|
||||
|
||||
// NewGasEstimator creates a new gas estimator.
|
||||
func NewGasEstimator(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) GasEstimator {
|
||||
gasConfig := GasConfig{
|
||||
Adjustment: cfg.GasAdjustment,
|
||||
MinGasPrice: cfg.GasPrice,
|
||||
MaxGasLimit: 10000000, // 10M gas limit
|
||||
Denom: cfg.StakingDenom,
|
||||
}
|
||||
|
||||
return &gasEstimator{
|
||||
grpcConn: grpcConn,
|
||||
config: cfg,
|
||||
txServiceClient: tx.NewServiceClient(grpcConn),
|
||||
gasConfig: gasConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// EstimateGas estimates gas for a list of messages.
|
||||
func (ge *gasEstimator) EstimateGas(ctx context.Context, msgs []sdktypes.Msg) (*GasEstimate, error) {
|
||||
if len(msgs) == 0 {
|
||||
return nil, fmt.Errorf("no messages provided for gas estimation")
|
||||
}
|
||||
|
||||
// Create a temporary transaction builder to build the transaction for simulation
|
||||
builder, err := NewTxBuilder(ge.config, ge.grpcConn)
|
||||
if err != nil {
|
||||
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to create transaction builder")
|
||||
}
|
||||
|
||||
// Add messages and build unsigned transaction
|
||||
for _, msg := range msgs {
|
||||
builder.AddMessage(msg)
|
||||
}
|
||||
|
||||
unsignedTx, err := builder.Build()
|
||||
if err != nil {
|
||||
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to build transaction for estimation")
|
||||
}
|
||||
|
||||
return ge.EstimateGasForTx(ctx, unsignedTx)
|
||||
}
|
||||
|
||||
// EstimateGasForTx estimates gas for an unsigned transaction.
|
||||
func (ge *gasEstimator) EstimateGasForTx(ctx context.Context, unsignedTx *UnsignedTx) (*GasEstimate, error) {
|
||||
// Create simulate request
|
||||
req := &tx.SimulateRequest{
|
||||
TxBytes: unsignedTx.SignBytes, // Use sign bytes for simulation
|
||||
}
|
||||
|
||||
// Simulate the transaction
|
||||
resp, err := ge.txServiceClient.Simulate(ctx, req)
|
||||
if err != nil {
|
||||
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to simulate transaction")
|
||||
}
|
||||
|
||||
gasUsed := resp.GasInfo.GasUsed
|
||||
gasWanted := resp.GasInfo.GasWanted
|
||||
|
||||
// Apply gas adjustment
|
||||
gasLimit := uint64(float64(gasUsed) * ge.gasConfig.Adjustment)
|
||||
|
||||
// Ensure gas limit doesn't exceed maximum
|
||||
if gasLimit > ge.gasConfig.MaxGasLimit {
|
||||
gasLimit = ge.gasConfig.MaxGasLimit
|
||||
}
|
||||
|
||||
// Calculate fee
|
||||
fee := ge.CalculateFee(gasLimit, ge.gasConfig.MinGasPrice, ge.gasConfig.Denom)
|
||||
|
||||
return &GasEstimate{
|
||||
GasWanted: gasWanted,
|
||||
GasUsed: gasUsed,
|
||||
GasLimit: gasLimit,
|
||||
Fee: fee,
|
||||
GasPrice: ge.gasConfig.MinGasPrice,
|
||||
GasAdjustment: ge.gasConfig.Adjustment,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CalculateFee calculates the transaction fee based on gas usage and price.
|
||||
func (ge *gasEstimator) CalculateFee(gasUsed uint64, gasPrice float64, denom string) sdktypes.Coins {
|
||||
// Calculate fee amount
|
||||
feeAmount := math.Ceil(float64(gasUsed) * gasPrice)
|
||||
|
||||
// Create coin
|
||||
feeCoin := sdktypes.NewInt64Coin(denom, int64(feeAmount))
|
||||
|
||||
return sdktypes.NewCoins(feeCoin)
|
||||
}
|
||||
|
||||
// CalculateFeeWithAdjustment calculates fee with a custom gas adjustment.
|
||||
func (ge *gasEstimator) CalculateFeeWithAdjustment(gasUsed uint64, gasPrice float64, adjustment float64, denom string) sdktypes.Coins {
|
||||
adjustedGas := uint64(float64(gasUsed) * adjustment)
|
||||
return ge.CalculateFee(adjustedGas, gasPrice, denom)
|
||||
}
|
||||
|
||||
// WithGasAdjustment sets the gas adjustment factor.
|
||||
func (ge *gasEstimator) WithGasAdjustment(adjustment float64) GasEstimator {
|
||||
ge.gasConfig.Adjustment = adjustment
|
||||
return ge
|
||||
}
|
||||
|
||||
// WithMinGasPrice sets the minimum gas price.
|
||||
func (ge *gasEstimator) WithMinGasPrice(price float64) GasEstimator {
|
||||
ge.gasConfig.MinGasPrice = price
|
||||
return ge
|
||||
}
|
||||
|
||||
// WithMaxGasLimit sets the maximum gas limit.
|
||||
func (ge *gasEstimator) WithMaxGasLimit(limit uint64) GasEstimator {
|
||||
ge.gasConfig.MaxGasLimit = limit
|
||||
return ge
|
||||
}
|
||||
|
||||
// GetRecommendedGasPrice returns the recommended gas price for the network.
|
||||
func (ge *gasEstimator) GetRecommendedGasPrice(ctx context.Context) (float64, error) {
|
||||
// TODO: Implement dynamic gas price discovery based on network conditions
|
||||
// Should query recent transactions to analyze gas price trends
|
||||
// Calculate percentile-based recommendations (e.g., 25th, 50th, 75th)
|
||||
// Consider network congestion and validator preferences
|
||||
// Return optimal gas price for desired transaction inclusion speed
|
||||
return ge.gasConfig.MinGasPrice, nil
|
||||
}
|
||||
|
||||
// GetNetworkGasInfo retrieves network-wide gas information.
|
||||
func (ge *gasEstimator) GetNetworkGasInfo(ctx context.Context) (*NetworkGasInfo, error) {
|
||||
// TODO: Implement dynamic network gas info retrieval
|
||||
// Should query validator minimum gas prices via gRPC
|
||||
// Analyze recent block gas usage patterns and limits
|
||||
// Calculate median and recommended gas prices from mempool
|
||||
// Monitor network congestion metrics for pricing recommendations
|
||||
// Query chain parameters for maximum gas limits and constraints
|
||||
return &NetworkGasInfo{
|
||||
MinGasPrice: ge.gasConfig.MinGasPrice,
|
||||
MedianGasPrice: ge.gasConfig.MinGasPrice,
|
||||
RecommendedGasPrice: ge.gasConfig.MinGasPrice,
|
||||
MaxGasLimit: ge.gasConfig.MaxGasLimit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Utility functions and constants
|
||||
|
||||
// Default gas values for different transaction types
|
||||
const (
|
||||
// DefaultGasLimitValue is the default gas limit for transactions
|
||||
DefaultGasLimitValue = 200000
|
||||
|
||||
// SendGasLimit is the typical gas limit for send transactions
|
||||
SendGasLimit = 100000
|
||||
|
||||
// DelegateGasLimit is the typical gas limit for delegation transactions
|
||||
DelegateGasLimit = 150000
|
||||
|
||||
// ContractCallGasLimit is the typical gas limit for smart contract calls
|
||||
ContractCallGasLimit = 500000
|
||||
|
||||
// MinGasAdjustment is the minimum recommended gas adjustment
|
||||
MinGasAdjustment = 1.1
|
||||
|
||||
// MaxGasAdjustment is the maximum reasonable gas adjustment
|
||||
MaxGasAdjustment = 3.0
|
||||
)
|
||||
|
||||
// GasLimitForMessageType returns a recommended gas limit for different message types.
|
||||
func GasLimitForMessageType(msgType string) uint64 {
|
||||
switch msgType {
|
||||
case "/cosmos.bank.v1beta1.MsgSend":
|
||||
return SendGasLimit
|
||||
case "/cosmos.staking.v1beta1.MsgDelegate":
|
||||
return DelegateGasLimit
|
||||
case "/cosmos.staking.v1beta1.MsgUndelegate":
|
||||
return DelegateGasLimit
|
||||
case "/cosmos.staking.v1beta1.MsgRedelegate":
|
||||
return DelegateGasLimit * 2
|
||||
default:
|
||||
return DefaultGasLimitValue
|
||||
}
|
||||
}
|
||||
|
||||
// EstimateGasForMessages provides a quick gas estimate based on message types.
|
||||
func EstimateGasForMessages(msgs []sdktypes.Msg) uint64 {
|
||||
var totalGas uint64
|
||||
|
||||
for _, msg := range msgs {
|
||||
msgType := sdktypes.MsgTypeURL(msg)
|
||||
gas := GasLimitForMessageType(msgType)
|
||||
totalGas += gas
|
||||
}
|
||||
|
||||
// Add base transaction overhead
|
||||
totalGas += 50000
|
||||
|
||||
return totalGas
|
||||
}
|
||||
|
||||
// ValidateGasPrice checks if a gas price is reasonable.
|
||||
func ValidateGasPrice(gasPrice float64) error {
|
||||
if gasPrice <= 0 {
|
||||
return fmt.Errorf("gas price must be positive")
|
||||
}
|
||||
|
||||
if gasPrice > 1.0 { // 1 SNR per gas unit seems excessive
|
||||
return fmt.Errorf("gas price %f seems too high", gasPrice)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateGasLimit checks if a gas limit is reasonable.
|
||||
func ValidateGasLimit(gasLimit uint64) error {
|
||||
if gasLimit == 0 {
|
||||
return fmt.Errorf("gas limit must be positive")
|
||||
}
|
||||
|
||||
if gasLimit > 50000000 { // 50M gas limit seems excessive
|
||||
return fmt.Errorf("gas limit %d seems too high", gasLimit)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptimizeGasConfig optimizes gas configuration based on network conditions.
|
||||
func OptimizeGasConfig(config *GasConfig, networkInfo *NetworkGasInfo) *GasConfig {
|
||||
optimized := *config
|
||||
|
||||
// Use recommended gas price if it's higher than our minimum
|
||||
if networkInfo.RecommendedGasPrice > config.MinGasPrice {
|
||||
optimized.MinGasPrice = networkInfo.RecommendedGasPrice
|
||||
}
|
||||
|
||||
// Ensure gas adjustment is within reasonable bounds
|
||||
if optimized.Adjustment < MinGasAdjustment {
|
||||
optimized.Adjustment = MinGasAdjustment
|
||||
}
|
||||
if optimized.Adjustment > MaxGasAdjustment {
|
||||
optimized.Adjustment = MaxGasAdjustment
|
||||
}
|
||||
|
||||
// Use network max gas limit if it's lower than our configured max
|
||||
if networkInfo.MaxGasLimit > 0 && networkInfo.MaxGasLimit < config.MaxGasLimit {
|
||||
optimized.MaxGasLimit = networkInfo.MaxGasLimit
|
||||
}
|
||||
|
||||
return &optimized
|
||||
}
|
||||
Reference in New Issue
Block a user