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 }