diff --git a/README.md b/README.md index f751c1b..d4ca273 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
-### my web3 projects and code +### web3 projects and code
@@ -11,9 +11,13 @@ * [**📚 web3-toolkit-py**](web3toolkit): - an *ongoing* development of a library and set of python scripts with my fav on-chain ops. +
+ * [**🔬 blockchain science repo**](https://github.com/go-outside-labs/blockchain-science): - a repo with several on-chain data research notebooks, trading bots, and other shenanigans. +
+ * [**🧮 published book on algorithms and data structures**](https://github.com/go-outside-labs/algorithms-book-py): - it's always good to actually understand those searches and sortings algorithms. published in 2015. @@ -42,8 +46,8 @@ * [magic pen](small-projects/magic-pen) * [maze puzzle](small-projects/maze-puzzle) * [blob boundary](small-projects/finding-blob-boundary) -* [parsing medium blog](small-projects/medium) -* [encoding and decoding](small-projects/enconding-decimals/) +* [parsing medium posts](small-projects/medium) +* [encoding, decoding](small-projects/enconding-decimals/)
@@ -51,10 +55,12 @@ ---- -### general links (just useful stuff) +### more resources
+##### general python + * [black](https://github.com/psf/black) * [flake8 ](https://flake8.pycqa.org/en/latest/) * [pre-commit](https://pre-commit.com/) @@ -62,5 +68,10 @@ * [nose](https://nose.readthedocs.io/en/latest/) * [tox](https://tox.wiki/en/latest/) * [google style guide](https://google.github.io/styleguide/pyguide.html) + +
+ +##### web3 specific + * [async web3py middleware for batching eth calls intos multicalls](https://github.com/BobTheBuidler/dank_mids) diff --git a/web3toolkit/scripts/get_transfer_logs_and_wallets_balance_for_a_token.py b/web3toolkit/scripts/get_transfer_logs_and_wallets_balance_for_a_token.py new file mode 100644 index 0000000..ca53a52 --- /dev/null +++ b/web3toolkit/scripts/get_transfer_logs_and_wallets_balance_for_a_token.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8 -*- +# author: steinkirch +# +############################################################################################ +# +# this script is used to get transfer logs through infura's api and then parse +# these logs to calculate the balance of a wallet for a given token. +# +# to run, create an .env file with the following variables: +# RPC_PROVIDER_URL = https://mainnet.infura.io/v3/ +# CHUNK_SIZE = 100000 +# NUM_ATTEMPTS = 3 +# TRANSFER_EVENT_TOPIC_HASH = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef +# TOKEN_ADDRESS = +# DECIMALS = +# +############################################################################################ + + +import os +import requests +from pathlib import Path +from decimal import Decimal +from dotenv import load_dotenv +from collections import defaultdict + + +def get_env(): + """Load environment variables from .env file""" + + load_dotenv() + env_path = Path('.')/'.env' + load_dotenv(dotenv_path=env_path) + + env_data = {} + env_data['RPC_PROVIDER_URL'] = os.getenv("RPC_PROVIDER_URL") + env_data['CHUNK_SIZE'] = os.getenv("CHUNK_SIZE") + env_data['NUM_ATTEMPTS'] = os.getenv("NUM_ATTEMPTS") + env_data['TRANSFER_EVENT_TOPIC_HASH'] = os.getenv("TRANSFER_EVENT_TOPIC_HASH") + env_data['TOKEN_ADDRESS'] = os.getenv("TOKEN_ADDRESS") + env_data['DECIMALS'] = os.getenv("DECIMALS") + + if not (bool(env_data['RPC_PROVIDER_URL']) or bool(env_data['CHUNK_SIZE']) or \ + bool(env_data['NUM_ATTEMPTS']) or bool(env_data['TRANSFER_EVENT_TOPIC_HASH']) or \ + bool(env_data['TOKEN_ADDRESS']) or bool(env_data['DECIMALS'])): + raise Exception('Please add config to .env file') + + return env_data + + +def convert_hex_to_int(hex_string: str) -> int: + """Convert a hex string to an integer""" + + return int(hex_string, 16) + + +def send_rpc_request(url, method, params=None) -> dict: + """Send a JSON-RPC request to a given URL""" + + params = params or [] + data = {'jsonrpc': '2.0', 'method': method, 'params': params, 'id': 1} + + try: + response = requests.post(url, headers={'Content-Type': 'application/json'}, json=data) + if response.status_code == 200: + return response.json()['result'] + else: + print('Query failed: {}.'.format(response.status_code)) + + except requests.exceptions.HTTPError as e: + print('Error querying to {0}: {1}'.format(url, e.response.text)) + + except KeyError: + print('Error querying to {0}: data not valid'.format(url)) + + +def get_logs(address: str, from_block: int, to_block: int, topic: str, url: str) -> list: + """Get logs from a given address between two blocks""" + + # https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/eth_getlogs + method = 'eth_getLogs' + print(f'loading blocks {from_block} to {to_block}') + + return send_rpc_request(url, method, + [{'address': address, + 'fromBlock': from_block, + 'toBlock': to_block, + 'topics': [topic] + }]) + + +def get_last_block_number(url: str) -> int: + """Get the last block number""" + + # https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/eth_blocknumber + method = 'eth_blockNumber' + return convert_hex_to_int(send_rpc_request(url, method)) + + +def get_transfer_logs(env_data: dict, address: str, decimals: int, + from_block=None, to_block=None, skip_chunks=False) -> list: + """Get transfer logs from a given address between two blocks""" + + from_block = from_block or 'earliest' + to_block = to_block or 'latest' + topic = env_data['TRANSFER_EVENT_TOPIC_HASH'] + url = env_data['RPC_PROVIDER_URL'] + + ################################# + # retrieve event logs by chunks + ################################# + if not skip_chunks: + + logs = [] + first_block = 1 + c_size = int(env_data['CHUNK_SIZE']) + attempts = int(env_data['NUM_ATTEMPTS']) + last_block = get_last_block_number(url) + + for block in range(first_block, last_block, c_size): + attempt = 0 + while attempt < attempts: + try: + logs += get_logs(address, hex(block), hex(block + c_size), topic, url) + break + except Exception: + attempt += 1 + + ################################# + # retrieve event logs in one go + ################################# + else: + logs = get_logs(address, hex(from_block), hex(to_block), topic, url) + + return logs + + +def ged_processed_logs(logs: list, decimals: int) -> list: + """Process logs to get from, to and amount""" + + decimal = Decimal('10') ** Decimal(f'-{decimals}') + processed_logs = defaultdict() + + try: + for log in logs: + processed_logs[log['transactionHash']] = {} + processed_logs[log['transactionHash']]['blockNumber'] = log['blockNumber'] + processed_logs[log['transactionHash']]['from'] = '0x' + log['topics'][1][26:] + processed_logs[log['transactionHash']]['to'] = '0x' + log['topics'][2][26:] + processed_logs[log['transactionHash']]['amount'] = Decimal(convert_hex_to_int(log['data'])) * decimal + except KeyError as e: + print(f'Error processing logs: {e}') + + return processed_logs + + +def get_balances(transfers: list) -> list: + """Get balances of all addresses that have received tokens""" + + balances = defaultdict(Decimal) + + for _, transfer_data in transfers.items(): + balances[transfer_data['from']] -= transfer_data['amount'] + balances[transfer_data['to']] += transfer_data['amount'] + + balances = [{'address': k, 'amount': v} for k, v in balances.items() if v > Decimal('0')] + return sorted(balances, key=lambda x: -abs(Decimal(x['amount']))) + + +if __name__ == '__main__': + + env_data = get_env() + + address = env_data['TOKEN_ADDRESS'] + decimals = env_data['DECIMALS'] + + transfer_logs = get_transfer_logs(env_data, address, decimals, from_block=16801268, to_block=16807268, skip_chunks=True) + processed_logs = ged_processed_logs(transfer_logs, decimals) + balances = get_balances(processed_logs) + + for balance in balances: + print(f'{balance["address"]} has {balance["amount"]} tokens')