-
Notifications
You must be signed in to change notification settings - Fork 0
feat: gas estimator #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
c7344bc
to
e44b74d
Compare
a7ee8ac
to
c21c8e6
Compare
21280d6
to
b6cfb2b
Compare
b6cfb2b
to
7493005
Compare
b7beaea
to
d9d02d6
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #7 +/- ##
===========================================
- Coverage 87.28% 42.98% -44.31%
===========================================
Files 5 32 +27
Lines 236 1724 +1488
===========================================
+ Hits 206 741 +535
- Misses 26 966 +940
- Partials 4 17 +13
🚀 New features to boost your workflow:
|
31c05bf
to
3a53827
Compare
|
||
// GetGasSuggestions retrieves gas fee suggestions from Infura's Gas API | ||
func (c *Client) GetGasSuggestions(ctx context.Context, networkID int) (*GasResponse, error) { | ||
url := fmt.Sprintf("%s/networks/%d/suggestedGasFees", c.baseURL, networkID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can move this pattern string as const on top of the file for more convenient editing later.
Also baseURL
used 2 times above could be a const on top.
wdyt?
gasPrice := suggestGasPrice(feeHistory, config.GasPriceEstimationBlocks) | ||
|
||
// Apply multiplier to BaseFee | ||
suggestedBaseFee := new(big.Int).Mul(gasPrice.BaseFeePerGas, big.NewInt(int64(config.BaseFeeMultiplier*1000))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: do we use multiplying and dividing to avoid using big.Float?
return big.NewInt(0) | ||
} | ||
|
||
index := (len(sortedData) * int(math.Ceil(percentile))) / 100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that this implementation tends to return bigger indexes. And the check index >= len
fixes only the last case.
If we will be looking for 10th percentile in array [10,20,30,40,50,60,70,80,90,100], it will return index 1, which will result in value 20.
Example testcase that I've used:
func TestGetPercentile_TenValues(t *testing.T) {
// Test with 10 values evenly distributed from 10 to 100
data := []*big.Int{
big.NewInt(10), big.NewInt(20), big.NewInt(30), big.NewInt(40), big.NewInt(50),
big.NewInt(60), big.NewInt(70), big.NewInt(80), big.NewInt(90), big.NewInt(100),
}
assert.Equal(t, big.NewInt(10), getPercentile(data, 10))
assert.Equal(t, big.NewInt(30), getPercentile(data, 25))
assert.Equal(t, big.NewInt(50), getPercentile(data, 50))
assert.Equal(t, big.NewInt(80), getPercentile(data, 75))
assert.Equal(t, big.NewInt(90), getPercentile(data, 90))
assert.Equal(t, big.NewInt(100), getPercentile(data, 95))
assert.Equal(t, big.NewInt(100), getPercentile(data, 99))
assert.Equal(t, big.NewInt(100), getPercentile(data, 100))
// Edge cases
assert.Equal(t, big.NewInt(10), getPercentile(data, 0)) // 0th percentile = minimum
assert.Equal(t, big.NewInt(10), getPercentile(data, 5)) // 5th percentile
}
As an alternative, here is a function with "nearest-rank" percentile methods and few more checks:
// getPercentile calculates the value at a given percentile from sorted data
// Uses the "nearest-rank" method: index = ceil(percentile/100 * n) - 1
func getPercentile(sortedData []*big.Int, percentile float64) *big.Int {
if len(sortedData) == 0 {
return big.NewInt(0)
}
n := len(sortedData)
// Handle edge cases
if percentile <= 0 {
return new(big.Int).Set(sortedData[0])
}
if percentile >= 100 {
return new(big.Int).Set(sortedData[n-1])
}
// Calculate the rank using nearest-rank method
rank := math.Ceil(percentile / 100.0 * float64(n))
// Convert to 0-based index
index := int(rank) - 1
// Ensure index is within bounds (should already be, but safety check)
if index < 0 {
index = 0
}
if index >= n {
index = n - 1
}
return new(big.Int).Set(sortedData[index])
}
) | ||
|
||
func suggestGasPrice(feeHistory *ethclient.FeeHistory, nBlocks int) *GasPrice { | ||
estimatedBaseFee := feeHistory.BaseFeePerGas[len(feeHistory.BaseFeePerGas)-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible panic if len is 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if feeHistory is empty for some reason we might want to return fallback prices
|
||
// Use congestion-based logic | ||
networkCongestion := calculateNetworkCongestionFromHistory(feeHistory, config.NetworkCongestionBlocks) | ||
congestionFactor := new(big.Float).SetFloat64(10 * networkCongestion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some magic numbers here and there. Wdyt about extracting them as constants or even moving to separate file?
// priorityfee.go:32
return big.NewInt(1000000000) // 1 gwei fallback
// suggestions_test.go:26
return 21000, nil // Default gas limit for simple transfers
// suggestions_l1.go:48
highBaseFee := new(big.Int).Mul(suggestedBaseFee, big.NewInt(2))
// suggestions_l2.go:37
highBaseFee := new(big.Int).Mul(suggestedBaseFee, big.NewInt(10))
2. Precision-related magic numbers:
// suggestions_l1.go:32-33
suggestedBaseFee := new(big.Int).Mul(gasPrice.BaseFeePerGas, big.NewInt(int64(config.BaseFeeMultiplier*1000)))
suggestedBaseFee.Div(suggestedBaseFee, big.NewInt(1000))
3. Congestion multiplier:
// suggestions_l1.go:37
congestionFactor := new(big.Float).SetFloat64(10 * networkCongestion)
Suggestion:
const (
// Gas estimation constants
DefaultGasLimit = 21000 // Standard ETH transfer gas limit
DefaultBaseFeeMultiplier = 1.025 // 2.5% buffer for base fee volatility
// Fee calculation precision
BaseFeeMultiplierPrecision = 1000 // For fixed-point arithmetic
// Priority fee multipliers
L1HighBaseFeeMultiplier = 2 // 2x base fee for L1 high priority
L2HighBaseFeeMultiplier = 10 // 10x base fee for L2 high priority
CongestionMultiplier = 10 // Congestion adjustment factor
// Fallback values (in wei)
FallbackPriorityFeeWei = 1000000000 // 1 gwei
)
|
||
medianIndex := len(sortedFees) / 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For an even number of elements, it will return the second middle element instead of the average. Suggestion:
medianIndex := len(sortedFees) / 2 | |
n := len(sortedFees) | |
if n%2 == 1 { | |
// Odd: [1,3,5] gwei -> median = 3 gwei | |
return new(big.Int).Set(sortedFees[n/2]) | |
} | |
// Even: [1,2,4,5] gwei -> median = (2+4)/2 = 3 gwei | |
median := new(big.Int).Add(sortedFees[n/2-1], sortedFees[n/2]) | |
return median.Div(median, big.NewInt(2)) | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, that's how the median value should be calculated, but for our current purpose, it doesn't matter too much.
func getL1Suggestions(ctx context.Context, ethClient EthClient, config SuggestionsConfig, callMsg *ethereum.CallMsg) (*TxSuggestions, error) { | ||
ret := &TxSuggestions{} | ||
|
||
gasLimit, err := estimateTxGas(ctx, ethClient, callMsg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need an additional estimateTxGas
function here (and in suggestions_l2.go)?
A few lines below, you call ethClient.FeeHistory
directly.
For uniformity, perhaps you could call ethClient.EstimateGas
here?
} | ||
|
||
func calculatePriorityFee(priorityFees []*big.Int) *big.Int { | ||
if len(priorityFees) == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this fallback value could be defined in SuggestionsConfig
and come here as the second parameter, defaultFee
.
@@ -0,0 +1,46 @@ | |||
package gas |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor. But maybe it's worth optimizing 90% mem allocations
package gas | |
func suggestGasPrice2(feeHistory *ethclient.FeeHistory, nBlocks int, fallbackFee *big.Int) *GasPrice { | |
if len(feeHistory.BaseFeePerGas) == 0 { | |
return &GasPrice{ | |
BaseFeePerGas: new(big.Int).Set(fallbackFee), | |
LowPriorityFeePerGas: new(big.Int).Set(fallbackFee), | |
MediumPriorityFeePerGas: new(big.Int).Set(fallbackFee), | |
HighPriorityFeePerGas: new(big.Int).Set(fallbackFee), | |
} | |
} | |
estimatedBaseFee := feeHistory.BaseFeePerGas[len(feeHistory.BaseFeePerGas)-1] | |
ret := &GasPrice{ | |
BaseFeePerGas: estimatedBaseFee, | |
LowPriorityFeePerGas: calculatePriorityFeeFromHistory2(feeHistory, nBlocks, LowPriorityFeeIndex, fallbackFee), | |
MediumPriorityFeePerGas: calculatePriorityFeeFromHistory2(feeHistory, nBlocks, MediumPriorityFeeIndex, fallbackFee), | |
HighPriorityFeePerGas: calculatePriorityFeeFromHistory2(feeHistory, nBlocks, HighPriorityFeeIndex, fallbackFee), | |
} | |
return ret | |
} | |
// calculatePriorityFeeFromHistory2 calculates priority fee directly from fee history without creating intermediate slices | |
func calculatePriorityFeeFromHistory2(feeHistory *ethclient.FeeHistory, nBlocks int, rewardsPercentileIndex int, fallbackFee *big.Int) *big.Int { | |
if len(feeHistory.Reward) == 0 { | |
return new(big.Int).Set(fallbackFee) | |
} | |
validFees := make([]*big.Int, 0, min(len(feeHistory.Reward), nBlocks)) | |
// Start from the end and work backwards, taking only nBlocks | |
startIdx := max(len(feeHistory.Reward)-nBlocks, 0) | |
for i := startIdx; i < len(feeHistory.Reward); i++ { | |
blockRewards := feeHistory.Reward[i] | |
if rewardsPercentileIndex < len(blockRewards) && blockRewards[rewardsPercentileIndex] != nil { | |
validFees = append(validFees, new(big.Int).Set(blockRewards[rewardsPercentileIndex])) | |
} | |
} | |
return calculatePriorityFee2(validFees, fallbackFee) | |
} | |
func calculatePriorityFee2(fees []*big.Int, fallbackFee *big.Int) *big.Int { | |
if len(fees) == 0 { | |
return new(big.Int).Set(fallbackFee) // fallback | |
} | |
// Sort in place | |
slices.SortFunc(fees, func(a, b *big.Int) int { | |
return a.Cmp(b) | |
}) | |
n := len(fees) | |
if n%2 == 1 { | |
return new(big.Int).Set(fees[n/2]) // Odd: middle element | |
} | |
// Even: average | |
mid1, mid2 := fees[n/2-1], fees[n/2] | |
median := new(big.Int).Add(mid1, mid2) | |
return median.Div(median, big.NewInt(2)) | |
} | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
It's easy to review small files. The examples work well.
Added a few suggestions
@@ -420,6 +420,15 @@ type withdrawalJSON struct { | |||
Amount *hexutil.Big `json:"amount"` | |||
} | |||
|
|||
// Transaction types. | |||
const ( | |||
LegacyTxType = 0x00 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We certainly depend on go-ethereum, why don't we use these constants directly from go-etherem's types
package?
|
||
medianIndex := len(sortedFees) / 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, that's how the median value should be calculated, but for our current purpose, it doesn't matter too much.
Introduce module to estimate/suggest:
Comparison output: