package fees import ( "context" "math" "math/big" "sort" "strings" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/params" gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle" "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/rpc/chain" "github.com/status-im/status-go/services/wallet/common" ) type GasFeeMode int const ( GasFeeLow GasFeeMode = iota GasFeeMedium GasFeeHigh ) type MaxFeesLevels struct { Low *hexutil.Big `json:"low"` Medium *hexutil.Big `json:"medium"` High *hexutil.Big `json:"high"` } type SuggestedFees struct { GasPrice *big.Int `json:"gasPrice"` BaseFee *big.Int `json:"baseFee"` MaxFeesLevels *MaxFeesLevels `json:"maxFeesLevels"` MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"` L1GasFee *big.Float `json:"l1GasFee,omitempty"` EIP1559Enabled bool `json:"eip1559Enabled"` } // ////////////////////////////////////////////////////////////////////////////// // TODO: remove `SuggestedFeesGwei` struct once new router is in place // ////////////////////////////////////////////////////////////////////////////// type SuggestedFeesGwei struct { GasPrice *big.Float `json:"gasPrice"` BaseFee *big.Float `json:"baseFee"` MaxPriorityFeePerGas *big.Float `json:"maxPriorityFeePerGas"` MaxFeePerGasLow *big.Float `json:"maxFeePerGasLow"` MaxFeePerGasMedium *big.Float `json:"maxFeePerGasMedium"` MaxFeePerGasHigh *big.Float `json:"maxFeePerGasHigh"` L1GasFee *big.Float `json:"l1GasFee,omitempty"` EIP1559Enabled bool `json:"eip1559Enabled"` } func (m *MaxFeesLevels) FeeFor(mode GasFeeMode) *big.Int { if mode == GasFeeLow { return m.Low.ToInt() } if mode == GasFeeHigh { return m.High.ToInt() } return m.Medium.ToInt() } func (s *SuggestedFees) FeeFor(mode GasFeeMode) *big.Int { return s.MaxFeesLevels.FeeFor(mode) } const inclusionThreshold = 0.95 type TransactionEstimation int const ( Unknown TransactionEstimation = iota LessThanOneMinute LessThanThreeMinutes LessThanFiveMinutes MoreThanFiveMinutes ) type FeeHistory struct { BaseFeePerGas []string `json:"baseFeePerGas"` } type FeeManager struct { RPCClient rpc.ClientInterface } func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*SuggestedFees, error) { backend, err := f.RPCClient.EthClient(chainID) if err != nil { return nil, err } gasPrice, err := backend.SuggestGasPrice(ctx) if err != nil { return nil, err } maxPriorityFeePerGas, err := backend.SuggestGasTipCap(ctx) if err != nil { return &SuggestedFees{ GasPrice: gasPrice, BaseFee: big.NewInt(0), MaxPriorityFeePerGas: big.NewInt(0), MaxFeesLevels: &MaxFeesLevels{ Low: (*hexutil.Big)(gasPrice), Medium: (*hexutil.Big)(gasPrice), High: (*hexutil.Big)(gasPrice), }, EIP1559Enabled: false, }, nil } baseFee, err := f.getBaseFee(ctx, backend) if err != nil { return nil, err } return &SuggestedFees{ GasPrice: gasPrice, BaseFee: baseFee, MaxPriorityFeePerGas: maxPriorityFeePerGas, MaxFeesLevels: &MaxFeesLevels{ Low: (*hexutil.Big)(new(big.Int).Add(baseFee, maxPriorityFeePerGas)), Medium: (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), maxPriorityFeePerGas)), High: (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(3)), maxPriorityFeePerGas)), }, EIP1559Enabled: true, }, nil } func (f *FeeManager) SuggestedFeesGwei(ctx context.Context, chainID uint64) (*SuggestedFeesGwei, error) { fees, err := f.SuggestedFees(ctx, chainID) if err != nil { return nil, err } return &SuggestedFeesGwei{ GasPrice: common.WeiToGwei(fees.GasPrice), BaseFee: common.WeiToGwei(fees.BaseFee), MaxPriorityFeePerGas: common.WeiToGwei(fees.MaxPriorityFeePerGas), MaxFeePerGasLow: common.WeiToGwei(fees.MaxFeesLevels.Low.ToInt()), MaxFeePerGasMedium: common.WeiToGwei(fees.MaxFeesLevels.Medium.ToInt()), MaxFeePerGasHigh: common.WeiToGwei(fees.MaxFeesLevels.High.ToInt()), EIP1559Enabled: fees.EIP1559Enabled, }, nil } func (f *FeeManager) getBaseFee(ctx context.Context, client chain.ClientInterface) (*big.Int, error) { header, err := client.HeaderByNumber(ctx, nil) if err != nil { return nil, err } chainID := client.NetworkID() config := params.MainnetChainConfig switch chainID { case common.EthereumSepolia, common.OptimismSepolia, common.ArbitrumSepolia: config = params.SepoliaChainConfig case common.EthereumGoerli, common.OptimismGoerli, common.ArbitrumGoerli: config = params.GoerliChainConfig } baseFee := misc.CalcBaseFee(config, header) return baseFee, nil } func (f *FeeManager) TransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Int) TransactionEstimation { fees, err := f.getFeeHistorySorted(chainID) if err != nil { return Unknown } // pEvent represents the probability of the transaction being included in a block, // we assume this one is static over time, in reality it is not. pEvent := 0.0 for idx, fee := range fees { if fee.Cmp(maxFeePerGas) == 1 || idx == len(fees)-1 { pEvent = float64(idx) / float64(len(fees)) break } } // Probability of next 4 blocks including the transaction (less than 1 minute) // Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 4 events and in our context P(A) == P(B) == pEvent // The factors are calculated using the combinations formula probability := pEvent*4 - 6*(math.Pow(pEvent, 2)) + 4*(math.Pow(pEvent, 3)) - (math.Pow(pEvent, 4)) if probability >= inclusionThreshold { return LessThanOneMinute } // Probability of next 12 blocks including the transaction (less than 5 minutes) // Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent // The factors are calculated using the combinations formula probability = pEvent*12 - 66*(math.Pow(pEvent, 2)) + 220*(math.Pow(pEvent, 3)) - 495*(math.Pow(pEvent, 4)) + 792*(math.Pow(pEvent, 5)) - 924*(math.Pow(pEvent, 6)) + 792*(math.Pow(pEvent, 7)) - 495*(math.Pow(pEvent, 8)) + 220*(math.Pow(pEvent, 9)) - 66*(math.Pow(pEvent, 10)) + 12*(math.Pow(pEvent, 11)) - math.Pow(pEvent, 12) if probability >= inclusionThreshold { return LessThanThreeMinutes } // Probability of next 20 blocks including the transaction (less than 5 minutes) // Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent // The factors are calculated using the combinations formula probability = pEvent*20 - 190*(math.Pow(pEvent, 2)) + 1140*(math.Pow(pEvent, 3)) - 4845*(math.Pow(pEvent, 4)) + 15504*(math.Pow(pEvent, 5)) - 38760*(math.Pow(pEvent, 6)) + 77520*(math.Pow(pEvent, 7)) - 125970*(math.Pow(pEvent, 8)) + 167960*(math.Pow(pEvent, 9)) - 184756*(math.Pow(pEvent, 10)) + 167960*(math.Pow(pEvent, 11)) - 125970*(math.Pow(pEvent, 12)) + 77520*(math.Pow(pEvent, 13)) - 38760*(math.Pow(pEvent, 14)) + 15504*(math.Pow(pEvent, 15)) - 4845*(math.Pow(pEvent, 16)) + 1140*(math.Pow(pEvent, 17)) - 190*(math.Pow(pEvent, 18)) + 20*(math.Pow(pEvent, 19)) - math.Pow(pEvent, 20) if probability >= inclusionThreshold { return LessThanFiveMinutes } return MoreThanFiveMinutes } func (f *FeeManager) getFeeHistorySorted(chainID uint64) ([]*big.Int, error) { var feeHistory FeeHistory err := f.RPCClient.Call(&feeHistory, chainID, "eth_feeHistory", 101, "latest", nil) if err != nil { return nil, err } fees := []*big.Int{} for _, fee := range feeHistory.BaseFeePerGas { i := new(big.Int) i.SetString(strings.Replace(fee, "0x", "", 1), 16) fees = append(fees, i) } sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 }) return fees, nil } // Returns L1 fee for placing a transaction to L1 chain, appicable only for txs made from L2. func (f *FeeManager) GetL1Fee(ctx context.Context, chainID uint64, input []byte) (uint64, error) { if chainID == common.EthereumMainnet || chainID == common.EthereumSepolia && chainID != common.EthereumGoerli { return 0, nil } ethClient, err := f.RPCClient.EthClient(chainID) if err != nil { return 0, err } contractAddress, err := gaspriceoracle.ContractAddress(chainID) if err != nil { return 0, err } contract, err := gaspriceoracle.NewGaspriceoracleCaller(contractAddress, ethClient) if err != nil { return 0, err } callOpt := &bind.CallOpts{} result, err := contract.GetL1Fee(callOpt, input) if err != nil { return 0, err } return result.Uint64(), nil }