diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index e8b29ef..16e4d42 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -19,4 +19,4 @@ jobs: - name: Lint with flake8 run: | pip install flake8 - flake8 . --count --max-complexity=10 --statistics + flake8 . --count --max-complexity=10 --statistics --exclude protos diff --git a/README.md b/README.md index f786db5..9ffbc1b 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ and formatted the same way as the [smartphone app expects](https://docs.helium.c Usage: ``` -client = MinerClient() +client = GatewayClient() result = client.create_add_gateway_txn('owner_address', 'payer_address', 'gateway_address') ``` diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..deb728a --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1 @@ +grpcio-tools==1.44.0 diff --git a/hm_pyhelper/exceptions.py b/hm_pyhelper/exceptions.py index c15aa87..7f05c5e 100644 --- a/hm_pyhelper/exceptions.py +++ b/hm_pyhelper/exceptions.py @@ -18,6 +18,10 @@ class MinerFailedToFetchMacAddress(Exception): pass +class MinerFailedToFetchEthernetAddress(Exception): + pass + + class UnknownVariantException(Exception): pass diff --git a/hm_pyhelper/gateway_grpc/__init__.py b/hm_pyhelper/gateway_grpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hm_pyhelper/gateway_grpc/client.py b/hm_pyhelper/gateway_grpc/client.py new file mode 100644 index 0000000..b04a5c5 --- /dev/null +++ b/hm_pyhelper/gateway_grpc/client.py @@ -0,0 +1,236 @@ +import base58 +import grpc +import subprocess +import json +from typing import Union + +from hm_pyhelper.protos import blockchain_txn_add_gateway_v1_pb2, \ + local_pb2_grpc, local_pb2, region_pb2 +from hm_pyhelper.gateway_grpc.exceptions import MinerMalformedAddGatewayTxn + +from hm_pyhelper.logger import get_logger + +LOGGER = get_logger(__name__) + + +def decode_pub_key(encoded_key: bytes) -> str: + # Addresses returned by the RPC response are missing a leading + # byte for the version. The version is currently always 0. + # https://github.com/helium/helium-js/blob/8d5cb76e156fb80de6fc80f239b43e3872c7b7d7/packages/crypto/src/Address.ts#L64 + version_byte = b'\x00' + + # Convert binary address to base58 + complete_key = version_byte + encoded_key + decoded_key = base58.b58encode_check(complete_key).decode() + return decoded_key + + +class GatewayClient(object): + ''' + GatewayClient wraps grpc api provided by helium gateway-rs + It provides some convenience methods to support the old api + to limit breaking changes. + Direct interaction with the grpc api can be achieved by + using GatewayClient.stub. + + All methods might return grpc pass through exceptions. + ''' + + def __init__(self, url='helium-miner:4467'): + self._url = url + self._channel = grpc.insecure_channel(url) + self._channel.subscribe(self._connect_state_handler) + self.stub = local_pb2_grpc.apiStub(self._channel) + + def _connect_state_handler(self, state): + if state == grpc.ChannelConnectivity.SHUTDOWN: + LOGGER.error('GRPC Channel shutdown : irrecoverable error') + + def __enter__(self): + return self + + def __exit__(self, _, _2, _3): + self._channel.close() + + def get_validator_info(self) -> local_pb2.height_res: + return self.stub.height(local_pb2.height_req()) + + def get_height(self) -> int: + return self.get_validator_info().height + + def get_region_enum(self) -> int: + ''' + Returns the current configured region of the gateway. + If not asserted or set in settings, defaults to 0 (US915) + ref: https://github.com/helium/proto/blob/master/src/region.proto + ''' + return self.stub.region(local_pb2.region_req()).region + + def get_region(self) -> str: + ''' + Returns the current configured region of the gateway. + If not asserted or set in settings, defaults to 0 (US915) + ''' + region_id = self.get_region_enum() + return region_pb2.region.Name(region_id) + + def sign(self, data: bytes) -> bytes: + ''' + Sign a message with the gateway private key + ''' + return self.stub.sign(local_pb2.sign_req(data=data)).signature + + def ecdh(self, address: bytes) -> bytes: + ''' + Return shared secret using ECDH + ''' + return self.stub.ecdh(local_pb2.ecdh_req(address=address)).secret + + def get_pubkey(self) -> str: + ''' + Returns decoded public key of the gateway + ''' + encoded_key = self.stub.pubkey(local_pb2.pubkey_req()).address + return decode_pub_key(encoded_key) + + def get_summary(self) -> dict: + ''' + Returns a dict with following information + { + "region": str + configured region eg. "US915", + "key": str + gateway/device public key, + "validator": { + "height": int + blockchain height, + "block_age": int + age of the last block in seconds, + "address": str + public key/address of the validator, + "uri": http url + http endpoint of the validator + } + } + ''' + validator_info = self.get_validator_info() + return { + 'region': self.get_region(), + 'key': self.get_pubkey(), + 'gateway_version': self.get_gateway_version(), + 'validator': { + 'height': validator_info.height, + 'block_age': validator_info.block_age, + 'address': decode_pub_key(validator_info.gateway.address), + 'uri': validator_info.gateway.uri + } + } + + def get_blockchain_config_variables(self, keys: list) -> local_pb2.config_res: + ''' + Allows one to query blockchain variables. For a complete list of chain variables ref + https://helium.plus/chain-vars + + Returns config_res which is a list of config_value for the given list + of blockchain variables. + ''' + return self.stub.config(local_pb2.config_req(keys=keys)) + + def get_blockchain_config_variable(self, key: str) -> local_pb2.config_value: + ''' + Convenience method to get a single variable from the blockchain + + Raises ValueError if the key is not found + ''' + values = self.get_blockchain_config_variables(keys=[key]).values + if not values[0].value: + raise ValueError(f'{key} not found on chain') + return values[0] + + def get_gateway_version(self) -> Union[str, None]: + ''' + Returns the current version of the gateway package installed + ''' + # NOTE:: there is a command line argument to helium-gateway + # but it is not exposed in the rpc, falling back to dpkg + try: + output = subprocess.check_output(['dpkg', '-s', 'helium_gateway']) + for line in output.decode().splitlines(): + if line.strip().startswith('Version'): + # dpkg has version without v but github tags begin with v + return "v" + line.split(':')[1].strip() + return None + except subprocess.CalledProcessError: + return None + + def create_add_gateway_txn(self, owner_address: str, payer_address: str, + staking_mode: local_pb2.gateway_staking_mode = local_pb2.light, + gateway_address: str = "") -> dict: + """ + Invokes the txn_add_gateway RPC endpoint on the gateway and returns + the same payload that the smartphone app traditionally expects. + https://docs.helium.com/mine-hnt/full-hotspots/become-a-maker/hotspot-integration-testing/#generate-an-add-hotspot-transaction + + Parameters: + - owner_address: The address of the account that owns the gateway. + - payer_address: The address of the account that will pay for the + transaction. This will typically be the + maker/Nebra's account. + - staking_mode: The staking mode of the gateway. + ref: + https://github.com/helium/proto/blob/master/src/service/local.proto#L38 + - gateway_address: The address of the miner itself. This is + an optional parameter because the miner + will always return it in the payload during + transaction generation. If the param is + provided, it will only be used as extra + validation. + """ + # NOTE:: this is unimplemented as of alpha23 release of the gateway + response = self.stub.add_gateway(local_pb2.add_gateway_req( + owner=owner_address.encode('utf-8'), + payer=payer_address.encode('utf-8'), + staking_mode=staking_mode + )) + result = json.loads(response.decode()) + if result["address"] != gateway_address: + raise MinerMalformedAddGatewayTxn + return result + + +def get_address_from_add_gateway_txn(add_gateway_txn: + blockchain_txn_add_gateway_v1_pb2, + address_type: str, + expected_address: str = None): + """ + Deserializes specified field in the blockchain_txn_add_gateway_v1_pb2 + protobuf to a base58 Helium address. + + Pararms: + - add_gateway_txn: The blockchain_txn_add_gateway_v1_pb2 to + inspect. + - address_type: 'owner', 'gateway', or 'payer'. + - expected_address (optional): Value we expect to be returned. + + Raises: + MinerMalformedAddGatewayTxn if expected_address supplied and + does not match the return value. + """ + + # Addresses returned by the RPC response are missing a leading + # byte for the version. The version is currently always 0. + # https://github.com/helium/helium-js/blob/8d5cb76e156fb80de6fc80f239b43e3872c7b7d7/packages/crypto/src/Address.ts#L64 + version_byte = b'\x00' + + # Convert binary address to base58 + address_bytes = version_byte + getattr(add_gateway_txn, address_type) + address = str(base58.b58encode_check(address_bytes), 'utf-8') + + # Ensure resulting address matches expectation + is_expected_address_defined = expected_address is not None + if is_expected_address_defined and address != expected_address: + msg = f"Expected {address_type} address to be {expected_address}," + \ + f"but is {address}" + raise MinerMalformedAddGatewayTxn(msg) + + return address diff --git a/hm_pyhelper/gateway_grpc/exceptions.py b/hm_pyhelper/gateway_grpc/exceptions.py new file mode 100644 index 0000000..8ad7b16 --- /dev/null +++ b/hm_pyhelper/gateway_grpc/exceptions.py @@ -0,0 +1,2 @@ +class MinerMalformedAddGatewayTxn(Exception): + pass diff --git a/hm_pyhelper/miner_json_rpc/__init__.py b/hm_pyhelper/miner_json_rpc/__init__.py deleted file mode 100644 index ad2fd40..0000000 --- a/hm_pyhelper/miner_json_rpc/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from hm_pyhelper.miner_json_rpc.client import Client as MinerClient # noqa diff --git a/hm_pyhelper/miner_json_rpc/client.py b/hm_pyhelper/miner_json_rpc/client.py deleted file mode 100644 index ca707d7..0000000 --- a/hm_pyhelper/miner_json_rpc/client.py +++ /dev/null @@ -1,169 +0,0 @@ -import requests -import base64 -import base58 - -from hm_pyhelper.protos import blockchain_txn_pb2, \ - blockchain_txn_add_gateway_v1_pb2 -from hm_pyhelper.miner_json_rpc.exceptions import MinerConnectionError, \ - MinerMalformedURL, \ - MinerRegionUnset, \ - MinerMalformedAddGatewayTxn - - -class Client(object): - - def __init__(self, url='http://helium-miner:4467'): - self.url = url - - def __fetch_data(self, method, **kwargs): - req_body = { - "jsonrpc": "2.0", - "id": 1, - "method": method, - } - if kwargs: - req_body["params"] = kwargs - try: - response = requests.post(self.url, json=req_body) - except requests.exceptions.ConnectionError: - raise MinerConnectionError( - "Unable to connect to miner %s" % self.url - ) - except requests.exceptions.MissingSchema: - raise MinerMalformedURL( - "Miner JSONRPC URL '%s' is not a valid URL" - % self.url - ) - - if not response.ok: - response.raise_for_status() - - return response.json().get('result') - - def get_height(self): - return self.__fetch_data('info_height') - - def get_region(self): - region = self.__fetch_data('info_region') - if not region.get('region'): - raise MinerRegionUnset( - "Miner at %s does not have an asserted region" - % self.url - ) - return region - - def get_summary(self): - return self.__fetch_data('info_summary') - - def get_peer_addr(self): - return self.__fetch_data('peer_addr') - - def get_peer_book(self): - return self.__fetch_data('peer_book', addr='self') - - def get_firmware_version(self): - summary = self.get_summary() - return summary.get('firmware_version') - - def create_add_gateway_txn(self, owner_address: str, payer_address: str, - gateway_address: str = None) -> dict: - """ - Invokes the txn_add_gateway RPC endpoint on the miner and returns - the same payload that the smartphone app traditionally expects. - https://docs.helium.com/mine-hnt/full-hotspots/become-a-maker/hotspot-integration-testing/#generate-an-add-hotspot-transaction - - Alternatively, the same thing can be accomplished with dbus, - like in the below, but RPC is generally easier to use. - https://github.com/NebraLtd/hm-config/blob/900aeed353fb9729b49bca97d7da8a9abf0a2029/gatewayconfig/bluetooth/characteristics/add_gateway_characteristic.py#L71 - - Parameters: - - owner_address: The address of the account that owns the gateway. - - payer_address: The address of the account that will pay for the - transaction. This will typically be the - maker/Nebra's account. - - gateway_address: The address of the miner itself. This is - an optional parameter because the miner - will always return it in the payload during - transaction generation. If the param is - provided, it will only be used as extra - validation. - - Raises: - - MinerMalformedAddGatewayTxn if returned transaction does - not correspond to the supplied parameters. - """ - # Invoke add_gateway_txn on miner - # https://github.com/helium/miner/blob/b9d2cd108cdcc864b641ccf4209f790b1461926d/src/jsonrpc/miner_jsonrpc_txn.erl#L31 - rpc_response = self.__fetch_data('txn_add_gateway', - owner=owner_address, - payer=payer_address) - encoded_wrapped_txn = rpc_response['result'] - - # Base64 decode - # https://github.com/helium/miner/blob/b9d2cd108cdcc864b641ccf4209f790b1461926d/src/jsonrpc/miner_jsonrpc_txn.erl#L38 - decoded_wrapped_txn = base64.b64decode(encoded_wrapped_txn) - - # Deserialize wrapped protobuf - # https://github.com/helium/blockchain-core/blob/3cd6bca6c5595a1363a9bbd625ef254383a4141b/src/transactions/blockchain_txn.erl#L160 - wrapped_txn = blockchain_txn_pb2.blockchain_txn() - wrapped_txn.ParseFromString(decoded_wrapped_txn) - - # Unwrap to get blockchain_txn_add_gateway_v1 - # https://github.com/helium/proto/blob/6dc60a9933628c3baf9d2f5386481f20a5d79bb8/src/blockchain_txn_add_gateway_v1.proto#L1 - add_gateway_txn = wrapped_txn.add_gateway - - txn_gateway_address = get_address_from_add_gateway_txn( - add_gateway_txn, 'gateway', gateway_address) - - txn_owner_address = get_address_from_add_gateway_txn( - add_gateway_txn, 'owner', owner_address) - - txn_payer_address = get_address_from_add_gateway_txn( - add_gateway_txn, 'payer', payer_address) - - return { - 'gateway_address': txn_gateway_address, - 'owner_address': txn_owner_address, - 'payer_address': txn_payer_address, - 'fee': add_gateway_txn.fee, - 'staking_fee': add_gateway_txn.staking_fee, - 'txn': encoded_wrapped_txn - } - - -def get_address_from_add_gateway_txn(add_gateway_txn: - blockchain_txn_add_gateway_v1_pb2, - address_type: str, - expected_address: str = None): - """ - Deserializes specified field in the blockchain_txn_add_gateway_v1_pb2 - protobuf to a base58 Helium address. - - Pararms: - - add_gateway_txn: The blockchain_txn_add_gateway_v1_pb2 to - inspect. - - address_type: 'owner', 'gateway', or 'payer'. - - expected_address (optional): Value we expect to be returned. - - Raises: - MinerMalformedAddGatewayTxn if expected_address supplied and - does not match the return value. - """ - - # Addresses returned by the RPC response are missing a leading - # byte for the version. The version is currently always 0. - # https://github.com/helium/helium-js/blob/8d5cb76e156fb80de6fc80f239b43e3872c7b7d7/packages/crypto/src/Address.ts#L64 - version_byte = b'\x00' - - # Convert binary address to base58 - address_bytes = version_byte + getattr(add_gateway_txn, address_type) - address = str(base58.b58encode_check(address_bytes), 'utf-8') - - # Ensure resulting address matches expectation - is_expected_address_defined = expected_address is not None - if is_expected_address_defined and address != expected_address: - msg = f"Expected {address_type} address to be {expected_address}," + \ - f"but is {address}" - raise MinerMalformedAddGatewayTxn(msg) - - return address diff --git a/hm_pyhelper/miner_json_rpc/exceptions.py b/hm_pyhelper/miner_json_rpc/exceptions.py deleted file mode 100644 index cceb09c..0000000 --- a/hm_pyhelper/miner_json_rpc/exceptions.py +++ /dev/null @@ -1,26 +0,0 @@ -class MinerJSONRPCException(Exception): - pass - - -class MinerConnectionError(MinerJSONRPCException): - pass - - -class MinerMalformedURL(MinerJSONRPCException): - pass - - -class MinerRegionUnset(MinerJSONRPCException): - pass - - -class MinerFailedFetchData(MinerJSONRPCException): - pass - - -class MinerFailedToFetchEthernetAddress(MinerJSONRPCException): - pass - - -class MinerMalformedAddGatewayTxn(MinerJSONRPCException): - pass diff --git a/hm_pyhelper/miner_param.py b/hm_pyhelper/miner_param.py index 27c3ab8..22ffb6a 100644 --- a/hm_pyhelper/miner_param.py +++ b/hm_pyhelper/miner_param.py @@ -8,8 +8,6 @@ SPIUnavailableException, ECCMalfunctionException, \ GatewayMFRFileNotFoundException, \ MinerFailedToFetchMacAddress -from hm_pyhelper.miner_json_rpc.exceptions import \ - MinerFailedToFetchEthernetAddress from hm_pyhelper.hardware_definitions import get_variant_attribute, \ UnknownVariantException, UnknownVariantAttributeException @@ -170,7 +168,7 @@ def is_miner_key_and_passed(test_result): def get_ethernet_addresses(diagnostics): - # Get ethernet MAC and WIFI address + # Get ethernet and wlan MAC address # The order of the values in the lists is important! # It determines which value will be available for which key @@ -182,13 +180,10 @@ def get_ethernet_addresses(diagnostics): for (path, key) in zip(path_to_files, keys): try: diagnostics[key] = get_mac_address(path) - except MinerFailedToFetchMacAddress as e: - diagnostics[key] = False - LOGGER.error(e) except Exception as e: diagnostics[key] = False LOGGER.error(e) - raise MinerFailedToFetchEthernetAddress(str(e)) + raise MinerFailedToFetchMacAddress(str(e)) def get_mac_address(path): @@ -206,8 +201,6 @@ def get_mac_address(path): The path must be a string value") try: file = open(path) - except MinerFailedToFetchMacAddress as e: - LOGGER.exception(str(e)) except FileNotFoundError as e: LOGGER.exception("Failed to find Miner" "Mac Address file at path %s" % path) diff --git a/hm_pyhelper/protos/local_pb2.py b/hm_pyhelper/protos/local_pb2.py new file mode 100644 index 0000000..f6b6130 --- /dev/null +++ b/hm_pyhelper/protos/local_pb2.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: local.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0blocal.proto\x12\x0chelium.local\"\x1d\n\npubkey_res\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\"\x0c\n\npubkey_req\"\x18\n\x08sign_req\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x1d\n\x08sign_res\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x1b\n\x08\x65\x63\x64h_req\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\"\x1a\n\x08\x65\x63\x64h_res\x12\x0e\n\x06secret\x18\x01 \x01(\x0c\"\x1a\n\nconfig_req\x12\x0c\n\x04keys\x18\x01 \x03(\t\"8\n\nconfig_res\x12*\n\x06values\x18\x01 \x03(\x0b\x32\x1a.helium.local.config_value\"9\n\x0c\x63onfig_value\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\x0c\")\n\tkeyed_uri\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x0b\n\x03uri\x18\x02 \x01(\t\"\x0c\n\nheight_req\"Y\n\nheight_res\x12\x0e\n\x06height\x18\x01 \x01(\x04\x12\x11\n\tblock_age\x18\x02 \x01(\x04\x12(\n\x07gateway\x18\x03 \x01(\x0b\x32\x17.helium.local.keyed_uri\"\x0c\n\nregion_req\"\x1c\n\nregion_res\x12\x0e\n\x06region\x18\x01 \x01(\x05\"i\n\x0f\x61\x64\x64_gateway_req\x12\r\n\x05owner\x18\x01 \x01(\x0c\x12\r\n\x05payer\x18\x02 \x01(\x0c\x12\x38\n\x0cstaking_mode\x18\x03 \x01(\x0e\x32\".helium.local.gateway_staking_mode\"*\n\x0f\x61\x64\x64_gateway_res\x12\x17\n\x0f\x61\x64\x64_gateway_txn\x18\x01 \x01(\x0c*9\n\x14gateway_staking_mode\x12\x0c\n\x08\x64\x61taonly\x10\x00\x12\x08\n\x04\x66ull\x10\x01\x12\t\n\x05light\x10\x02\x32\xba\x03\n\x03\x61pi\x12<\n\x06pubkey\x12\x18.helium.local.pubkey_req\x1a\x18.helium.local.pubkey_res\x12\x36\n\x04sign\x12\x16.helium.local.sign_req\x1a\x16.helium.local.sign_res\x12\x36\n\x04\x65\x63\x64h\x12\x16.helium.local.ecdh_req\x1a\x16.helium.local.ecdh_res\x12<\n\x06\x63onfig\x12\x18.helium.local.config_req\x1a\x18.helium.local.config_res\x12<\n\x06height\x12\x18.helium.local.height_req\x1a\x18.helium.local.height_res\x12<\n\x06region\x12\x18.helium.local.region_req\x1a\x18.helium.local.region_res\x12K\n\x0b\x61\x64\x64_gateway\x12\x1d.helium.local.add_gateway_req\x1a\x1d.helium.local.add_gateway_resb\x06proto3') + +_GATEWAY_STAKING_MODE = DESCRIPTOR.enum_types_by_name['gateway_staking_mode'] +gateway_staking_mode = enum_type_wrapper.EnumTypeWrapper(_GATEWAY_STAKING_MODE) +dataonly = 0 +full = 1 +light = 2 + + +_PUBKEY_RES = DESCRIPTOR.message_types_by_name['pubkey_res'] +_PUBKEY_REQ = DESCRIPTOR.message_types_by_name['pubkey_req'] +_SIGN_REQ = DESCRIPTOR.message_types_by_name['sign_req'] +_SIGN_RES = DESCRIPTOR.message_types_by_name['sign_res'] +_ECDH_REQ = DESCRIPTOR.message_types_by_name['ecdh_req'] +_ECDH_RES = DESCRIPTOR.message_types_by_name['ecdh_res'] +_CONFIG_REQ = DESCRIPTOR.message_types_by_name['config_req'] +_CONFIG_RES = DESCRIPTOR.message_types_by_name['config_res'] +_CONFIG_VALUE = DESCRIPTOR.message_types_by_name['config_value'] +_KEYED_URI = DESCRIPTOR.message_types_by_name['keyed_uri'] +_HEIGHT_REQ = DESCRIPTOR.message_types_by_name['height_req'] +_HEIGHT_RES = DESCRIPTOR.message_types_by_name['height_res'] +_REGION_REQ = DESCRIPTOR.message_types_by_name['region_req'] +_REGION_RES = DESCRIPTOR.message_types_by_name['region_res'] +_ADD_GATEWAY_REQ = DESCRIPTOR.message_types_by_name['add_gateway_req'] +_ADD_GATEWAY_RES = DESCRIPTOR.message_types_by_name['add_gateway_res'] +pubkey_res = _reflection.GeneratedProtocolMessageType('pubkey_res', (_message.Message,), { + 'DESCRIPTOR' : _PUBKEY_RES, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.pubkey_res) + }) +_sym_db.RegisterMessage(pubkey_res) + +pubkey_req = _reflection.GeneratedProtocolMessageType('pubkey_req', (_message.Message,), { + 'DESCRIPTOR' : _PUBKEY_REQ, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.pubkey_req) + }) +_sym_db.RegisterMessage(pubkey_req) + +sign_req = _reflection.GeneratedProtocolMessageType('sign_req', (_message.Message,), { + 'DESCRIPTOR' : _SIGN_REQ, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.sign_req) + }) +_sym_db.RegisterMessage(sign_req) + +sign_res = _reflection.GeneratedProtocolMessageType('sign_res', (_message.Message,), { + 'DESCRIPTOR' : _SIGN_RES, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.sign_res) + }) +_sym_db.RegisterMessage(sign_res) + +ecdh_req = _reflection.GeneratedProtocolMessageType('ecdh_req', (_message.Message,), { + 'DESCRIPTOR' : _ECDH_REQ, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.ecdh_req) + }) +_sym_db.RegisterMessage(ecdh_req) + +ecdh_res = _reflection.GeneratedProtocolMessageType('ecdh_res', (_message.Message,), { + 'DESCRIPTOR' : _ECDH_RES, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.ecdh_res) + }) +_sym_db.RegisterMessage(ecdh_res) + +config_req = _reflection.GeneratedProtocolMessageType('config_req', (_message.Message,), { + 'DESCRIPTOR' : _CONFIG_REQ, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.config_req) + }) +_sym_db.RegisterMessage(config_req) + +config_res = _reflection.GeneratedProtocolMessageType('config_res', (_message.Message,), { + 'DESCRIPTOR' : _CONFIG_RES, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.config_res) + }) +_sym_db.RegisterMessage(config_res) + +config_value = _reflection.GeneratedProtocolMessageType('config_value', (_message.Message,), { + 'DESCRIPTOR' : _CONFIG_VALUE, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.config_value) + }) +_sym_db.RegisterMessage(config_value) + +keyed_uri = _reflection.GeneratedProtocolMessageType('keyed_uri', (_message.Message,), { + 'DESCRIPTOR' : _KEYED_URI, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.keyed_uri) + }) +_sym_db.RegisterMessage(keyed_uri) + +height_req = _reflection.GeneratedProtocolMessageType('height_req', (_message.Message,), { + 'DESCRIPTOR' : _HEIGHT_REQ, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.height_req) + }) +_sym_db.RegisterMessage(height_req) + +height_res = _reflection.GeneratedProtocolMessageType('height_res', (_message.Message,), { + 'DESCRIPTOR' : _HEIGHT_RES, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.height_res) + }) +_sym_db.RegisterMessage(height_res) + +region_req = _reflection.GeneratedProtocolMessageType('region_req', (_message.Message,), { + 'DESCRIPTOR' : _REGION_REQ, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.region_req) + }) +_sym_db.RegisterMessage(region_req) + +region_res = _reflection.GeneratedProtocolMessageType('region_res', (_message.Message,), { + 'DESCRIPTOR' : _REGION_RES, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.region_res) + }) +_sym_db.RegisterMessage(region_res) + +add_gateway_req = _reflection.GeneratedProtocolMessageType('add_gateway_req', (_message.Message,), { + 'DESCRIPTOR' : _ADD_GATEWAY_REQ, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.add_gateway_req) + }) +_sym_db.RegisterMessage(add_gateway_req) + +add_gateway_res = _reflection.GeneratedProtocolMessageType('add_gateway_res', (_message.Message,), { + 'DESCRIPTOR' : _ADD_GATEWAY_RES, + '__module__' : 'local_pb2' + # @@protoc_insertion_point(class_scope:helium.local.add_gateway_res) + }) +_sym_db.RegisterMessage(add_gateway_res) + +_API = DESCRIPTOR.services_by_name['api'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _GATEWAY_STAKING_MODE._serialized_start=676 + _GATEWAY_STAKING_MODE._serialized_end=733 + _PUBKEY_RES._serialized_start=29 + _PUBKEY_RES._serialized_end=58 + _PUBKEY_REQ._serialized_start=60 + _PUBKEY_REQ._serialized_end=72 + _SIGN_REQ._serialized_start=74 + _SIGN_REQ._serialized_end=98 + _SIGN_RES._serialized_start=100 + _SIGN_RES._serialized_end=129 + _ECDH_REQ._serialized_start=131 + _ECDH_REQ._serialized_end=158 + _ECDH_RES._serialized_start=160 + _ECDH_RES._serialized_end=186 + _CONFIG_REQ._serialized_start=188 + _CONFIG_REQ._serialized_end=214 + _CONFIG_RES._serialized_start=216 + _CONFIG_RES._serialized_end=272 + _CONFIG_VALUE._serialized_start=274 + _CONFIG_VALUE._serialized_end=331 + _KEYED_URI._serialized_start=333 + _KEYED_URI._serialized_end=374 + _HEIGHT_REQ._serialized_start=376 + _HEIGHT_REQ._serialized_end=388 + _HEIGHT_RES._serialized_start=390 + _HEIGHT_RES._serialized_end=479 + _REGION_REQ._serialized_start=481 + _REGION_REQ._serialized_end=493 + _REGION_RES._serialized_start=495 + _REGION_RES._serialized_end=523 + _ADD_GATEWAY_REQ._serialized_start=525 + _ADD_GATEWAY_REQ._serialized_end=630 + _ADD_GATEWAY_RES._serialized_start=632 + _ADD_GATEWAY_RES._serialized_end=674 + _API._serialized_start=736 + _API._serialized_end=1178 +# @@protoc_insertion_point(module_scope) diff --git a/hm_pyhelper/protos/local_pb2_grpc.py b/hm_pyhelper/protos/local_pb2_grpc.py new file mode 100644 index 0000000..b9f55d7 --- /dev/null +++ b/hm_pyhelper/protos/local_pb2_grpc.py @@ -0,0 +1,264 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import hm_pyhelper.protos.local_pb2 as local__pb2 + + +class apiStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.pubkey = channel.unary_unary( + '/helium.local.api/pubkey', + request_serializer=local__pb2.pubkey_req.SerializeToString, + response_deserializer=local__pb2.pubkey_res.FromString, + ) + self.sign = channel.unary_unary( + '/helium.local.api/sign', + request_serializer=local__pb2.sign_req.SerializeToString, + response_deserializer=local__pb2.sign_res.FromString, + ) + self.ecdh = channel.unary_unary( + '/helium.local.api/ecdh', + request_serializer=local__pb2.ecdh_req.SerializeToString, + response_deserializer=local__pb2.ecdh_res.FromString, + ) + self.config = channel.unary_unary( + '/helium.local.api/config', + request_serializer=local__pb2.config_req.SerializeToString, + response_deserializer=local__pb2.config_res.FromString, + ) + self.height = channel.unary_unary( + '/helium.local.api/height', + request_serializer=local__pb2.height_req.SerializeToString, + response_deserializer=local__pb2.height_res.FromString, + ) + self.region = channel.unary_unary( + '/helium.local.api/region', + request_serializer=local__pb2.region_req.SerializeToString, + response_deserializer=local__pb2.region_res.FromString, + ) + self.add_gateway = channel.unary_unary( + '/helium.local.api/add_gateway', + request_serializer=local__pb2.add_gateway_req.SerializeToString, + response_deserializer=local__pb2.add_gateway_res.FromString, + ) + + +class apiServicer(object): + """Missing associated documentation comment in .proto file.""" + + def pubkey(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def sign(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ecdh(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def config(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def height(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def region(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def add_gateway(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_apiServicer_to_server(servicer, server): + rpc_method_handlers = { + 'pubkey': grpc.unary_unary_rpc_method_handler( + servicer.pubkey, + request_deserializer=local__pb2.pubkey_req.FromString, + response_serializer=local__pb2.pubkey_res.SerializeToString, + ), + 'sign': grpc.unary_unary_rpc_method_handler( + servicer.sign, + request_deserializer=local__pb2.sign_req.FromString, + response_serializer=local__pb2.sign_res.SerializeToString, + ), + 'ecdh': grpc.unary_unary_rpc_method_handler( + servicer.ecdh, + request_deserializer=local__pb2.ecdh_req.FromString, + response_serializer=local__pb2.ecdh_res.SerializeToString, + ), + 'config': grpc.unary_unary_rpc_method_handler( + servicer.config, + request_deserializer=local__pb2.config_req.FromString, + response_serializer=local__pb2.config_res.SerializeToString, + ), + 'height': grpc.unary_unary_rpc_method_handler( + servicer.height, + request_deserializer=local__pb2.height_req.FromString, + response_serializer=local__pb2.height_res.SerializeToString, + ), + 'region': grpc.unary_unary_rpc_method_handler( + servicer.region, + request_deserializer=local__pb2.region_req.FromString, + response_serializer=local__pb2.region_res.SerializeToString, + ), + 'add_gateway': grpc.unary_unary_rpc_method_handler( + servicer.add_gateway, + request_deserializer=local__pb2.add_gateway_req.FromString, + response_serializer=local__pb2.add_gateway_res.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'helium.local.api', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class api(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def pubkey(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helium.local.api/pubkey', + local__pb2.pubkey_req.SerializeToString, + local__pb2.pubkey_res.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def sign(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helium.local.api/sign', + local__pb2.sign_req.SerializeToString, + local__pb2.sign_res.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ecdh(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helium.local.api/ecdh', + local__pb2.ecdh_req.SerializeToString, + local__pb2.ecdh_res.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def config(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helium.local.api/config', + local__pb2.config_req.SerializeToString, + local__pb2.config_res.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def height(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helium.local.api/height', + local__pb2.height_req.SerializeToString, + local__pb2.height_res.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def region(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helium.local.api/region', + local__pb2.region_req.SerializeToString, + local__pb2.region_res.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def add_gateway(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helium.local.api/add_gateway', + local__pb2.add_gateway_req.SerializeToString, + local__pb2.add_gateway_res.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/hm_pyhelper/protos/region_pb2.py b/hm_pyhelper/protos/region_pb2.py new file mode 100644 index 0000000..3227a35 --- /dev/null +++ b/hm_pyhelper/protos/region_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: region.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cregion.proto\x12\x06helium*\x94\x01\n\x06region\x12\t\n\x05US915\x10\x00\x12\t\n\x05\x45U868\x10\x01\x12\t\n\x05\x45U433\x10\x02\x12\t\n\x05\x43N470\x10\x03\x12\t\n\x05\x43N779\x10\x04\x12\t\n\x05\x41U915\x10\x05\x12\x0b\n\x07\x41S923_1\x10\x06\x12\t\n\x05KR920\x10\x07\x12\t\n\x05IN865\x10\x08\x12\x0b\n\x07\x41S923_2\x10\t\x12\x0b\n\x07\x41S923_3\x10\n\x12\x0b\n\x07\x41S923_4\x10\x0b\x62\x06proto3') + +_REGION = DESCRIPTOR.enum_types_by_name['region'] +region = enum_type_wrapper.EnumTypeWrapper(_REGION) +US915 = 0 +EU868 = 1 +EU433 = 2 +CN470 = 3 +CN779 = 4 +AU915 = 5 +AS923_1 = 6 +KR920 = 7 +IN865 = 8 +AS923_2 = 9 +AS923_3 = 10 +AS923_4 = 11 + + +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _REGION._serialized_start=25 + _REGION._serialized_end=173 +# @@protoc_insertion_point(module_scope) diff --git a/hm_pyhelper/tests/test_gateway_grpc.py b/hm_pyhelper/tests/test_gateway_grpc.py new file mode 100644 index 0000000..cdd0a37 --- /dev/null +++ b/hm_pyhelper/tests/test_gateway_grpc.py @@ -0,0 +1,141 @@ +import unittest +from unittest.mock import patch +import grpc +from concurrent import futures +from hm_pyhelper.gateway_grpc.client import GatewayClient + +from hm_pyhelper.protos import local_pb2 +from hm_pyhelper.protos import local_pb2_grpc + + +class TestData: + server_port = 4468 + height_res = local_pb2.height_res( + height=43, + block_age=42, + gateway=local_pb2.keyed_uri( + address=b"\000\177\327E-\223\222e\002e[*\250\260\361p\271\267/" + b"\220\026\010\360\213t\304\313\022\316>\254\347?", + uri="http://32.23.54.23:8080" # NOSONAR + ) + ) + validator_address_decoded = "11yJXQPG9deHqvw2ac6VWtNP7gZj8X3t3Qb3Gqm9j729p4AsdaA" + pubkey_encoded = b"\x01\xc3\x06\x7f\xb9\x19}\xd1n2\xe2M\xeb\xb5\x11\x7f" \ + b"\xbc\x12\xebT\xb9\x84R\xc7\xca\xf8o\xdddx\xea~\xab" + pubkey_decoded = "14RdqcZC2rbdTBwNaTsj5EVWYaM7BKGJ44ycq6wWJy9Hg7RKCii" + chain_vars = { + "block_size_limit": local_pb2.config_value(name="block_size_limit", + value=b"5242880", + type="int"), + "min_assert_h3_res": local_pb2.config_value(name="min_assert_h3_res", + value=b"12", + type="int") + } + region_enum = 0 + region_name = "US915" + dpkg_output = b"""Package: helium_gateway\n + Status: install ok installed\n + Priority: optional\n + Section: utility\n + Installed-Size: 3729\n + Maintainer: Marc Nijdam \n + Architecture: amd64\n + Version: 1.0.0~alpha.23\n + Depends: curl\n + Conffiles:\n + /etc/helium_gateway/settings.toml 4d6fb434f97a50066b8163a371d5c208\n + Description: Helium Gateway for LoRa packet forwarders\n + The Helium Gateway to attach your LoRa gateway to the Helium Blockchain.\n""" + expected_summary = { + 'region': region_name, + 'key': pubkey_decoded, + 'gateway_version': "v1.0.0~alpha.23", + 'validator': { + 'height': height_res.height, + 'block_age': height_res.block_age, + 'address': validator_address_decoded, + 'uri': height_res.gateway.uri + } + } + + +class MockServicer(local_pb2_grpc.apiServicer): + def height(self, request, context): + return TestData.height_res + + def region(self, request, context): + return local_pb2.region_res(region=0) + + def pubkey(self, request, context): + return local_pb2.pubkey_res(address=TestData.pubkey_encoded) + + def config(self, request, context): + result = local_pb2.config_res() + for key in request.keys: + if key in TestData.chain_vars.keys(): + result.values.append(TestData.chain_vars.get(key)) + else: + result.values.append(local_pb2.config_value(name=key)) + return result + + +class TestGatewayGRPCClient(unittest.TestCase): + + # we can start the real service hear by installing dpkg. But AFAIK + # our testing methods, real service exposes us to random failures + def setUp(self): + self.mock_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + local_pb2_grpc.add_apiServicer_to_server(MockServicer(), self.mock_server) + self.mock_server.add_insecure_port(f'[::]:{TestData.server_port}') + self.mock_server.start() + + def tearDown(self): + self.mock_server.stop(None) + + def test_get_pubkey(self): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + self.assertEqual(client.get_pubkey(), TestData.pubkey_decoded) + + def test_get_validator_info(self): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + self.assertEqual(client.get_validator_info(), TestData.height_res) + + def test_get_height(self): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + self.assertEqual(client.get_height(), client.get_validator_info().height) + + def test_get_region(self): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + self.assertEqual(client.get_region_enum(), TestData.region_enum) + self.assertEqual(client.get_region(), TestData.region_name) + + def test_get_blockchain_variable(self): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + for key in TestData.chain_vars: + self.assertEqual(client.get_blockchain_config_variable(key), + TestData.chain_vars.get(key)) + + def test_get_summary(self): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + # summary when helium_gateway is not installed + test_summary_copy = TestData.expected_summary.copy() + test_summary_copy['gateway_version'] = None + self.assertIn(client.get_summary(), + [TestData.expected_summary, test_summary_copy]) + + @patch('subprocess.check_output', return_value=TestData.dpkg_output) + def test_get_gateway_version(self, mock_check_output): + mock_check_output.return_value = TestData.dpkg_output + with GatewayClient(f'localhost:{TestData.server_port}') as client: + self.assertIn(client.get_gateway_version(), + [TestData.expected_summary['gateway_version'], None]) + + def test_connection_failure(self): + with self.assertRaises(grpc.RpcError): + with GatewayClient('localhost:1234') as client: + client.get_pubkey() + + def test_invalid_chain_var(self): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + with self.assertRaises(ValueError): + client.get_blockchain_config_variable('not_a_key') diff --git a/hm_pyhelper/tests/test_miner_json_rpc.py b/hm_pyhelper/tests/test_miner_json_rpc.py deleted file mode 100644 index b740e48..0000000 --- a/hm_pyhelper/tests/test_miner_json_rpc.py +++ /dev/null @@ -1,244 +0,0 @@ -import unittest -import mock -import responses -import requests -from hm_pyhelper.miner_json_rpc import MinerClient -from hm_pyhelper.miner_json_rpc.exceptions import MinerRegionUnset -from hm_pyhelper.miner_json_rpc.exceptions import MinerMalformedURL -from hm_pyhelper.miner_json_rpc.exceptions import MinerConnectionError - -BASE_URL = 'http://helium-miner:4467' - - -@responses.activate -def response_result(data, status): - url = "https://fake_url" - responses.add(responses.POST, url, json=data, status=status) - resp = requests.post(url) - print(resp.json()) - return resp - - -def return_payload_with_method(method): - return {'jsonrpc': '2.0', 'id': 1, 'method': method} - - -class Result(object): - def __init__(self, result={'my': 'data'}): - self.result = result - - -class Response(object): - def __init__(self, data=Result()): - self.data = data - - -class TestMinerJSONRPC(unittest.TestCase): - def test_instantiation(self): - client = MinerClient() - self.assertIsInstance(client, MinerClient) - self.assertEqual(client.url, BASE_URL) - - def test_malformed_url(self): - client = MinerClient(url='fakeurl') - - exception_raised = False - exception_type = None - try: - client.get_height() - except Exception as exc: - exception_raised = True - exception_type = exc - - self.assertTrue(exception_raised) - self.assertIsInstance(exception_type, MinerMalformedURL) - - def test_connection_error(self): - client = MinerClient(url='http://notarealminer:9999') - - exception_raised = False - exception_type = None - try: - client.get_height() - except Exception as exc: - exception_raised = True - exception_type = exc - - self.assertTrue(exception_raised) - self.assertIsInstance(exception_type, MinerConnectionError) - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result( - {"result": {'epoch': 25612, 'height': 993640}, "id": 1}, - 200)) - def test_get_height(self, mock_json_rpc_client): - client = MinerClient() - result = client.get_height() - mock_json_rpc_client.assert_called_with( - BASE_URL, json=return_payload_with_method('info_height')) - - self.assertEqual(result, {'epoch': 25612, 'height': 993640}) - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result( - {"result": {'region': None}, "id": 1}, 200)) - def test_get_region_not_asserted(self, mock_json_rpc_client): - client = MinerClient() - exception_raised = False - exception_type = None - - try: - client.get_region() - except Exception as exc: - exception_raised = True - exception_type = exc - - self.assertTrue(exception_raised) - self.assertIsInstance(exception_type, MinerRegionUnset) - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result( - {"result": {'region': "EU868"}, "id": 1}, 200)) - def test_get_region(self, mock_json_rpc_client): - client = MinerClient() - result = client.get_region() - mock_json_rpc_client.assert_called_with( - BASE_URL, json=return_payload_with_method('info_region') - ) - self.assertEqual(result, {'region': 'EU868'}) - - summary = { - 'block_age': 1136610, - 'epoch': 25612, - 'firmware_version': "0.1", - 'gateway_details': 'undefined', - 'height': 993640, - 'mac_addresses': [ - {'eth0': '0242AC110002'}, - {'ip6tnl0': '00000000000000000000000000000000'}, - {'tunl0': '00000000'}, - {'lo': '000000000000'} - ], - 'name': 'scruffy-chocolate-shell', - 'peer_book_entry_count': 3, - 'sync_height': 993640, - 'uptime': 144, - 'version': 10010005 - } - - result_json = {"result": summary, "id": 1} - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result(result_json, 200)) - def test_get_summary(self, mock_json_rpc_client): - client = MinerClient() - result = client.get_summary() - mock_json_rpc_client.assert_called_with( - BASE_URL, json=return_payload_with_method('info_summary') - ) - self.assertEqual(result, self.summary) - - peer_addr = '/p2p/11jr2kMp1bZvSC6pd3XkNvs9Q43qCgEzxRwV6vpuqXanC5UcLEs' - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result( - {"result": {'peer_addr': peer_addr}, "id": 1}, 200)) - def test_get_peer_addr(self, mock_json_rpc_client): - - client = MinerClient() - result = client.get_peer_addr() - mock_json_rpc_client.assert_called_with( - BASE_URL, json=return_payload_with_method('peer_addr') - ) - self.assertEqual(result, {'peer_addr': self.peer_addr}) - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result( - {"result": [], "id": 1}, - 200)) - def test_get_peer_book(self, mock_json_rpc_client): - - client = MinerClient() - result = client.get_peer_book() - payload = return_payload_with_method('peer_book') - payload["params"] = {'addr': 'self'} - mock_json_rpc_client.assert_called_with( - BASE_URL, json=payload - ) - self.assertEqual(result, []) - - firmware_version = '2021.10.18.0' - data_response = { - 'block_age': 1136610, - 'epoch': 25612, - 'firmware_version': firmware_version, - 'gateway_details': 'undefined', - 'height': 993640, - 'mac_addresses': [ - {'eth0': '0242AC110002'}, - {'ip6tnl0': '00000000000000000000000000000000'}, - {'tunl0': '00000000'}, - {'lo': '000000000000'} - ], - 'name': 'scruffy-chocolate-shell', - 'peer_book_entry_count': 3, - 'sync_height': 993640, - 'uptime': 144, - 'version': 10010005 - } - - result_response = {"result": data_response, "id": 1} - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result( - result_response, 200)) - def test_get_firmware_version(self, mock_json_rpc_client): - client = MinerClient() - result = client.get_firmware_version() - mock_json_rpc_client.assert_called_with( - BASE_URL, json=return_payload_with_method('info_summary') - ) - self.assertEqual(result, self.firmware_version) - - add_gateway_response = { - "jsonrpc": "2.0", - "result": { - "result": "CroBCiEBwPbb63LQD8x/m/ZDLLyOLgtxypQIjh+xPPS+d8g/i24SIQB" - "8XdzWqrIF201DNKHpXKFtsMtgvZeqBBc1wOk9sV2j4SJGMEQCIGE82g" - "Hbn0z/AOyaDXsuQDptC/I15fHCF//QEgzoxodrAiBsoRUiw8zMVttkP" - "hEOoMfM0smmdCZPKX6tVOgK0s/0KiohAVHXaw1kNly2VOt47MlzTfkC" - "IUTOgW34Orw1LSJt+9mCOICS9AFA6PsD" - }, - "id": "1" - } - - @mock.patch('hm_pyhelper.miner_json_rpc.client.requests.post', - return_value=response_result( - add_gateway_response, 200)) - def test_create_add_gateway_txn(self, mock_json_rpc_client): - self.maxDiff = None - client = MinerClient() - actual_result = client.create_add_gateway_txn( - '14QjC3A5DEH2uFwhDxyBHdFir7YWGG23Fic1wGvkCr6qrWC7Q47', - '13Zni1he7KY9pUmkXMhEhTwfUpL9AcEV1m2UbbvFsrU9QPTMgE3') - - expected_result = { - 'gateway_address': - '11wmnCAfvFkdx3Az1hesTsSv9YeBUhs8JZKjCqcs2vmRwRazpBa', - - 'owner_address': - '14QjC3A5DEH2uFwhDxyBHdFir7YWGG23Fic1wGvkCr6qrWC7Q47', - - 'payer_address': - '13Zni1he7KY9pUmkXMhEhTwfUpL9AcEV1m2UbbvFsrU9QPTMgE3', - - 'fee': 65000, - 'staking_fee': 4000000, - 'txn': "CroBCiEBwPbb63LQD8x/m/ZDLLyOLgtxypQIjh+xPPS+d8g/i24SIQB8Xd" - "zWqrIF201DNKHpXKFtsMtgvZeqBBc1wOk9sV2j4SJGMEQCIGE82gHbn0z/" - "AOyaDXsuQDptC/I15fHCF//QEgzoxodrAiBsoRUiw8zMVttkPhEOoMfM0s" - "mmdCZPKX6tVOgK0s/0KiohAVHXaw1kNly2VOt47MlzTfkCIUTOgW34Orw1" - "LSJt+9mCOICS9AFA6PsD" - } - - self.assertDictEqual(actual_result, expected_result) diff --git a/protos/README.md b/protos/README.md index fd14e46..4479460 100644 --- a/protos/README.md +++ b/protos/README.md @@ -16,3 +16,5 @@ DST_DIR=/PATH/TO/hm-pyhelper/hm_pyhelper/protos protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/blockchain_txn.proto protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/blockchain_txn_add_gateway_v1.proto ``` + +For frequently changing proto files, we use [this] (https://github.com/NebraLtd/hm-pyhelper/blob/master/protos/update_protos.sh) script to download and generate the python code. \ No newline at end of file diff --git a/protos/local.proto b/protos/local.proto new file mode 100644 index 0000000..d2a640d --- /dev/null +++ b/protos/local.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package helium.local; + +message pubkey_res { bytes address = 1; } +message pubkey_req {} + +message sign_req { bytes data = 1; } +message sign_res { bytes signature = 1; } + +message ecdh_req { bytes address = 1; } +message ecdh_res { bytes secret = 1; } + +message config_req { repeated string keys = 1; } +message config_res { repeated config_value values = 1; } + +message config_value { + string name = 1; + string type = 2; + bytes value = 3; +} + +message keyed_uri { + bytes address = 1; + string uri = 2; +} + +message height_req {} +message height_res { + uint64 height = 1; + uint64 block_age = 2; + keyed_uri gateway = 3; +} + +message region_req {} +message region_res { int32 region = 1; } + +enum gateway_staking_mode { + dataonly = 0; + full = 1; + light = 2; +} +message add_gateway_req { + bytes owner = 1; + bytes payer = 2; + gateway_staking_mode staking_mode = 3; +} + +message add_gateway_res { bytes add_gateway_txn = 1; } + +service api { + rpc pubkey(pubkey_req) returns (pubkey_res); + rpc sign(sign_req) returns (sign_res); + rpc ecdh(ecdh_req) returns (ecdh_res); + rpc config(config_req) returns (config_res); + rpc height(height_req) returns (height_res); + rpc region(region_req) returns (region_res); + rpc add_gateway(add_gateway_req) returns (add_gateway_res); +} diff --git a/protos/region.proto b/protos/region.proto new file mode 100644 index 0000000..85a8656 --- /dev/null +++ b/protos/region.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package helium; + +enum region { + US915 = 0; + EU868 = 1; + EU433 = 2; + CN470 = 3; + CN779 = 4; + AU915 = 5; + AS923_1 = 6; + KR920 = 7; + IN865 = 8; + AS923_2 = 9; + AS923_3 = 10; + AS923_4 = 11; +} diff --git a/protos/update_protos.sh b/protos/update_protos.sh new file mode 100644 index 0000000..1c37e6b --- /dev/null +++ b/protos/update_protos.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +SRC_DIR=. +DST_DIR=../hm_pyhelper/protos + +function update_proto() { + echo "Updating $1" + wget $1 + + # replace old if succcessful + if [[ $? -eq 0 ]]; then + mv $2.1 $2 + fi +} + +update_proto https://raw.githubusercontent.com/helium/proto/master/src/service/local.proto local.proto +update_proto https://raw.githubusercontent.com/helium/proto/master/src/region.proto region.proto + +python -m grpc_tools.protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/region.proto +python -m grpc_tools.protoc -I=$SRC_DIR --python_out=$DST_DIR --grpc_python_out=$DST_DIR $SRC_DIR/local.proto + +sed -i -e 's/import *local_pb2/import hm_pyhelper.protos.local_pb2/' $DST_DIR/local_pb2_grpc.py diff --git a/requirements.txt b/requirements.txt index 2947bde..97666b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests==2.26.0 retry==0.9.2 base58==2.1.1 -protobuf==3.19.3 \ No newline at end of file +protobuf==3.19.3 +grpcio==1.44.0 diff --git a/setup.py b/setup.py index 3bcf082..c166a2b 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hm_pyhelper', - version='0.13.16', + version='0.13.17', author="Nebra Ltd", author_email="support@nebra.com", description="Helium Python Helper",