text: # Copyright 2020 Intel Corporation
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  
  import os
  import json
  import web3
  import random
  import asyncio
  import logging
  import argparse
  from urllib.parse import urlparse
  import crypto_utils.crypto_utility as crypto_utility
  from avalon_sdk.worker.worker_details import WorkerType
  import avalon_sdk.worker.worker_details as worker_details
  from avalon_sdk.work_order.work_order_params import WorkOrderParams
  
  from avalon_sdk.ethereum.ethereum_work_order \
      import EthereumWorkOrderProxyImpl
  from avalon_sdk.ethereum.ethereum_worker_registry \
      import EthereumWorkerRegistryImpl
  from avalon_sdk.ethereum.ethereum_wrapper \
      import EthereumWrapper
  from avalon_sdk.direct.jrpc.jrpc_worker_registry \
      import JRPCWorkerRegistryImpl
  from avalon_sdk.direct.jrpc.jrpc_work_order \
      import JRPCWorkOrderImpl
  
  
  logging.basicConfig(
      format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
  
  LISTENER_SLEEP_DURATION = 5  # seconds
  
  
  class EthereumConnector:
  
      """
      This class is the bridge between the Ethereum blockchain and the Avalon
      core. It listens for events generated by the Ethereum blockchain.
      It handles event data corresponding to the event (eg: workOrderSubmitted
      and submits requests to Avalon on behalf of the client. The service also
      invokes smart contract APIs (eg: workOrderComplete).
      """
  
      def __init__(self, config):
  
          self._config = config
          self._eth_client = EthereumWrapper(config)
          tcf_home = os.environ.get("TCF_HOME", "../../")
  
          worker_reg_contract_file = tcf_home + "/" + \
              config["ethereum"]["proxy_worker_registry_contract_file"]
          worker_reg_contract_address = \
              config["ethereum"]["proxy_worker_registry_contract_address"]
          self._worker_reg_contract_instance = self._eth_client\
              .get_contract_instance(
                  worker_reg_contract_file, worker_reg_contract_address)
  
          work_order_contract_file = tcf_home + "/" + \
              config["ethereum"]["work_order_contract_file"]
          work_order_contract_address = \
              config["ethereum"]["work_order_contract_address"]
          self._work_order_contract_instance = self._eth_client\
              .get_contract_instance(
                  work_order_contract_file, work_order_contract_address)
  
      def _retrieve_first_worker_details(self):
          """
          This function retrieves the first worker from shared kv using
          worker_lookup direct API.
          Returns details of worker
          """
          jrpc_req_id = random.randint(0, 100000)
          worker_registry = JRPCWorkerRegistryImpl(self._config)
  
          # Get first worker from worker registry
          worker_id = None
          worker_lookup_result = worker_registry.worker_lookup(
              worker_type=WorkerType.TEE_SGX, id=jrpc_req_id
          )
          logging.info("\n Worker lookup response: {}\n".format(
              json.dumps(worker_lookup_result, indent=4)
          ))
          if "result" in worker_lookup_result and \
                  "ids" in worker_lookup_result["result"].keys():
              if worker_lookup_result["result"]["totalCount"] != 0:
                  worker_id = worker_lookup_result["result"]["ids"][0]
              else:
                  logging.error("No workers found")
                  worker_id = None
          else:
              logging.error("Failed to lookup worker")
              worker_id = None
          if worker_id is None:
              logging.error("Unable to get a worker")
              sys.exit(-1)
  
          # Retrieve worker details
          jrpc_req_id += 1
          worker_info = worker_registry.worker_retrieve(worker_id, jrpc_req_id)
          logging.info("Worker retrieve response: {}"
                       .format(json.dumps(worker_info)))
  
          if "error" in worker_info:
              logging.error("Unable to retrieve worker details\n")
              sys.exit(1)
          return worker_id, worker_info["result"]
  
      def _add_worker_to_chain(self, worker_id, worker_info):
          """
          This function adds a worker to the Ethereum blockchain
          """
          worker_id = web3.Web3.toBytes(hexstr=worker_id)
          worker_type = worker_info["workerType"]
          org_id = web3.Web3.toBytes(hexstr=worker_info["organizationId"])
          app_type_ids = web3.Web3.toBytes(
              hexstr=worker_info["applicationTypeId"])
          details = json.dumps(worker_info["details"])
  
          txn_dict = self._worker_reg_contract_instance.functions.workerRegister(
              worker_id, worker_type, org_id, [app_type_ids], details)\
              .buildTransaction(self._eth_client.get_transaction_params())
          try:
              txn_receipt = self._eth_client.execute_transaction(txn_dict)
          except Exception as e:
              logging.error("Error while adding worker to ethereum"
                            + " blockchain : "+str(e))
  
      def _submit_work_order_and_get_result(self, work_order_id, worker_id,
                                            requester_id, work_order_params):
          """
          This function submits work order using work_order_submit direct API
          """
          work_order_impl = JRPCWorkOrderImpl(self._config)
          response = work_order_impl\
              .work_order_submit(work_order_id, worker_id, requester_id,
                                 work_order_params, id=random.randint(0, 100000))
          logging.info("Work order submit response : {}"
                       .format(json.dumps(response, indent=4)))
  
          work_order_result = work_order_impl\
              .work_order_get_result(work_order_id,
                                     id=random.randint(0, 100000))
  
          logging.info("Work order get result : {} "
                       .format(json.dumps(work_order_result, indent=4)))
  
          return work_order_result
  
      def _add_work_order_result_to_chain(self, work_order_id, response):
          """
          This function adds a work order result to the Ethereum blockchain
          """
  
          txn_dict = self._work_order_contract_instance.functions\
              .workOrderComplete(web3.Web3.toBytes(hexstr=work_order_id),
                                 json.dumps(response))\
              .buildTransaction(self._eth_client.get_transaction_params())
          try:
              txn_receipt = self._eth_client.execute_transaction(txn_dict)
          except Exception as e:
              logging.error("Error adding work order result to ethereum"
                            + " blockchain : "+str(e))
  
      def handleEvent(self, event, account, contract):
          """
          The function retrieves pertinent information from the event received
          and makes request to listener using Direct API and writes back result
          to the blockchain
          """
          work_order_request = json.loads(event["args"]["workOrderRequest"])
  
          work_order_id = work_order_request["workOrderId"]
          worker_id = work_order_request["workerId"]
          requester_id = work_order_request["requesterId"]
          work_order_params = event["args"]["workOrderRequest"]
  
          response = self\
              ._submit_work_order_and_get_result(work_order_id, worker_id,
                                                 requester_id, work_order_params)
          self._add_work_order_result_to_chain(work_order_id, response)
  
      def start(self):
          logging.info("Ethereum Connector service started")
  
          # Fetch first worker details from shared KV (via direct API)
          # and add the worker to block chain.
          # TODO: Fetch all workers from shared KV and block chain
          # and do 2-way sync.
          worker_id, worker_info = self._retrieve_first_worker_details()
          self._add_worker_to_chain(worker_id, worker_info)
  
          # Start an event listener that listens for events from the proxy
          # blockchain, extracts request payload from there and make a request
          # to avalon-listener
  
          w3 = BlockchainInterface(self._config["ethereum"]["provider"])
  
          contract = self._work_order_contract_instance
          # Listening only for workOrderSubmitted event now
          listener = w3.newListener(contract, "workOrderSubmitted")
  
          try:
              daemon = EventProcessor()
              asyncio.get_event_loop().run_until_complete(daemon.start(
                  listener,
                  self.handleEvent,
                  account=None,
                  contract=contract,
              ))
          except KeyboardInterrupt:
              asyncio.get_event_loop().run_until_complete(daemon.stop())
  
  
  class BlockchainInterface:
      def __init__(self, gateway):
          # TODO: store list of contracts?
          pass
  
      def newListener(self, contract, event, fromBlock='latest'):
          return contract.events[event].createFilter(fromBlock=fromBlock)
  
  
  class EventProcessor:
      async def listener(self, eventListener):
          while True:
              for event in eventListener.get_new_entries():
                  await self.queue.put(event)
                  logging.debug("Event pushed into listener Queue")
              await asyncio.sleep(LISTENER_SLEEP_DURATION)
  
      async def handler(self, callback, *kargs, **kwargs):
          while True:
              event = await self.queue.get()
              logging.debug("Popped event from listener Queue")
              callback(event, *kargs, **kwargs)
              self.queue.task_done()
  
      async def start(self, eventListener, callback, *kargs, **kwargs):
          self.queue = asyncio.Queue()
          loop = asyncio.get_event_loop()
          self.listeners = [loop.create_task(
              self.listener(eventListener)) for _ in range(1)]
          self.handlers = [loop.create_task(self.handler(
              callback, *kargs, **kwargs)) for _ in range(8)]
  
          await asyncio.gather(*self.listeners)  # infinite loop
          await self.queue.join()  # this code should never run
          await self.stop()  # this code should never run
  
      async def stop(self):
          for process in self.listeners:
              process.cancel()
          for process in self.handlers:
              process.cancel()
          logging.debug("---exit---")
  