diff --git a/. circleci/config.yml b/. circleci/config.yml new file mode 100644 index 0000000..1a03a7b --- /dev/null +++ b/. circleci/config.yml @@ -0,0 +1,31 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 9d4faec..9a9469a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,4 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with a single custom sponsorship URL +github: rose2161 +open_collective: Cryptob3auty diff --git a/.github/python-publish.yaml b/.github/python-publish.yaml new file mode 100644 index 0000000..bb8c9c2 --- /dev/null +++ b/.github/python-publish.yaml @@ -0,0 +1,30 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/workflows/python-publish.yaml b/.github/workflows/python-publish.yaml new file mode 100644 index 0000000..1a03a7b --- /dev/null +++ b/.github/workflows/python-publish.yaml @@ -0,0 +1,31 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/README.md b/README.md index d53fd3d..00e494f 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# py-etherscan-api module +in# py-etherscan-api module [![Build Status](https://secure.travis-ci.org/corpetty/py-etherscan-api.png?branch=master)](http://travis-ci.org/corpetty/py-etherscan-api) [![Join the chat at https://gitter.im/py-etherscan/Lobby](https://badges.gitter.im/py-etherscan/Lobby.svg)](https://gitter.im/py-etherscan/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -36,6 +36,8 @@ Currently, only the following Etherscan.io API modules are available: - proxies - blocks - transactions +- Logs +- Gas Tracker The remaining available modules provided by Etherscan.io will be added eventually... @@ -58,15 +60,8 @@ Jupyter notebooks area also included in each directory to show all examples - Package and submit to PyPI - Add the following modules: - - event logs - geth proxy - websockets - Add robust documentation - Add unit test suite - Add request throttling based on Etherscan's suggestions - -## Holla at ya' boy - -BTC: 16Ny72US78VEjL5GUinSAavDwARb8dXWKG - -ETH: 0x5E8047fc033499BD5d8C463ADb29f10f11165ed0 diff --git a/__init__.py b/__init__.py index 745e2a5..2a9b8cd 100755 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ -__author__ = 'Corey Petty' -name = "py-etherscan-api" \ No newline at end of file +contributor= 'Aisha Redl-Sherwood' +name = "mypy-etherscan-api, forked from "Core Petty's, 'py-etherscan-api'" diff --git a/api_key.json b/api_key.json new file mode 100644 index 0000000..e1b535d --- /dev/null +++ b/api_key.json @@ -0,0 +1 @@ +{ "key" : "${{secrets.API_KEY}}" } diff --git a/etherscan/__init__.py b/etherscan/__init__.py index 9900b50..87e9910 100755 --- a/etherscan/__init__.py +++ b/etherscan/__init__.py @@ -1 +1 @@ -__author__ = 'Corey Petty' +__author__ = 'Aisha Redl-Sherwood' diff --git a/etherscan/api_key.json b/etherscan/api_key.json new file mode 100644 index 0000000..e1b535d --- /dev/null +++ b/etherscan/api_key.json @@ -0,0 +1 @@ +{ "key" : "${{secrets.API_KEY}}" } diff --git a/etherscan/client.py b/etherscan/client.py index 0802433..83da7a1 100755 --- a/etherscan/client.py +++ b/etherscan/client.py @@ -30,6 +30,10 @@ class BadRequest(ClientException): """Invalid request passed""" +class InvalidAPIKey(ClientException): + """Invalid API key""" + + # Assume user puts his API key in the api_key.json # file under variable name "key" class Client(object): @@ -59,6 +63,11 @@ class Client(object): TAG = '&tag=' BOOLEAN = '&boolean=' INDEX = '&index=' + FROM_BLOCK = '&fromBlock=' + TO_BLOCK = '&toBlock=' + TOPIC0 = '&topic0=' + TOPIC0_1_OPR = '&topic0_1_opr=' + TOPIC1 = '&topic1=' API_KEY = '&apikey=' url_dict = {} @@ -86,7 +95,12 @@ def __init__(self, address, api_key=''): (self.TAG, ''), (self.BOOLEAN, ''), (self.INDEX, ''), - (self.API_KEY, api_key)]) + (self.API_KEY, api_key), + (self.FROM_BLOCK, ''), + (self.TO_BLOCK, ''), + (self.TOPIC0, ''), + (self.TOPIC0_1_OPR, ''), + (self.TOPIC1, '')]) # Var initialization should take place within init self.url = None @@ -119,6 +133,8 @@ def connect(self): status = data.get('status') if status == '1' or self.check_keys_api(data): return data + elif status == '0' and data.get('result') == "Invalid API Key": + raise InvalidAPIKey(data.get('result')) else: raise EmptyResponse(data.get('message', 'no message')) raise BadRequest( diff --git a/etherscan/gas_tracker.py b/etherscan/gas_tracker.py new file mode 100644 index 0000000..7efc516 --- /dev/null +++ b/etherscan/gas_tracker.py @@ -0,0 +1,44 @@ +from .client import Client + + +class GasTrackerException(Exception): + """Base class for exceptions in this module.""" + pass + + +class GasTracker(Client): + def __init__(self, api_key='YourApiKeyToken'): + Client.__init__(self, address='', api_key=api_key) + self.url_dict[self.MODULE] = 'gastracker' + + def get_estimation_of_confirmation_time(self, gas_price: str) -> str: + """ + Returns the estimated time, in seconds, for a transaction to be confirmed on the blockchain. + + Args: + gas_price (str): the price paid per unit of gas, in wei + + Returns: + str: The result is returned in seconds. + """ + self.url_dict[self.ACTION] = 'gasestimate' + self.url_dict[self.GAS_PRICE] = gas_price + self.build_url() + req = self.connect() + return req['result'] + + def get_gas_oracle(self) -> dict: + """ + Returns the current Safe, Proposed and Fast gas prices. + + Returns: + dict: The gas prices are returned in Gwei. + """ + self.url_dict[self.ACTION] = 'gasoracle' + self.build_url() + req = self.connect() + return req['result'] + + def get_daily_average_gas_limit(self, start_date, end_date) -> list: + # TODO API Pro + pass diff --git a/etherscan/logs.py b/etherscan/logs.py new file mode 100644 index 0000000..60b448f --- /dev/null +++ b/etherscan/logs.py @@ -0,0 +1,48 @@ +from .client import Client + + +class LogsException(Exception): + """Base class for exceptions in this module.""" + pass + + +class Logs(Client): + """ + The Event Log API was designed to provide an alternative to the native eth_getLogs. + """ + def __init__(self, api_key='YourApiKeyToken'): + Client.__init__(self, address='', api_key=api_key) + self.url_dict[self.MODULE] = 'logs' + + def get_logs(self, from_block: str, to_block='latest', + topic0='', topic1='', topic0_1_opr='and',) -> list: + """ + Get Event Logs from block number [from_block] to block [to_block] , + where log address = [address], topic[0] = [topic0] 'AND' topic[1] = [topic1] + + Args: + from_block (str): start block number + to_block (str, optional): end block number. Defaults to 'latest'. + topic0 (str, optional): Defaults to ''. + topic1 (str, optional): Defaults to ''. + topic0_1_opr (str, optional): and|or between topic0 & topic1. Defaults to 'and'. + + Returns: + list: [description] + """ + # TODO: support multi topics + if not topic0 and topic1: + raise(LogsException('can not only set topic1 while topic0 is empty')) + self.url_dict[self.ACTION] = 'getLogs' + self.url_dict[self.FROM_BLOCK] = from_block if type( + from_block) is str else str(from_block) + self.url_dict[self.TO_BLOCK] = to_block if type( + to_block) is str else str(to_block) + self.url_dict[self.TOPIC0] = topic0 if type( + topic0) is str else hex(topic0) + self.url_dict[self.TOPIC1] = topic1 if type( + topic1) is str else hex(topic1) + self.url_dict[self.TOPIC0_1_OPR] = topic0_1_opr + self.build_url() + req = self.connect() + return req['result'] diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100755 index 9900b50..0000000 --- a/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'Corey Petty' diff --git a/examples/accounts/__init__.py b/examples/accounts/__init__.py index 9900b50..2d200b2 100755 --- a/examples/accounts/__init__.py +++ b/examples/accounts/__init__.py @@ -1 +1 @@ -__author__ = 'Corey Petty' +__author__ = 'Aisha Redl' diff --git a/examples/tokens/get_token_balance.py b/examples/tokens/get_token_balance.py index 744b2f8..bbf2c21 100755 --- a/examples/tokens/get_token_balance.py +++ b/examples/tokens/get_token_balance.py @@ -4,8 +4,8 @@ with open('../../api_key.json', mode='r') as key_file: key = json.loads(key_file.read())['key'] -address = '0xe04f27eb70e025b78871a2ad7eabe85e61212761' -api = Tokens(contract_address='0x57d90b64a1a57749b0f932f1a3395792e12e7055', +address = '0x4f19f6f747f43a3d9fcf8bb7d2214e798cd2ece8' +api = Tokens(contract_address='0xdAC17F958D2ee523a2206206994597C13D831ec7', api_key=key) balance = api.get_token_balance(address=address) print(balance) diff --git a/examples/tokens/get_total_supply.py b/examples/tokens/get_total_supply.py index 3e98ce4..97ca259 100755 --- a/examples/tokens/get_total_supply.py +++ b/examples/tokens/get_total_supply.py @@ -8,6 +8,6 @@ # DGD # MKR # TheDAO -api = Tokens(tokenname='SNT', api_key=key) +api = Tokens(tokenname='MKR', api_key=key) supply = api.get_total_supply() print(supply) diff --git a/pip-requirements.txt b/pip-requirements.txt old mode 100755 new mode 100644 index 5d0fbdb..0230bb4 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -1,2 +1,2 @@ -requests>=2.20.0 +requests>=2.32.0 typing==3.6.4 diff --git a/setup.py b/setup.py index a547046..0516099 100755 --- a/setup.py +++ b/setup.py @@ -2,13 +2,13 @@ setuptools.setup( name='py_etherscan_api', - version='0.8.0', + version='0.9.0', packages=['examples', 'examples.stats', 'examples.tokens', 'examples.accounts', 'examples.blocks', 'examples.transactions', 'etherscan'], - url='https://github.com/corpetty/py-etherscan-api', - license='MIT', - author='coreypetty', - author_email='petty.btc@gmail.com', + url='https://github.com/rose2161/py-etherscan-api', + license='Gnup', + author='aisha redl', + author_email='aisharose2161@gmail.com', description='Python Bindings to Etherscan.io API', install_requires=[ 'requests>=2.20.0', diff --git a/tests/test_accounts.py b/tests/test_accounts.py index a5129d1..8c7ff88 100755 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,8 +1,9 @@ import unittest +import warnings from etherscan.accounts import Account -SINGLE_BALANCE = '40807178566070000000000' +SINGLE_BALANCE = '40891626854930000000000' SINGLE_ACCOUNT = '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a' MULTI_ACCOUNT = [ '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a', @@ -10,15 +11,18 @@ ] MULTI_BALANCE = [ {'account': '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a', - 'balance': '40807178566070000000000'}, + 'balance': '40891626854930000000000'}, {'account': '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a', - 'balance': '40807178566070000000000'} + 'balance': '40891626854930000000000'} ] API_KEY = 'YourAPIkey' class AccountsTestCase(unittest.TestCase): + def setUp(self): + warnings.simplefilter('ignore', ResourceWarning) + def test_get_balance(self): api = Account(address=SINGLE_ACCOUNT, api_key=API_KEY) self.assertEqual(api.get_balance(), SINGLE_BALANCE) diff --git a/tests/test_blocks.py b/tests/test_blocks.py index e3d59ff..0bdae81 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -1,4 +1,5 @@ import unittest +import warnings from etherscan.blocks import Blocks @@ -10,6 +11,9 @@ class BlocksTestCase(unittest.TestCase): + def setUp(self): + warnings.simplefilter('ignore', ResourceWarning) + def test_get_block_reward(self): api = Blocks(api_key=(API_KEY)) reward_object = api.get_block_reward(BLOCK) diff --git a/tests/test_gas_tracker.py b/tests/test_gas_tracker.py new file mode 100644 index 0000000..37d2303 --- /dev/null +++ b/tests/test_gas_tracker.py @@ -0,0 +1,27 @@ +import unittest +import warnings + +from etherscan.gas_tracker import GasTracker + +GAS_PRICE = '2000000000' +PRICE_ORACLE_RESULT_DICT_KEYS = ("SafeGasPrice", + "ProposeGasPrice", + "FastGasPrice", + "suggestBaseFee") +API_KEY = 'YourAPIkey' + + +class BlocksTestCase(unittest.TestCase): + + def setUp(self): + warnings.simplefilter('ignore', ResourceWarning) + self.api = GasTracker(api_key=API_KEY) + + def test_get_estimation_of_confirmation_time(self): + estimated_time = self.api.get_estimation_of_confirmation_time(GAS_PRICE) + self.assertTrue(int(estimated_time) > 0) + + def test_get_gas_oracle(self): + oracle_price = self.api.get_gas_oracle() + for key in PRICE_ORACLE_RESULT_DICT_KEYS: + self.assertTrue(key in oracle_price and float(oracle_price[key]) > 0) diff --git a/tests/test_logs.py b/tests/test_logs.py new file mode 100644 index 0000000..f4d36d9 --- /dev/null +++ b/tests/test_logs.py @@ -0,0 +1,40 @@ +import unittest +import warnings + +from etherscan.logs import Logs, LogsException +from etherscan.client import InvalidAPIKey + +FROM_BLOCK = 379224 +TO_BLOCK = 400000 +ADDRESS = '0x33990122638b9132ca29c723bdf037f1a891a70c' +TOPIC0 = '0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545' +TOPIC1 = '0x72657075746174696f6e00000000000000000000000000000000000000000000' +TOPIC0_1_OPR = 'and' +API_KEY = 'YourAPIkey' + + +class BlocksTestCase(unittest.TestCase): + + def setUp(self): + warnings.simplefilter('ignore', ResourceWarning) + self.api = Logs(api_key=(API_KEY)) + + def test_invalid_api_key(self): + with self.assertRaises(InvalidAPIKey): + api = Logs(api_key=('invalid' + API_KEY)) + api.get_logs(from_block=FROM_BLOCK, topic0=TOPIC0) + + def test_get_logs_error(self): + with self.assertRaises(LogsException): + self.api.get_logs(from_block=FROM_BLOCK, topic1=TOPIC1) + + def test_get_logs_one_topic(self): + topics = self.api.get_logs(from_block=FROM_BLOCK, topic0=TOPIC0) + for topic in topics: + self.assertTrue(TOPIC0 in topic.get('topics', '')) + + def test_get_logs_two_topics(self): + topics = self.api.get_logs(from_block=FROM_BLOCK, topic0=TOPIC0, topic1=TOPIC1) + for topic in topics: + self.assertTrue(TOPIC0 in topic.get('topics', '') + and TOPIC1 in topic.get('topics', '')) diff --git a/tests/test_proxies.py b/tests/test_proxies.py index 9bc5e64..5067be0 100755 --- a/tests/test_proxies.py +++ b/tests/test_proxies.py @@ -1,5 +1,6 @@ import re import unittest +import warnings from etherscan.proxies import Proxies @@ -23,11 +24,14 @@ class ProxiesTestCase(unittest.TestCase): + def setUp(self): + warnings.simplefilter('ignore', ResourceWarning) + def test_get_most_recent_block(self): api = Proxies(api_key=API_KEY) most_recent = int(api.get_most_recent_block(), 16) print(most_recent) - p = re.compile('^[0-9]{7}$') + p = re.compile('^[0-9]{8}$') self.assertTrue(p.match(str(most_recent))) def test_get_block_by_number(self): diff --git a/tests/test_token.py b/tests/test_token.py index fd1860e..2ce91ac 100755 --- a/tests/test_token.py +++ b/tests/test_token.py @@ -1,4 +1,5 @@ import unittest +import warnings from etherscan.tokens import Tokens @@ -11,6 +12,9 @@ class TokensTestCase(unittest.TestCase): + def setUp(self): + warnings.simplefilter('ignore', ResourceWarning) + def test_get_token_supply(self): api = Tokens(contract_address=CONTRACT_ADDRESS, api_key=(API_KEY)) self.assertEqual(api.get_total_supply(), ELCOIN_TOKEN_SUPPLY) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 7dec60c..1cb898a 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1,8 +1,9 @@ import unittest +import warnings from etherscan.transactions import Transactions -API_KEY = 'YourAPIkey' +API_KEY = '{API_KEY}' TX_1 = '0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a' TX_2 = '0x513c1ba0bebf66436b5fed86ab668452b7805593c05073eb2d51d3a52f480a76' ERROR_STRING = 'Bad jump destination' @@ -10,6 +11,9 @@ class TransactionsTestCase(unittest.TestCase): + def setUp(self): + warnings.simplefilter('ignore', ResourceWarning) + def test_get_status(self): api = Transactions(api_key=(API_KEY)) status = api.get_status(TX_1)