1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
//! This file contains a gas estimator struct one that can be generally used in any case where
//! waiting for lower than average gas prices is an advantage.
use crate::client::Web3;
use clarity::Uint256;
use std::cmp::Ordering;
use std::collections::VecDeque;
use std::time::Instant;
/// internal storage type for the GasTracker struct right now the
/// sample_time is only used for stale identification but it should
/// be generally useful in improving accuracy elsewhere
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GasPriceEntry {
pub sample_time: Instant,
pub sample: Uint256,
}
impl GasPriceEntry {
/// Creates a new GasPriceEntry with sample_time now()
pub fn new(sample: Uint256) -> Self {
GasPriceEntry {
sample_time: Instant::now(),
sample,
}
}
}
// implement ord ignoring sample_time
impl Ord for GasPriceEntry {
fn cmp(&self, other: &Self) -> Ordering {
let size1 = &self.sample;
let size2 = &other.sample;
if size1 < size2 {
return Ordering::Less;
}
if size1 > size2 {
return Ordering::Greater;
}
Ordering::Equal
}
}
// boilerplate partial ord impl using above Ord
impl PartialOrd for GasPriceEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// A struct for storing gas prices and estimating when it's a good
/// idea to perform some gas intensive operation
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GasTracker {
history: VecDeque<GasPriceEntry>,
size: usize,
}
impl GasTracker {
/// create a new gas tracker with size
/// internal sample size and number of samples before which
/// it will not give an estimate
pub fn new(size: usize) -> Self {
GasTracker {
history: VecDeque::new(),
size,
}
}
/// Returns the current number of stored gas prices
pub fn get_current_size(&self) -> usize {
self.history.len()
}
/// Returns a copy of the stored gas price history
pub fn get_history(&self) -> VecDeque<GasPriceEntry> {
self.history.clone()
}
/// Increases the history size limit
/// returns an error if the history is already larger than the input size
pub fn expand_history_size(&mut self, size: usize) {
if self.history.len() > size {
return;
}
self.size = size;
}
/// Gets the most recently stored gas price
pub fn latest_gas_price(&self) -> Option<Uint256> {
self.history.front().map(|price| price.sample)
}
/// Samples Ethereum gas prices and creates a new GasPriceEntry on success
/// If you are not running GasTracker multi-threaded, consider sample_and_update()
pub async fn sample(web30: &Web3) -> Option<GasPriceEntry> {
match web30.eth_gas_price().await {
Ok(price) => Some(GasPriceEntry::new(price)),
Err(e) => {
warn!("Unable to sample gas prices with: {:?}", e);
None
}
}
}
/// Updates the latest gas price and adds it to the array
/// To obtain a sample, use GasTracker::sample(), or use sample_and_update() if
/// you are not running the GasTracker multi-threaded
pub fn update(&mut self, sample: GasPriceEntry) {
match self.history.len().cmp(&self.size) {
Ordering::Less => {
self.history.push_front(sample);
}
Ordering::Equal => {
//vec is full, remove oldest entry
self.history.pop_back();
self.history.push_front(sample);
}
Ordering::Greater => {
panic!("Vec size greater than max size, error in GasTracker vecDeque logic")
}
}
}
/// Gets the latest gas price and adds it to the array if this fails
/// the sample is skipped, returns a gas price if one is successfully added
pub async fn sample_and_update(&mut self, web30: &Web3) -> Option<Uint256> {
let sample = GasTracker::sample(web30).await;
match sample {
Some(entry) => {
self.update(entry.clone());
Some(entry.sample)
}
None => {
warn!("Failed to update gas price sample");
None
}
}
}
/// Look through all the gas prices in the history range and determine the highest
/// acceptable price to pay as provided by a user percentage
pub fn get_acceptable_gas_price(&self, percentage: f32) -> Option<Uint256> {
// if there are no entries, return that no gas price should currently
// be taken
if self.history.is_empty() {
return None;
}
let mut vector: Vec<&GasPriceEntry> = Vec::from_iter(self.history.iter());
vector.sort();
// this should never panic as percentage is less than 1 and vector len is
// included as a factor
let lowest: usize = (percentage * vector.len() as f32).floor() as usize;
Some(vector[lowest].sample)
}
}
/// Tests actual gas price storage by simultaneously requesting gas price and updating the GasTracker
#[test]
fn test_gas_storage() {
use actix::System;
use futures::future::join;
use std::time::Duration;
let runner = System::new();
let web3 = Web3::new("https://eth.althea.net", Duration::from_secs(5));
runner.block_on(async move {
let mut tracker = GasTracker::new(10);
let gas_fut = web3.eth_gas_price();
let track_fut = tracker.sample_and_update(&web3);
let (gas, track) = join(gas_fut, track_fut).await;
let gas = gas.expect("Actix failure");
assert!(
track.is_some() && gas == track.unwrap(),
"bad gas price stored - actual {gas} != stored {track:?}"
);
});
}
/// Checks that the acceptable gas prices are as expected with prices in the range of 0-99
#[test]
fn test_acceptable_gas_price() {
use std::time::Instant;
// use env_logger::{Builder, Env};
// Builder::from_env(Env::default().default_filter_or("info")).init(); // Change log level
// the numbers 0-99 in no particular order
let history_values: Vec<u8> = vec![
33, 67, 22, 57, 78, 1, 56, 49, 81, 18, 17, 7, 50, 99, 84, 89, 13, 59, 14, 27, 75, 24, 82,
63, 31, 2, 4, 41, 79, 92, 45, 20, 30, 34, 25, 64, 21, 0, 86, 46, 32, 19, 11, 51, 71, 70,
62, 29, 35, 88, 94, 77, 43, 9, 65, 44, 69, 8, 90, 16, 58, 97, 87, 83, 15, 12, 61, 60, 48,
37, 73, 53, 74, 95, 98, 96, 23, 93, 91, 10, 40, 66, 42, 5, 36, 55, 54, 72, 47, 39, 28, 85,
6, 3, 76, 38, 80, 68, 52, 26,
];
// Create a gas tracker with the above values and unimportant sample_times
let history = history_values.iter().map(|v| GasPriceEntry {
sample: (*v).into(),
sample_time: Instant::now(),
});
let tracker = GasTracker {
history: VecDeque::from_iter(history),
size: 100,
};
// All the values directly align to percentage values, so we ensure the gas tracker returns
// x +- 1 when requesting the lowest x% price
for i in history_values {
if i == 0 {
// expected_low panics on i = 0
continue;
}
let expect = f32::from(i).floor();
let percent = expect / 100.0;
let expected_high = Uint256::from((expect as u32) + 1u32);
let expected_low = Uint256::from((expect as u32) - 1u32);
let acceptable = tracker.get_acceptable_gas_price(percent);
assert!(
acceptable.is_some(),
"got None from get_acceptable_gas_price with nonempty history"
);
let acceptable = acceptable.unwrap();
assert!(
acceptable <= expected_high && acceptable >= expected_low,
"percentage {percent:.8} expected range [{expected_low:?} <= {acceptable:?} <= {expected_high:?}]",
)
}
}