diff --git a/README.rst b/README.rst index b42a1c7..20f9724 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,14 @@ ============================== -Welcome to ScanWatch 0.1.3 +Welcome to ScanWatch 0.2.0 ============================== + Note ---- This library is developed and maintained by EtWnn, feel free to drop your suggestions or remarks in -the discussion tab of the git repo. You are also welcome to contribute by submitting PRs. +the `discussion tab `_. +You are also welcome to contribute by submitting PRs. **Source Code:** https://github.com/EtWnn/ScanWatch @@ -14,24 +16,39 @@ the discussion tab of the git repo. You are also welcome to contribute by submit https://scanwatch.readthedocs.io -This library is a local tracker of transactions for the Ethereum chain and the Binance Smart chain. -It is a simple interface with the `etherscan `_ and the -`bscscan `_ APIs and will save locally the results to gain time and avoid over-calling the APIs. +| This library is a local tracker of transactions for the Ethereum chain, the Binance Smart chain and the Polygon chain. +| It is a simple single-point interface with the `etherscan `__, `bscscan `__ + and `polygonscan `__ APIs. +| This library will save locally the transactions to gain time and avoid over-calling the APIs. + Announcement ------------ +|siren| |siren| |siren| + If you previously used this library with a version inferior to 0.1.3, please head `here `_ to correct a potential bug in the database. +|siren| |siren| |siren| + Quick Tour ---------- -You will need to generate an API token to use this library. -Go on `etherscan `__ for the Ethereum chain and on -`bscscan `__ for the BSC chain. -(If you want to use both chains, you will need an API token for each). +1. API Keys +~~~~~~~~~~~ + +You will need to generate API tokens to use this library: + +1. Ethereum chain: go on `etherscan `__ +2. Binance Smart chain: go on `bscscan `__ +3. Polygon chain: go on `polygonscan `__ + +(If you want to use several chains, you will need an API token for each). + +2. Installation +~~~~~~~~~~~~~~~ ``ScanWatch`` is available on `PYPI `_, install with ``pip``: @@ -45,7 +62,11 @@ You can also install the latest developments (not stable): pip install git+https://github.com/EtWnn/ScanWatch.git@develop -You can then use your API token to instantiate the manager. +3. Manager +~~~~~~~~~~ + +| The manager is the object that you will use to update and get the transactions. +| It is instantiated with an API token and an address. Example for Ethereum: @@ -71,6 +92,21 @@ Example for BSC: manager = ScanManager(address, NETWORK.BSC, api_token) +Example for Polygon: + +.. code:: python + + from ScanWatch.ScanManager import ScanManager + from ScanWatch.utils.enums import NETWORK + + api_token = "" + address = "" + + manager = ScanManager(address, NETWORK.POLYGON, api_token) + +4. Transactions Update +~~~~~~~~~~~~~~~~~~~~~~ + Once the manager is setup, you can update the locally saved transactions: .. code:: python @@ -79,8 +115,12 @@ Once the manager is setup, you can update the locally saved transactions: # all transactions updated for address 0xaAC...748E8: 100%|████████████| 4/4 [00:02<00:00, 1.86it/s] This needs to be done only when new transactions have been made since the last time you called the update method. -Otherwise you can just fetch the transactions that have been previously saved, as shown below -(see the documentation for more details). + +5. Transactions +~~~~~~~~~~~~~~~ + +To fetch the transactions that have been previously saved, just use the methods below. +(see the `documentation `_ for more details). .. code:: python @@ -94,6 +134,54 @@ Otherwise you can just fetch the transactions that have been previously saved, a manager.get_transactions(TRANSACTION.INTERNAL) # internal transactions +6. Holdings +~~~~~~~~~~~ + +The manager can also give you the current tokens hold by an address: + +For erc20 tokens: + +.. code:: python + + manager.get_erc20_holdings() + + +.. code:: bash + + { + 'USDC': Decimal('50'), + 'AllianceBlock Token': Decimal('12458.494516884'), + 'Blockchain Certified Data Token': Decimal('75174'), + 'Compound': Decimal('784.24998156'), + 'ZRX': Decimal('3.1') + } + +For erc721 tokens: + +.. code:: python + + manager.get_erc721_holdings() + + +.. code:: bash + + [ + { + 'contractAddress': '0x8azd48c9ze46azx1e984fraz4da9zz8dssad49ct', + 'tokenID': '78941', + 'count': 1, + 'tokenName': 'SUPER NFT GAME', + 'tokenSymbol': 'Hero' + }, + { + 'contractAddress': '0x6edd39bdba2fazs3db5fxd86908789cbd905f04d', + 'tokenID': '33001', + 'count': 1, + 'tokenName': 'MY FAV NFT ARTIST HANDMADE THIS', + 'tokenSymbol': 'dubious thing' + } + ] + Main / test nets ---------------- @@ -107,14 +195,19 @@ If you want to switch from main to test nets, you can specify the net name at th Supported nets are: - For Ethereum: "main", "goerli", "kovan", "rinkeby", "ropsten" - For BSC: "main", "test" + - For Polygon: "main", "test" Donation -------- -If this library has helped you in any way, feel free to donate: +| If this library has helped you in any way, feel free to help me |blush| +| With your donation, I will be able to keep working on this project and add new features. Thank you! - **BTC**: 14ou4fMYoMVYbWEKnhADPJUNVytWQWx9HG -- **ETH**: 0xfb0ebcf8224ce561bfb06a56c3b9a43e1a4d1be2 +- **ETH**, **BSC**, **Polygon**: 0xA20be1f02B1C9D4FF1442a0F0e7c089fcDd59407 - **LTC**: LfHgc969RFUjnmyLn41SRDvmT146jUg9tE - **EGLD**: erd1qk98xm2hgztvmq6s4jwtk06g6laattewp6vh20z393drzy5zzfrq0gaefh + +.. |siren| replace:: 🚨 +.. |blush| replace:: 😊 \ No newline at end of file diff --git a/ScanWatch/Client.py b/ScanWatch/Client.py index 3991b89..ceece8f 100644 --- a/ScanWatch/Client.py +++ b/ScanWatch/Client.py @@ -8,9 +8,10 @@ class Client: """ - Client for the etherscan.io API or the bscscan.io API + Client the API: https://etherscan.io/apis https://bscscan.com/apis + https://polygonscan.com/apis """ BASE_URLS = { NETWORK.BSC: { @@ -23,6 +24,10 @@ class Client: "kovan": "https://api-kovan.etherscan.io/api", "rinkeby": "https://api-rinkeby.etherscan.io/api", "ropsten": "https://api-ropsten.etherscan.io/api" + }, + NETWORK.POLYGON: { + "main": "https://api.polygonscan.com/api", + "test": "https://api-testnet.polygonscan.com/api" } } @@ -62,7 +67,7 @@ def get_mined_blocks(self, address: str, start_block: Optional[int] = None, end_ def get_erc721_transactions(self, address: str, start_block: Optional[int] = None, end_block: Optional[int] = None): """ - fetch erc721 transactions on an eth / bsc address + fetch erc721 transactions on an address :param address: address :type address: str @@ -77,7 +82,7 @@ def get_erc721_transactions(self, address: str, start_block: Optional[int] = Non def get_erc20_transactions(self, address: str, start_block: Optional[int] = None, end_block: Optional[int] = None): """ - fetch erc20 transactions on an eth / bsc address + fetch erc20 transactions on an address :param address: address :type address: str @@ -117,7 +122,7 @@ def get_erc20_transactions(self, address: str, start_block: Optional[int] = None def get_normal_transactions(self, address: str, start_block: Optional[int] = None, end_block: Optional[int] = None): """ - fetch normal transactions on an eth / bsc address + fetch normal transactions on an address :param address: address :type address: str @@ -157,7 +162,7 @@ def get_normal_transactions(self, address: str, start_block: Optional[int] = Non def get_internal_transactions(self, address: str, start_block: Optional[int] = None, end_block: Optional[int] = None): """ - fetch internal transactions on an eth / bsc address + fetch internal transactions on an address :param address: address :type address: str @@ -173,7 +178,7 @@ def get_internal_transactions(self, address: str, start_block: Optional[int] = N def _get_transactions(self, address: str, action: str, start_block: Optional[int] = None, end_block: Optional[int] = None): """ - fetch transactions on an eth / bsc address + fetch transactions on an address :param address: address :type address: str @@ -208,7 +213,7 @@ def _get_transactions(self, address: str, action: str, start_block: Optional[int def get_balance(self, address: str) -> float: """ - fetch the current eth / bnb balance of an address + fetch the current balance of an address :param address: address :type address: str @@ -224,7 +229,7 @@ def get_balance(self, address: str) -> float: def get_url_request(self, **kwargs) -> str: """ - Construct the url to make a request to the etherscan.io / bscscan.com API + Construct the url to make a request to the API :param kwargs: keywords args for the endpoint :type kwargs: Any @@ -244,7 +249,7 @@ def get_result(url: str): """ call the API with an url, raise if the status is not ok and return the API result - :param url: url to request for the etherscan.io or bscscan.com + :param url: url to request :type url: str :return: API result :rtype: depend of the endpoint diff --git a/ScanWatch/ScanManager.py b/ScanWatch/ScanManager.py index 3cc3600..a7f6c3b 100644 --- a/ScanWatch/ScanManager.py +++ b/ScanWatch/ScanManager.py @@ -1,3 +1,6 @@ +from decimal import Decimal +from typing import Dict, List + from tqdm import tqdm from ScanWatch.Client import Client @@ -78,3 +81,63 @@ def get_transactions(self, tr_type: TRANSACTION): :rtype: List[Dict] """ return self.db.get_transactions(self.address, self.nt_type, self.net, tr_type) + + def get_erc20_holdings(self) -> Dict: + """ + Return the amount of every erc20 the address holds at the last update time. + WARNING: Some tokens trigger non-erc20 events, such as internal exchange fee. This will not be picked up by + this function. As a consequence, the balance of such tokens might be wrong. + + :return: a dictionary of token amount per token name + :rtype: Dict + """ + txs = self.get_transactions(TRANSACTION.ERC20) + holdings = {} + for tx in txs: + amount = Decimal(tx['value']) / Decimal(10 ** int(tx['tokenDecimal'])) + if self.address.lower() == tx['from']: + amount *= -1 + try: + holdings[tx['tokenName']] += amount + except KeyError: + if amount < 0: + raise ValueError(f"First operation on an asset is a removal {tx}") + holdings[tx['tokenName']] = amount + return {k: v for k, v in holdings.items() if v != 0} + + def get_erc721_holdings(self) -> List[Dict]: + """ + Return the erc721 tokens that the address holds at the time of the last update + + :return: List of erc721 tokens owned by the address + :rtype: List[Dict] + """ + txs = self.get_transactions(TRANSACTION.ERC721) + holdings = {} + for tx in txs: + amount = 1 + if self.address.lower() == tx['from']: + amount = -1 + try: + holdings[tx['contractAddress']][tx['tokenID']]['count'] += amount + except KeyError: + if amount < 0: + raise ValueError(f"First operation on an asset is a removal {tx}") + try: + holdings[tx['contractAddress']][tx['tokenID']] = {'count': amount, + 'tokenName': tx['tokenName'], + 'tokenSymbol': tx['tokenSymbol']} + except KeyError: + holdings[tx['contractAddress']] = {tx['tokenID']: {'count': amount, + 'tokenName': tx['tokenName'], + 'tokenSymbol': tx['tokenSymbol']} + } + # Present the result in a single list + result = [] + for contract, nfts in holdings.items(): + for token_id, nft in nfts.items(): + if nft['count'] != 0: + result.append({'contractAddress': contract, + 'tokenID': token_id, + **nft}) + return result diff --git a/ScanWatch/__init__.py b/ScanWatch/__init__.py index 70f87e3..abe49c9 100644 --- a/ScanWatch/__init__.py +++ b/ScanWatch/__init__.py @@ -1,2 +1,2 @@ __author__ = 'EtWnn' -__version__ = '0.1.3' +__version__ = '0.2.0' diff --git a/ScanWatch/utils/enums.py b/ScanWatch/utils/enums.py index 2f0b0eb..293d4e4 100644 --- a/ScanWatch/utils/enums.py +++ b/ScanWatch/utils/enums.py @@ -4,6 +4,7 @@ class NETWORK(Enum): ETHER = 1 BSC = 2 + POLYGON = 3 class TRANSACTION(Enum): diff --git a/requirements-dev.txt b/requirements-dev.txt index 9f8a77a..96b5b5e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ setuptools~=58.0.4 sphinx~=4.3.2 sphinx_rtd_theme~=1.0.0 -twine~=3.7.1 \ No newline at end of file +twine~=3.7.1 +restructuredtext-lint~=1.3.2 \ No newline at end of file diff --git a/setup.py b/setup.py index fec7799..8b0379d 100644 --- a/setup.py +++ b/setup.py @@ -22,11 +22,11 @@ author='EtWnn', author_email='EtWnn0x@gmail.com', license='MIT', - description='Local tracker of an eth address for ETH and BSC scan', + description='Local tracker for address on Ethereum, BSC and Polygon chains', long_description=long_description, long_description_content_type='text/x-rst', install_requires=requirements, - keywords='eth bsc wallet save tracking history ethereum tracker binance smartchain smart chain', + keywords='eth bsc polygon wallet save tracking history ethereum matic bnb tracker binance smartchain smart chain', classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License',