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 088e351 commit f9b00ad
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 73 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 @@ -465,6 +465,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
}
180 changes: 169 additions & 11 deletions services/wallet/router/fees/estimated_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
"math"
"math/big"
"sort"
"strings"

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

const inclusionThreshold = 0.95
Expand All @@ -20,6 +21,18 @@ const (
MoreThanFiveMinutes
)

const (
EstimatedTimeUnknown = 0
EstimatedTime5Sec = 5
EstimatedTime10Sec = 10
EstimatedTime15Sec = 15
EstimatedTime20Sec = 20
EstimatedTime30Sec = 30
EstimatedTime45Sec = 45
EstimatedTime1Min = 60
EstimatedTimeMoreThan1Min = 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 +43,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 +115,159 @@ 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
}

// 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 {
blockCount := uint64(10) // use the last 10 blocks for L1 chains
if chainID != common.EthereumMainnet && chainID != common.EthereumSepolia {
blockCount = 50 // use the last 50 blocks for L2 chains
}
feeHistory, err := f.getFeeHistory(ctx, chainID, blockCount, "latest", []int{RewardPercentiles2})
if err != nil {
return 0
}

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

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 (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)) * 0.6)
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)) * 0.5)
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)) * 0.4)
priorityFeeForThirdTwoBlocks := mediumPriorityFeesSorted[priorityFeePercentileIndex]

// To include the transaction in the next block its base fee has to be in a higher than 80 percentile
// of the base fees in the last 100 blocks (corrected by the network congestion)
baseFeePercentileIndex := int(float64(len(sortedBaseFees)) * 0.8 * networkCongestion)
// index correction
if baseFeePercentileIndex >= len(sortedBaseFees) {
baseFeePercentileIndex = len(sortedBaseFees) - 1
}
// check if the transaction is included in the next block
if txPriorityFee.Cmp(priorityFeeForFirstTwoBlock) >= 0 && txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 {
return calculateTimeForInclusion(chainID, 1)
}

// To include the transaction in the next 2 blocks its base fee has to be in a higher than 70 percentile
// of the base fees in the last 100 blocks (corrected by the network congestion)
baseFeePercentileIndex = int(float64(len(sortedBaseFees)) * 0.7 * networkCongestion)
// index correction
if baseFeePercentileIndex >= len(sortedBaseFees) {
baseFeePercentileIndex = len(sortedBaseFees) - 1
}
// check if the transaction is included in the next 2 blocks
if txPriorityFee.Cmp(priorityFeeForFirstTwoBlock) >= 0 && txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 {
return calculateTimeForInclusion(chainID, 2)
}

// To include the transaction in the next 3 blocks its base fee has to be in a higher than 60 percentile
// of the base fees in the last 100 blocks (corrected by the network congestion)
baseFeePercentileIndex = int(float64(len(sortedBaseFees)) * 0.6 * networkCongestion)
// index correction
if baseFeePercentileIndex >= len(sortedBaseFees) {
baseFeePercentileIndex = len(sortedBaseFees) - 1
}
// check if the transaction is included in the next 3 blocks
if txPriorityFee.Cmp(priorityFeeForSecondTwoBlocks) >= 0 && txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 {
return calculateTimeForInclusion(chainID, 3)
}

// To include the transaction in the next 4 blocks its base fee has to be in a higher than 50 percentile
// of the base fees in the last 100 blocks (corrected by the network congestion)
baseFeePercentileIndex = int(float64(len(sortedBaseFees)) * 0.5 * networkCongestion)
// index correction
if baseFeePercentileIndex >= len(sortedBaseFees) {
baseFeePercentileIndex = len(sortedBaseFees) - 1
}
// check if the transaction is included in the next 4 blocks
if txPriorityFee.Cmp(priorityFeeForSecondTwoBlocks) >= 0 && txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 {
return calculateTimeForInclusion(chainID, 4)
}

// To include the transaction in the next 5 blocks its base fee has to be in a higher than 40 percentile
// of the base fees in the last 100 blocks (corrected by the network congestion)
baseFeePercentileIndex = int(float64(len(sortedBaseFees)) * 0.4 * networkCongestion)
// index correction
if baseFeePercentileIndex >= len(sortedBaseFees) {
baseFeePercentileIndex = len(sortedBaseFees) - 1
}
// check if the transaction is included in the next 5 blocks
if txPriorityFee.Cmp(priorityFeeForThirdTwoBlocks) >= 0 && txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 {
return calculateTimeForInclusion(chainID, 5)
}

// To include the transaction in the next 6 blocks its base fee has to be in a higher than 30 percentile
// of the base fees in the last 100 blocks (corrected by the network congestion)
baseFeePercentileIndex = int(float64(len(sortedBaseFees)) * 0.3 * networkCongestion)
// index correction
if baseFeePercentileIndex >= len(sortedBaseFees) {
baseFeePercentileIndex = len(sortedBaseFees) - 1
}
// check if the transaction is included in the next 6 blocks
if txPriorityFee.Cmp(priorityFeeForThirdTwoBlocks) >= 0 && txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 {
return calculateTimeForInclusion(chainID, 6)
}

sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 })
return fees, nil
return EstimatedTimeMoreThan1Min
}
51 changes: 29 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,10 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges
}
}

suggestedFees.MaxFeesLevels.LowEstimatedTime = f.TransactionEstimatedTimeV2(ctx, chainID, suggestedFees.MaxFeesLevels.Low.ToInt(), suggestedFees.MaxFeesLevels.LowPriority.ToInt())
suggestedFees.MaxFeesLevels.MediumEstimatedTime = f.TransactionEstimatedTimeV2(ctx, chainID, suggestedFees.MaxFeesLevels.Medium.ToInt(), suggestedFees.MaxFeesLevels.MediumPriority.ToInt())
suggestedFees.MaxFeesLevels.HighEstimatedTime = f.TransactionEstimatedTimeV2(ctx, chainID, suggestedFees.MaxFeesLevels.High.ToInt(), suggestedFees.MaxFeesLevels.HighPriority.ToInt())

return suggestedFees, nil
}

Expand Down
Loading

0 comments on commit f9b00ad

Please sign in to comment.