class PaymentProcessorFunctionalTest(DatabaseFixture):
    """ In this suite we test Ethereum state changes done by PaymentProcessor.
    """
    def setUp(self):
        DatabaseFixture.setUp(self)
        self.state = tester.state()
        gnt_addr = self.state.evm(decode_hex(TestGNT.INIT_HEX))
        self.state.mine()
        self.gnt = tester.ABIContract(self.state, TestGNT.ABI, gnt_addr)
        PaymentProcessor.TESTGNT_ADDR = gnt_addr
        self.privkey = tester.k1
        self.client = mock.MagicMock(spec=Client)
        self.client.get_peer_count.return_value = 0
        self.client.is_syncing.return_value = False
        self.client.get_transaction_count.side_effect = \
            lambda addr: self.state.block.get_nonce(decode_hex(addr[2:]))
        self.client.get_balance.side_effect = \
            lambda addr: self.state.block.get_balance(decode_hex(addr[2:]))

        def call(_from, to, data, **kw):
            # pyethereum does not have direct support for non-mutating calls.
            # The implemenation just copies the state and discards it after.
            # Here we apply real transaction, but with gas price 0.
            # We assume the transaction does not modify the state, but nonce
            # will be bumped no matter what.
            _from = _from[2:].decode('hex')
            data = data[2:].decode('hex')
            nonce = self.state.block.get_nonce(_from)
            value = kw.get('value', 0)
            tx = Transaction(nonce, 0, 100000, to, value, data)
            assert _from == tester.a1
            tx.sign(tester.k1)
            block = kw['block']
            assert block == 'pending'
            success, output = apply_transaction(self.state.block, tx)
            assert success
            return '0x' + output.encode('hex')

        def send(tx):
            success, _ = apply_transaction(self.state.block, tx)
            assert success  # What happens in real RPC eth_send?
            return '0x' + tx.hash.encode('hex')

        self.client.call.side_effect = call
        self.client.send.side_effect = send
        self.pp = PaymentProcessor(self.client, self.privkey)
        self.clock = Clock()
        self.pp._loopingCall.clock = self.clock

    def test_initial_eth_balance(self):
        # ethereum.tester assigns this amount to predefined accounts.
        assert self.pp.eth_balance() == 1000000000000000000000000

    def check_synchronized(self):
        assert not self.pp.synchronized()
        self.client.get_peer_count.return_value = 1
        assert not self.pp.synchronized()
        I = PaymentProcessor.SYNC_CHECK_INTERVAL = SYNC_TEST_INTERVAL
        assert self.pp.SYNC_CHECK_INTERVAL == SYNC_TEST_INTERVAL
        time.sleep(1.5 * PaymentProcessor.SYNC_CHECK_INTERVAL)
        assert not self.pp.synchronized()
        time.sleep(1.5 * PaymentProcessor.SYNC_CHECK_INTERVAL)
        assert self.pp.synchronized()
        PaymentProcessor.SYNC_CHECK_INTERVAL = I

    def test_synchronized(self):
        self.pp.start()
        self.check_synchronized()
        self.pp.stop()

    def test_gnt_faucet(self, *_):
        self.pp._PaymentProcessor__faucet = True
        self.pp._run()
        assert self.pp.eth_balance() > 0
        assert self.pp.gnt_balance() == 0
        self.check_synchronized()
        self.state.mine()
        self.clock.advance(60)
        self.pp._run()
        assert self.pp.gnt_balance(True) == 1000 * denoms.ether

    def test_single_payment(self, *_):
        self.pp._run()
        self.gnt.create(sender=self.privkey)
        self.state.mine()
        self.check_synchronized()
        assert self.pp.gnt_balance() == 1000 * denoms.ether

        payee = urandom(20)
        b = self.pp.gnt_balance()
        # FIXME: Big values does not fit into the database
        value = random.randint(0, b / 1000)
        p1 = Payment.create(subtask="p1", payee=payee, value=value)
        assert self.pp._gnt_available() == b
        assert self.pp._gnt_reserved() == 0
        self.pp.add(p1)
        assert self.pp._gnt_available() == b - value
        assert self.pp._gnt_reserved() == value

        # Sendout.
        self.pp.deadline = int(time.time())
        self.pp._run()
        assert self.pp.gnt_balance(True) == b - value
        assert self.pp._gnt_available() == b - value
        assert self.pp._gnt_reserved() == 0

        assert self.gnt.balanceOf(payee) == value
        assert self.gnt.balanceOf(tester.a1) == self.pp._gnt_available()

        # Confirm.
        assert self.pp.gnt_balance(True) == b - value
        assert self.pp._gnt_reserved() == 0

    def test_get_ether(self, *_):
        def exception(*_):
            raise Exception

        def failure(*_):
            return False

        def success(*_):
            return True

        self.pp.monitor_progress = Mock()
        self.pp.synchronized = lambda *_: True
        self.pp.sendout = Mock()

        self.pp.get_gnt_from_faucet = failure
        self.pp.get_ether_from_faucet = failure

        self.pp._run()
        assert not self.pp._waiting_for_faucet
        assert not self.pp.monitor_progress.called
        assert not self.pp.sendout.called

        self.pp.get_ether_from_faucet = success

        self.pp._run()
        assert not self.pp._waiting_for_faucet
        assert not self.pp.monitor_progress.called
        assert not self.pp.sendout.called

        self.pp.get_gnt_from_faucet = success

        self.pp._run()
        assert not self.pp._waiting_for_faucet
        assert self.pp.monitor_progress.called
        assert self.pp.sendout.called

    def test_no_gnt_available(self):
        self.pp.start()
        self.gnt.create(sender=self.privkey)
        self.state.mine()
        self.check_synchronized()
        assert self.pp.gnt_balance() == 1000 * denoms.ether

        payee = urandom(20)
        b = self.pp.gnt_balance()
        value = b / 5 - 100
        for i in range(5):
            subtask_id = 's{}'.format(i)
            p = Payment.create(subtask=subtask_id, payee=payee, value=value)
            assert self.pp.add(p)

        q = Payment.create(subtask='F', payee=payee, value=value)
        assert not self.pp.add(q)
class EthereumTransactionSystem(TransactionSystem):
    """ Transaction system connected with Ethereum """
    def __init__(self, datadir, node_priv_key):
        """ Create new transaction system instance for node with given id
        :param node_priv_key str: node's private key for Ethereum account (32b)
        """
        super(EthereumTransactionSystem, self).__init__()

        # FIXME: Passing private key all around might be a security issue.
        #        Proper account managment is needed.
        if not isinstance(node_priv_key, basestring)\
                or len(node_priv_key) != 32:
            raise ValueError("Invalid private key: {}".format(node_priv_key))
        self.__node_address = keys.privtoaddr(node_priv_key)
        log.info("Node Ethereum address: " + self.get_payment_address())

        self.__eth_node = Client(datadir)
        self.__proc = PaymentProcessor(self.__eth_node,
                                       node_priv_key,
                                       faucet=True)
        self.__proc.start()

    def stop(self):
        if self.__proc.running:
            self.__proc.stop()
        if self.__eth_node.node is not None:
            self.__eth_node.node.stop()

    def add_payment_info(self, *args, **kwargs):
        payment = super(EthereumTransactionSystem,
                        self).add_payment_info(*args, **kwargs)
        self.__proc.add(payment)
        return payment

    def get_payment_address(self):
        """ Human readable Ethereum address for incoming payments."""
        return '0x' + self.__node_address.encode('hex')

    def get_balance(self):
        if not self.__proc.balance_known():
            return None, None, None
        gnt = self.__proc.gnt_balance()
        av_gnt = self.__proc._gnt_available()
        eth = self.__proc.eth_balance()
        return gnt, av_gnt, eth

    def pay_for_task(self, task_id, payments):
        """ Pay for task using Ethereum connector
        :param task_id: pay for task with given id
        :param dict payments: all payments group by ethereum address
        """
        pass

    def sync(self):
        syncing = True
        while syncing:
            try:
                syncing = self.__eth_node.is_syncing()
            except Exception as e:
                log.error("IPC error: {}".format(e))
                syncing = False
            else:
                sleep(0.5)