Exemple #1
0
class MQTTRPC(MQTTClient):
    client_uid = CLIENT_UID
    cleansession = True
    mqtt_reply_timeout = MQTT_REPLY_TIMEOUT
    mqtt_url = MQTT_URL
    request_count = 0
    rpc_replies = {}
    replied = Event(
    )  # This event is triggered every time a new reply has come.
    subscriptions = [
    ]  # We hold a list of our subscriptions not to subscribe to

    # every request to the same client.

    def __init__(self, mqtt_url=None, client_uid=None, loop=None, config=None):
        if not loop:
            loop = asyncio.get_event_loop()
        self.loop = loop
        self.protocol = JSONRPCProtocol()
        self.dispatcher = dispatcher
        self.config = config
        if mqtt_url:
            self.mqtt_url = mqtt_url
        if client_uid:
            self.client_uid = client_uid
        super(MQTTRPC, self).__init__(client_id=self.client_uid,
                                      loop=loop,
                                      config=self.config)
        for signame in ('SIGINT', 'SIGTERM'):
            self.loop.add_signal_handler(
                getattr(signal, signame),
                lambda: asyncio.ensure_future(self.stop()))

        logger.info('Client {} initialized'.format(self.client_uid))

    async def stop(self):
        logger.info('Stopping mqttrpc...')
        # Check subscriptions
        if self._connected_state.is_set():
            await self.unsubscribe(self.subscriptions)
            await self.disconnect()
        tasks = [
            task for task in asyncio.Task.all_tasks()
            if task is not asyncio.tasks.Task.current_task()
        ]
        list(map(lambda task: task.cancel(), tasks))
        results = await asyncio.gather(*tasks, return_exceptions=True)
        logger.debug('Finished cancelling tasks, result: {}'.format(results))
        self.loop.stop()

    async def process_messages(self):
        self.mqtt_url = self.config.get('broker', {}).get('uri', self.mqtt_url)
        logger.info('Connecting to {}'.format(self.mqtt_url))
        await self.connect(self.mqtt_url, cleansession=self.cleansession)
        logger.info('Connected.')
        await self.subscribe([
            ('rpc/{}/+'.format(self.client_uid), QOS_2),
        ])
        logger.debug('Starting process messages.')
        while True:
            try:
                self.loop.create_task(
                    self.process_message(await self.deliver_message()))
            except asyncio.CancelledError:
                return

    async def process_message(self, message):
        logger.debug('Message at topic {}'.format(message.topic))

        if re.search('^rpc/(\w+)/(\w+)$', message.topic):
            # RPC request
            logger.debug('RPC request at {}'.format(message.topic))
            _, _, context = message.topic.split('/')
            data_str = message.data.decode()
            await self.receive_rpc_request(context, data_str)

        elif re.search('^rpc/(\w+)/(\w+)/reply$', message.topic):
            # RPC reply
            logger.debug('RPC reply at {}'.format(message.topic))
            _, _, context, _ = message.topic.split('/')
            data_str = message.data.decode()
            waiting_replies = self.rpc_replies.get(message.topic)
            if not waiting_replies:
                logger.warning(
                    'Got an unexpected RPC reply from {}: {}'.format(
                        message.topic, data_str))
            else:
                try:
                    data_js = json.loads(data_str)
                except json.decoder.JSONDecodeError:
                    logger.error(
                        'RPC reply bad json data: {}'.format(data_str))
                else:
                    request_id = data_js.get('id')
                    if request_id not in waiting_replies.keys():
                        logger.warning(
                            'Got a reply from {} to bad request id: {}'.format(
                                message.topic, data_str))
                    else:
                        # Finally matched the request by id
                        logger.debug(
                            'Waiting reply found for request {}'.format(
                                request_id))
                        await waiting_replies[request_id].put(data_str)
        else:
            logger.debug('Passing to on_message handler')
            await self.on_message(message)

    async def on_message(self, message):
        # Override it to implement other handlres.
        logger.debug('Not implemented')

    async def receive_rpc_request(self, context, data):
        logger.debug('Request: {}'.format(data))
        self.request_count += 1
        if type(data) != str:
            data = json.dumps(data)

        message = data

        async def handle_message(context, message):
            try:
                request = self.protocol.parse_request(message)
            except RPCError as e:
                response = e.error_respond()
            else:
                # Hack to add first params as self
                if not self in request.args:
                    request.args.insert(0, self)
                response = await self.dispatcher.dispatch(
                    request, getattr(self.protocol, '_caller', None))

            # send reply
            if response is not None:
                result = response.serialize()
                logger.debug('RPC reply to {}: {}'.format(context, result))
                self.loop.create_task(
                    self.publish(
                        'rpc/{}/{}/reply'.format(self.client_uid, context),
                        result.encode()))

        await handle_message(context, message)

    def get_proxy_for(self, destination, one_way=False):
        return RPCProxy(self, destination, one_way)

    async def _send_and_handle_reply(self,
                                     destination,
                                     req,
                                     one_way,
                                     no_exception=False):
        # Convert to bytes and send to destination
        if one_way:
            # We do not need a reply it's a notification call
            await self.publish(
                'rpc/{}/{}'.format(destination, self.client_uid),
                req.serialize().encode())
            return

        # We need a reply
        reply_topic = ('rpc/{}/{}/reply'.format(destination, self.client_uid))
        self.rpc_replies.setdefault(reply_topic, {})[req.unique_id] = Queue()
        if reply_topic not in self.subscriptions:
            logger.debug(
                'Adding subscrption to reply topic {}'.format(reply_topic))
            self.subscriptions.append(reply_topic)
            await self.subscribe([(reply_topic, QOS_2)])
            logger.debug('Subscribed to reply topic {}'.format(reply_topic))
        else:
            logger.debug('Already subscribed for topic {}'.format(reply_topic))
        await self.publish('rpc/{}/{}'.format(destination, self.client_uid),
                           req.serialize().encode())
        logger.debug('Published request id {} to {}'.format(
            req.unique_id, destination))
        try:
            reply_data = await asyncio.wait_for(
                self.rpc_replies[reply_topic][req.unique_id].get(),
                self.mqtt_reply_timeout)
            self.rpc_replies[reply_topic][req.unique_id].task_done()

        except asyncio.TimeoutError:
            del self.rpc_replies[reply_topic][req.unique_id]
            raise RPCError(
                'Reply Timeout, topic {}, id {}, method {}, args {}, kwargs {}'
                .format(reply_topic, req.unique_id, req.method, req.args,
                        req.kwargs))

        else:
            # We got a reply, handle it.
            logger.debug('Got a reply for request id: {}'.format(
                req.unique_id))
            rpc_response = self.protocol.parse_reply(reply_data)
            del self.rpc_replies[reply_topic][req.unique_id]
            # Check that there is no RPC errors.
            if not no_exception and hasattr(rpc_response, 'error'):
                raise RPCError('Error calling remote procedure: %s' %\
                               rpc_response.error)
            return rpc_response

    async def _call(self, destination, method, args, kwargs, one_way=False):
        req = self.protocol.create_request(method, args, kwargs, one_way)
        rep = await self._send_and_handle_reply(destination, req, one_way)
        if one_way:
            return
        return rep.result
Exemple #2
0
class RPCClient(object):
    """Client for making RPC calls to connected servers.
    :param protocol: An :py:class:`~tinyrpc.RPCProtocol` instance.
    :param transport: A :py:class:`~tinyrpc.transports.ClientTransport`
                      instance.
    """
    JSON_RPC_VERSION = "2.0"
    _ALLOWED_REPLY_KEYS = sorted(['id', 'jsonrpc', 'error', 'result'])
    _ALLOWED_REQUEST_KEYS = sorted(['id', 'jsonrpc', 'method', 'params'])

    def parse_reply(self, data):
        try:
            rep = json.loads(data)
        except Exception as e:
            raise InvalidReplyError(e)

        for k in rep.keys():
            if not k in self._ALLOWED_REPLY_KEYS:
                raise InvalidReplyError('Key not allowed: %s' % k)

        if not 'jsonrpc' in rep:
            raise InvalidReplyError('Missing jsonrpc (version) in response.')

        if rep['jsonrpc'] != self.JSON_RPC_VERSION:
            raise InvalidReplyError('Wrong JSONRPC version')

        if not 'id' in rep:
            raise InvalidReplyError('Missing id in response')

        if ('error' in rep) == ('result' in rep):
            raise InvalidReplyError(
                'Reply must contain exactly one of result and error.')

        if 'error' in rep:
            response = JSONRPCErrorResponse()
            error = rep['error']
            response.error = error['message']
            response._jsonrpc_error_code = error['code']
        else:
            response = JSONRPCSuccessResponse()
            response.result = rep.get('result', None)

        response.unique_id = rep['id']

        return response

    def __init__(self, url):
        self.protocol = JSONRPCProtocol()
        self.url = url

    def _send_and_handle_reply(self, req):
        #        print (req.serialize()) # show JSON string
        headers = {'content-type': 'application/json'}
        reply = requests.post(self.url, req.serialize(), headers=headers)
        #        print (reply.json());
        response = self.parse_reply(str(reply.json()).replace("'", '"'))

        if hasattr(response, 'error'):
            raise RPCError('Error calling remote procedure: %s' % \
                           response.error)

        return response

    def call(self, method, args, kwargs, one_way=False):
        """Calls the requested method and returns the result.
        If an error occured, an :py:class:`~tinyrpc.exc.RPCError` instance
        is raised.
        :param method: Name of the method to call.
        :param args: Arguments to pass to the method.
        :param kwargs: Keyword arguments to pass to the method.
        :param one_way: Whether or not a reply is desired.
        """
        req = self.protocol.create_request(method, args, kwargs, one_way)

        return self._send_and_handle_reply(req).result

    def get_proxy(self, prefix='', one_way=False):
        """Convenience method for creating a proxy.
        :param prefix: Passed on to :py:class:`~tinyrpc.client.RPCProxy`.
        :param one_way: Passed on to :py:class:`~tinyrpc.client.RPCProxy`.
        :return: :py:class:`~tinyrpc.client.RPCProxy` instance."""
        return RPCProxy(self, prefix, one_way)

    def batch_call(self, calls):
        """Experimental, use at your own peril."""
        req = self.protocol.create_batch_request()

        for call_args in calls:
            req.append(self.protocol.create_request(*call_args))

        return self._send_and_handle_reply(req)
Exemple #3
0
class JSONRPCClient(object):
    """ Ethereum JSON RPC client.

    Args:
        host (str): Ethereum node host address.
        port (int): Ethereum node port number.
        privkey (bin): Local user private key, used to sign transactions.
        nonce_update_interval (float): Update the account nonce every
            `nonce_update_interval` seconds.
        nonce_offset (int): Network's default base nonce number.
    """
    def __init__(self,
                 host,
                 port,
                 privkey,
                 nonce_update_interval=5.0,
                 nonce_offset=0):
        endpoint = 'http://{}:{}'.format(host, port)
        session = requests.Session()
        adapter = requests.adapters.HTTPAdapter(pool_maxsize=50)
        session.mount(endpoint, adapter)

        self.transport = HttpPostClientTransport(
            endpoint,
            post_method=session.post,
            headers={'content-type': 'application/json'},
        )

        self.port = port
        self.privkey = privkey
        self.protocol = JSONRPCProtocol()
        self.sender = privatekey_to_address(privkey)

        self.nonce_last_update = 0
        self.nonce_current_value = None
        self.nonce_lock = Semaphore()
        self.nonce_update_interval = nonce_update_interval
        self.nonce_offset = nonce_offset

    def __repr__(self):
        return '<JSONRPCClient @%d>' % self.port

    def blocknumber(self):
        """ Return the most recent block. """
        return quantity_decoder(self.call('eth_blockNumber'))

    def nonce(self, address):
        if len(address) == 40:
            address = unhexlify(address)

        with self.nonce_lock:
            initialized = self.nonce_current_value is not None
            query_time = now()

            if self.nonce_last_update > query_time:
                # Python's 2.7 time is not monotonic and it's affected by clock
                # resets, force an update.
                self.nonce_update_interval = query_time - self.nonce_update_interval
                needs_update = True

            else:
                last_update_interval = query_time - self.nonce_last_update
                needs_update = last_update_interval > self.nonce_update_interval

            if initialized and not needs_update:
                self.nonce_current_value += 1
                return self.nonce_current_value

            pending_transactions_hex = self.call(
                'eth_getTransactionCount',
                address_encoder(address),
                'pending',
            )
            pending_transactions = quantity_decoder(pending_transactions_hex)
            nonce = pending_transactions + self.nonce_offset

            # we may have hammered the server and not all tx are
            # registered as `pending` yet
            while nonce < self.nonce_current_value:
                log.debug(
                    'nonce on server too low; retrying',
                    server=nonce,
                    local=self.nonce_current_value,
                )

                query_time = now()
                pending_transactions_hex = self.call(
                    'eth_getTransactionCount',
                    address_encoder(address),
                    'pending',
                )
                pending_transactions = quantity_decoder(
                    pending_transactions_hex)
                nonce = pending_transactions + self.nonce_offset

            self.nonce_current_value = nonce
            self.nonce_last_update = query_time

            return self.nonce_current_value

    def balance(self, account):
        """ Return the balance of the account of given address. """
        res = self.call('eth_getBalance', address_encoder(account), 'pending')
        return quantity_decoder(res)

    def gaslimit(self):
        last_block = self.call('eth_getBlockByNumber', 'latest', True)
        gas_limit = quantity_decoder(last_block['gasLimit'])
        return gas_limit

    def new_contract_proxy(self, contract_interface, address):
        """ Return a proxy for interacting with a smart contract.

        Args:
            contract_interface: The contract interface as defined by the json.
            address: The contract's address.
        """
        return ContractProxy(
            self.sender,
            contract_interface,
            address,
            self.eth_call,
            self.send_transaction,
            self.eth_estimateGas,
        )

    def deploy_solidity_contract(
            self,  # pylint: disable=too-many-locals
            sender,
            contract_name,
            all_contracts,
            libraries,
            constructor_parameters,
            contract_path=None,
            timeout=None,
            gasprice=GAS_PRICE):
        """
        Deploy a solidity contract.
        Args:
            sender (address): the sender address
            contract_name (str): the name of the contract to compile
            all_contracts (dict): the json dictionary containing the result of compiling a file
            libraries (list): A list of libraries to use in deployment
            constructor_parameters (tuple): A tuple of arguments to pass to the constructor
            contract_path (str): If we are dealing with solc >= v0.4.9 then the path
                                 to the contract is a required argument to extract
                                 the contract data from the `all_contracts` dict.
            timeout (int): Amount of time to poll the chain to confirm deployment
            gasprice: The gasprice to provide for the transaction
        """

        if contract_name in all_contracts:
            contract_key = contract_name

        elif contract_path is not None:
            _, filename = os.path.split(contract_path)
            contract_key = filename + ':' + contract_name

            if contract_key not in all_contracts:
                raise ValueError('Unknown contract {}'.format(contract_name))
        else:
            raise ValueError(
                'Unknown contract {} and no contract_path given'.format(
                    contract_name))

        libraries = dict(libraries)
        contract = all_contracts[contract_key]
        contract_interface = contract['abi']
        symbols = solidity_unresolved_symbols(contract['bin_hex'])

        if symbols:
            available_symbols = map(solidity_library_symbol,
                                    all_contracts.keys())

            unknown_symbols = set(symbols) - set(available_symbols)
            if unknown_symbols:
                msg = 'Cannot deploy contract, known symbols {}, unresolved symbols {}.'.format(
                    available_symbols,
                    unknown_symbols,
                )
                raise Exception(msg)

            dependencies = deploy_dependencies_symbols(all_contracts)
            deployment_order = dependencies_order_of_build(
                contract_key, dependencies)

            deployment_order.pop()  # remove `contract_name` from the list

            log.debug('Deploying dependencies: {}'.format(
                str(deployment_order)))

            for deploy_contract in deployment_order:
                dependency_contract = all_contracts[deploy_contract]

                hex_bytecode = solidity_resolve_symbols(
                    dependency_contract['bin_hex'], libraries)
                bytecode = unhexlify(hex_bytecode)

                dependency_contract['bin_hex'] = hex_bytecode
                dependency_contract['bin'] = bytecode

                transaction_hash_hex = self.send_transaction(
                    sender,
                    to='',
                    data=bytecode,
                    gasprice=gasprice,
                )
                transaction_hash = unhexlify(transaction_hash_hex)

                self.poll(transaction_hash, timeout=timeout)
                receipt = self.eth_getTransactionReceipt(transaction_hash)

                contract_address = receipt['contractAddress']
                # remove the hexadecimal prefix 0x from the address
                contract_address = contract_address[2:]

                libraries[deploy_contract] = contract_address

                deployed_code = self.eth_getCode(unhexlify(contract_address))

                if deployed_code == '0x':
                    raise RuntimeError(
                        'Contract address has no code, check gas usage.')

            hex_bytecode = solidity_resolve_symbols(contract['bin_hex'],
                                                    libraries)
            bytecode = unhexlify(hex_bytecode)

            contract['bin_hex'] = hex_bytecode
            contract['bin'] = bytecode

        if constructor_parameters:
            translator = ContractTranslator(contract_interface)
            parameters = translator.encode_constructor_arguments(
                constructor_parameters)
            bytecode = contract['bin'] + parameters
        else:
            bytecode = contract['bin']

        transaction_hash_hex = self.send_transaction(
            sender,
            to='',
            data=bytecode,
            gasprice=gasprice,
        )
        transaction_hash = unhexlify(transaction_hash_hex)

        self.poll(transaction_hash, timeout=timeout)
        receipt = self.eth_getTransactionReceipt(transaction_hash)
        contract_address = receipt['contractAddress']

        deployed_code = self.eth_getCode(unhexlify(contract_address[2:]))

        if deployed_code == '0x':
            raise RuntimeError(
                'Deployment of {} failed. Contract address has no code, check gas usage.'
                .format(contract_name, ))

        return self.new_contract_proxy(
            contract_interface,
            contract_address,
        )

    def new_filter(self,
                   fromBlock=None,
                   toBlock=None,
                   address=None,
                   topics=None):
        """ Creates a filter object, based on filter options, to notify when
        the state changes (logs). To check if the state has changed, call
        eth_getFilterChanges.
        """

        json_data = {
            'fromBlock': block_tag_encoder(fromBlock or ''),
            'toBlock': block_tag_encoder(toBlock or ''),
        }

        if address is not None:
            json_data['address'] = address_encoder(address)

        if topics is not None:
            if not isinstance(topics, list):
                raise ValueError('topics must be a list')

            json_data['topics'] = [topic_encoder(topic) for topic in topics]

        filter_id = self.call('eth_newFilter', json_data)
        return quantity_decoder(filter_id)

    def filter_changes(self, fid):
        changes = self.call('eth_getFilterChanges', quantity_encoder(fid))

        if not changes:
            return None

        if isinstance(changes, bytes):
            return data_decoder(changes)

        decoders = {
            'blockHash': data_decoder,
            'transactionHash': data_decoder,
            'data': data_decoder,
            'address': address_decoder,
            'topics': lambda x: [topic_decoder(t) for t in x],
            'blockNumber': quantity_decoder,
            'logIndex': quantity_decoder,
            'transactionIndex': quantity_decoder
        }
        return [{k: decoders[k](v)
                 for k, v in c.items() if v is not None} for c in changes]

    @check_node_connection
    def call(self, method, *args):
        """ Do the request and return the result.

        Args:
            method (str): The RPC method.
            args: The encoded arguments expected by the method.
                - Object arguments must be supplied as a dictionary.
                - Quantity arguments must be hex encoded starting with '0x' and
                without left zeros.
                - Data arguments must be hex encoded starting with '0x'
        """
        request = self.protocol.create_request(method, args)
        reply = self.transport.send_message(request.serialize())

        jsonrpc_reply = self.protocol.parse_reply(reply)
        if isinstance(jsonrpc_reply, JSONRPCSuccessResponse):
            return jsonrpc_reply.result
        elif isinstance(jsonrpc_reply, JSONRPCErrorResponse):
            raise EthNodeCommunicationError(jsonrpc_reply.error)
        else:
            raise EthNodeCommunicationError('Unknown type of JSONRPC reply')

    def send_transaction(self,
                         sender,
                         to,
                         value=0,
                         data='',
                         startgas=0,
                         gasprice=GAS_PRICE,
                         nonce=None):
        """ Helper to send signed messages.

        This method will use the `privkey` provided in the constructor to
        locally sign the transaction. This requires an extended server
        implementation that accepts the variables v, r, and s.
        """

        if not self.privkey and not sender:
            raise ValueError('Either privkey or sender needs to be supplied.')

        if self.privkey:
            privkey_address = privatekey_to_address(self.privkey)
            sender = sender or privkey_address

            if sender != privkey_address:
                raise ValueError('sender for a different privkey.')

            if nonce is None:
                nonce = self.nonce(sender)
        else:
            if nonce is None:
                nonce = 0

        if not startgas:
            startgas = self.gaslimit() - 1

        tx = Transaction(nonce,
                         gasprice,
                         startgas,
                         to=to,
                         value=value,
                         data=data)

        if self.privkey:
            tx.sign(self.privkey)
            result = self.call(
                'eth_sendRawTransaction',
                data_encoder(rlp.encode(tx)),
            )
            return result[2 if result.startswith('0x') else 0:]

        else:

            # rename the fields to match the eth_sendTransaction signature
            tx_dict = tx.to_dict()
            tx_dict.pop('hash')
            tx_dict['sender'] = sender
            tx_dict['gasPrice'] = tx_dict.pop('gasprice')
            tx_dict['gas'] = tx_dict.pop('startgas')

            res = self.eth_sendTransaction(**tx_dict)

        assert len(res) in (20, 32)
        return hexlify(res)

    def eth_sendTransaction(self,
                            nonce=None,
                            sender='',
                            to='',
                            value=0,
                            data='',
                            gasPrice=GAS_PRICE,
                            gas=GAS_PRICE):
        """ Creates new message call transaction or a contract creation, if the
        data field contains code.

        Args:
            sender (address): The 20 bytes address the transaction is sent from.
            to (address): DATA, 20 Bytes - (optional when creating new
                contract) The address the transaction is directed to.
            gas (int): Gas provided for the transaction execution. It will
                return unused gas.
            gasPrice (int): gasPrice used for each unit of gas paid.
            value (int): Value sent with this transaction.
            data (bin): The compiled code of a contract OR the hash of the
                invoked method signature and encoded parameters.
            nonce (int): This allows to overwrite your own pending transactions
                that use the same nonce.
        """

        if to == '' and data.isalnum():
            warnings.warn(
                'Verify that the data parameter is _not_ hex encoded, if this is the case '
                'the data will be double encoded and result in unexpected '
                'behavior.')

        if to == '0' * 40:
            warnings.warn(
                'For contract creation the empty string must be used.')

        if sender is None:
            raise ValueError('sender needs to be provided.')

        json_data = {
            'to': data_encoder(normalize_address(to, allow_blank=True)),
            'value': quantity_encoder(value),
            'gasPrice': quantity_encoder(gasPrice),
            'gas': quantity_encoder(gas),
            'data': data_encoder(data),
            'from': address_encoder(sender),
        }

        if nonce is not None:
            json_data['nonce'] = quantity_encoder(nonce)

        res = self.call('eth_sendTransaction', json_data)

        return data_decoder(res)

    def eth_call(self,
                 sender='',
                 to='',
                 value=0,
                 data='',
                 startgas=GAS_PRICE,
                 gasprice=GAS_PRICE,
                 block_number='latest'):
        """ Executes a new message call immediately without creating a
        transaction on the blockchain.

        Args:
            sender: The address the transaction is sent from.
            to: The address the transaction is directed to.
            gas (int): Gas provided for the transaction execution. eth_call
                consumes zero gas, but this parameter may be needed by some
                executions.
            gasPrice (int): gasPrice used for unit of gas paid.
            value (int): Integer of the value sent with this transaction.
            data (bin): Hash of the method signature and encoded parameters.
                For details see Ethereum Contract ABI.
            block_number: Determines the state of ethereum used in the
                call.
        """

        json_data = format_data_for_call(
            sender,
            to,
            value,
            data,
            startgas,
            gasprice,
        )
        res = self.call('eth_call', json_data, block_number)

        return data_decoder(res)

    def eth_estimateGas(self,
                        sender='',
                        to='',
                        value=0,
                        data='',
                        startgas=GAS_PRICE,
                        gasprice=GAS_PRICE):
        """ Makes a call or transaction, which won't be added to the blockchain
        and returns the used gas, which can be used for estimating the used
        gas.

        Args:
            sender: The address the transaction is sent from.
            to: The address the transaction is directed to.
            gas (int): Gas provided for the transaction execution. eth_call
                consumes zero gas, but this parameter may be needed by some
                executions.
            gasPrice (int): gasPrice used for unit of gas paid.
            value (int): Integer of the value sent with this transaction.
            data (bin): Hash of the method signature and encoded parameters.
                For details see Ethereum Contract ABI.
            block_number: Determines the state of ethereum used in the
                call.
        """

        json_data = format_data_for_call(
            sender,
            to,
            value,
            data,
            startgas,
            gasprice,
        )
        res = self.call('eth_estimateGas', json_data)

        return quantity_decoder(res)

    def eth_getTransactionReceipt(self, transaction_hash):
        """ Returns the receipt of a transaction by transaction hash.

        Args:
            transaction_hash: Hash of a transaction.

        Returns:
            A dict representing the transaction receipt object, or null when no
            receipt was found.
        """
        if transaction_hash.startswith('0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior')

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)')

        transaction_hash = data_encoder(transaction_hash)
        return self.call('eth_getTransactionReceipt', transaction_hash)

    def eth_getCode(self, address, block='latest'):
        """ Returns code at a given address.

        Args:
            address: An address.
            block: Integer block number, or the string 'latest',
                'earliest' or 'pending'.
        """
        if address.startswith('0x'):
            warnings.warn(
                'address seems to be already encoded, this will result '
                'in unexpected behavior')

        if len(address) != 20:
            raise ValueError(
                'address length must be 20 (it might be hex encoded)')

        return self.call(
            'eth_getCode',
            address_encoder(address),
            block,
        )

    def eth_getTransactionByHash(self, transaction_hash):
        """ Returns the information about a transaction requested by
        transaction hash.
        """

        if transaction_hash.startswith('0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior')

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)')

        transaction_hash = data_encoder(transaction_hash)
        return self.call('eth_getTransactionByHash', transaction_hash)

    def poll(self, transaction_hash, confirmations=None, timeout=None):
        """ Wait until the `transaction_hash` is applied or rejected.
        If timeout is None, this could wait indefinitely!

        Args:
            transaction_hash (hash): Transaction hash that we are waiting for.
            confirmations (int): Number of block confirmations that we will
                wait for.
            timeout (float): Timeout in seconds, raise an Excpetion on
                timeout.
        """
        if transaction_hash.startswith('0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior')

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)')

        transaction_hash = data_encoder(transaction_hash)

        deadline = None
        if timeout:
            deadline = gevent.Timeout(timeout)
            deadline.start()

        try:
            # used to check if the transaction was removed, this could happen
            # if gas price is too low:
            #
            # > Transaction (acbca3d6) below gas price (tx=1 Wei ask=18
            # > Shannon). All sequential txs from this address(7d0eae79)
            # > will be ignored
            #
            last_result = None

            while True:
                # Could return None for a short period of time, until the
                # transaction is added to the pool
                transaction = self.call('eth_getTransactionByHash',
                                        transaction_hash)

                # if the transaction was added to the pool and then removed
                if transaction is None and last_result is not None:
                    raise Exception('invalid transaction, check gas price')

                # the transaction was added to the pool and mined
                if transaction and transaction['blockNumber'] is not None:
                    break

                last_result = transaction

                gevent.sleep(.5)

            if confirmations:
                # this will wait for both APPLIED and REVERTED transactions
                transaction_block = quantity_decoder(
                    transaction['blockNumber'])
                confirmation_block = transaction_block + confirmations

                block_number = self.blocknumber()

                while block_number < confirmation_block:
                    gevent.sleep(.5)
                    block_number = self.blocknumber()

        except gevent.Timeout:
            raise Exception('timeout when polling for transaction')

        finally:
            if deadline:
                deadline.cancel()
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol

rpc = JSONRPCProtocol()

# again, code below is protocol-independent

# assuming you want to call method(*args, **kwargs)

request = rpc.create_request(method, args, kwargs)
reply = send_to_server_and_get_reply(request)

response = rpc.parse_reply(reply)

if hasattr(response, 'error'):
    pass
else:
    # the return value is found in response.result
    do_something_with(response.result)
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
"""
membuat rpc
1. buat instance dari JSONRPCProtocol
2. tambahkan method create_request
"""

ini_instance = JSONRPCProtocol()
request = ini_instance.create_request(
    'flight-event-subscribe'
)  #bisa juga menambahkan parameter misal request = self.rpc.create_request('authenticate', {'id' : self.client_id})

#contoh request dengan param
request4 = ini_instance.create_request('flight-event-subscribe', {'id': '1'})
"""
melihat uniq id yg dibuat secara otomatis oleh rpc
"""
request2 = ini_instance.create_request('flight-event-subscribe2', )
request3 = ini_instance.create_request('flight-event-subscribe3')
print request.unique_id
print request2.unique_id
print request3.unique_id
print request4.unique_id
"""
Hasil Akhir yang siap dikirim
"""
print "type request", type(request)
print "type request.serialize =", type(request.serialize())
print request.serialize()

#lihat bedanya dengan yang ada paramnya
class JSONRPCClient:
    """ Ethereum JSON RPC client.

    Args:
        host: Ethereum node host address.
        port: Ethereum node port number.
        privkey: Local user private key, used to sign transactions.
        nonce_update_interval: Update the account nonce every
            `nonce_update_interval` seconds.
        nonce_offset: Network's default base nonce number.
    """

    def __init__(
            self,
            host: str,
            port: int,
            privkey: bytes,
            nonce_update_interval: float = 5.0,
            nonce_offset: int = 0):

        endpoint = 'http://{}:{}'.format(host, port)
        session = requests.Session()
        adapter = requests.adapters.HTTPAdapter(pool_maxsize=50)
        session.mount(endpoint, adapter)

        self.transport = HttpPostClientTransport(
            endpoint,
            post_method=session.post,
            headers={'content-type': 'application/json'},
        )

        self.port = port
        self.privkey = privkey
        self.protocol = JSONRPCProtocol()
        self.sender = privatekey_to_address(privkey)
        # Needs to be initialized to None in the beginning since JSONRPCClient
        # gets constructed before the RaidenService Object.
        self.stop_event = None

        self.nonce_last_update = 0
        self.nonce_current_value = None
        self.nonce_lock = Semaphore()
        self.nonce_update_interval = nonce_update_interval
        self.nonce_offset = nonce_offset

    def __repr__(self):
        return '<JSONRPCClient @%d>' % self.port

    def block_number(self):
        """ Return the most recent block. """
        return quantity_decoder(self.call('eth_blockNumber'))

    def nonce(self, address):
        if len(address) == 40:
            address = unhexlify(address)

        with self.nonce_lock:
            initialized = self.nonce_current_value is not None
            query_time = now()

            if self.nonce_last_update > query_time:
                # Python's 2.7 time is not monotonic and it's affected by clock
                # resets, force an update.
                self.nonce_update_interval = query_time - self.nonce_update_interval
                needs_update = True

            else:
                last_update_interval = query_time - self.nonce_last_update
                needs_update = last_update_interval > self.nonce_update_interval

            if initialized and not needs_update:
                self.nonce_current_value += 1
                return self.nonce_current_value

            pending_transactions_hex = self.call(
                'eth_getTransactionCount',
                address_encoder(address),
                'pending',
            )
            pending_transactions = quantity_decoder(pending_transactions_hex)
            nonce = pending_transactions + self.nonce_offset

            # we may have hammered the server and not all tx are
            # registered as `pending` yet
            if initialized:
                while nonce < self.nonce_current_value:
                    log.debug(
                        'nonce on server too low; retrying',
                        server=nonce,
                        local=self.nonce_current_value,
                    )

                    query_time = now()
                    pending_transactions_hex = self.call(
                        'eth_getTransactionCount',
                        address_encoder(address),
                        'pending',
                    )
                    pending_transactions = quantity_decoder(pending_transactions_hex)
                    nonce = pending_transactions + self.nonce_offset

            self.nonce_current_value = nonce
            self.nonce_last_update = query_time

            return self.nonce_current_value

    def inject_stop_event(self, event):
        self.stop_event = event

    def balance(self, account: address):
        """ Return the balance of the account of given address. """
        res = self.call('eth_getBalance', address_encoder(account), 'pending')
        return quantity_decoder(res)

    def gaslimit(self) -> int:
        last_block = self.call('eth_getBlockByNumber', 'latest', True)
        gas_limit = quantity_decoder(last_block['gasLimit'])
        return gas_limit

    def new_contract_proxy(self, contract_interface, contract_address: address):
        """ Return a proxy for interacting with a smart contract.

        Args:
            contract_interface: The contract interface as defined by the json.
            address: The contract's address.
        """
        return ContractProxy(
            self.sender,
            contract_interface,
            contract_address,
            self.eth_call,
            self.send_transaction,
            self.eth_estimateGas,
        )

    def deploy_solidity_contract(
            self,  # pylint: disable=too-many-locals
            sender,
            contract_name,
            all_contracts,
            libraries,
            constructor_parameters,
            contract_path=None,
            timeout=None,
            gasprice=GAS_PRICE):
        """
        Deploy a solidity contract.
        Args:
            sender (address): the sender address
            contract_name (str): the name of the contract to compile
            all_contracts (dict): the json dictionary containing the result of compiling a file
            libraries (list): A list of libraries to use in deployment
            constructor_parameters (tuple): A tuple of arguments to pass to the constructor
            contract_path (str): If we are dealing with solc >= v0.4.9 then the path
                                 to the contract is a required argument to extract
                                 the contract data from the `all_contracts` dict.
            timeout (int): Amount of time to poll the chain to confirm deployment
            gasprice: The gasprice to provide for the transaction
        """

        if contract_name in all_contracts:
            contract_key = contract_name

        elif contract_path is not None:
            _, filename = os.path.split(contract_path)
            contract_key = filename + ':' + contract_name

            if contract_key not in all_contracts:
                raise ValueError('Unknown contract {}'.format(contract_name))
        else:
            raise ValueError(
                'Unknown contract {} and no contract_path given'.format(contract_name)
            )

        libraries = dict(libraries)
        contract = all_contracts[contract_key]
        contract_interface = contract['abi']
        symbols = solidity_unresolved_symbols(contract['bin_hex'])

        if symbols:
            available_symbols = list(map(solidity_library_symbol, all_contracts.keys()))

            unknown_symbols = set(symbols) - set(available_symbols)
            if unknown_symbols:
                msg = 'Cannot deploy contract, known symbols {}, unresolved symbols {}.'.format(
                    available_symbols,
                    unknown_symbols,
                )
                raise Exception(msg)

            dependencies = deploy_dependencies_symbols(all_contracts)
            deployment_order = dependencies_order_of_build(contract_key, dependencies)

            deployment_order.pop()  # remove `contract_name` from the list

            log.debug('Deploying dependencies: {}'.format(str(deployment_order)))

            for deploy_contract in deployment_order:
                dependency_contract = all_contracts[deploy_contract]

                hex_bytecode = solidity_resolve_symbols(dependency_contract['bin_hex'], libraries)
                bytecode = unhexlify(hex_bytecode)

                dependency_contract['bin_hex'] = hex_bytecode
                dependency_contract['bin'] = bytecode

                transaction_hash_hex = self.send_transaction(
                    sender,
                    to=b'',
                    data=bytecode,
                    gasprice=gasprice,
                )
                transaction_hash = unhexlify(transaction_hash_hex)

                self.poll(transaction_hash, timeout=timeout)
                receipt = self.eth_getTransactionReceipt(transaction_hash)

                contract_address = receipt['contractAddress']
                # remove the hexadecimal prefix 0x from the address
                contract_address = contract_address[2:]

                libraries[deploy_contract] = contract_address

                deployed_code = self.eth_getCode(address_decoder(contract_address))

                if len(deployed_code) == 0:
                    raise RuntimeError('Contract address has no code, check gas usage.')

            hex_bytecode = solidity_resolve_symbols(contract['bin_hex'], libraries)
            bytecode = unhexlify(hex_bytecode)

            contract['bin_hex'] = hex_bytecode
            contract['bin'] = bytecode

        if constructor_parameters:
            translator = ContractTranslator(contract_interface)
            parameters = translator.encode_constructor_arguments(constructor_parameters)
            bytecode = contract['bin'] + parameters
        else:
            bytecode = contract['bin']

        transaction_hash_hex = self.send_transaction(
            sender,
            to=b'',
            data=bytecode,
            gasprice=gasprice,
        )
        transaction_hash = unhexlify(transaction_hash_hex)

        self.poll(transaction_hash, timeout=timeout)
        receipt = self.eth_getTransactionReceipt(transaction_hash)
        contract_address = receipt['contractAddress']

        deployed_code = self.eth_getCode(address_decoder(contract_address))

        if len(deployed_code) == 0:
            raise RuntimeError(
                'Deployment of {} failed. Contract address has no code, check gas usage.'.format(
                    contract_name,
                )
            )

        return self.new_contract_proxy(
            contract_interface,
            contract_address,
        )

    def new_filter(self, fromBlock=None, toBlock=None, address=None, topics=None):
        """ Creates a filter object, based on filter options, to notify when
        the state changes (logs). To check if the state has changed, call
        eth_getFilterChanges.
        """

        json_data = {
            'fromBlock': block_tag_encoder(fromBlock or ''),
            'toBlock': block_tag_encoder(toBlock or ''),
        }

        if address is not None:
            json_data['address'] = address_encoder(address)

        if topics is not None:
            if not isinstance(topics, list):
                raise ValueError('topics must be a list')

            json_data['topics'] = [topic_encoder(topic) for topic in topics]

        filter_id = self.call('eth_newFilter', json_data)
        return quantity_decoder(filter_id)

    def filter_changes(self, fid: int) -> List:
        changes = self.call('eth_getFilterChanges', quantity_encoder(fid))

        if not changes:
            return list()

        assert isinstance(changes, list)
        decoders = {
            'blockHash': data_decoder,
            'transactionHash': data_decoder,
            'data': data_decoder,
            'address': address_decoder,
            'topics': lambda x: [topic_decoder(t) for t in x],
            'blockNumber': quantity_decoder,
            'logIndex': quantity_decoder,
            'transactionIndex': quantity_decoder
        }
        return [
            {k: decoders[k](v) for k, v in c.items() if v is not None}
            for c in changes
        ]

    @check_node_connection
    def call(self, method: str, *args):
        """ Do the request and return the result.

        Args:
            method: The RPC method.
            args: The encoded arguments expected by the method.
                - Object arguments must be supplied as a dictionary.
                - Quantity arguments must be hex encoded starting with '0x' and
                without left zeros.
                - Data arguments must be hex encoded starting with '0x'
        """
        request = self.protocol.create_request(method, args)
        reply = self.transport.send_message(request.serialize().encode())

        jsonrpc_reply = self.protocol.parse_reply(reply)
        if isinstance(jsonrpc_reply, JSONRPCSuccessResponse):
            return jsonrpc_reply.result
        elif isinstance(jsonrpc_reply, JSONRPCErrorResponse):
            raise EthNodeCommunicationError(jsonrpc_reply.error, jsonrpc_reply._jsonrpc_error_code)
        else:
            raise EthNodeCommunicationError('Unknown type of JSONRPC reply')

    def send_transaction(
            self,
            sender: address,
            to: address,
            value: int = 0,
            data: bytes = b'',
            startgas: int = 0,
            gasprice: int = GAS_PRICE,
            nonce: Optional[int] = None):
        """ Helper to send signed messages.

        This method will use the `privkey` provided in the constructor to
        locally sign the transaction. This requires an extended server
        implementation that accepts the variables v, r, and s.
        """

        if not self.privkey and not sender:
            raise ValueError('Either privkey or sender needs to be supplied.')

        if self.privkey:
            privkey_address = privatekey_to_address(self.privkey)
            sender = sender or privkey_address

            if sender != privkey_address:
                raise ValueError('sender for a different privkey.')

            if nonce is None:
                nonce = self.nonce(sender)
        else:
            if nonce is None:
                nonce = 0

        if not startgas:
            startgas = self.gaslimit() - 1

        tx = Transaction(nonce, gasprice, startgas, to=to, value=value, data=data)

        if self.privkey:
            tx.sign(self.privkey)
            result = self.call(
                'eth_sendRawTransaction',
                data_encoder(rlp.encode(tx)),
            )
            return result[2 if result.startswith('0x') else 0:]

        else:

            # rename the fields to match the eth_sendTransaction signature
            tx_dict = tx.to_dict()
            tx_dict.pop('hash')
            tx_dict['sender'] = sender
            tx_dict['gasPrice'] = tx_dict.pop('gasprice')
            tx_dict['gas'] = tx_dict.pop('startgas')

            res = self.eth_sendTransaction(**tx_dict)

        assert len(res) in (20, 32)
        return hexlify(res)

    def eth_sendTransaction(
            self,
            sender: address = b'',
            to: address = b'',
            value: int = 0,
            data: bytes = b'',
            gasPrice: int = GAS_PRICE,
            gas: int = GAS_PRICE,
            nonce: Optional[int] = None):
        """ Creates new message call transaction or a contract creation, if the
        data field contains code.

        Args:
            sender: The address the transaction is sent from.
            to: The address the transaction is directed to.
                (optional when creating new contract)
            gas: Gas provided for the transaction execution. It will
                return unused gas.
            gasPrice: gasPrice used for each unit of gas paid.
            value: Value sent with this transaction.
            data: The compiled code of a contract OR the hash of the
                invoked method signature and encoded parameters.
            nonce: This allows to overwrite your own pending transactions
                that use the same nonce.
        """

        if to == b'' and data.isalnum():
            warnings.warn(
                'Verify that the data parameter is _not_ hex encoded, if this is the case '
                'the data will be double encoded and result in unexpected '
                'behavior.'
            )

        if to == b'0' * 40:
            warnings.warn('For contract creation the empty string must be used.')

        if sender is None:
            raise ValueError('sender needs to be provided.')

        json_data = format_data_for_call(
            sender,
            to,
            value,
            data,
            gas,
            gasPrice
        )

        if nonce is not None:
            json_data['nonce'] = quantity_encoder(nonce)

        res = self.call('eth_sendTransaction', json_data)

        return data_decoder(res)

    def eth_call(
            self,
            sender: address = b'',
            to: address = b'',
            value: int = 0,
            data: bytes = b'',
            startgas: int = GAS_PRICE,
            gasprice: int = GAS_PRICE,
            block_number: Union[str, int] = 'latest'):
        """ Executes a new message call immediately without creating a
        transaction on the blockchain.

        Args:
            sender: The address the transaction is sent from.
            to: The address the transaction is directed to.
            gas: Gas provided for the transaction execution. eth_call
                consumes zero gas, but this parameter may be needed by some
                executions.
            gasPrice: gasPrice used for unit of gas paid.
            value: Integer of the value sent with this transaction.
            data: Hash of the method signature and encoded parameters.
                For details see Ethereum Contract ABI.
            block_number: Determines the state of ethereum used in the
                call.
        """

        json_data = format_data_for_call(
            sender,
            to,
            value,
            data,
            startgas,
            gasprice,
        )
        res = self.call('eth_call', json_data, block_number)

        return data_decoder(res)

    def eth_estimateGas(
            self,
            sender: address = b'',
            to: address = b'',
            value: int = 0,
            data: bytes = b'',
            startgas: int = GAS_PRICE,
            gasprice: int = GAS_PRICE) -> int:
        """ Makes a call or transaction, which won't be added to the blockchain
        and returns the used gas, which can be used for estimating the used
        gas.

        Args:
            sender: The address the transaction is sent from.
            to: The address the transaction is directed to.
            gas: Gas provided for the transaction execution. eth_call
                consumes zero gas, but this parameter may be needed by some
                executions.
            gasPrice: gasPrice used for unit of gas paid.
            value: Integer of the value sent with this transaction.
            data: Hash of the method signature and encoded parameters.
                For details see Ethereum Contract ABI.
            block_number: Determines the state of ethereum used in the
                call.
        """

        json_data = format_data_for_call(
            sender,
            to,
            value,
            data,
            startgas,
            gasprice,
        )
        try:
            res = self.call('eth_estimateGas', json_data)
        except EthNodeCommunicationError as e:
            tx_would_fail = e.error_code and e.error_code in (-32015, -32000)
            if tx_would_fail:  # -32015 is parity and -32000 is geth
                return None
            else:
                raise e

        return quantity_decoder(res)

    def eth_getTransactionReceipt(self, transaction_hash: bytes) -> Dict:
        """ Returns the receipt of a transaction by transaction hash.

        Args:
            transaction_hash: Hash of a transaction.

        Returns:
            A dict representing the transaction receipt object, or null when no
            receipt was found.
        """
        if transaction_hash.startswith(b'0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior'
            )

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)'
            )

        transaction_hash = data_encoder(transaction_hash)
        return self.call('eth_getTransactionReceipt', transaction_hash)

    def eth_getCode(self, code_address: address, block: Union[int, str] = 'latest') -> bytes:
        """ Returns code at a given address.

        Args:
            code_address: An address.
            block: Integer block number, or the string 'latest',
                'earliest' or 'pending'. Default is 'latest'.
        """
        if code_address.startswith(b'0x'):
            warnings.warn(
                'address seems to be already encoded, this will result '
                'in unexpected behavior'
            )

        if len(code_address) != 20:
            raise ValueError(
                'address length must be 20 (it might be hex encoded)'
            )

        result = self.call('eth_getCode', address_encoder(code_address), block)
        return data_decoder(result)

    def eth_getTransactionByHash(self, transaction_hash: bytes):
        """ Returns the information about a transaction requested by
        transaction hash.
        """

        if transaction_hash.startswith(b'0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior'
            )

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)'
            )

        transaction_hash = data_encoder(transaction_hash)
        return self.call('eth_getTransactionByHash', transaction_hash)

    def poll(
            self,
            transaction_hash: bytes,
            confirmations: Optional[int] = None,
            timeout: Optional[float] = None):
        """ Wait until the `transaction_hash` is applied or rejected.
        If timeout is None, this could wait indefinitely!

        Args:
            transaction_hash: Transaction hash that we are waiting for.
            confirmations: Number of block confirmations that we will
                wait for.
            timeout: Timeout in seconds, raise an Excpetion on timeout.
        """
        if transaction_hash.startswith(b'0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior'
            )

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)'
            )

        transaction_hash = data_encoder(transaction_hash)

        deadline = None
        if timeout:
            deadline = gevent.Timeout(timeout)
            deadline.start()

        try:
            # used to check if the transaction was removed, this could happen
            # if gas price is too low:
            #
            # > Transaction (acbca3d6) below gas price (tx=1 Wei ask=18
            # > Shannon). All sequential txs from this address(7d0eae79)
            # > will be ignored
            #
            last_result = None

            while True:
                # Could return None for a short period of time, until the
                # transaction is added to the pool
                transaction = self.call('eth_getTransactionByHash', transaction_hash)

                # if the transaction was added to the pool and then removed
                if transaction is None and last_result is not None:
                    raise Exception('invalid transaction, check gas price')

                # the transaction was added to the pool and mined
                if transaction and transaction['blockNumber'] is not None:
                    break

                last_result = transaction

                gevent.sleep(.5)

            if confirmations:
                # this will wait for both APPLIED and REVERTED transactions
                transaction_block = quantity_decoder(transaction['blockNumber'])
                confirmation_block = transaction_block + confirmations

                block_number = self.block_number()

                while block_number < confirmation_block:
                    gevent.sleep(.5)
                    block_number = self.block_number()

        except gevent.Timeout:
            raise Exception('timeout when polling for transaction')

        finally:
            if deadline:
                deadline.cancel()
Exemple #7
0
class JSONRPCClient:
    """ Ethereum JSON RPC client.

    Args:
        host: Ethereum node host address.
        port: Ethereum node port number.
        privkey: Local user private key, used to sign transactions.
        nonce_update_interval: Update the account nonce every
            `nonce_update_interval` seconds.
        nonce_offset: Network's default base nonce number.
    """
    def __init__(self,
                 host: str,
                 port: int,
                 privkey: bytes,
                 gasprice: int = None,
                 nonce_update_interval: float = 5.0,
                 nonce_offset: int = 0):

        if privkey is None or len(privkey) != 32:
            raise ValueError('Invalid private key')

        endpoint = 'http://{}:{}'.format(host, port)
        self.session = requests.Session()
        adapter = requests.adapters.HTTPAdapter(pool_maxsize=50)
        self.session.mount(endpoint, adapter)

        self.transport = HttpPostClientTransport(
            endpoint,
            post_method=self.session.post,
            headers={'content-type': 'application/json'},
        )

        self.port = port
        self.privkey = privkey
        self.protocol = JSONRPCProtocol()
        self.sender = privatekey_to_address(privkey)
        # Needs to be initialized to None in the beginning since JSONRPCClient
        # gets constructed before the RaidenService Object.
        self.stop_event = None

        self.nonce_last_update = 0
        self.nonce_available_value = None
        self.nonce_lock = Semaphore()
        self.nonce_update_interval = nonce_update_interval
        self.nonce_offset = nonce_offset
        self.given_gas_price = gasprice

        cache = cachetools.TTLCache(
            maxsize=1,
            ttl=RPC_CACHE_TTL,
        )
        cache_wrapper = cachetools.cached(cache=cache)
        self.gaslimit = cache_wrapper(self._gaslimit)
        cache = cachetools.TTLCache(
            maxsize=1,
            ttl=RPC_CACHE_TTL,
        )
        cache_wrapper = cachetools.cached(cache=cache)
        self.gasprice = cache_wrapper(self._gasprice)

    def __del__(self):
        self.session.close()

    def __repr__(self):
        return '<JSONRPCClient @%d>' % self.port

    def block_number(self):
        """ Return the most recent block. """
        return quantity_decoder(self.rpccall_with_retry('eth_blockNumber'))

    def nonce_needs_update(self):
        if self.nonce_available_value is None:
            return True

        now = time.time()

        # Python's 2.7 time is not monotonic and it's affected by clock resets,
        # force an update.
        if self.nonce_last_update > now:
            return True

        return now - self.nonce_last_update > self.nonce_update_interval

    def nonce_update_from_node(self):
        nonce = -2
        nonce_available_value = self.nonce_available_value or -1

        # Wait until all tx are registered as pending
        while nonce < nonce_available_value:
            pending_transactions_hex = self.rpccall_with_retry(
                'eth_getTransactionCount',
                address_encoder(self.sender),
                'pending',
            )
            pending_transactions = quantity_decoder(pending_transactions_hex)
            nonce = pending_transactions + self.nonce_offset

            log.debug(
                'updated nonce from server',
                server=nonce,
                local=nonce_available_value,
            )

        self.nonce_last_update = time.time()
        self.nonce_available_value = nonce

    def nonce(self):
        with self.nonce_lock:
            if self.nonce_needs_update():
                self.nonce_update_from_node()

            self.nonce_available_value += 1
            return self.nonce_available_value - 1

    def inject_stop_event(self, event):
        self.stop_event = event

    def balance(self, account: Address):
        """ Return the balance of the account of given address. """
        res = self.rpccall_with_retry('eth_getBalance',
                                      address_encoder(account), 'pending')
        return quantity_decoder(res)

    def _gaslimit(self, location='pending') -> int:
        last_block = self.rpccall_with_retry('eth_getBlockByNumber', location,
                                             True)
        gas_limit = quantity_decoder(last_block['gasLimit'])
        return gas_limit * 8 // 10

    def _gasprice(self) -> int:
        if self.given_gas_price:
            return self.given_gas_price

        gas_price = self.rpccall_with_retry('eth_gasPrice')
        return quantity_decoder(gas_price)

    def check_startgas(self, startgas):
        if not startgas:
            return self.gaslimit()
        return startgas

    def new_contract_proxy(self, contract_interface,
                           contract_address: Address):
        """ Return a proxy for interacting with a smart contract.

        Args:
            contract_interface: The contract interface as defined by the json.
            address: The contract's address.
        """
        return ContractProxy(
            self.sender,
            contract_interface,
            contract_address,
            self.eth_call,
            self.send_transaction,
            self.eth_estimateGas,
        )

    def deploy_solidity_contract(
            self,  # pylint: disable=too-many-locals
            contract_name,
            all_contracts,
            libraries,
            constructor_parameters,
            contract_path=None,
            timeout=None):
        """
        Deploy a solidity contract.
        Args:
            sender (address): the sender address
            contract_name (str): the name of the contract to compile
            all_contracts (dict): the json dictionary containing the result of compiling a file
            libraries (list): A list of libraries to use in deployment
            constructor_parameters (tuple): A tuple of arguments to pass to the constructor
            contract_path (str): If we are dealing with solc >= v0.4.9 then the path
                                 to the contract is a required argument to extract
                                 the contract data from the `all_contracts` dict.
            timeout (int): Amount of time to poll the chain to confirm deployment
        """

        if contract_name in all_contracts:
            contract_key = contract_name

        elif contract_path is not None:
            _, filename = os.path.split(contract_path)
            contract_key = filename + ':' + contract_name

            if contract_key not in all_contracts:
                raise ValueError('Unknown contract {}'.format(contract_name))
        else:
            raise ValueError(
                'Unknown contract {} and no contract_path given'.format(
                    contract_name))

        libraries = dict(libraries)
        contract = all_contracts[contract_key]
        contract_interface = contract['abi']
        symbols = solidity_unresolved_symbols(contract['bin_hex'])

        if symbols:
            available_symbols = list(
                map(solidity_library_symbol, all_contracts.keys()))

            unknown_symbols = set(symbols) - set(available_symbols)
            if unknown_symbols:
                msg = 'Cannot deploy contract, known symbols {}, unresolved symbols {}.'.format(
                    available_symbols,
                    unknown_symbols,
                )
                raise Exception(msg)

            dependencies = deploy_dependencies_symbols(all_contracts)
            deployment_order = dependencies_order_of_build(
                contract_key, dependencies)

            deployment_order.pop()  # remove `contract_name` from the list

            log.debug('Deploying dependencies: {}'.format(
                str(deployment_order)))

            for deploy_contract in deployment_order:
                dependency_contract = all_contracts[deploy_contract]

                hex_bytecode = solidity_resolve_symbols(
                    dependency_contract['bin_hex'], libraries)
                bytecode = unhexlify(hex_bytecode)

                dependency_contract['bin_hex'] = hex_bytecode
                dependency_contract['bin'] = bytecode

                transaction_hash_hex = self.send_transaction(
                    to=b'',
                    data=bytecode,
                )
                transaction_hash = unhexlify(transaction_hash_hex)

                self.poll(transaction_hash, timeout=timeout)
                receipt = self.eth_getTransactionReceipt(transaction_hash)

                contract_address = receipt['contractAddress']
                # remove the hexadecimal prefix 0x from the address
                contract_address = contract_address[2:]

                libraries[deploy_contract] = contract_address

                deployed_code = self.eth_getCode(
                    address_decoder(contract_address))

                if not deployed_code:
                    raise RuntimeError(
                        'Contract address has no code, check gas usage.')

            hex_bytecode = solidity_resolve_symbols(contract['bin_hex'],
                                                    libraries)
            bytecode = unhexlify(hex_bytecode)

            contract['bin_hex'] = hex_bytecode
            contract['bin'] = bytecode

        if constructor_parameters:
            translator = ContractTranslator(contract_interface)
            parameters = translator.encode_constructor_arguments(
                constructor_parameters)
            bytecode = contract['bin'] + parameters
        else:
            bytecode = contract['bin']

        transaction_hash_hex = self.send_transaction(
            to=b'',
            data=bytecode,
        )
        transaction_hash = unhexlify(transaction_hash_hex)

        self.poll(transaction_hash, timeout=timeout)
        receipt = self.eth_getTransactionReceipt(transaction_hash)
        contract_address = receipt['contractAddress']

        deployed_code = self.eth_getCode(address_decoder(contract_address))

        if not deployed_code:
            raise RuntimeError(
                'Deployment of {} failed. Contract address has no code, check gas usage.'
                .format(contract_name, ))

        return self.new_contract_proxy(
            contract_interface,
            contract_address,
        )

    def new_filter(self,
                   fromBlock=None,
                   toBlock=None,
                   address=None,
                   topics=None):
        """ Creates a filter object, based on filter options, to notify when
        the state changes (logs). To check if the state has changed, call
        eth_getFilterChanges.
        """

        json_data = {
            'fromBlock': block_tag_encoder(fromBlock or ''),
            'toBlock': block_tag_encoder(toBlock or ''),
        }

        if address is not None:
            json_data['address'] = address_encoder(address)

        if topics is not None:
            if not isinstance(topics, list):
                raise ValueError('topics must be a list')

            json_data['topics'] = [topic_encoder(topic) for topic in topics]

        filter_id = self.rpccall_with_retry('eth_newFilter', json_data)
        return quantity_decoder(filter_id)

    def filter_changes(self, fid: int) -> List:
        changes = self.rpccall_with_retry('eth_getFilterChanges',
                                          quantity_encoder(fid))

        if not changes:
            return list()

        assert isinstance(changes, list)
        decoders = {
            'blockHash': data_decoder,
            'transactionHash': data_decoder,
            'data': data_decoder,
            'address': address_decoder,
            'topics': lambda x: [topic_decoder(t) for t in x],
            'blockNumber': quantity_decoder,
            'logIndex': quantity_decoder,
            'transactionIndex': quantity_decoder
        }
        return [{k: decoders[k](v)
                 for k, v in c.items() if v is not None} for c in changes]

    def rpccall_with_retry(self, method: str, *args):
        """ Do the request and return the result.

        Args:
            method: The JSON-RPC method.
            args: The encoded arguments expected by the method.
                - Object arguments must be supplied as a dictionary.
                - Quantity arguments must be hex encoded starting with '0x' and
                without left zeros.
                - Data arguments must be hex encoded starting with '0x'
        """
        request = self.protocol.create_request(method, args)
        request_serialized = request.serialize().encode()

        for i, timeout in enumerate(timeout_two_stage(10, 3, 10)):
            if self.stop_event and self.stop_event.is_set():
                raise RaidenShuttingDown()

            try:
                reply = self.transport.send_message(request_serialized)
            except (requests.exceptions.ConnectionError, InvalidReplyError):
                log.info(
                    'Timeout in eth client connection to {}. Is the client offline? Trying '
                    'again in {}s.'.format(self.transport.endpoint, timeout))
            else:
                if self.stop_event and self.stop_event.is_set():
                    raise RaidenShuttingDown()

                if i > 0:
                    log.info('Client reconnected')

                jsonrpc_reply = self.protocol.parse_reply(reply)

                if isinstance(jsonrpc_reply, JSONRPCSuccessResponse):
                    return jsonrpc_reply.result
                elif isinstance(jsonrpc_reply, JSONRPCErrorResponse):
                    raise EthNodeCommunicationError(
                        jsonrpc_reply.error,
                        jsonrpc_reply._jsonrpc_error_code,  # pylint: disable=protected-access
                    )
                else:
                    raise EthNodeCommunicationError(
                        'Unknown type of JSONRPC reply')

            gevent.sleep(timeout)

    def send_transaction(
        self,
        to: Address,
        value: int = 0,
        data: bytes = b'',
        startgas: int = None,
    ):
        """ Helper to send signed messages.

        This method will use the `privkey` provided in the constructor to
        locally sign the transaction. This requires an extended server
        implementation that accepts the variables v, r, and s.
        """

        if to == b'' and data.isalnum():
            warnings.warn(
                'Verify that the data parameter is _not_ hex encoded, if this is the case '
                'the data will be double encoded and result in unexpected '
                'behavior.')

        if to == b'0' * 40:
            warnings.warn(
                'For contract creation the empty string must be used.')

        nonce = self.nonce()
        startgas = self.check_startgas(startgas)

        tx = Transaction(
            nonce,
            self.gasprice(),
            startgas,
            to=to,
            value=value,
            data=data,
        )

        tx.sign(self.privkey)
        result = self.rpccall_with_retry(
            'eth_sendRawTransaction',
            data_encoder(rlp.encode(tx)),
        )
        return result[2 if result.startswith('0x') else 0:]

    def eth_call(self,
                 sender: Address = b'',
                 to: Address = b'',
                 value: int = 0,
                 data: bytes = b'',
                 startgas: int = None,
                 block_number: Union[str, int] = 'latest') -> bytes:
        """ Executes a new message call immediately without creating a
        transaction on the blockchain.

        Args:
            sender: The address the transaction is sent from.
            to: The address the transaction is directed to.
            gas: Gas provided for the transaction execution. eth_call
                consumes zero gas, but this parameter may be needed by some
                executions.
            gasPrice: gasPrice used for unit of gas paid.
            value: Integer of the value sent with this transaction.
            data: Hash of the method signature and encoded parameters.
                For details see Ethereum Contract ABI.
            block_number: Determines the state of ethereum used in the
                call.
        """
        startgas = self.check_startgas(startgas)
        json_data = format_data_for_rpccall(
            sender,
            to,
            value,
            data,
            startgas,
            self.gasprice(),
        )
        res = self.rpccall_with_retry('eth_call', json_data, block_number)

        return data_decoder(res)

    def eth_estimateGas(self,
                        sender: Address = b'',
                        to: Address = b'',
                        value: int = 0,
                        data: bytes = b'',
                        startgas: int = None) -> Optional[int]:
        """ Makes a call or transaction, which won't be added to the blockchain
        and returns the used gas, which can be used for estimating the used
        gas.

        Args:
            sender: The address the transaction is sent from.
            to: The address the transaction is directed to.
            gas: Gas provided for the transaction execution. eth_call
                consumes zero gas, but this parameter may be needed by some
                executions.
            value: Integer of the value sent with this transaction.
            data: Hash of the method signature and encoded parameters.
                For details see Ethereum Contract ABI.
            block_number: Determines the state of ethereum used in the
                call.
        """
        startgas = self.check_startgas(startgas)
        json_data = format_data_for_rpccall(sender, to, value, data, startgas)
        try:
            res = self.rpccall_with_retry('eth_estimateGas', json_data)
        except EthNodeCommunicationError as e:
            tx_would_fail = e.error_code and e.error_code in (-32015, -32000)
            if tx_would_fail:  # -32015 is parity and -32000 is geth
                return None
            else:
                raise e

        return quantity_decoder(res)

    def eth_getTransactionReceipt(self, transaction_hash: bytes) -> Dict:
        """ Returns the receipt of a transaction by transaction hash.

        Args:
            transaction_hash: Hash of a transaction.

        Returns:
            A dict representing the transaction receipt object, or null when no
            receipt was found.
        """
        if transaction_hash.startswith(b'0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior')

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)')

        transaction_hash = data_encoder(transaction_hash)
        return self.rpccall_with_retry('eth_getTransactionReceipt',
                                       transaction_hash)

    def eth_getCode(self,
                    code_address: Address,
                    block: Union[int, str] = 'latest') -> bytes:
        """ Returns code at a given address.

        Args:
            code_address: An address.
            block: Integer block number, or the string 'latest',
                'earliest' or 'pending'. Default is 'latest'.
        """
        if code_address.startswith(b'0x'):
            warnings.warn(
                'address seems to be already encoded, this will result '
                'in unexpected behavior')

        if len(code_address) != 20:
            raise ValueError(
                'address length must be 20 (it might be hex encoded)')

        result = self.rpccall_with_retry('eth_getCode',
                                         address_encoder(code_address), block)
        return data_decoder(result)

    def eth_getTransactionByHash(self, transaction_hash: bytes):
        """ Returns the information about a transaction requested by
        transaction hash.
        """

        if transaction_hash.startswith(b'0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior')

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)')

        transaction_hash = data_encoder(transaction_hash)
        return self.rpccall_with_retry('eth_getTransactionByHash',
                                       transaction_hash)

    def poll(self,
             transaction_hash: bytes,
             confirmations: int = None,
             timeout: float = None):
        """ Wait until the `transaction_hash` is applied or rejected.
        If timeout is None, this could wait indefinitely!

        Args:
            transaction_hash: Transaction hash that we are waiting for.
            confirmations: Number of block confirmations that we will
                wait for.
            timeout: Timeout in seconds, raise an Excpetion on timeout.
        """
        if transaction_hash.startswith(b'0x'):
            warnings.warn(
                'transaction_hash seems to be already encoded, this will'
                ' result in unexpected behavior')

        if len(transaction_hash) != 32:
            raise ValueError(
                'transaction_hash length must be 32 (it might be hex encoded)')

        transaction_hash = data_encoder(transaction_hash)

        deadline = None
        if timeout:
            deadline = gevent.Timeout(timeout)
            deadline.start()

        try:
            # used to check if the transaction was removed, this could happen
            # if gas price is too low:
            #
            # > Transaction (acbca3d6) below gas price (tx=1 Wei ask=18
            # > Shannon). All sequential txs from this address(7d0eae79)
            # > will be ignored
            #
            last_result = None

            while True:
                # Could return None for a short period of time, until the
                # transaction is added to the pool
                transaction = self.rpccall_with_retry(
                    'eth_getTransactionByHash',
                    transaction_hash,
                )

                # if the transaction was added to the pool and then removed
                if transaction is None and last_result is not None:
                    raise Exception('invalid transaction, check gas price')

                # the transaction was added to the pool and mined
                if transaction and transaction['blockNumber'] is not None:
                    break

                last_result = transaction

                gevent.sleep(.5)

            if confirmations:
                # this will wait for both APPLIED and REVERTED transactions
                transaction_block = quantity_decoder(
                    transaction['blockNumber'])
                confirmation_block = transaction_block + confirmations

                block_number = self.block_number()

                while block_number < confirmation_block:
                    gevent.sleep(.5)
                    block_number = self.block_number()

        except gevent.Timeout:
            raise Exception('timeout when polling for transaction')

        finally:
            if deadline:
                deadline.cancel()
Exemple #8
0
class GJsonRpcClient(GObject.GObject):
    """
    Wrap a raw socket and uses JSON-RPC over it.

    Supports calling methods, but not receiving server-initiated messages (ie: signals)
    """

    __gsignals__ = {
        "connection-closed": (
            GObject.SIGNAL_RUN_LAST,
            GObject.TYPE_NONE,
            (),
        ),
        "response": (
            GObject.SIGNAL_RUN_LAST,
            GObject.TYPE_NONE,
            (GObject.TYPE_INT, GObject.TYPE_STRING),
        ),
        "response-error": (
            GObject.SIGNAL_RUN_LAST,
            GObject.TYPE_NONE,
            (GObject.TYPE_INT, GObject.TYPE_STRING),
        ),
    }

    MAX_LINESIZE = 1024

    def __init__(self, sock: socket.socket):
        GObject.GObject.__init__(self)
        self.protocol = JSONRPCProtocol()
        self.sock = sock
        self.buffer = b""

    def run(self):
        GLib.io_add_watch(self.sock.fileno(), GLib.IO_IN, self._on_data)
        GLib.io_add_watch(self.sock.fileno(), GLib.IO_HUP | GLib.IO_ERR,
                          self._on_close)

    def call_async(self, method: str, args=[], kwargs={}):
        req = self.protocol.create_request(method, args, kwargs)
        print('call async', req.unique_id)
        output = req.serialize() + "\n"
        self.sock.send(output.encode("utf8"))

    def _on_close(self, *args):
        self.emit('connection-closed')

    def _on_data(self, *args):
        self.buffer += self.sock.recv(self.MAX_LINESIZE)
        while b"\n" in self.buffer:
            newline_pos = self.buffer.find(b"\n")
            msg = self.buffer[:newline_pos]
            self.buffer = self.buffer[newline_pos + 1:]
            try:
                response = self.protocol.parse_reply(msg)
            except BadReplyError:
                return
            if hasattr(response, "error"):
                self.emit("response-error", response.unique_id, response.error)
            else:
                self.emit("response", response.unique_id, response.result)
        return True