From c011a12b48746b0449dc6399d8a2b4802f9ee52f Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Wed, 8 Jul 2020 21:19:42 -0400 Subject: [PATCH] Add eth utils and readme --- README.md | 17 ++++ build/lib/eth_utils/__init__.py | 1 + build/lib/eth_utils/decimals.py | 7 ++ build/lib/eth_utils/jsonrpc.py | 62 +++++++++++++ .../lib/tendermint_utils}/__init__.py | 0 .../lib/tendermint_utils}/rpc.py | 0 chainwalkers_utils.egg-info/PKG-INFO | 21 ++++- chainwalkers_utils.egg-info/SOURCES.txt | 8 +- chainwalkers_utils.egg-info/top_level.txt | 3 +- dist/chainwalkers_utils-0.0.4-py3.7.egg | Bin 3807 -> 10410 bytes eth_utils/__init__.py | 1 + eth_utils/decimals.py | 7 ++ eth_utils/jsonrpc.py | 62 +++++++++++++ setup.py | 5 ++ tendermint_utils/__init__.py | 1 + tendermint_utils/rpc.py | 85 ++++++++++++++++++ 16 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 README.md create mode 100644 build/lib/eth_utils/__init__.py create mode 100644 build/lib/eth_utils/decimals.py create mode 100644 build/lib/eth_utils/jsonrpc.py rename {tendermint => build/lib/tendermint_utils}/__init__.py (100%) rename {tendermint => build/lib/tendermint_utils}/rpc.py (100%) create mode 100644 eth_utils/__init__.py create mode 100644 eth_utils/decimals.py create mode 100644 eth_utils/jsonrpc.py create mode 100644 tendermint_utils/__init__.py create mode 100644 tendermint_utils/rpc.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..be10ebb --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Chainwalker Utilities + +This repo is home to Flipsides chainwalkers python utilities package. A central repo that packeges up various python modules that can be used across chainwalkers projects. + +## Modules + +### Tendermint Utils + +Collection of common methods used to interact with tendermint based chains (Cosmos, Kava, Binance) + +### Eth utils + +Collection of common methods used across eth based chains + +## Install + +`pip install git+ssh://git@github.com/FlipsideCrypto/chainwalkers-utils.git` \ No newline at end of file diff --git a/build/lib/eth_utils/__init__.py b/build/lib/eth_utils/__init__.py new file mode 100644 index 0000000..cdd4ae2 --- /dev/null +++ b/build/lib/eth_utils/__init__.py @@ -0,0 +1 @@ +from eth_utils.jsonrpc import JsonRpcCaller diff --git a/build/lib/eth_utils/decimals.py b/build/lib/eth_utils/decimals.py new file mode 100644 index 0000000..2f05b5a --- /dev/null +++ b/build/lib/eth_utils/decimals.py @@ -0,0 +1,7 @@ +def hex_to_decimal(hex_value): + if hex_value == '0x': + return 0 + return int(hex_value, 16) + +def decimal_to_hex(decimal): + return hex(decimal) \ No newline at end of file diff --git a/build/lib/eth_utils/jsonrpc.py b/build/lib/eth_utils/jsonrpc.py new file mode 100644 index 0000000..f452cd6 --- /dev/null +++ b/build/lib/eth_utils/jsonrpc.py @@ -0,0 +1,62 @@ +import requests +import json +import base64 +import time + +class RpcCallFailedException(Exception): + pass + +class JsonRpcCaller(object): + + def __init__(self, node_url, user=None, password=None, tls=False, tlsVerify=False): + self.url = node_url + self.user = user + self.password = password + self.tls = tls + self.tlsVerify = tlsVerify + + def _make_rpc_call(self, headers, payload, json): + try: + response = requests.post( + self.url, + headers=headers, + data=payload, + json=json, + verify=(self.tls and self.tlsVerify) + ) + except Exception as e: + raise RpcCallFailedException(e) + + if response.status_code != 200: + raise RpcCallFailedException("Invalid status code: %s" % response.status_code) + + responseJson = response.json(parse_float=lambda f: f) + + if type(responseJson) != list: + if "error" in responseJson and responseJson["error"] is not None: + raise RpcCallFailedException("RPC call error: %s" % responseJson["error"]) + else: + return responseJson.get('result') + else: + result = [] + for subResult in responseJson: + if "error" in subResult and subResult["error"] is not None: + raise RpcCallFailedException("RPC call error: %s" % subResult["error"]) + else: + result.append(subResult["result"]) + return result + + def call(self, method, params=None, query=None): + if params is None: + params = [] + headers = {'content-type': 'application/json'} + payload = json.dumps({"jsonrpc": "2.0", "id": "0", "method": method, "params": params}) + if query: # GQL Hack + return self._make_rpc_call(headers, payload=None, json={'query': query}) + return self._make_rpc_call(headers, payload, json=None) + + def bulk_call(self, methodParamsTuples): + headers = {'content-type': 'application/json'} + payload = json.dumps([{"jsonrpc": "2.0", "id": "0", "method": method, "params": params} + for method, params in methodParamsTuples]) + return self._make_rpc_call(headers, payload) \ No newline at end of file diff --git a/tendermint/__init__.py b/build/lib/tendermint_utils/__init__.py similarity index 100% rename from tendermint/__init__.py rename to build/lib/tendermint_utils/__init__.py diff --git a/tendermint/rpc.py b/build/lib/tendermint_utils/rpc.py similarity index 100% rename from tendermint/rpc.py rename to build/lib/tendermint_utils/rpc.py diff --git a/chainwalkers_utils.egg-info/PKG-INFO b/chainwalkers_utils.egg-info/PKG-INFO index fc8c369..da02d7e 100644 --- a/chainwalkers_utils.egg-info/PKG-INFO +++ b/chainwalkers_utils.egg-info/PKG-INFO @@ -1,4 +1,4 @@ -Metadata-Version: 1.0 +Metadata-Version: 2.1 Name: chainwalkers-utils Version: 0.0.4 Summary: Collection of utilities to be used across chainwalkers repos @@ -6,5 +6,22 @@ Home-page: git@github.com:FlipsideCrypto/chainwalkers-utils.git Author: Brian Ford Author-email: brian@flipsidecrypto.com License: unlicense -Description: UNKNOWN +Description: # Chainwalker Utilities + + This repo is home to Flipsides chainwalkers python utilities package. A central repo that packeges up various python modules that can be used across chainwalkers projects. + + ## Modules + + ### Tendermint Utils + + Collection of common methods used to interact with tendermint based chains (Cosmos, Kava, Binance) + + ### Eth utils + + Collection of common methods used across eth based chains + + ## Install + + `pip install git+ssh://git@github.com/FlipsideCrypto/chainwalkers-utils.git` Platform: UNKNOWN +Description-Content-Type: text/markdown diff --git a/chainwalkers_utils.egg-info/SOURCES.txt b/chainwalkers_utils.egg-info/SOURCES.txt index d9842ea..334b411 100644 --- a/chainwalkers_utils.egg-info/SOURCES.txt +++ b/chainwalkers_utils.egg-info/SOURCES.txt @@ -1,8 +1,12 @@ +README.md setup.py chainwalkers_utils.egg-info/PKG-INFO chainwalkers_utils.egg-info/SOURCES.txt chainwalkers_utils.egg-info/dependency_links.txt chainwalkers_utils.egg-info/not-zip-safe chainwalkers_utils.egg-info/top_level.txt -tendermint/__init__.py -tendermint/rpc.py \ No newline at end of file +eth_utils/__init__.py +eth_utils/decimals.py +eth_utils/jsonrpc.py +tendermint_utils/__init__.py +tendermint_utils/rpc.py \ No newline at end of file diff --git a/chainwalkers_utils.egg-info/top_level.txt b/chainwalkers_utils.egg-info/top_level.txt index 9059c68..4c82be1 100644 --- a/chainwalkers_utils.egg-info/top_level.txt +++ b/chainwalkers_utils.egg-info/top_level.txt @@ -1 +1,2 @@ -tendermint +eth_utils +tendermint_utils diff --git a/dist/chainwalkers_utils-0.0.4-py3.7.egg b/dist/chainwalkers_utils-0.0.4-py3.7.egg index 1e52836acdbd29e4723eceae6a6b55c432a83879..fc2dba80aa00dc24ed68398aa7342eea291c882c 100644 GIT binary patch delta 6061 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 D-=+x! diff --git a/eth_utils/__init__.py b/eth_utils/__init__.py new file mode 100644 index 0000000..cdd4ae2 --- /dev/null +++ b/eth_utils/__init__.py @@ -0,0 +1 @@ +from eth_utils.jsonrpc import JsonRpcCaller diff --git a/eth_utils/decimals.py b/eth_utils/decimals.py new file mode 100644 index 0000000..2f05b5a --- /dev/null +++ b/eth_utils/decimals.py @@ -0,0 +1,7 @@ +def hex_to_decimal(hex_value): + if hex_value == '0x': + return 0 + return int(hex_value, 16) + +def decimal_to_hex(decimal): + return hex(decimal) \ No newline at end of file diff --git a/eth_utils/jsonrpc.py b/eth_utils/jsonrpc.py new file mode 100644 index 0000000..f452cd6 --- /dev/null +++ b/eth_utils/jsonrpc.py @@ -0,0 +1,62 @@ +import requests +import json +import base64 +import time + +class RpcCallFailedException(Exception): + pass + +class JsonRpcCaller(object): + + def __init__(self, node_url, user=None, password=None, tls=False, tlsVerify=False): + self.url = node_url + self.user = user + self.password = password + self.tls = tls + self.tlsVerify = tlsVerify + + def _make_rpc_call(self, headers, payload, json): + try: + response = requests.post( + self.url, + headers=headers, + data=payload, + json=json, + verify=(self.tls and self.tlsVerify) + ) + except Exception as e: + raise RpcCallFailedException(e) + + if response.status_code != 200: + raise RpcCallFailedException("Invalid status code: %s" % response.status_code) + + responseJson = response.json(parse_float=lambda f: f) + + if type(responseJson) != list: + if "error" in responseJson and responseJson["error"] is not None: + raise RpcCallFailedException("RPC call error: %s" % responseJson["error"]) + else: + return responseJson.get('result') + else: + result = [] + for subResult in responseJson: + if "error" in subResult and subResult["error"] is not None: + raise RpcCallFailedException("RPC call error: %s" % subResult["error"]) + else: + result.append(subResult["result"]) + return result + + def call(self, method, params=None, query=None): + if params is None: + params = [] + headers = {'content-type': 'application/json'} + payload = json.dumps({"jsonrpc": "2.0", "id": "0", "method": method, "params": params}) + if query: # GQL Hack + return self._make_rpc_call(headers, payload=None, json={'query': query}) + return self._make_rpc_call(headers, payload, json=None) + + def bulk_call(self, methodParamsTuples): + headers = {'content-type': 'application/json'} + payload = json.dumps([{"jsonrpc": "2.0", "id": "0", "method": method, "params": params} + for method, params in methodParamsTuples]) + return self._make_rpc_call(headers, payload) \ No newline at end of file diff --git a/setup.py b/setup.py index 04aad3c..45b3a7f 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,14 @@ import setuptools +with open("README.md", "r") as fh: + long_description = fh.read() + setuptools.setup( name='chainwalkers_utils', version='0.0.4', description='Collection of utilities to be used across chainwalkers repos', + long_description=long_description, + long_description_content_type="text/markdown", url='git@github.com:FlipsideCrypto/chainwalkers-utils.git', author='Brian Ford', author_email='brian@flipsidecrypto.com', diff --git a/tendermint_utils/__init__.py b/tendermint_utils/__init__.py new file mode 100644 index 0000000..6e9328b --- /dev/null +++ b/tendermint_utils/__init__.py @@ -0,0 +1 @@ +from tendermint.rpc import TendermintRPC \ No newline at end of file diff --git a/tendermint_utils/rpc.py b/tendermint_utils/rpc.py new file mode 100644 index 0000000..2d874d3 --- /dev/null +++ b/tendermint_utils/rpc.py @@ -0,0 +1,85 @@ +import json +import requests + +class TendermintRPC: + + def __init__(self, node_url): + self.node_url = node_url + + def get_block_height(self): + try: + response = requests.get(self.node_url+ '/abci_info?') + response.raise_for_status() + data = response.json() + return data['result']['response']['last_block_height'] + except Exception as err: + print(f'An error occured retrieving the latest block height: {err}') + + def get_block(self, height): + try: + response = requests.get(self.node_url + '/block?height=' + str(height)) + response.raise_for_status() + data = response.json() + block = self.init_block(data['result']) + + block_results = self.get_block_results(height) + + # Capture transactions and underlying events + block_transactions = self.get_transactions_by_block(height) + if block_transactions['txs']: + for tx in block_transactions['txs']: + block_tx = self.get_transactions_by_hash(tx['hash']) + block.add_transaction(block_tx) + + # Capture begin block events () + if block_results['results']['begin_block']: + for event in block_results['results']['begin_block']['events']: + block.begin_block.append(event) + + block.end_block = block_results['results']['end_block'] + + block_validators = self.get_block_validators(height) + for validator in block_validators['validators']: + block.validators.append(validator) + + return block + except Exception as err: + print(f'An error occured retrieving block: {err}') + + def get_block_results(self, height): + try: + response = requests.get(self.node_url + '/block_results?height=' + str(height)) + response.raise_for_status() + data = response.json() + return data['result'] + except Exception as err: + print(f'An error occured retrieving the results of block height: {err}') + + # Need ANKR to turn on indexing at node level for this call to work properly + def get_transactions_by_block(self, height): + try: + response = requests.get(self.node_url + '/tx_search?query=\"tx.height=' + str(height) + '\"&prove=true') + response.raise_for_status() + data = response.json() + return data['result'] + except Exception as err: + print(f'An error occured retrieving the transactions in block: {err}') + + def get_transactions_by_hash(self, tx_hash): + try: + response = requests.get(self.node_url + '/tx?hash=' + tx_hash) + response.raise_for_status() + data = response.json() + return data + except Exception as err: + print(f'An error occured retrieving the transaction by hash: {err}') + + # Currently returning the following response "Height must be less than or equal to the current blockchain height" + def get_block_validators(self, height): + try: + response = requests.get(self.node_url + '/validators?height=' + str(height)) + response.raise_for_status() + data = response.json() + return data['result'] + except Exception as err: + print(f'An error occured retrieving the validators for block height: {err}') \ No newline at end of file