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/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/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..f0f23fd --- /dev/null +++ b/hm_pyhelper/gateway_grpc/client.py @@ -0,0 +1,232 @@ +import base58 +import grpc +import subprocess +import json + +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 GatewayMalformedAddGatewayTxn + +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) -> str: + ''' + 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 + )) + return json.loads(response.decode()) + + +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 GatewayMalformedAddGatewayTxn(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..4d1e21c --- /dev/null +++ b/hm_pyhelper/gateway_grpc/exceptions.py @@ -0,0 +1,6 @@ +class GatewayGRPCException(Exception): + pass + + +class GatewayMalformedAddGatewayTxn(GatewayGRPCException): + pass 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..3c8d267 --- /dev/null +++ b/hm_pyhelper/tests/test_gateway_grpc.py @@ -0,0 +1,137 @@ +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 = """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: + self.assertEqual(client.get_summary(), TestData.expected_summary) + + def test_get_gateway_version(self): + with patch('subprocess.check_output', + return_value=TestData.dpkg_output.encode('utf-8')): + with GatewayClient(f'localhost:{TestData.server_port}') as client: + self.assertEqual(client.get_gateway_version(), + TestData.expected_summary['gateway_version']) + + 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/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",