Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bb2cf96
Cleaned up code to comply with flake8 linter
taylorjdawson Jun 24, 2018
598d305
updated prefix
dpdanpittman Aug 9, 2018
17a9f7f
updated readme
dpdanpittman Aug 9, 2018
cf147e6
Merge pull request #40 from taylorjdawson/linting
corpetty Aug 28, 2018
6b8fcc3
fixed file extension
dpdanpittman Aug 28, 2018
a483e32
added erc20 and sourcecode functionality, added some tests and examples
corpetty Aug 28, 2018
352f9af
updated readme to some changes
corpetty Aug 28, 2018
883117b
removed vscode, updated gitignore
corpetty Aug 28, 2018
18ee101
Merge pull request #41 from Qwoyn/master
corpetty Sep 17, 2018
945450e
Updates requests dependency (security)
AndreMiras Nov 6, 2018
0344ed0
Fixes unit tests and linting
AndreMiras Nov 6, 2018
663b54a
Merge pull request #44 from AndreMiras/feature/fix_unit_tests
corpetty Nov 8, 2018
38fc863
Merge pull request #43 from AndreMiras/feature/update_requests_dep
corpetty Nov 8, 2018
3fecebf
updated
corpetty Jan 3, 2019
3c68b57
changed setup to work with pypi
corpetty Jan 3, 2019
b1e4586
updated tests
corpetty Jan 3, 2019
8712bcf
updated conflicts
corpetty Jan 3, 2019
6ad7187
updated readme to new install instructions
corpetty Jan 3, 2019
f488fe1
Addition of Blocks module.
RichHorrocks Feb 4, 2019
5db2cdf
Blocks module test and example files.
RichHorrocks Feb 4, 2019
0c45767
Addition of Transactions module
RichHorrocks Feb 23, 2019
b15381b
Proxies module additions.
RichHorrocks Mar 1, 2019
1165709
Merge pull request #52 from RichHorrocks/proxies-module
corpetty Mar 14, 2019
afa8129
Create FUNDING.yml
corpetty Jun 19, 2019
f31036b
Fix the test case bugs for now, not the robust way.
adamzhang1987 Dec 10, 2021
9a3accf
Merge pull request #113 from adamzhang1987/feature-new-modules-support
corpetty Dec 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 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
3 changes: 3 additions & 0 deletions .gitignore
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,6 @@ venv.bak/

# mypy
.mypy_cache/

# vscode
.vscode/
Empty file modified .travis.yml
100644 → 100755
Empty file.
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2018 The Python Packaging Authority

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
38 changes: 27 additions & 11 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,54 @@

[![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)


EtherScan.io API python bindings

## Description
This module is written as an effort to provide python bindings to the EtherScan.io API, which can be found at:
https://etherscan.io/apis

This module is written as an effort to provide python bindings to the EtherScan.io API, which can be found at:
https://etherscan.io/apis. If you are interacting with a contract on the Ropsten Testnet please use
https://ropsten.etherscan.io/apis.
In order to use this, you must attain an Etherscan user account, and generate an API key.

In order to use the API, you must provide an API key at runtime, which can be found at the Etherscan.io API website.
If you'd like to use the provided examples without altering them, then the JSON file `api_key.json` must be stored in
the base directory. Its format is as follows:
the base directory. Its format is as follows:

{ "key" : "YourApiKeyToken" }

with `YourApiKeyToken` is your provided API key token from EtherScan.io

## Installation

To install the package to your computer, simply run the following command in the base directory:

python setup.py install
python3 -m pip install py-etherscan-api

## Available bindings

Currently, only the following Etherscan.io API modules are available:

- accounts
- contracts
- stats
- tokens
- proxies
- blocks
- transactions
- Logs
- Gas Tracker

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

The remaining available modules provided by Etherscan.io will be added shortly
## Available Networks

Currently, this works for the following networks:

- Mainnet
- Ropsten

## Examples

All possible calls have an associated example file in the examples folder to show how to call the binding

These of course will be fleshed out with more details and explanation in time
Expand All @@ -43,15 +60,14 @@ 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
- 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
1 change: 1 addition & 0 deletions __init__.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__author__ = 'Corey Petty'
name = "py-etherscan-api"
11 changes: 0 additions & 11 deletions changelog.md

This file was deleted.

Empty file modified etherscan/__init__.py
100644 → 100755
Empty file.
46 changes: 34 additions & 12 deletions etherscan/accounts.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@


class Account(Client):
PAGE_NUM_PATTERN = re.compile(
r'[1-9](?:\d{0,2})(?:,\d{3})*(?:\.\d*[1-9])?|0?\.\d*[1-9]|0')

def __init__(self, address=Client.dao_address, api_key='YourApiKeyToken'):
Client.__init__(self, address=address, api_key=api_key)
self.url_dict[self.MODULE] = 'account'
Expand All @@ -21,9 +24,11 @@ def get_balance_multiple(self):
req = self.connect()
return req['result']

def get_transaction_page(self, page=1, offset=10000, sort='asc', internal=False) -> list:
def get_transaction_page(self, page=1, offset=10000, sort='asc',
internal=False, erc20=False) -> list:
"""
Get a page of transactions, each transaction returns list of dict with keys:
Get a page of transactions, each transaction
returns list of dict with keys:
nonce
hash
cumulativeGasUsed
Expand All @@ -47,12 +52,20 @@ def get_transaction_page(self, page=1, offset=10000, sort='asc', internal=False)
'asc' -> ascending order
'desc' -> descending order

internal options:
True -> Gets the internal transactions of a smart contract
internal options: (currently marked at Beta for etherscan.io)
True -> Gets the internal transactions of the address
False -> (default) get normal external transactions

erc20 options: (currently marked at Beta for etherscan.io)
True -> Gets the erc20 token transcations of the address
False -> (default) get normal external transactions

NOTE: not sure if this works for contract addresses, requires testing
"""
if internal:
self.url_dict[self.ACTION] = 'txlistinternal'
elif erc20:
self.url_dict[self.ACTION] = 'tokentx'
else:
self.url_dict[self.ACTION] = 'txlist'
self.url_dict[self.PAGE] = str(page)
Expand All @@ -62,7 +75,8 @@ def get_transaction_page(self, page=1, offset=10000, sort='asc', internal=False)
req = self.connect()
return req['result']

def get_all_transactions(self, offset=10000, sort='asc', internal=False) -> list:
def get_all_transactions(self, offset=10000, sort='asc',
internal=False) -> list:
if internal:
self.url_dict[self.ACTION] = 'txlistinternal'
else:
Expand All @@ -77,19 +91,23 @@ def get_all_transactions(self, offset=10000, sort='asc', internal=False) -> list
self.build_url()
req = self.connect()
if "No transactions found" in req['message']:
print("Total number of transactions: {}".format(len(trans_list)))
print(
"Total number of transactions: {}".format(len(trans_list)))
self.page = ''
return trans_list
else:
trans_list += req['result']
# Find any character block that is a integer of any length
page_number = re.findall(r'[1-9](?:\d{0,2})(?:,\d{3})*(?:\.\d*[1-9])?|0?\.\d*[1-9]|0', self.url_dict[self.PAGE])
page_number = re.findall(Account.PAGE_NUM_PATTERN,
self.url_dict[self.PAGE])
print("page {} added".format(page_number[0]))
self.url_dict[self.PAGE] = str(int(page_number[0]) + 1)

def get_blocks_mined_page(self, blocktype='blocks', page=1, offset=10000) -> list:
def get_blocks_mined_page(self, blocktype='blocks', page=1,
offset=10000) -> list:
"""
Get a page of blocks mined by given address, returns list of dict with keys:
Get a page of blocks mined by given address,
returns list of dict with keys:
blockReward (in wei)
blockNumber
timeStamp
Expand Down Expand Up @@ -117,12 +135,15 @@ def get_all_blocks_mined(self, blocktype='blocks', offset=10000) -> list:
req = self.connect()
print(req['message'])
if "No transactions found" in req['message']:
print("Total number of blocks mined: {}".format(len(blocks_list)))
print(
"Total number of blocks mined: {}".format(
len(blocks_list)))
return blocks_list
else:
blocks_list += req['result']
# Find any character block that is a integer of any length
page_number = re.findall(r'[1-9](?:\d{0,2})(?:,\d{3})*(?:\.\d*[1-9])?|0?\.\d*[1-9]|0', self.url_dict[self.PAGE])
page_number = re.findall(Account.PAGE_NUM_PATTERN,
self.url_dict[self.PAGE])
print("page {} added".format(page_number[0]))
self.url_dict[self.PAGE] = str(int(page_number[0]) + 1)

Expand All @@ -135,6 +156,7 @@ def get_internal_by_hash(self, tx_hash=''):

def update_transactions(self, address, trans):
"""
Gets last page of transactions (last 10k trans) and updates current trans book (book)
Gets last page of transactions (last 10k trans)
and updates current trans book (book)
"""
pass
16 changes: 16 additions & 0 deletions etherscan/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .client import Client
from typing import Union


class Blocks(Client):
def __init__(self, api_key='YourApiKeyToken'):
Client.__init__(self, address='', api_key=api_key)
self.url_dict[self.MODULE] = 'block'

def get_block_reward(self, block_number: Union[str, int]):
self.url_dict[self.ACTION] = 'getblockreward'
self.url_dict[self.BLOCKNO] = block_number if type(
block_number) is str else str(block_number)
self.build_url()
req = self.connect()
return req['result']
46 changes: 33 additions & 13 deletions etherscan/client.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ class BadRequest(ClientException):
"""Invalid request passed"""


# Assume user puts his API key in the api_key.json file under variable name "key"
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):
dao_address = '0xbb9bc244d798123fde783fcc1c72d3bb8c189413'

Expand All @@ -47,7 +52,7 @@ class Client(object):
TO = '&to='
VALUE = '&value='
DATA = '&data='
POSITION = '&='
POSITION = '&position='
HEX = '&hex='
GAS_PRICE = '&gasPrice='
GAS = '&gas='
Expand All @@ -58,14 +63,18 @@ 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 = {}

def __init__(self, address, api_key=''):
self.http = requests.session()
self.url_dict = collections.OrderedDict([

(self.MODULE, ''),
(self.ADDRESS, ''),
(self.OFFSET, ''),
Expand All @@ -86,12 +95,17 @@ 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

# self.url_dict[API_KEY] = str(api_key)
self.check_and_get_api()
# self.key = self.URL_BASES['key'] + self.API_KEY

if (len(address) > 20) and (type(address) == list):
raise BadRequest("Etherscan only takes 20 addresses at a time")
Expand All @@ -101,7 +115,9 @@ def __init__(self, address, api_key=''):
self.url_dict[self.ADDRESS] = address

def build_url(self):
self.url = self.PREFIX + ''.join([param + val if val else '' for param, val in self.url_dict.items()])
self.url = self.PREFIX + ''.join(
[param + val if val else '' for param, val in
self.url_dict.items()])

def connect(self):
# TODO: deal with "unknown exception" error
Expand All @@ -117,16 +133,20 @@ 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("Problem with connection, status code: %s" % req.status_code)
raise BadRequest(
"Problem with connection, status code: %s" % req.status_code)

def check_and_get_api(self):
if self.url_dict[self.API_KEY]: # Check if api_key is empty string
pass
else:
self.url_dict[self.API_KEY] = input('Please type your EtherScan.io API key: ')
self.url_dict[self.API_KEY] = input(
'Please type your EtherScan.io API key: ')

def check_keys_api(self, data):
return all (k in data for k in ('jsonrpc', 'id', 'result'))

@staticmethod
def check_keys_api(data):
return all(k in data for k in ('jsonrpc', 'id', 'result'))
Loading