Sale Djenic 00559692bc chore!: router code organization improvements
It's a breaking change due to errors' changes, the list of mapped old error codes:
- `"WR-001"` is now `"WRR-001"`
- `"WR-002"` is now `"WRR-002"`
- `"WR-003"` is now `"WRR-003"`
- `"WR-004"` is now `"WRR-004"`
- `"WR-005"` is now `"WRR-005"`
- `"WR-006"` is now `"WRR-006"`
- `"WR-007"` is now `"WRR-007"`
- `"WR-008"` is now `"WRR-008"`
- `"WR-009"` is now `"WRR-009"`
- `"WR-010"` is now `"WRR-010"`
- `"WR-011"` is now `"WRR-011"`
- `"WR-012"` is now `"WRR-012"`
- `"WR-013"` is now `"WRR-013"`
- `"WR-014"` is now `"WRR-014"`
- `"WR-015"` is now `"WRR-015"`
- `"WR-019"` is now `"WRR-016"`
- `"WR-020"` is now `"WRR-017"`
- `"WR-021"` is now `"WRR-018"`
- `"WR-025"` is now `"WRR-019"`

- `"WR-016"` is now `"WR-001"`
- `"WR-017"` is now `"WR-002"`
- `"WR-018"` is now `"WR-003"`
- `"WR-022"` is now `"WR-004"`
- `"WR-023"` is now `"WR-005"`
- `"WR-024"` is now `"WR-006"`
- `"WR-026"` is now `"WR-007"`
- `"WR-027"` is now `"WR-008"`

Other changes:
- `RouteInputParams` type moved to `requests` package and code updated accordingly
- `SuggestedFees` type moved to a new `fees` package and code updated accordingly
- `SendType` type moved to a new `sendtype` package and code updated accordingly
- the following functions moved to `common` package
  - `ArrayContainsElement`
  - `ArraysWithSameElements`
  - `SameSingleChainTransfer`
  - `CopyMapGeneric`
  - `GweiToEth`
  - `WeiToGwei`
- the following consts moved to `common` package
  - `HexAddressLength`
  - `SupportedNetworks`
  - `SupportedTestNetworks`
2024-09-11 13:51:51 +02:00

294 lines
8.7 KiB
Go

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.Client
}
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
}