362 lines
10 KiB
Go
362 lines
10 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
|
"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
|
|
)
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// TODO: remove `SuggestedFees` struct once new router is in place
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
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"`
|
|
L1GasFee *big.Float `json:"l1GasFee,omitempty"`
|
|
EIP1559Enabled bool `json:"eip1559Enabled"`
|
|
}
|
|
|
|
type PriorityFees struct {
|
|
Low *big.Int `json:"low"`
|
|
Medium *big.Int `json:"medium"`
|
|
High *big.Int `json:"high"`
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// TODO: remove `suggestedFees` function once new router is in place
|
|
//
|
|
// But we should check the client since this function is exposed to API as `GetSuggestedFees` call.
|
|
// Maybe we should keep it and remove it later when the client is ready for that change.
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
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
|
|
}
|
|
|
|
baseFee, err := f.getBaseFee(ctx, backend)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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) 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:
|
|
case common.OptimismSepolia:
|
|
case common.ArbitrumSepolia:
|
|
config = params.SepoliaChainConfig
|
|
case common.EthereumGoerli:
|
|
case common.OptimismGoerli:
|
|
case common.ArbitrumGoerli:
|
|
config = params.GoerliChainConfig
|
|
}
|
|
|
|
baseFee := misc.CalcBaseFee(config, header)
|
|
return baseFee, nil
|
|
}
|
|
|
|
func (f *FeeManager) getPriorityFees(ctx context.Context, client chain.ClientInterface, baseFee *big.Int) (PriorityFees, error) {
|
|
var priorityFee PriorityFees
|
|
fees, err := f.getFeeHistorySorted(client.NetworkID())
|
|
if err != nil {
|
|
return priorityFee, err
|
|
}
|
|
|
|
suggestedPriorityFee, err := client.SuggestGasTipCap(ctx)
|
|
if err != nil {
|
|
return priorityFee, err
|
|
}
|
|
|
|
// Calculate Low priority fee
|
|
priorityFee.Low = fees[int64(0.1*float64(len(fees)))-1]
|
|
|
|
// Calculate Medium priority fee
|
|
priorityFee.Medium = fees[int64(0.2*float64(len(fees)))-1]
|
|
|
|
if baseFee.Cmp(priorityFee.Medium) > 0 {
|
|
priorityFee.Medium = baseFee
|
|
}
|
|
|
|
if suggestedPriorityFee.Cmp(priorityFee.Medium) > 0 {
|
|
priorityFee.Medium = suggestedPriorityFee
|
|
}
|
|
|
|
// Calculate High priority fee
|
|
priorityFee.High = new(big.Int).Mul(suggestedPriorityFee, big.NewInt(2))
|
|
twoTimesBaseFee := new(big.Int).Mul(baseFee, big.NewInt(2))
|
|
if twoTimesBaseFee.Cmp(priorityFee.High) > 0 {
|
|
priorityFee.High = twoTimesBaseFee
|
|
}
|
|
|
|
return priorityFee, 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
|
|
}
|
|
|
|
// 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
|
|
}
|