Skip to content

Commit

Permalink
feat(wallet)_: calculating tx estimated time
Browse files Browse the repository at this point in the history
  • Loading branch information
saledjenic committed Feb 17, 2025
1 parent b2cd7bd commit facec0d
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 95 deletions.
2 changes: 1 addition & 1 deletion services/connector/commands/send_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (c *SendTransactionCommand) Execute(ctx context.Context, request RPCRequest
if !fetchedFees.EIP1559Enabled {
params.GasPrice = (*hexutil.Big)(fetchedFees.GasPrice)
} else {
maxFees, priorityFee, err := fetchedFees.FeeFor(fees.GasFeeMedium)
maxFees, priorityFee, _, err := fetchedFees.FeeFor(fees.GasFeeMedium)
if err != nil {
return "", err
}
Expand Down
5 changes: 5 additions & 0 deletions services/wallet/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,11 @@ func (api *API) GetTransactionEstimatedTime(ctx context.Context, chainID uint64,
return api.s.router.GetFeesManager().TransactionEstimatedTime(ctx, chainID, gweiToWei(maxFeePerGas)), nil
}

func (api *API) GetTransactionEstimatedTimeV2(ctx context.Context, chainID uint64, maxFeePerGas *hexutil.Big, maxPriorityFeePerGas *hexutil.Big) (uint, error) {
logutils.ZapLogger().Debug("call to getTransactionEstimatedTimeV2")
return api.s.router.GetFeesManager().TransactionEstimatedTimeV2(ctx, chainID, maxFeePerGas.ToInt(), maxPriorityFeePerGas.ToInt()), nil
}

func gweiToWei(val *big.Float) *big.Int {
res, _ := new(big.Float).Mul(val, big.NewFloat(1000000000)).Int(nil)
return res
Expand Down
9 changes: 9 additions & 0 deletions services/wallet/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"math/big"
"reflect"
"time"

gethParams "github.com/ethereum/go-ethereum/params"
"github.com/status-im/status-go/params"
Expand Down Expand Up @@ -75,3 +76,11 @@ func WeiToGwei(val *big.Int) *big.Float {

return result.Quo(result, new(big.Float).SetInt(unit))
}

func GetBlockCreationTimeForChain(chainID uint64) time.Duration {
blockDuration, found := AverageBlockDurationForChain[ChainID(chainID)]
if !found {
blockDuration = AverageBlockDurationForChain[ChainID(UnknownChainID)]
}
return blockDuration
}
160 changes: 148 additions & 12 deletions services/wallet/router/fees/estimated_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,24 @@ import (
"math"
"math/big"
"sort"
"strings"

"github.com/status-im/status-go/services/wallet/common"
)

const inclusionThreshold = 0.95
const (
inclusionThreshold = 0.95

priorityFeePercentileHigh = 0.6
priorityFeePercentileMedium = 0.5
priorityFeePercentileLow = 0.4

baseFeePercentileFirstBlock = 0.8
baseFeePercentileSecondBlock = 0.7
baseFeePercentileThirdBlock = 0.6
baseFeePercentileFourthBlock = 0.5
baseFeePercentileFifthBlock = 0.4
baseFeePercentileSixthBlock = 0.3
)

type TransactionEstimation int

Expand All @@ -20,6 +34,18 @@ const (
MoreThanFiveMinutes
)

const (
EstimatedTimeUnknown = uint(0)
EstimatedTime5Sec = uint(5)
EstimatedTime10Sec = uint(10)
EstimatedTime15Sec = uint(15)
EstimatedTime20Sec = uint(20)
EstimatedTime30Sec = uint(30)
EstimatedTime45Sec = uint(45)
EstimatedTime1Min = uint(60)
EstimatedTimeMoreThan1Min = uint(61) // the client should display "more than 1 minute" if the expected inclusion time is more than 61 seconds
)

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 {
Expand All @@ -30,8 +56,8 @@ func (f *FeeManager) TransactionEstimatedTime(ctx context.Context, chainID uint6
}

func (f *FeeManager) estimatedTime(feeHistory *FeeHistory, maxFeePerGas *big.Int) TransactionEstimation {
fees, err := f.getFeeHistorySorted(feeHistory)
if err != nil || len(fees) == 0 {
fees := f.convertToBigIntAndSort(feeHistory.BaseFeePerGas)
if len(fees) == 0 {
return Unknown
}

Expand Down Expand Up @@ -102,14 +128,124 @@ func (f *FeeManager) estimatedTime(feeHistory *FeeHistory, maxFeePerGas *big.Int
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)
func (f *FeeManager) convertToBigIntAndSort(hexArray []string) []*big.Int {
values := []*big.Int{}
for _, sValue := range hexArray {
iValue := new(big.Int)
_, ok := iValue.SetString(sValue, 0)
if !ok {
continue
}
values = append(values, iValue)
}

sort.Slice(values, func(i, j int) bool { return values[i].Cmp(values[j]) < 0 })
return values
}

func (f *FeeManager) getFeeHistoryForTimeEstimation(ctx context.Context, chainID uint64) (*FeeHistory, error) {
blockCount := uint64(10) // use the last 10 blocks for L1 chains
if chainID != common.EthereumMainnet &&
chainID != common.EthereumSepolia &&
chainID != common.AnvilMainnet {
blockCount = 50 // use the last 50 blocks for L2 chains
}
return f.getFeeHistory(ctx, chainID, blockCount, "latest", []int{RewardPercentiles2})
}

func calculateTimeForInclusion(chainID uint64, expectedInclusionInBlock int) uint {
blockCreationTime := common.GetBlockCreationTimeForChain(chainID)
blockCreationTimeInSeconds := uint(blockCreationTime.Seconds())

expectedInclusionTime := uint(expectedInclusionInBlock) * blockCreationTimeInSeconds
if expectedInclusionTime < EstimatedTime5Sec {
return EstimatedTime5Sec
} else if expectedInclusionTime < EstimatedTime10Sec {
return EstimatedTime10Sec
} else if expectedInclusionTime < EstimatedTime15Sec {
return EstimatedTime15Sec
} else if expectedInclusionTime < EstimatedTime20Sec {
return EstimatedTime20Sec
} else if expectedInclusionTime < EstimatedTime30Sec {
return EstimatedTime30Sec
} else if expectedInclusionTime < EstimatedTime45Sec {
return EstimatedTime45Sec
} else if expectedInclusionTime < EstimatedTime1Min {
return EstimatedTime1Min
}

return EstimatedTimeMoreThan1Min
}

func getBaseFeePercentileIndex(sortedBaseFees []*big.Int, percentile float64, networkCongestion float64) int {
// calculate the index of the base fee for the given percentile corrected by the network congestion
index := int(float64(len(sortedBaseFees)) * percentile * networkCongestion)
if index >= len(sortedBaseFees) {
return len(sortedBaseFees) - 1
}
return index
}

// TransactionEstimatedTimeV2 returns the estimated time in seconds for a transaction to be included in a block
func (f *FeeManager) TransactionEstimatedTimeV2(ctx context.Context, chainID uint64, maxFeePerGas *big.Int, priorityFee *big.Int) uint {
feeHistory, err := f.getFeeHistoryForTimeEstimation(ctx, chainID)
if err != nil {
return 0
}

return f.estimatedTimeV2(feeHistory, maxFeePerGas, priorityFee, chainID)
}

func (f *FeeManager) estimatedTimeV2(feeHistory *FeeHistory, txMaxFeePerGas *big.Int, txPriorityFee *big.Int, chainID uint64) uint {
sortedBaseFees := f.convertToBigIntAndSort(feeHistory.BaseFeePerGas)
if len(sortedBaseFees) == 0 {
return EstimatedTimeUnknown
}

var mediumPriorityFees []string // based on 50th percentile in the last 100 blocks
for _, fee := range feeHistory.Reward {
mediumPriorityFees = append(mediumPriorityFees, fee[0])
}
mediumPriorityFeesSorted := f.convertToBigIntAndSort(mediumPriorityFees)
if len(mediumPriorityFeesSorted) == 0 {
return EstimatedTimeUnknown
}

txBaseFee := new(big.Int).Sub(txMaxFeePerGas, txPriorityFee)

networkCongestion := calculateNetworkCongestion(feeHistory)

// Priority fee for the first two blocks has to be higher than 60th percentile of the mediumPriorityFeesSorted
priorityFeePercentileIndex := int(float64(len(mediumPriorityFeesSorted)) * priorityFeePercentileHigh)
priorityFeeForFirstTwoBlock := mediumPriorityFeesSorted[priorityFeePercentileIndex]
// Priority fee for the second two blocks has to be higher than 50th percentile of the mediumPriorityFeesSorted
priorityFeePercentileIndex = int(float64(len(mediumPriorityFeesSorted)) * priorityFeePercentileMedium)
priorityFeeForSecondTwoBlocks := mediumPriorityFeesSorted[priorityFeePercentileIndex]
// Priority fee for the third two blocks has to be higher than 40th percentile of the mediumPriorityFeesSorted
priorityFeePercentileIndex = int(float64(len(mediumPriorityFeesSorted)) * priorityFeePercentileLow)
priorityFeeForThirdTwoBlocks := mediumPriorityFeesSorted[priorityFeePercentileIndex]

// To include the transaction in the block `inclusionInBlock` its base fee has to be in a higher than `baseFeePercentile`
// and its priority fee has to be higher than the `priorityFee`
inclusions := []struct {
inclusionInBlock int
baseFeePercentile float64
priorityFee *big.Int
}{
{1, baseFeePercentileFirstBlock, priorityFeeForFirstTwoBlock},
{2, baseFeePercentileSecondBlock, priorityFeeForFirstTwoBlock},
{3, baseFeePercentileThirdBlock, priorityFeeForSecondTwoBlocks},
{4, baseFeePercentileFourthBlock, priorityFeeForSecondTwoBlocks},
{5, baseFeePercentileFifthBlock, priorityFeeForThirdTwoBlocks},
{6, baseFeePercentileSixthBlock, priorityFeeForThirdTwoBlocks},
}

for _, p := range inclusions {
baseFeePercentileIndex := getBaseFeePercentileIndex(sortedBaseFees, p.baseFeePercentile, networkCongestion)
if txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 && txPriorityFee.Cmp(p.priorityFee) >= 0 {
return calculateTimeForInclusion(chainID, p.inclusionInBlock)
}
}

sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 })
return fees, nil
return EstimatedTimeMoreThan1Min
}
55 changes: 33 additions & 22 deletions services/wallet/router/fees/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,31 @@ var (
)

type MaxFeesLevels struct {
Low *hexutil.Big `json:"low"`
LowPriority *hexutil.Big `json:"lowPriority"`
Medium *hexutil.Big `json:"medium"`
MediumPriority *hexutil.Big `json:"mediumPriority"`
High *hexutil.Big `json:"high"`
HighPriority *hexutil.Big `json:"highPriority"`
Low *hexutil.Big `json:"low"` // Low max fee per gas in WEI
LowPriority *hexutil.Big `json:"lowPriority"` // Low priority fee in WEI
LowEstimatedTime uint `json:"lowEstimatedTime"` // Estimated time for low fees in seconds
Medium *hexutil.Big `json:"medium"` // Medium max fee per gas in WEI
MediumPriority *hexutil.Big `json:"mediumPriority"` // Medium priority fee in WEI
MediumEstimatedTime uint `json:"mediumEstimatedTime"` // Estimated time for medium fees in seconds
High *hexutil.Big `json:"high"` // High max fee per gas in WEI
HighPriority *hexutil.Big `json:"highPriority"` // High priority fee in WEI
HighEstimatedTime uint `json:"highEstimatedTime"` // Estimated time for high fees in seconds
}

type MaxPriorityFeesSuggestedBounds struct {
Lower *big.Int
Upper *big.Int
Lower *big.Int // Lower bound for priority fee per gas in WEI
Upper *big.Int // Upper bound for priority fee per gas in WEI
}

type SuggestedFees struct {
GasPrice *big.Int
BaseFee *big.Int
CurrentBaseFee *big.Int // Current network base fee (in ETH WEI)
MaxFeesLevels *MaxFeesLevels
MaxPriorityFeePerGas *big.Int // TODO: remove once clients stop using this field
MaxPriorityFeeSuggestedBounds *MaxPriorityFeesSuggestedBounds
L1GasFee *big.Float
EIP1559Enabled bool
GasPrice *big.Int // TODO: remove once clients stop using this field, used for EIP-1559 incompatible chains, not in use anymore
BaseFee *big.Int // TODO: remove once clients stop using this field, current network base fee (in ETH WEI), kept for backward compatibility
CurrentBaseFee *big.Int // Current network base fee (in ETH WEI)
MaxFeesLevels *MaxFeesLevels // Max fees levels for low, medium and high fee modes
MaxPriorityFeePerGas *big.Int // TODO: remove once clients stop using this field, kept for backward compatibility
MaxPriorityFeeSuggestedBounds *MaxPriorityFeesSuggestedBounds // Lower and upper bounds for priority fee per gas in WEI
L1GasFee *big.Float // TODO: remove once clients stop using this field, not in use anymore
EIP1559Enabled bool // TODO: remove it since all chains we have support EIP-1559
}

// //////////////////////////////////////////////////////////////////////////////
Expand All @@ -71,23 +74,23 @@ type SuggestedFeesGwei struct {
EIP1559Enabled bool `json:"eip1559Enabled"`
}

func (m *MaxFeesLevels) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, error) {
func (m *MaxFeesLevels) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, uint, error) {
if mode == GasFeeCustom {
return nil, nil, ErrCustomFeeModeNotAvailableInSuggestedFees
return nil, nil, 0, ErrCustomFeeModeNotAvailableInSuggestedFees
}

if mode == GasFeeLow {
return m.Low.ToInt(), m.LowPriority.ToInt(), nil
return m.Low.ToInt(), m.LowPriority.ToInt(), m.LowEstimatedTime, nil
}

if mode == GasFeeHigh {
return m.High.ToInt(), m.HighPriority.ToInt(), nil
return m.High.ToInt(), m.HighPriority.ToInt(), m.MediumEstimatedTime, nil
}

return m.Medium.ToInt(), m.MediumPriority.ToInt(), nil
return m.Medium.ToInt(), m.MediumPriority.ToInt(), m.HighEstimatedTime, nil
}

func (s *SuggestedFees) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, error) {
func (s *SuggestedFees) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, uint, error) {
return s.MaxFeesLevels.FeeFor(mode)
}

Expand Down Expand Up @@ -150,6 +153,14 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges
}
}

feeHistory, err = f.getFeeHistoryForTimeEstimation(ctx, chainID)
if err != nil {
return nil, err
}
suggestedFees.MaxFeesLevels.LowEstimatedTime = f.estimatedTimeV2(feeHistory, suggestedFees.MaxFeesLevels.Low.ToInt(), suggestedFees.MaxFeesLevels.LowPriority.ToInt(), chainID)
suggestedFees.MaxFeesLevels.MediumEstimatedTime = f.estimatedTimeV2(feeHistory, suggestedFees.MaxFeesLevels.Medium.ToInt(), suggestedFees.MaxFeesLevels.MediumPriority.ToInt(), chainID)
suggestedFees.MaxFeesLevels.HighEstimatedTime = f.estimatedTimeV2(feeHistory, suggestedFees.MaxFeesLevels.High.ToInt(), suggestedFees.MaxFeesLevels.HighPriority.ToInt(), chainID)

return suggestedFees, nil
}

Expand Down
Loading

0 comments on commit facec0d

Please sign in to comment.