Skip to content

Commit 9a3accf

Browse files
authored
Merge pull request #113 from adamzhang1987/feature-new-modules-support
Add two modules and test cases.
2 parents afa8129 + f31036b commit 9a3accf

File tree

12 files changed

+203
-7
lines changed

12 files changed

+203
-7
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Currently, only the following Etherscan.io API modules are available:
3636
- proxies
3737
- blocks
3838
- transactions
39+
- Logs
40+
- Gas Tracker
3941

4042
The remaining available modules provided by Etherscan.io will be added eventually...
4143

@@ -58,7 +60,6 @@ Jupyter notebooks area also included in each directory to show all examples
5860

5961
- Package and submit to PyPI
6062
- Add the following modules:
61-
- event logs
6263
- geth proxy
6364
- websockets
6465
- Add robust documentation

etherscan/client.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class BadRequest(ClientException):
3030
"""Invalid request passed"""
3131

3232

33+
class InvalidAPIKey(ClientException):
34+
"""Invalid API key"""
35+
36+
3337
# Assume user puts his API key in the api_key.json
3438
# file under variable name "key"
3539
class Client(object):
@@ -59,6 +63,11 @@ class Client(object):
5963
TAG = '&tag='
6064
BOOLEAN = '&boolean='
6165
INDEX = '&index='
66+
FROM_BLOCK = '&fromBlock='
67+
TO_BLOCK = '&toBlock='
68+
TOPIC0 = '&topic0='
69+
TOPIC0_1_OPR = '&topic0_1_opr='
70+
TOPIC1 = '&topic1='
6271
API_KEY = '&apikey='
6372

6473
url_dict = {}
@@ -86,7 +95,12 @@ def __init__(self, address, api_key=''):
8695
(self.TAG, ''),
8796
(self.BOOLEAN, ''),
8897
(self.INDEX, ''),
89-
(self.API_KEY, api_key)])
98+
(self.API_KEY, api_key),
99+
(self.FROM_BLOCK, ''),
100+
(self.TO_BLOCK, ''),
101+
(self.TOPIC0, ''),
102+
(self.TOPIC0_1_OPR, ''),
103+
(self.TOPIC1, '')])
90104

91105
# Var initialization should take place within init
92106
self.url = None
@@ -119,6 +133,8 @@ def connect(self):
119133
status = data.get('status')
120134
if status == '1' or self.check_keys_api(data):
121135
return data
136+
elif status == '0' and data.get('result') == "Invalid API Key":
137+
raise InvalidAPIKey(data.get('result'))
122138
else:
123139
raise EmptyResponse(data.get('message', 'no message'))
124140
raise BadRequest(

etherscan/gas_tracker.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from .client import Client
2+
3+
4+
class GasTrackerException(Exception):
5+
"""Base class for exceptions in this module."""
6+
pass
7+
8+
9+
class GasTracker(Client):
10+
def __init__(self, api_key='YourApiKeyToken'):
11+
Client.__init__(self, address='', api_key=api_key)
12+
self.url_dict[self.MODULE] = 'gastracker'
13+
14+
def get_estimation_of_confirmation_time(self, gas_price: str) -> str:
15+
"""
16+
Returns the estimated time, in seconds, for a transaction to be confirmed on the blockchain.
17+
18+
Args:
19+
gas_price (str): the price paid per unit of gas, in wei
20+
21+
Returns:
22+
str: The result is returned in seconds.
23+
"""
24+
self.url_dict[self.ACTION] = 'gasestimate'
25+
self.url_dict[self.GAS_PRICE] = gas_price
26+
self.build_url()
27+
req = self.connect()
28+
return req['result']
29+
30+
def get_gas_oracle(self) -> dict:
31+
"""
32+
Returns the current Safe, Proposed and Fast gas prices.
33+
34+
Returns:
35+
dict: The gas prices are returned in Gwei.
36+
"""
37+
self.url_dict[self.ACTION] = 'gasoracle'
38+
self.build_url()
39+
req = self.connect()
40+
return req['result']
41+
42+
def get_daily_average_gas_limit(self, start_date, end_date) -> list:
43+
# TODO API Pro
44+
pass

etherscan/logs.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from .client import Client
2+
3+
4+
class LogsException(Exception):
5+
"""Base class for exceptions in this module."""
6+
pass
7+
8+
9+
class Logs(Client):
10+
"""
11+
The Event Log API was designed to provide an alternative to the native eth_getLogs.
12+
"""
13+
def __init__(self, api_key='YourApiKeyToken'):
14+
Client.__init__(self, address='', api_key=api_key)
15+
self.url_dict[self.MODULE] = 'logs'
16+
17+
def get_logs(self, from_block: str, to_block='latest',
18+
topic0='', topic1='', topic0_1_opr='and',) -> list:
19+
"""
20+
Get Event Logs from block number [from_block] to block [to_block] ,
21+
where log address = [address], topic[0] = [topic0] 'AND' topic[1] = [topic1]
22+
23+
Args:
24+
from_block (str): start block number
25+
to_block (str, optional): end block number. Defaults to 'latest'.
26+
topic0 (str, optional): Defaults to ''.
27+
topic1 (str, optional): Defaults to ''.
28+
topic0_1_opr (str, optional): and|or between topic0 & topic1. Defaults to 'and'.
29+
30+
Returns:
31+
list: [description]
32+
"""
33+
# TODO: support multi topics
34+
if not topic0 and topic1:
35+
raise(LogsException('can not only set topic1 while topic0 is empty'))
36+
self.url_dict[self.ACTION] = 'getLogs'
37+
self.url_dict[self.FROM_BLOCK] = from_block if type(
38+
from_block) is str else str(from_block)
39+
self.url_dict[self.TO_BLOCK] = to_block if type(
40+
to_block) is str else str(to_block)
41+
self.url_dict[self.TOPIC0] = topic0 if type(
42+
topic0) is str else hex(topic0)
43+
self.url_dict[self.TOPIC1] = topic1 if type(
44+
topic1) is str else hex(topic1)
45+
self.url_dict[self.TOPIC0_1_OPR] = topic0_1_opr
46+
self.build_url()
47+
req = self.connect()
48+
return req['result']

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setuptools.setup(
44
name='py_etherscan_api',
5-
version='0.8.0',
5+
version='0.9.0',
66
packages=['examples', 'examples.stats', 'examples.tokens',
77
'examples.accounts', 'examples.blocks', 'examples.transactions', 'etherscan'],
88
url='https://github.com/corpetty/py-etherscan-api',

tests/test_accounts.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
import unittest
2+
import warnings
23

34
from etherscan.accounts import Account
45

5-
SINGLE_BALANCE = '40807178566070000000000'
6+
SINGLE_BALANCE = '40891626854930000000000'
67
SINGLE_ACCOUNT = '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a'
78
MULTI_ACCOUNT = [
89
'0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a',
910
'0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a',
1011
]
1112
MULTI_BALANCE = [
1213
{'account': '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a',
13-
'balance': '40807178566070000000000'},
14+
'balance': '40891626854930000000000'},
1415
{'account': '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a',
15-
'balance': '40807178566070000000000'}
16+
'balance': '40891626854930000000000'}
1617
]
1718
API_KEY = 'YourAPIkey'
1819

1920

2021
class AccountsTestCase(unittest.TestCase):
2122

23+
def setUp(self):
24+
warnings.simplefilter('ignore', ResourceWarning)
25+
2226
def test_get_balance(self):
2327
api = Account(address=SINGLE_ACCOUNT, api_key=API_KEY)
2428
self.assertEqual(api.get_balance(), SINGLE_BALANCE)

tests/test_blocks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
import warnings
23

34
from etherscan.blocks import Blocks
45

@@ -10,6 +11,9 @@
1011

1112
class BlocksTestCase(unittest.TestCase):
1213

14+
def setUp(self):
15+
warnings.simplefilter('ignore', ResourceWarning)
16+
1317
def test_get_block_reward(self):
1418
api = Blocks(api_key=(API_KEY))
1519
reward_object = api.get_block_reward(BLOCK)

tests/test_gas_tracker.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import unittest
2+
import warnings
3+
4+
from etherscan.gas_tracker import GasTracker
5+
6+
GAS_PRICE = '2000000000'
7+
PRICE_ORACLE_RESULT_DICT_KEYS = ("SafeGasPrice",
8+
"ProposeGasPrice",
9+
"FastGasPrice",
10+
"suggestBaseFee")
11+
API_KEY = 'YourAPIkey'
12+
13+
14+
class BlocksTestCase(unittest.TestCase):
15+
16+
def setUp(self):
17+
warnings.simplefilter('ignore', ResourceWarning)
18+
self.api = GasTracker(api_key=API_KEY)
19+
20+
def test_get_estimation_of_confirmation_time(self):
21+
estimated_time = self.api.get_estimation_of_confirmation_time(GAS_PRICE)
22+
self.assertTrue(int(estimated_time) > 0)
23+
24+
def test_get_gas_oracle(self):
25+
oracle_price = self.api.get_gas_oracle()
26+
for key in PRICE_ORACLE_RESULT_DICT_KEYS:
27+
self.assertTrue(key in oracle_price and float(oracle_price[key]) > 0)

tests/test_logs.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import unittest
2+
import warnings
3+
4+
from etherscan.logs import Logs, LogsException
5+
from etherscan.client import InvalidAPIKey
6+
7+
FROM_BLOCK = 379224
8+
TO_BLOCK = 400000
9+
ADDRESS = '0x33990122638b9132ca29c723bdf037f1a891a70c'
10+
TOPIC0 = '0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545'
11+
TOPIC1 = '0x72657075746174696f6e00000000000000000000000000000000000000000000'
12+
TOPIC0_1_OPR = 'and'
13+
API_KEY = 'YourAPIkey'
14+
15+
16+
class BlocksTestCase(unittest.TestCase):
17+
18+
def setUp(self):
19+
warnings.simplefilter('ignore', ResourceWarning)
20+
self.api = Logs(api_key=(API_KEY))
21+
22+
def test_invalid_api_key(self):
23+
with self.assertRaises(InvalidAPIKey):
24+
api = Logs(api_key=('invalid' + API_KEY))
25+
api.get_logs(from_block=FROM_BLOCK, topic0=TOPIC0)
26+
27+
def test_get_logs_error(self):
28+
with self.assertRaises(LogsException):
29+
self.api.get_logs(from_block=FROM_BLOCK, topic1=TOPIC1)
30+
31+
def test_get_logs_one_topic(self):
32+
topics = self.api.get_logs(from_block=FROM_BLOCK, topic0=TOPIC0)
33+
for topic in topics:
34+
self.assertTrue(TOPIC0 in topic.get('topics', ''))
35+
36+
def test_get_logs_two_topics(self):
37+
topics = self.api.get_logs(from_block=FROM_BLOCK, topic0=TOPIC0, topic1=TOPIC1)
38+
for topic in topics:
39+
self.assertTrue(TOPIC0 in topic.get('topics', '')
40+
and TOPIC1 in topic.get('topics', ''))

tests/test_proxies.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import unittest
3+
import warnings
34

45
from etherscan.proxies import Proxies
56

@@ -23,11 +24,14 @@
2324

2425
class ProxiesTestCase(unittest.TestCase):
2526

27+
def setUp(self):
28+
warnings.simplefilter('ignore', ResourceWarning)
29+
2630
def test_get_most_recent_block(self):
2731
api = Proxies(api_key=API_KEY)
2832
most_recent = int(api.get_most_recent_block(), 16)
2933
print(most_recent)
30-
p = re.compile('^[0-9]{7}$')
34+
p = re.compile('^[0-9]{8}$')
3135
self.assertTrue(p.match(str(most_recent)))
3236

3337
def test_get_block_by_number(self):

0 commit comments

Comments
 (0)