package wallet import ( "context" "math" "math/big" "sort" "strings" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/params" "github.com/status-im/status-go/rpc" ) type GasFeeMode int const ( GasFeeLow GasFeeMode = iota GasFeeMedium GasFeeHigh ) type SuggestedFees 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"` EIP1559Enabled bool `json:"eip1559Enabled"` } func (s *SuggestedFees) feeFor(mode GasFeeMode) *big.Float { if !s.EIP1559Enabled { return s.GasPrice } if mode == GasFeeLow { return s.MaxFeePerGasLow } if mode == GasFeeHigh { return s.MaxFeePerGasHigh } return s.MaxFeePerGasMedium } 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.Client } func weiToGwei(val *big.Int) *big.Float { result := new(big.Float) result.SetInt(val) unit := new(big.Int) unit.SetInt64(params.GWei) return result.Quo(result, new(big.Float).SetInt(unit)) } func gweiToEth(val *big.Float) *big.Float { return new(big.Float).Quo(val, big.NewFloat(1000000000)) } func gweiToWei(val *big.Float) *big.Int { res, _ := new(big.Float).Mul(val, big.NewFloat(1000000000)).Int(nil) return res } 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: weiToGwei(gasPrice), BaseFee: big.NewFloat(0), MaxPriorityFeePerGas: big.NewFloat(0), MaxFeePerGasLow: big.NewFloat(0), MaxFeePerGasMedium: big.NewFloat(0), MaxFeePerGasHigh: big.NewFloat(0), EIP1559Enabled: false, }, nil } block, err := backend.BlockByNumber(ctx, nil) if err != nil { return nil, err } config := params.MainnetChainConfig baseFee := misc.CalcBaseFee(config, block.Header()) fees, err := f.getFeeHistorySorted(chainID) if err != nil { return &SuggestedFees{ GasPrice: weiToGwei(gasPrice), BaseFee: weiToGwei(baseFee), MaxPriorityFeePerGas: weiToGwei(maxPriorityFeePerGas), MaxFeePerGasLow: weiToGwei(maxPriorityFeePerGas), MaxFeePerGasMedium: weiToGwei(maxPriorityFeePerGas), MaxFeePerGasHigh: weiToGwei(maxPriorityFeePerGas), EIP1559Enabled: false, }, nil } perc10 := fees[int64(0.1*float64(len(fees)))-1] perc20 := fees[int64(0.2*float64(len(fees)))-1] var maxFeePerGasMedium *big.Int if baseFee.Cmp(perc20) >= 0 { maxFeePerGasMedium = baseFee } else { maxFeePerGasMedium = perc20 } if maxPriorityFeePerGas.Cmp(maxFeePerGasMedium) > 0 { maxFeePerGasMedium = maxPriorityFeePerGas } maxFeePerGasHigh := new(big.Int).Mul(maxPriorityFeePerGas, big.NewInt(2)) twoTimesBaseFee := new(big.Int).Mul(baseFee, big.NewInt(2)) if twoTimesBaseFee.Cmp(maxFeePerGasHigh) > 0 { maxFeePerGasHigh = twoTimesBaseFee } return &SuggestedFees{ GasPrice: weiToGwei(gasPrice), BaseFee: weiToGwei(baseFee), MaxPriorityFeePerGas: weiToGwei(maxPriorityFeePerGas), MaxFeePerGasLow: weiToGwei(perc10), MaxFeePerGasMedium: weiToGwei(maxFeePerGasMedium), MaxFeePerGasHigh: weiToGwei(maxFeePerGasHigh), EIP1559Enabled: true, }, nil } func (f *FeeManager) transactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Float) TransactionEstimation { fees, err := f.getFeeHistorySorted(chainID) if err != nil { return Unknown } maxFeePerGasWei := gweiToWei(maxFeePerGas) // 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(maxFeePerGasWei) == 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 }