├── LICENSE ├── README.md ├── gasExpress.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ethgasstation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gasstation-express 2 | A Lightweight Ethereum Gas Price Oracle for Anyone Running a Full Node 3 | 4 | This is a simple gas price oracle that can be used if you are running a local geth or parity node. It will look at gasprices over the last 200 blocks and provide gas price estimates based on the minimum gas price accepted in a percentage of blocks. 5 | 6 | 7 | usage: python3 gasExpress.py 8 | 9 | requirements: pip3 install -r requirements.txt 10 | -------------------------------------------------------------------------------- /gasExpress.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import json 4 | import math 5 | import traceback 6 | import os 7 | import pandas as pd 8 | import numpy as np 9 | from web3 import Web3, HTTPProvider 10 | 11 | 12 | web3 = Web3(HTTPProvider('http://localhost:8545')) 13 | 14 | ### These are the threholds used for % blocks accepting to define the recommended gas prices. can be edited here if desired 15 | 16 | SAFELOW = 35 17 | STANDARD = 60 18 | FAST = 90 19 | 20 | class Timers(): 21 | """ 22 | class to keep track of time relative to network block 23 | """ 24 | def __init__(self, start_block): 25 | self.start_block = start_block 26 | self.current_block = start_block 27 | self.process_block = start_block 28 | 29 | def update_time(self, block): 30 | self.current_block = block 31 | self.process_block = self.process_block + 1 32 | 33 | 34 | class CleanTx(): 35 | """transaction object / methods for pandas""" 36 | def __init__(self, tx_obj): 37 | self.hash = tx_obj.hash 38 | self.block_mined = tx_obj.blockNumber 39 | self.gas_price = tx_obj['gasPrice'] 40 | self.round_gp_10gwei() 41 | 42 | def to_dataframe(self): 43 | data = {self.hash: {'block_mined':self.block_mined, 'gas_price':self.gas_price, 'round_gp_10gwei':self.gp_10gwei}} 44 | return pd.DataFrame.from_dict(data, orient='index') 45 | 46 | def round_gp_10gwei(self): 47 | """Rounds the gas price to gwei""" 48 | gp = self.gas_price/1e8 49 | if gp >= 1 and gp < 10: 50 | gp = np.floor(gp) 51 | elif gp >= 10: 52 | gp = gp/10 53 | gp = np.floor(gp) 54 | gp = gp*10 55 | else: 56 | gp = 0 57 | self.gp_10gwei = gp 58 | 59 | class CleanBlock(): 60 | """block object/methods for pandas""" 61 | def __init__(self, block_obj, timemined, mingasprice=None): 62 | self.block_number = block_obj.number 63 | self.time_mined = timemined 64 | self.blockhash = block_obj.hash 65 | self.mingasprice = mingasprice 66 | 67 | def to_dataframe(self): 68 | data = {0:{'block_number':self.block_number, 'blockhash':self.blockhash, 'time_mined':self.time_mined, 'mingasprice':self.mingasprice}} 69 | return pd.DataFrame.from_dict(data, orient='index') 70 | 71 | def write_to_json(gprecs, prediction_table): 72 | """write json data""" 73 | try: 74 | prediction_table['gasprice'] = prediction_table['gasprice']/10 75 | prediction_tableout = prediction_table.to_json(orient='records') 76 | filepath_gprecs = 'ethgasAPI.json' 77 | filepath_prediction_table = 'predictTable.json' 78 | 79 | with open(filepath_gprecs, 'w') as outfile: 80 | json.dump(gprecs, outfile) 81 | 82 | with open(filepath_prediction_table, 'w') as outfile: 83 | outfile.write(prediction_tableout) 84 | 85 | except Exception as e: 86 | print(e) 87 | 88 | def process_block_transactions(block): 89 | """get tx data from block""" 90 | block_df = pd.DataFrame() 91 | block_obj = web3.eth.getBlock(block, True) 92 | for transaction in block_obj.transactions: 93 | clean_tx = CleanTx(transaction) 94 | block_df = block_df.append(clean_tx.to_dataframe(), ignore_index = False) 95 | block_df['time_mined'] = block_obj.timestamp 96 | return(block_df, block_obj) 97 | 98 | def process_block_data(block_df, block_obj): 99 | """process block to dataframe""" 100 | if len(block_obj.transactions) > 0: 101 | block_mingasprice = block_df['round_gp_10gwei'].min() 102 | else: 103 | block_mingasprice = np.nan 104 | timemined = block_df['time_mined'].min() 105 | clean_block = CleanBlock(block_obj, timemined, block_mingasprice) 106 | return(clean_block.to_dataframe()) 107 | 108 | def get_hpa(gasprice, hashpower): 109 | """gets the hash power accpeting the gas price over last 200 blocks""" 110 | hpa = hashpower.loc[gasprice >= hashpower.index, 'hashp_pct'] 111 | if gasprice > hashpower.index.max(): 112 | hpa = 100 113 | elif gasprice < hashpower.index.min(): 114 | hpa = 0 115 | else: 116 | hpa = hpa.max() 117 | return int(hpa) 118 | 119 | def analyze_last200blocks(block, blockdata): 120 | recent_blocks = blockdata.loc[blockdata['block_number'] > (block-200), ['mingasprice', 'block_number']] 121 | #create hashpower accepting dataframe based on mingasprice accepted in block 122 | hashpower = recent_blocks.groupby('mingasprice').count() 123 | hashpower = hashpower.rename(columns={'block_number': 'count'}) 124 | hashpower['cum_blocks'] = hashpower['count'].cumsum() 125 | totalblocks = hashpower['count'].sum() 126 | hashpower['hashp_pct'] = hashpower['cum_blocks']/totalblocks*100 127 | #get avg blockinterval time 128 | blockinterval = recent_blocks.sort_values('block_number').diff() 129 | blockinterval.loc[blockinterval['block_number'] > 1, 'time_mined'] = np.nan 130 | blockinterval.loc[blockinterval['time_mined']< 0, 'time_mined'] = np.nan 131 | avg_timemined = blockinterval['time_mined'].mean() 132 | if np.isnan(avg_timemined): 133 | avg_timemined = 15 134 | return(hashpower, avg_timemined) 135 | 136 | 137 | def make_predictTable(block, alltx, hashpower, avg_timemined): 138 | 139 | #predictiontable 140 | predictTable = pd.DataFrame({'gasprice' : range(10, 1010, 10)}) 141 | ptable2 = pd.DataFrame({'gasprice' : range(0, 10, 1)}) 142 | predictTable = predictTable.append(ptable2).reset_index(drop=True) 143 | predictTable = predictTable.sort_values('gasprice').reset_index(drop=True) 144 | predictTable['hashpower_accepting'] = predictTable['gasprice'].apply(get_hpa, args=(hashpower,)) 145 | return(predictTable) 146 | 147 | def get_gasprice_recs(prediction_table, block_time, block): 148 | 149 | def get_safelow(): 150 | series = prediction_table.loc[prediction_table['hashpower_accepting'] >= SAFELOW, 'gasprice'] 151 | safelow = series.min() 152 | return float(safelow) 153 | 154 | def get_average(): 155 | series = prediction_table.loc[prediction_table['hashpower_accepting'] >= STANDARD, 'gasprice'] 156 | average = series.min() 157 | return float(average) 158 | 159 | def get_fast(): 160 | series = prediction_table.loc[prediction_table['hashpower_accepting'] >= FAST, 'gasprice'] 161 | fastest = series.min() 162 | return float(fastest) 163 | 164 | def get_fastest(): 165 | hpmax = prediction_table['hashpower_accepting'].max() 166 | fastest = prediction_table.loc[prediction_table['hashpower_accepting'] == hpmax, 'gasprice'].values[0] 167 | return float(fastest) 168 | 169 | gprecs = {} 170 | gprecs['safeLow'] = get_safelow()/10 171 | gprecs['standard'] = get_average()/10 172 | gprecs['fast'] = get_fast()/10 173 | gprecs['fastest'] = get_fastest()/10 174 | gprecs['block_time'] = block_time 175 | gprecs['blockNum'] = block 176 | return(gprecs) 177 | 178 | def master_control(): 179 | 180 | def init (block): 181 | nonlocal alltx 182 | nonlocal blockdata 183 | print("\n\n**** ETH Gas Station Express Oracle ****") 184 | print ("\nSafelow = " +str(SAFELOW)+ "% of blocks accepting. Usually confirms in less than 30min.") 185 | print ("Standard= " +str(STANDARD)+ "% of blocks accepting. Usually confirms in less than 5 min.") 186 | print ("Fast = " +str(FAST)+ "% of blocks accepting. Usually confirms in less than 1 minute") 187 | print ("Fastest = all blocks accepting. As fast as possible but you are probably overpaying.") 188 | print("\nnow loading gasprice data from last 100 blocks...give me a minute") 189 | 190 | for pastblock in range((block-100), (block), 1): 191 | (mined_blockdf, block_obj) = process_block_transactions(pastblock) 192 | alltx = alltx.combine_first(mined_blockdf) 193 | block_sumdf = process_block_data(mined_blockdf, block_obj) 194 | blockdata = blockdata.append(block_sumdf, ignore_index = True) 195 | print ("done. now reporting gasprice recs in gwei: \n") 196 | 197 | print ("\npress ctrl-c at any time to stop monitoring\n") 198 | print ("**** And the oracle says...**** \n") 199 | 200 | 201 | 202 | def append_new_tx(clean_tx): 203 | nonlocal alltx 204 | if not clean_tx.hash in alltx.index: 205 | alltx = alltx.append(clean_tx.to_dataframe(), ignore_index = False) 206 | 207 | def update_dataframes(block): 208 | nonlocal alltx 209 | nonlocal blockdata 210 | nonlocal timer 211 | 212 | try: 213 | #get minedtransactions and blockdata from previous block 214 | mined_block_num = block-3 215 | (mined_blockdf, block_obj) = process_block_transactions(mined_block_num) 216 | alltx = alltx.combine_first(mined_blockdf) 217 | 218 | #process block data 219 | block_sumdf = process_block_data(mined_blockdf, block_obj) 220 | 221 | #add block data to block dataframe 222 | blockdata = blockdata.append(block_sumdf, ignore_index = True) 223 | 224 | #get hashpower table from last 200 blocks 225 | (hashpower, block_time) = analyze_last200blocks(block, blockdata) 226 | predictiondf = make_predictTable(block, alltx, hashpower, block_time) 227 | 228 | #get gpRecs 229 | gprecs = get_gasprice_recs (predictiondf, block_time, block) 230 | print(gprecs) 231 | 232 | #every block, write gprecs, predictions 233 | write_to_json(gprecs, predictiondf) 234 | return True 235 | 236 | except: 237 | print(traceback.format_exc()) 238 | 239 | alltx = pd.DataFrame() 240 | blockdata = pd.DataFrame() 241 | timer = Timers(web3.eth.blockNumber) 242 | start_time = time.time() 243 | init (web3.eth.blockNumber) 244 | 245 | while True: 246 | try: 247 | block = web3.eth.blockNumber 248 | if (timer.process_block < block): 249 | updated = update_dataframes(timer.process_block) 250 | timer.process_block = timer.process_block + 1 251 | except: 252 | pass 253 | 254 | time.sleep(1) 255 | 256 | master_control() 257 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2017.11.5 2 | chardet==3.0.4 3 | cytoolz==0.8.2 4 | eth-abi==0.5.0 5 | eth-keyfile==0.4.0 6 | eth-keys==0.1.0b3 7 | eth-tester==0.1.0b5 8 | eth-utils==0.7.1 9 | idna==2.6 10 | numpy==1.13.3 11 | pandas==0.21.0 12 | pycryptodome==3.4.7 13 | pylru==1.0.9 14 | pysha3==1.0.2 15 | python-dateutil==2.6.1 16 | pytz==2017.3 17 | requests==2.18.4 18 | rlp==0.6.0 19 | scipy==1.0.0 20 | semantic-version==2.6.0 21 | six==1.11.0 22 | toolz==0.8.2 23 | typing==3.6.2 24 | urllib3==1.24.2 25 | web3==3.16.4 26 | --------------------------------------------------------------------------------