Esempio n. 1
0
    def broadcast_signed_request(self, signed_request, payer_address, payment_amounts=None,
                                 additional_payments=None):
        """ Broadcast a signed Request.

            Currently the Request API only supports signing requests as the payee,
            therefore this function only supports broadcasting a signed request
            as the payer.

        :param signed_request: The previously-signed Request to broadcast
        :type signed_request: types.Request
        :param payment_amounts: A list of integers specifying how much should be
            paid to each payee when the Request is created.
            The amount is in the currency of the Request.
        :type payment_amounts: [int]
        :param additional_payments: Additional amounts to pay on top of the `payment_amounts`.
        :type additional_payments: [int]
        :param options: Dictionary of options (not yet supported)
        :type options: dict
        :return: The transaction hash of the transaction which, if successfully
            included in a block, will create (and possibly pay, depending on `payment_amounts`)
            this Request.
        """
        am = ArtifactManager()
        service_class = am.get_service_class_by_address(signed_request.currency_contract_address)
        # currency = signed_request.currency_cont
        service_args = {
            'signed_request': signed_request,
            'creation_payments': payment_amounts,  # TODO decide naming convention, stick to it
            'additional_payments': additional_payments,
            'payer_address': payer_address
        }
        service = service_class()
        return service.broadcast_signed_request_as_payer(**service_args)
Esempio n. 2
0
 def _get_currency_contract_data(self):
     """ Return the currency contract for the given currency. `artifact_name` could
         be `last-RequestEthereum`, or `last-requesterc20-{token_address}`.
     """
     artifact_name = self._get_currency_contract_artifact_name()
     artifact_manager = ArtifactManager()
     contract_data = artifact_manager.get_contract_data(artifact_name)
     return contract_data
Esempio n. 3
0
    def get_request_by_transaction_hash(self, transaction_hash):
        """ Get a Request from an Ethereum transaction hash.

        :param transaction_hash: The hash of the transaction which created the Request
        :return: A Request instance
        :rtype: request_network.types.Request
        """
        tx_data = w3.eth.getTransaction(transaction_hash)
        if not tx_data:
            raise TransactionNotFound(transaction_hash)

        am = ArtifactManager()
        currency_contract = am.get_contract_instance(tx_data['to'])

        # Decode the transaction input data to get the function arguments
        func = currency_contract.get_function_by_selector(tx_data['input'][:10])
        arg_types = [i['type'] for i in func.abi['inputs']]
        arg_names = [i['name'] for i in func.abi['inputs']]
        arg_values = decode_abi(arg_types, Web3.toBytes(hexstr=tx_data['input'][10:]))
        function_args = dict(zip(arg_names, arg_values))

        # If this is a 'simple' Request we can take the ID from the transaction input.
        if '_requestId' in function_args:
            return self.get_request_by_id(
                Web3.toHex(function_args['_requestId']))

        # For more complex Requests (e.g. those created by broadcasting a signed Request)
        # we need to find the 'Created' event log that was emitted and take the ID from there.
        tx_receipt = w3.eth.getTransactionReceipt(transaction_hash)
        if not tx_receipt:
            raise Exception('TODO could not get tx receipt')

        # Extract the event args from the tx_receipt to retrieve the request_id
        core_contract = am.get_contract_instance(tx_receipt['logs'][0].address)
        # Work around Solidity bug. See note in read_padded_data_from_stream.
        with mock.patch.object(
                StringDecoder,
                'read_data_from_stream',
                new=read_padded_data_from_stream):
            logs = core_contract.events.Created().processReceipt(tx_receipt)
        request_id = logs[0].args.requestId

        return self.get_request_by_id(
            Web3.toHex(request_id),
            block_number=tx_data['blockNumber'])
Esempio n. 4
0
class ArtifactMangerTestCase(unittest.TestCase):
    def setUp(self):
        super().setUp()
        self.am = ArtifactManager()

    def test_get_erc20_req(self):
        artifact_name = 'last-requesterc20-{}'.format(TEST_TOKEN_ADDRESS)
        request_currency_contract = self.am.get_contract_data(artifact_name)
        self.assertEqual(TEST_CURRENCY_CONTRACT_ADDRESS,
                         request_currency_contract['address'])

    def test_get_invalid_token(self):
        # artifact_name = 'last-requesterc20-{}'.format(TEST_TOKEN_ADDRESS)
        with self.assertRaises(ArtifactNotFound):
            self.am.get_contract_data('foo')

    def test_get_valid_token_from_invalid_network(self):
        am = ArtifactManager()
        am.ethereum_network = 'fake-network'
        artifact_name = 'last-requesterc20-{}'.format(TEST_TOKEN_ADDRESS)
        with self.assertRaises(ArtifactNotFound):
            am.get_contract_data(artifact_name)

    def test_get_service_by_address(self):
        service_class = self.am.get_service_class_by_address(
            TEST_CURRENCY_CONTRACT_ADDRESS)
        self.assertEqual(RequestERC20Service, service_class)

    def test_get_invalid_service_class(self):
        with self.assertRaises(ArtifactNotFound):
            self.am.get_service_class_by_address('foo')

    def test_get_service_class_from_invalid_network(self):
        am = ArtifactManager()
        am.ethereum_network = 'fake-network'
        # with self.assertRaises(ArtifactNotFound):
        with self.assertRaises(ArtifactNotFound):
            am.get_service_class_by_address('foo')
Esempio n. 5
0
 def test_get_service_class_from_invalid_network(self):
     am = ArtifactManager()
     am.ethereum_network = 'fake-network'
     # with self.assertRaises(ArtifactNotFound):
     with self.assertRaises(ArtifactNotFound):
         am.get_service_class_by_address('foo')
Esempio n. 6
0
 def test_get_valid_token_from_invalid_network(self):
     am = ArtifactManager()
     am.ethereum_network = 'fake-network'
     artifact_name = 'last-requesterc20-{}'.format(TEST_TOKEN_ADDRESS)
     with self.assertRaises(ArtifactNotFound):
         am.get_contract_data(artifact_name)
Esempio n. 7
0
 def setUp(self):
     super().setUp()
     self.am = ArtifactManager()
Esempio n. 8
0
    def get_request_by_id(self, request_id, block_number=None):
        """ Get a Request from its ID.

        :param request_id: The Request ID as a 32 byte hex string
        :param block_number: If provided, only search for Created events from this block onwards.
        :return: A Request instance
        :rtype: request_network.types.Request
        """
        core_contract_address = Web3.toChecksumAddress(request_id[:42])
        am = ArtifactManager()
        core_contract_data = am.get_contract_data(core_contract_address)

        core_contract = w3.eth.contract(
            address=core_contract_address,
            abi=core_contract_data['abi'])

        # Converts the data returned from 'RequestCore:getRequest' into a friendly object
        RequestContractData = namedtuple('RequestContractData', [
            'payer_address', 'currency_contract_address', 'state',
            'payee_id_address', 'amount', 'balance'
        ])

        try:
            request_data = RequestContractData(*core_contract.functions.getRequest(
                request_id).call())
        except ValueError:
            # web3 will raise a ValueError if the contract at core_contract_address is not
            # a valid contract address. This could happen if the given Request ID contains
            # an invalid core_contract_address, so we treat it as an invalid Request ID.
            raise RequestNotFound('Request ID {} has an invalid core contract address {}'.format(
                request_id,
                core_contract_address
            ))

        if request_data.payer_address == EMPTY_BYTES_20:
            raise RequestNotFound('Request ID {} not found on core contract {}'.format(
                request_id,
                core_contract_address
            ))

        # Payment addresses for payees are not stored with the Request in the contract,
        # so they need to be looked up separately
        service_contract = am.get_contract_instance(request_data.currency_contract_address)
        payees = [
            Payee(
                id_address=request_data.payee_id_address,
                amount=request_data.amount,
                balance=request_data.balance,
                payment_address=service_contract.functions.payeesPaymentAddress(
                    request_id, 0).call()
            )
        ]

        sub_payees_count = core_contract.functions.getSubPayeesCount(request_id).call()
        for i in range(sub_payees_count):
            (address, amount, balance) = core_contract.functions.subPayees(request_id, i).call()
            payment_address = service_contract.functions.payeesPaymentAddress(
                request_id, i + 1).call()
            payees.append(Payee(
                id_address=address,
                payment_address=payment_address,
                balance=balance,
                amount=amount
            ))

        # To find the creator and data for a Request we need to find the Created event
        # that was emitted when the Request was created
        # web3.py provides helpers for getting logs for a specific contract event but
        # they rely on `eth_newFilter` which is not supported on Infura. As a workaround
        # the logs are retrieved with `web3.eth`getLogs` which does not require a new
        # filter to be created.
        created_event_signature = Web3.toHex(event_abi_to_log_topic(
            event_abi=core_contract.events.Created().abi
        ))
        logs = w3.eth.getLogs({
            'fromBlock': block_number if block_number else core_contract_data['block_number'],
            'address': core_contract_address,
            'topics': [created_event_signature, request_id]
        })
        assert len(logs) == 1, "Incorrect number of logs returned"

        # Work around Solidity bug. See note in read_padded_data_from_stream.
        with mock.patch.object(
                StringDecoder,
                'read_data_from_stream',
                new=read_padded_data_from_stream):
            created_event_data = get_event_data(
                event_abi=core_contract.events.Created().abi,
                log_entry=logs[0]
            )

        # creator = log_data.args.creator
        # See if we have an IPFS hash, and get the file if so
        if created_event_data.args.data != '':
            ipfs_hash = created_event_data.args.data
            data = retrieve_ipfs_data(ipfs_hash)
        else:
            ipfs_hash = None
            data = {}

        # Iterate through UpdateBalance events to build a list of payments made for this request
        updated_event_signature = Web3.toHex(event_abi_to_log_topic(
            event_abi=core_contract.events.UpdateBalance().abi
        ))
        logs = w3.eth.getLogs({
            'fromBlock': block_number if block_number else core_contract_data['block_number'],
            'address': core_contract_address,
            'topics': [updated_event_signature, request_id]
        })

        payments = []
        for log in logs:
            event_data = get_event_data(
                event_abi=core_contract.events.UpdateBalance().abi,
                log_entry=log
            )
            payments.append(Payment(
                payee_index=event_data.args.payeeIndex,
                delta_amount=event_data.args.deltaAmount
            ))
            payees[event_data.args.payeeIndex].paid_amount += event_data.args.deltaAmount

        # TODO set request state
        return Request(
            id=request_id,
            creator=created_event_data.args.creator,
            currency_contract_address=request_data.currency_contract_address,
            payer=request_data.payer_address,
            payees=payees,
            payments=payments,
            ipfs_hash=ipfs_hash,
            data=data,
            transaction_hash=Web3.toHex(created_event_data.transactionHash)
        )