feat(wallet)_: added suggested min and max priority fee and current base fee for the path

- `Path` type extended with the following fields:
  - `SuggestedMinPriorityFee`, suggested min priority fee by the network
  - `SuggestedMaxPriorityFee`, suggested max priority fee by the network
  - `CurrentBaseFee`, current network base fee

- The following wallet api endpoints marked as deprecated:
  - `GetSuggestedFees`
  - `GetTransactionEstimatedTime`
This commit is contained in:
Sale Djenic 2025-01-09 22:41:31 +01:00 committed by saledjenic
parent 67134d9811
commit 90f4740add
10 changed files with 567 additions and 181 deletions

View File

@ -2,7 +2,6 @@ package commands
import (
"encoding/json"
"errors"
"math/big"
"testing"
"time"
@ -13,6 +12,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/eth-node/types"
mock_client "github.com/status-im/status-go/rpc/chain/mock/client"
"github.com/status-im/status-go/services/wallet/router/fees"
"github.com/status-im/status-go/services/wallet/wallettypes"
"github.com/status-im/status-go/signal"
)
@ -82,9 +82,10 @@ func TestSendTransactionWithSignalTimout(t *testing.T) {
WalletResponseMaxInterval = 1 * time.Millisecond
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
feeHistory := &fees.FeeHistory{}
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)
@ -127,9 +128,10 @@ func TestSendTransactionWithSignalAccepted(t *testing.T) {
t.Cleanup(signal.ResetMobileSignalHandler)
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
feeHistory := &fees.FeeHistory{}
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)
@ -169,9 +171,10 @@ func TestSendTransactionWithSignalRejected(t *testing.T) {
t.Cleanup(signal.ResetMobileSignalHandler)
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
feeHistory := &fees.FeeHistory{}
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)

View File

@ -2,7 +2,6 @@ package connector
import (
"encoding/json"
"errors"
"fmt"
"math/big"
"testing"
@ -15,6 +14,7 @@ import (
"github.com/status-im/status-go/services/connector/chainutils"
"github.com/status-im/status-go/services/connector/commands"
walletCommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/router/fees"
"github.com/status-im/status-go/signal"
)
@ -104,9 +104,10 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) {
// Send transaction
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
feeHistory := &fees.FeeHistory{}
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)

View File

@ -438,6 +438,7 @@ func (api *API) FetchTokenDetails(ctx context.Context, symbols []string) (map[st
return api.s.marketManager.FetchTokenDetails(symbols)
}
// @deprecated we should remove it once clients fully switched to wallet router, `GetSuggestedRoutesAsync` should be used instead
func (api *API) GetSuggestedFees(ctx context.Context, chainID uint64) (*fees.SuggestedFeesGwei, error) {
logutils.ZapLogger().Debug("call to GetSuggestedFees")
return api.s.router.GetFeesManager().SuggestedFeesGwei(ctx, chainID)
@ -448,6 +449,7 @@ func (api *API) GetEstimatedLatestBlockNumber(ctx context.Context, chainID uint6
return api.s.blockChainState.GetEstimatedLatestBlockNumber(ctx, chainID)
}
// @deprecated we should remove it once clients fully switched to wallet router, `GetSuggestedRoutesAsync` should be used instead
func (api *API) GetTransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Float) (fees.TransactionEstimation, error) {
logutils.ZapLogger().Debug("call to getTransactionEstimatedTime")
return api.s.router.GetFeesManager().TransactionEstimatedTime(ctx, chainID, gweiToWei(maxFeePerGas)), nil

View File

@ -0,0 +1,115 @@
package fees
import (
"context"
"math"
"math/big"
"sort"
"strings"
)
const inclusionThreshold = 0.95
type TransactionEstimation int
const (
Unknown TransactionEstimation = iota
LessThanOneMinute
LessThanThreeMinutes
LessThanFiveMinutes
MoreThanFiveMinutes
)
func (f *FeeManager) TransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Int) TransactionEstimation {
feeHistory, err := f.getFeeHistory(ctx, chainID, 100, "latest", nil)
if err != nil {
return Unknown
}
return f.estimatedTime(feeHistory, maxFeePerGas)
}
func (f *FeeManager) estimatedTime(feeHistory *FeeHistory, maxFeePerGas *big.Int) TransactionEstimation {
fees, err := f.getFeeHistorySorted(feeHistory)
if err != nil || len(fees) == 0 {
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(feeHistory *FeeHistory) ([]*big.Int, error) {
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
}

View File

@ -2,16 +2,11 @@ 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/errors"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/rpc/chain"
@ -29,6 +24,8 @@ const (
var (
ErrCustomFeeModeNotAvailableInSuggestedFees = &errors.ErrorResponse{Code: errors.ErrorCode("WRF-001"), Details: "custom fee mode is not available in suggested fees"}
ErrEIP1559IncompaibleChain = &errors.ErrorResponse{Code: errors.ErrorCode("WRF-002"), Details: "EIP-1559 is not supported on this chain"}
ErrInvalidRewardData = &errors.ErrorResponse{Code: errors.ErrorCode("WRF-003"), Details: "invalid reward data"}
)
type MaxFeesLevels struct {
@ -37,13 +34,20 @@ type MaxFeesLevels struct {
High *hexutil.Big `json:"high"`
}
type MaxPriorityFeesSuggestedBounds struct {
Lower *big.Int
Upper *big.Int
}
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"`
GasPrice *big.Int
BaseFee *big.Int
CurrentBaseFee *big.Int // Current network base fee (in ETH WEI)
MaxFeesLevels *MaxFeesLevels
MaxPriorityFeePerGas *big.Int
MaxPriorityFeeSuggestedBounds *MaxPriorityFeesSuggestedBounds
L1GasFee *big.Float
EIP1559Enabled bool
}
// //////////////////////////////////////////////////////////////////////////////
@ -81,58 +85,29 @@ func (s *SuggestedFees) FeeFor(mode GasFeeMode) (*big.Int, error) {
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)
feeHistory, err := f.getFeeHistory(ctx, chainID, 300, "latest", []int{25, 50, 75})
if err != nil {
return nil, err
return f.getNonEIP1559SuggestedFees(ctx, chainID)
}
gasPrice, err := backend.SuggestGasPrice(ctx)
maxPriorityFeePerGasLowerBound, maxPriorityFeePerGas, maxPriorityFeePerGasUpperBound, baseFee, err := getEIP1559SuggestedFees(feeHistory)
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 f.getNonEIP1559SuggestedFees(ctx, chainID)
}
return &SuggestedFees{
GasPrice: gasPrice,
BaseFee: baseFee,
CurrentBaseFee: baseFee,
MaxPriorityFeePerGas: maxPriorityFeePerGas,
MaxPriorityFeeSuggestedBounds: &MaxPriorityFeesSuggestedBounds{
Lower: maxPriorityFeePerGasLowerBound,
Upper: maxPriorityFeePerGasUpperBound,
},
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)),
@ -142,6 +117,9 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges
}, nil
}
// //////////////////////////////////////////////////////////////////////////////
// TODO: remove `SuggestedFeesGwei` once mobile app fully switched to router, this function should not be exposed via api
// //////////////////////////////////////////////////////////////////////////////
func (f *FeeManager) SuggestedFeesGwei(ctx context.Context, chainID uint64) (*SuggestedFeesGwei, error) {
fees, err := f.SuggestedFees(ctx, chainID)
if err != nil {
@ -175,126 +153,3 @@ func (f *FeeManager) getBaseFee(ctx context.Context, client chain.ClientInterfac
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 {
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
}

View File

@ -0,0 +1,71 @@
package fees
import (
"context"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle"
"github.com/status-im/status-go/services/wallet/common"
)
type FeeHistory struct {
BaseFeePerGas []string `json:"baseFeePerGas"`
GasUsedRatio []float64 `json:"gasUsedRatio"`
OldestBlock string `json:"oldestBlock"`
Reward [][]string `json:"reward,omitempty"`
}
func (fh *FeeHistory) isEIP1559Compatible() bool {
if len(fh.BaseFeePerGas) == 0 {
return false
}
for _, fee := range fh.BaseFeePerGas {
if fee != "0x0" {
return true
}
}
return false
}
func (f *FeeManager) getFeeHistory(ctx context.Context, chainID uint64, blockCount uint64, newestBlock string, rewardPercentiles []int) (feeHistory *FeeHistory, err error) {
feeHistory = &FeeHistory{}
err = f.RPCClient.Call(feeHistory, chainID, "eth_feeHistory", blockCount, newestBlock, rewardPercentiles)
if err != nil {
return nil, err
}
return feeHistory, nil
}
// GetL1Fee 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 {
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
}

View File

@ -0,0 +1,169 @@
package fees
import (
"context"
"math/big"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
mock_client "github.com/status-im/status-go/rpc/chain/mock/client"
mock_rpcclient "github.com/status-im/status-go/rpc/mock/client"
)
type testState struct {
ctx context.Context
mockCtrl *gomock.Controller
rpcClient *mock_rpcclient.MockClientInterface
feeManager *FeeManager
}
func setupTest(t *testing.T) (state testState) {
state.ctx = context.Background()
state.mockCtrl = gomock.NewController(t)
state.rpcClient = mock_rpcclient.NewMockClientInterface(state.mockCtrl)
state.feeManager = &FeeManager{
RPCClient: state.rpcClient,
}
return state
}
func TestEstimatedTime(t *testing.T) {
state := setupTest(t)
// no fee history
feeHistory := &FeeHistory{}
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(100), "latest", nil).Times(1).Return(nil)
maxFeesPerGas := big.NewInt(2e9)
estimation := state.feeManager.TransactionEstimatedTime(context.Background(), uint64(1), maxFeesPerGas)
assert.Equal(t, Unknown, estimation)
// there is fee history
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(100), "latest", nil).Times(1).Return(nil).
Do(func(feeHistory, chainID, method any, args ...any) {
feeHistoryResponse := &FeeHistory{
BaseFeePerGas: []string{
"0x12f0e070b",
"0x13f10da8b",
"0x126c30d5e",
"0x136e4fe51",
"0x134180d5a",
"0x134e32c33",
"0x137da8d22",
},
}
*feeHistory.(*FeeHistory) = *feeHistoryResponse
})
maxFeesPerGas = big.NewInt(100e9)
estimation = state.feeManager.TransactionEstimatedTime(context.Background(), uint64(1), maxFeesPerGas)
assert.Equal(t, LessThanOneMinute, estimation)
}
func TestSuggestedFeesForNotEIP1559CompatibleChains(t *testing.T) {
state := setupTest(t)
chainID := uint64(1)
gasPrice := big.NewInt(1)
feeHistory := &FeeHistory{}
state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
state.rpcClient.EXPECT().EthClient(chainID).Times(1).Return(mockedChainClient, nil)
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(gasPrice, nil)
suggestedFees, err := state.feeManager.SuggestedFees(context.Background(), chainID)
assert.NoError(t, err)
assert.NotNil(t, suggestedFees)
assert.Equal(t, gasPrice, suggestedFees.GasPrice)
assert.False(t, suggestedFees.EIP1559Enabled)
}
func TestSuggestedFeesForEIP1559CompatibleChains(t *testing.T) {
state := setupTest(t)
chainID := uint64(1)
feeHistory := &FeeHistory{}
state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil).
Do(func(feeHistory, chainID, method any, args ...any) {
feeHistoryResponse := &FeeHistory{
BaseFeePerGas: []string{
"0x12f0e070b",
"0x13f10da8b",
"0x126c30d5e",
"0x136e4fe51",
"0x134180d5a",
"0x134e32c33",
"0x137da8d22",
},
GasUsedRatio: []float64{
0.7113286209349903,
0.19531163333333335,
0.7189235666666667,
0.4639678021079083,
0.5103012666666666,
0.538413,
0.16543626666666666,
},
OldestBlock: "0x1497d4b",
Reward: [][]string{
{
"0x2faf080",
"0x39d10680",
"0x722d7ef5",
},
{
"0x5f5e100",
"0x3b9aca00",
"0x59682f00",
},
{
"0x342e4a2",
"0x39d10680",
"0x77359400",
},
{
"0x14a22237",
"0x40170350",
"0x77359400",
},
{
"0x9134860",
"0x39d10680",
"0x618400ad",
},
{
"0x2faf080",
"0x39d10680",
"0x77359400",
},
{
"0x1ed69035",
"0x39d10680",
"0x41d0a8d6",
},
},
}
*feeHistory.(*FeeHistory) = *feeHistoryResponse
})
suggestedFees, err := state.feeManager.SuggestedFees(context.Background(), chainID)
assert.NoError(t, err)
assert.NotNil(t, suggestedFees)
assert.Nil(t, suggestedFees.GasPrice)
assert.Equal(t, big.NewInt(6958609414), suggestedFees.BaseFee)
assert.Equal(t, big.NewInt(6958609414), suggestedFees.CurrentBaseFee)
assert.Equal(t, big.NewInt(7928609414), suggestedFees.MaxFeesLevels.Low.ToInt())
assert.Equal(t, big.NewInt(14887218828), suggestedFees.MaxFeesLevels.Medium.ToInt())
assert.Equal(t, big.NewInt(21845828242), suggestedFees.MaxFeesLevels.High.ToInt())
assert.Equal(t, big.NewInt(970000000), suggestedFees.MaxPriorityFeePerGas)
assert.Equal(t, big.NewInt(54715554), suggestedFees.MaxPriorityFeeSuggestedBounds.Lower)
assert.Equal(t, big.NewInt(1636040877), suggestedFees.MaxPriorityFeeSuggestedBounds.Upper)
assert.True(t, suggestedFees.EIP1559Enabled)
}

View File

@ -0,0 +1,138 @@
package fees
import (
"context"
"errors"
"math/big"
"sort"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
)
const baseFeeIncreaseFactor = 1.33
func hexStringToBigInt(value string) (*big.Int, error) {
valueWitoutPrefix := strings.TrimPrefix(value, "0x")
val, success := new(big.Int).SetString(valueWitoutPrefix, 16)
if !success {
return nil, errors.New("failed to convert hex string to big.Int")
}
return val, nil
}
func scaleBaseFeePerGas(value string) (*big.Int, error) {
val, err := hexStringToBigInt(value)
if err != nil {
return nil, err
}
valueDouble := new(big.Float).SetInt(val)
valueDouble.Mul(valueDouble, big.NewFloat(baseFeeIncreaseFactor))
scaledValue := new(big.Int)
valueDouble.Int(scaledValue)
return scaledValue, nil
}
func (f *FeeManager) getNonEIP1559SuggestedFees(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
}
return &SuggestedFees{
GasPrice: gasPrice,
BaseFee: big.NewInt(0),
MaxPriorityFeePerGas: big.NewInt(0),
MaxPriorityFeeSuggestedBounds: &MaxPriorityFeesSuggestedBounds{
Lower: big.NewInt(0),
Upper: big.NewInt(0),
},
MaxFeesLevels: &MaxFeesLevels{
Low: (*hexutil.Big)(gasPrice),
Medium: (*hexutil.Big)(gasPrice),
High: (*hexutil.Big)(gasPrice),
},
EIP1559Enabled: false,
}, nil
}
// getEIP1559SuggestedFees returns suggested fees for EIP-1559 compatible chains
// source https://github.com/brave/brave-core/blob/master/components/brave_wallet/browser/eth_gas_utils.cc
func getEIP1559SuggestedFees(feeHistory *FeeHistory) (lowPriorityFee, avgPriorityFee, highPriorityFee, suggestedBaseFee *big.Int, err error) {
if feeHistory == nil || !feeHistory.isEIP1559Compatible() {
return nil, nil, nil, nil, ErrEIP1559IncompaibleChain
}
pendingBaseFee := feeHistory.BaseFeePerGas[len(feeHistory.BaseFeePerGas)-1]
suggestedBaseFee, err = scaleBaseFeePerGas(pendingBaseFee)
if err != nil {
return nil, nil, nil, nil, err
}
fallbackPriorityFee := big.NewInt(2e9) // 2 Gwei
lowPriorityFee = new(big.Int).Set(fallbackPriorityFee)
avgPriorityFee = new(big.Int).Set(fallbackPriorityFee)
highPriorityFee = new(big.Int).Set(fallbackPriorityFee)
if len(feeHistory.Reward) == 0 {
return lowPriorityFee, avgPriorityFee, highPriorityFee, suggestedBaseFee, nil
}
priorityFees := make([][]*big.Int, 3)
for i := 0; i < 3; i++ {
currentPriorityFees := []*big.Int{}
invalidData := false
for _, r := range feeHistory.Reward {
if len(r) != 3 {
invalidData = true
break
}
fee, err := hexStringToBigInt(r[i])
if err != nil {
invalidData = true
break
}
currentPriorityFees = append(currentPriorityFees, fee)
}
if invalidData {
return nil, nil, nil, nil, ErrInvalidRewardData
}
sort.Slice(currentPriorityFees, func(a, b int) bool {
return currentPriorityFees[a].Cmp(currentPriorityFees[b]) < 0
})
percentileIndex := int(float64(len(currentPriorityFees)) * 0.4)
if i == 0 {
lowPriorityFee = currentPriorityFees[percentileIndex]
} else if i == 1 {
avgPriorityFee = currentPriorityFees[percentileIndex]
} else {
highPriorityFee = currentPriorityFees[percentileIndex]
}
priorityFees[i] = currentPriorityFees
}
// Adjust low priority fee if it's equal to avg
lowIndex := int(float64(len(priorityFees[0])) * 0.4)
for lowIndex > 0 && lowPriorityFee == avgPriorityFee {
lowIndex--
lowPriorityFee = priorityFees[0][lowIndex]
}
// Adjust high priority fee if it's equal to avg
highIndex := int(float64(len(priorityFees[2])) * 0.4)
for highIndex < len(priorityFees[2])-1 && highPriorityFee == avgPriorityFee {
highIndex++
highPriorityFee = priorityFees[2][highIndex]
}
return lowPriorityFee, avgPriorityFee, highPriorityFee, suggestedBaseFee, nil
}

View File

@ -186,6 +186,19 @@ func (r *Router) applyCustomFields(ctx context.Context, path *routes.Path, fetch
r.lastInputParamsMutex.Lock()
defer r.lastInputParamsMutex.Unlock()
// set network fields
if fetchedFees.CurrentBaseFee != nil {
path.CurrentBaseFee = (*hexutil.Big)(fetchedFees.CurrentBaseFee)
}
if fetchedFees.MaxPriorityFeeSuggestedBounds != nil {
if fetchedFees.MaxPriorityFeeSuggestedBounds.Lower != nil {
path.SuggestedMinPriorityFee = (*hexutil.Big)(fetchedFees.MaxPriorityFeeSuggestedBounds.Lower)
}
if fetchedFees.MaxPriorityFeeSuggestedBounds.Upper != nil {
path.SuggestedMaxPriorityFee = (*hexutil.Big)(fetchedFees.MaxPriorityFeeSuggestedBounds.Upper)
}
}
// set appropriate nonce/s, and update later in this function if custom nonce/s are provided
err := r.resolveNonceForPath(ctx, path, r.lastInputParams.AddrFrom, usedNonces)
if err != nil {
@ -341,7 +354,11 @@ func (r *Router) evaluateAndUpdatePathDetails(ctx context.Context, path *routes.
path.TxEstimatedTime = r.feesManager.TransactionEstimatedTime(ctx, path.FromChain.ChainID, path.TxMaxFeesPerGas.ToInt())
if path.ApprovalRequired {
path.ApprovalEstimatedTime = r.feesManager.TransactionEstimatedTime(ctx, path.FromChain.ChainID, path.ApprovalMaxFeesPerGas.ToInt())
if path.TxMaxFeesPerGas.ToInt().Cmp(path.ApprovalMaxFeesPerGas.ToInt()) == 0 {
path.ApprovalEstimatedTime = path.TxEstimatedTime
} else {
path.ApprovalEstimatedTime = r.feesManager.TransactionEstimatedTime(ctx, path.FromChain.ChainID, path.ApprovalMaxFeesPerGas.ToInt())
}
}
return

View File

@ -22,7 +22,10 @@ type Path struct {
AmountInLocked bool // Is the amount locked
AmountOut *hexutil.Big // Amount that will be received on the destination chain
SuggestedLevelsForMaxFeesPerGas *fees.MaxFeesLevels // Suggested max fees for the transaction (in ETH WEI)
SuggestedLevelsForMaxFeesPerGas *fees.MaxFeesLevels // Suggested max fees by the network (in ETH WEI)
SuggestedMinPriorityFee *hexutil.Big // Suggested min priority fee by the network (in ETH WEI)
SuggestedMaxPriorityFee *hexutil.Big // Suggested max priority fee by the network (in ETH WEI)
CurrentBaseFee *hexutil.Big // Current network base fee (in ETH WEI)
TxNonce *hexutil.Uint64 // Nonce for the transaction
TxMaxFeesPerGas *hexutil.Big // Max fees per gas (determined by client via GasFeeMode, in ETH WEI)
@ -117,6 +120,18 @@ func (p *Path) Copy() *Path {
}
}
if p.SuggestedMinPriorityFee != nil {
newPath.SuggestedMinPriorityFee = (*hexutil.Big)(big.NewInt(0).Set(p.SuggestedMinPriorityFee.ToInt()))
}
if p.SuggestedMaxPriorityFee != nil {
newPath.SuggestedMaxPriorityFee = (*hexutil.Big)(big.NewInt(0).Set(p.SuggestedMaxPriorityFee.ToInt()))
}
if p.CurrentBaseFee != nil {
newPath.CurrentBaseFee = (*hexutil.Big)(big.NewInt(0).Set(p.CurrentBaseFee.ToInt()))
}
if p.TxNonce != nil {
txNonce := *p.TxNonce
newPath.TxNonce = &txNonce