github.com/status-im/status-go@v1.1.0/services/wallet/router/fees/fees.go (about)

     1  package fees
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"math/big"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    11  	"github.com/ethereum/go-ethereum/common/hexutil"
    12  	"github.com/ethereum/go-ethereum/consensus/misc"
    13  	"github.com/ethereum/go-ethereum/params"
    14  	gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle"
    15  	"github.com/status-im/status-go/rpc"
    16  	"github.com/status-im/status-go/rpc/chain"
    17  	"github.com/status-im/status-go/services/wallet/common"
    18  )
    19  
    20  type GasFeeMode int
    21  
    22  const (
    23  	GasFeeLow GasFeeMode = iota
    24  	GasFeeMedium
    25  	GasFeeHigh
    26  )
    27  
    28  type MaxFeesLevels struct {
    29  	Low    *hexutil.Big `json:"low"`
    30  	Medium *hexutil.Big `json:"medium"`
    31  	High   *hexutil.Big `json:"high"`
    32  }
    33  
    34  type SuggestedFees struct {
    35  	GasPrice             *big.Int       `json:"gasPrice"`
    36  	BaseFee              *big.Int       `json:"baseFee"`
    37  	MaxFeesLevels        *MaxFeesLevels `json:"maxFeesLevels"`
    38  	MaxPriorityFeePerGas *big.Int       `json:"maxPriorityFeePerGas"`
    39  	L1GasFee             *big.Float     `json:"l1GasFee,omitempty"`
    40  	EIP1559Enabled       bool           `json:"eip1559Enabled"`
    41  }
    42  
    43  // //////////////////////////////////////////////////////////////////////////////
    44  // TODO: remove `SuggestedFeesGwei` struct once new router is in place
    45  // //////////////////////////////////////////////////////////////////////////////
    46  type SuggestedFeesGwei struct {
    47  	GasPrice             *big.Float `json:"gasPrice"`
    48  	BaseFee              *big.Float `json:"baseFee"`
    49  	MaxPriorityFeePerGas *big.Float `json:"maxPriorityFeePerGas"`
    50  	MaxFeePerGasLow      *big.Float `json:"maxFeePerGasLow"`
    51  	MaxFeePerGasMedium   *big.Float `json:"maxFeePerGasMedium"`
    52  	MaxFeePerGasHigh     *big.Float `json:"maxFeePerGasHigh"`
    53  	L1GasFee             *big.Float `json:"l1GasFee,omitempty"`
    54  	EIP1559Enabled       bool       `json:"eip1559Enabled"`
    55  }
    56  
    57  func (m *MaxFeesLevels) FeeFor(mode GasFeeMode) *big.Int {
    58  	if mode == GasFeeLow {
    59  		return m.Low.ToInt()
    60  	}
    61  
    62  	if mode == GasFeeHigh {
    63  		return m.High.ToInt()
    64  	}
    65  
    66  	return m.Medium.ToInt()
    67  }
    68  
    69  func (s *SuggestedFees) FeeFor(mode GasFeeMode) *big.Int {
    70  	return s.MaxFeesLevels.FeeFor(mode)
    71  }
    72  
    73  const inclusionThreshold = 0.95
    74  
    75  type TransactionEstimation int
    76  
    77  const (
    78  	Unknown TransactionEstimation = iota
    79  	LessThanOneMinute
    80  	LessThanThreeMinutes
    81  	LessThanFiveMinutes
    82  	MoreThanFiveMinutes
    83  )
    84  
    85  type FeeHistory struct {
    86  	BaseFeePerGas []string `json:"baseFeePerGas"`
    87  }
    88  
    89  type FeeManager struct {
    90  	RPCClient *rpc.Client
    91  }
    92  
    93  func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*SuggestedFees, error) {
    94  	backend, err := f.RPCClient.EthClient(chainID)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	gasPrice, err := backend.SuggestGasPrice(ctx)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	maxPriorityFeePerGas, err := backend.SuggestGasTipCap(ctx)
   103  	if err != nil {
   104  		return &SuggestedFees{
   105  			GasPrice:             gasPrice,
   106  			BaseFee:              big.NewInt(0),
   107  			MaxPriorityFeePerGas: big.NewInt(0),
   108  			MaxFeesLevels: &MaxFeesLevels{
   109  				Low:    (*hexutil.Big)(gasPrice),
   110  				Medium: (*hexutil.Big)(gasPrice),
   111  				High:   (*hexutil.Big)(gasPrice),
   112  			},
   113  			EIP1559Enabled: false,
   114  		}, nil
   115  	}
   116  	baseFee, err := f.getBaseFee(ctx, backend)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	return &SuggestedFees{
   122  		GasPrice:             gasPrice,
   123  		BaseFee:              baseFee,
   124  		MaxPriorityFeePerGas: maxPriorityFeePerGas,
   125  		MaxFeesLevels: &MaxFeesLevels{
   126  			Low:    (*hexutil.Big)(new(big.Int).Add(baseFee, maxPriorityFeePerGas)),
   127  			Medium: (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), maxPriorityFeePerGas)),
   128  			High:   (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(3)), maxPriorityFeePerGas)),
   129  		},
   130  		EIP1559Enabled: true,
   131  	}, nil
   132  }
   133  
   134  func (f *FeeManager) SuggestedFeesGwei(ctx context.Context, chainID uint64) (*SuggestedFeesGwei, error) {
   135  	fees, err := f.SuggestedFees(ctx, chainID)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	return &SuggestedFeesGwei{
   140  		GasPrice:             common.WeiToGwei(fees.GasPrice),
   141  		BaseFee:              common.WeiToGwei(fees.BaseFee),
   142  		MaxPriorityFeePerGas: common.WeiToGwei(fees.MaxPriorityFeePerGas),
   143  		MaxFeePerGasLow:      common.WeiToGwei(fees.MaxFeesLevels.Low.ToInt()),
   144  		MaxFeePerGasMedium:   common.WeiToGwei(fees.MaxFeesLevels.Medium.ToInt()),
   145  		MaxFeePerGasHigh:     common.WeiToGwei(fees.MaxFeesLevels.High.ToInt()),
   146  		EIP1559Enabled:       fees.EIP1559Enabled,
   147  	}, nil
   148  }
   149  
   150  func (f *FeeManager) getBaseFee(ctx context.Context, client chain.ClientInterface) (*big.Int, error) {
   151  	header, err := client.HeaderByNumber(ctx, nil)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	chainID := client.NetworkID()
   157  	config := params.MainnetChainConfig
   158  	switch chainID {
   159  	case common.EthereumSepolia,
   160  		common.OptimismSepolia,
   161  		common.ArbitrumSepolia:
   162  		config = params.SepoliaChainConfig
   163  	case common.EthereumGoerli,
   164  		common.OptimismGoerli,
   165  		common.ArbitrumGoerli:
   166  		config = params.GoerliChainConfig
   167  	}
   168  	baseFee := misc.CalcBaseFee(config, header)
   169  	return baseFee, nil
   170  }
   171  
   172  func (f *FeeManager) TransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Int) TransactionEstimation {
   173  	fees, err := f.getFeeHistorySorted(chainID)
   174  	if err != nil {
   175  		return Unknown
   176  	}
   177  
   178  	// pEvent represents the probability of the transaction being included in a block,
   179  	// we assume this one is static over time, in reality it is not.
   180  	pEvent := 0.0
   181  	for idx, fee := range fees {
   182  		if fee.Cmp(maxFeePerGas) == 1 || idx == len(fees)-1 {
   183  			pEvent = float64(idx) / float64(len(fees))
   184  			break
   185  		}
   186  	}
   187  
   188  	// Probability of next 4 blocks including the transaction (less than 1 minute)
   189  	// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 4 events and in our context P(A) == P(B) == pEvent
   190  	// The factors are calculated using the combinations formula
   191  	probability := pEvent*4 - 6*(math.Pow(pEvent, 2)) + 4*(math.Pow(pEvent, 3)) - (math.Pow(pEvent, 4))
   192  	if probability >= inclusionThreshold {
   193  		return LessThanOneMinute
   194  	}
   195  
   196  	// Probability of next 12 blocks including the transaction (less than 5 minutes)
   197  	// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent
   198  	// The factors are calculated using the combinations formula
   199  	probability = pEvent*12 -
   200  		66*(math.Pow(pEvent, 2)) +
   201  		220*(math.Pow(pEvent, 3)) -
   202  		495*(math.Pow(pEvent, 4)) +
   203  		792*(math.Pow(pEvent, 5)) -
   204  		924*(math.Pow(pEvent, 6)) +
   205  		792*(math.Pow(pEvent, 7)) -
   206  		495*(math.Pow(pEvent, 8)) +
   207  		220*(math.Pow(pEvent, 9)) -
   208  		66*(math.Pow(pEvent, 10)) +
   209  		12*(math.Pow(pEvent, 11)) -
   210  		math.Pow(pEvent, 12)
   211  	if probability >= inclusionThreshold {
   212  		return LessThanThreeMinutes
   213  	}
   214  
   215  	// Probability of next 20 blocks including the transaction (less than 5 minutes)
   216  	// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent
   217  	// The factors are calculated using the combinations formula
   218  	probability = pEvent*20 -
   219  		190*(math.Pow(pEvent, 2)) +
   220  		1140*(math.Pow(pEvent, 3)) -
   221  		4845*(math.Pow(pEvent, 4)) +
   222  		15504*(math.Pow(pEvent, 5)) -
   223  		38760*(math.Pow(pEvent, 6)) +
   224  		77520*(math.Pow(pEvent, 7)) -
   225  		125970*(math.Pow(pEvent, 8)) +
   226  		167960*(math.Pow(pEvent, 9)) -
   227  		184756*(math.Pow(pEvent, 10)) +
   228  		167960*(math.Pow(pEvent, 11)) -
   229  		125970*(math.Pow(pEvent, 12)) +
   230  		77520*(math.Pow(pEvent, 13)) -
   231  		38760*(math.Pow(pEvent, 14)) +
   232  		15504*(math.Pow(pEvent, 15)) -
   233  		4845*(math.Pow(pEvent, 16)) +
   234  		1140*(math.Pow(pEvent, 17)) -
   235  		190*(math.Pow(pEvent, 18)) +
   236  		20*(math.Pow(pEvent, 19)) -
   237  		math.Pow(pEvent, 20)
   238  	if probability >= inclusionThreshold {
   239  		return LessThanFiveMinutes
   240  	}
   241  
   242  	return MoreThanFiveMinutes
   243  }
   244  
   245  func (f *FeeManager) getFeeHistorySorted(chainID uint64) ([]*big.Int, error) {
   246  	var feeHistory FeeHistory
   247  
   248  	err := f.RPCClient.Call(&feeHistory, chainID, "eth_feeHistory", 101, "latest", nil)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  
   253  	fees := []*big.Int{}
   254  	for _, fee := range feeHistory.BaseFeePerGas {
   255  		i := new(big.Int)
   256  		i.SetString(strings.Replace(fee, "0x", "", 1), 16)
   257  		fees = append(fees, i)
   258  	}
   259  
   260  	sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 })
   261  	return fees, nil
   262  }
   263  
   264  // Returns L1 fee for placing a transaction to L1 chain, appicable only for txs made from L2.
   265  func (f *FeeManager) GetL1Fee(ctx context.Context, chainID uint64, input []byte) (uint64, error) {
   266  	if chainID == common.EthereumMainnet || chainID == common.EthereumSepolia && chainID != common.EthereumGoerli {
   267  		return 0, nil
   268  	}
   269  
   270  	ethClient, err := f.RPCClient.EthClient(chainID)
   271  	if err != nil {
   272  		return 0, err
   273  	}
   274  
   275  	contractAddress, err := gaspriceoracle.ContractAddress(chainID)
   276  	if err != nil {
   277  		return 0, err
   278  	}
   279  
   280  	contract, err := gaspriceoracle.NewGaspriceoracleCaller(contractAddress, ethClient)
   281  	if err != nil {
   282  		return 0, err
   283  	}
   284  
   285  	callOpt := &bind.CallOpts{}
   286  
   287  	result, err := contract.GetL1Fee(callOpt, input)
   288  	if err != nil {
   289  		return 0, err
   290  	}
   291  
   292  	return result.Uint64(), nil
   293  }