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:?}]",
        )
    }
}