Beispiel #1
0
class TransactionIOBalancing(AsyncioTestCase):
    async def asyncSetUp(self):
        self.ledger = Ledger({
            'db': Database(':memory:'),
            'headers': Headers(':memory:')
        })
        await self.ledger.db.open()
        self.account = Account.from_dict(
            self.ledger, Wallet(), {
                "seed":
                "carbon smart garage balance margin twelve chest sword "
                "toast envelope bottom stomach absent"
            })

        addresses = await self.account.ensure_address_gap()
        self.pubkey_hash = [
            self.ledger.address_to_hash160(a) for a in addresses
        ]
        self.hash_cycler = cycle(self.pubkey_hash)

    async def asyncTearDown(self):
        await self.ledger.db.close()

    def txo(self, amount, address=None):
        return get_output(int(amount * COIN), address
                          or next(self.hash_cycler))

    def txi(self, txo):
        return Input.spend(txo)

    def tx(self, inputs, outputs):
        return Transaction.create(inputs, outputs, [self.account],
                                  self.account)

    async def create_utxos(self, amounts):
        utxos = [self.txo(amount) for amount in amounts]

        self.funding_tx = Transaction(is_verified=True) \
            .add_inputs([self.txi(self.txo(sum(amounts)+0.1))]) \
            .add_outputs(utxos)

        await self.ledger.db.insert_transaction(self.funding_tx)

        for utxo in utxos:
            await self.ledger.db.save_transaction_io(
                self.funding_tx,
                self.ledger.hash160_to_address(
                    utxo.script.values['pubkey_hash']),
                utxo.script.values['pubkey_hash'], '')

        return utxos

    @staticmethod
    def inputs(tx):
        return [round(i.amount / COIN, 2) for i in tx.inputs]

    @staticmethod
    def outputs(tx):
        return [round(o.amount / COIN, 2) for o in tx.outputs]

    async def test_basic_use_cases(self):
        self.ledger.fee_per_byte = int(.01 * CENT)

        # available UTXOs for filling missing inputs
        utxos = await self.create_utxos([1, 1, 3, 5, 10])

        # pay 3 coins (3.02 w/ fees)
        tx = await self.tx(
            [],  # inputs
            [self.txo(3)]  # outputs
        )
        # best UTXO match is 5 (as UTXO 3 will be short 0.02 to cover fees)
        self.assertListEqual(self.inputs(tx), [5])
        # a change of 1.98 is added to reach balance
        self.assertListEqual(self.outputs(tx), [3, 1.98])

        await self.ledger.release_outputs(utxos)

        # pay 2.98 coins (3.00 w/ fees)
        tx = await self.tx(
            [],  # inputs
            [self.txo(2.98)]  # outputs
        )
        # best UTXO match is 3 and no change is needed
        self.assertListEqual(self.inputs(tx), [3])
        self.assertListEqual(self.outputs(tx), [2.98])

        await self.ledger.release_outputs(utxos)

        # supplied input and output, but input is not enough to cover output
        tx = await self.tx(
            [self.txi(self.txo(10))],  # inputs
            [self.txo(11)]  # outputs
        )
        # additional input is chosen (UTXO 3)
        self.assertListEqual([10, 3], self.inputs(tx))
        # change is now needed to consume extra input
        self.assertListEqual([11, 1.96], self.outputs(tx))

        await self.ledger.release_outputs(utxos)

        # liquidating a UTXO
        tx = await self.tx(
            [self.txi(self.txo(10))],  # inputs
            []  # outputs
        )
        self.assertListEqual([10], self.inputs(tx))
        # missing change added to consume the amount
        self.assertListEqual([9.98], self.outputs(tx))

        await self.ledger.release_outputs(utxos)

        # liquidating at a loss, requires adding extra inputs
        tx = await self.tx(
            [self.txi(self.txo(0.01))],  # inputs
            []  # outputs
        )
        # UTXO 1 is added to cover some of the fee
        self.assertListEqual([0.01, 1], self.inputs(tx))
        # change is now needed to consume extra input
        self.assertListEqual([0.97], self.outputs(tx))
Beispiel #2
0
class TestQueries(AsyncioTestCase):
    async def asyncSetUp(self):
        self.ledger = Ledger({
            'db': Database(':memory:'),
            'headers': Headers(':memory:')
        })
        self.wallet = Wallet()
        await self.ledger.db.open()

    async def asyncTearDown(self):
        await self.ledger.db.close()

    async def create_account(self, wallet=None):
        account = Account.generate(self.ledger, wallet or self.wallet)
        await account.ensure_address_gap()
        return account

    async def create_tx_from_nothing(self, my_account, height):
        to_address = await my_account.receiving.get_or_create_usable_address()
        to_hash = Ledger.address_to_hash160(to_address)
        tx = Transaction(height=height, is_verified=True) \
            .add_inputs([self.txi(self.txo(1, sha256(str(height).encode())))]) \
            .add_outputs([self.txo(1, to_hash)])
        await self.ledger.db.insert_transaction(tx)
        await self.ledger.db.save_transaction_io(tx, to_address, to_hash, '')
        return tx

    async def create_tx_from_txo(self, txo, to_account, height):
        from_hash = txo.script.values['pubkey_hash']
        from_address = self.ledger.hash160_to_address(from_hash)
        to_address = await to_account.receiving.get_or_create_usable_address()
        to_hash = Ledger.address_to_hash160(to_address)
        tx = Transaction(height=height, is_verified=True) \
            .add_inputs([self.txi(txo)]) \
            .add_outputs([self.txo(1, to_hash)])
        await self.ledger.db.insert_transaction(tx)
        await self.ledger.db.save_transaction_io(tx, from_address, from_hash,
                                                 '')
        await self.ledger.db.save_transaction_io(tx, to_address, to_hash, '')
        return tx

    async def create_tx_to_nowhere(self, txo, height):
        from_hash = txo.script.values['pubkey_hash']
        from_address = self.ledger.hash160_to_address(from_hash)
        to_hash = NULL_HASH
        tx = Transaction(height=height, is_verified=True) \
            .add_inputs([self.txi(txo)]) \
            .add_outputs([self.txo(1, to_hash)])
        await self.ledger.db.insert_transaction(tx)
        await self.ledger.db.save_transaction_io(tx, from_address, from_hash,
                                                 '')
        return tx

    def txo(self, amount, address):
        return get_output(int(amount * COIN), address)

    def txi(self, txo):
        return Input.spend(txo)

    async def test_large_tx_doesnt_hit_variable_limits(self):
        # SQLite is usually compiled with 999 variables limit: https://www.sqlite.org/limits.html
        # This can be removed when there is a better way. See: https://github.com/lbryio/lbry-sdk/issues/2281
        fetchall = self.ledger.db.db.execute_fetchall

        def check_parameters_length(sql, parameters):
            self.assertLess(len(parameters or []), 999)
            return fetchall(sql, parameters)

        self.ledger.db.db.execute_fetchall = check_parameters_length
        account = await self.create_account()
        tx = await self.create_tx_from_nothing(account, 0)
        for height in range(1, 1200):
            tx = await self.create_tx_from_txo(tx.outputs[0],
                                               account,
                                               height=height)
        variable_limit = self.ledger.db.MAX_QUERY_VARIABLES
        for limit in range(variable_limit - 2, variable_limit + 2):
            txs = await self.ledger.get_transactions(
                accounts=self.wallet.accounts,
                limit=limit,
                order_by='height asc')
            self.assertEqual(len(txs), limit)
            inputs, outputs, last_tx = set(), set(), txs[0]
            for tx in txs[1:]:
                self.assertEqual(len(tx.inputs), 1)
                self.assertEqual(tx.inputs[0].txo_ref.tx_ref.id, last_tx.id)
                self.assertEqual(len(tx.outputs), 1)
                last_tx = tx

    async def test_queries(self):
        wallet1 = Wallet()
        account1 = await self.create_account(wallet1)
        self.assertEqual(
            26, await self.ledger.db.get_address_count(accounts=[account1]))
        wallet2 = Wallet()
        account2 = await self.create_account(wallet2)
        account3 = await self.create_account(wallet2)
        self.assertEqual(
            26, await self.ledger.db.get_address_count(accounts=[account2]))

        self.assertEqual(
            0, await self.ledger.db.get_transaction_count(
                accounts=[account1, account2, account3]))
        self.assertEqual(0, await self.ledger.db.get_utxo_count())
        self.assertListEqual([], await self.ledger.db.get_utxos())
        self.assertEqual(0, await self.ledger.db.get_txo_count())
        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet1))
        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet2))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account1]))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account2]))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account3]))

        tx1 = await self.create_tx_from_nothing(account1, 1)
        self.assertEqual(
            1, await self.ledger.db.get_transaction_count(accounts=[account1]))
        self.assertEqual(
            0, await self.ledger.db.get_transaction_count(accounts=[account2]))
        self.assertEqual(
            1, await self.ledger.db.get_utxo_count(accounts=[account1]))
        self.assertEqual(
            1, await self.ledger.db.get_txo_count(accounts=[account1]))
        self.assertEqual(
            0, await self.ledger.db.get_txo_count(accounts=[account2]))
        self.assertEqual(10**8, await
                         self.ledger.db.get_balance(wallet=wallet1))
        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet2))
        self.assertEqual(10**8, await
                         self.ledger.db.get_balance(accounts=[account1]))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account2]))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account3]))

        tx2 = await self.create_tx_from_txo(tx1.outputs[0], account2, 2)
        tx2b = await self.create_tx_from_nothing(account3, 2)
        self.assertEqual(
            2, await self.ledger.db.get_transaction_count(accounts=[account1]))
        self.assertEqual(
            1, await self.ledger.db.get_transaction_count(accounts=[account2]))
        self.assertEqual(
            1, await self.ledger.db.get_transaction_count(accounts=[account3]))
        self.assertEqual(
            0, await self.ledger.db.get_utxo_count(accounts=[account1]))
        self.assertEqual(
            1, await self.ledger.db.get_txo_count(accounts=[account1]))
        self.assertEqual(
            1, await self.ledger.db.get_utxo_count(accounts=[account2]))
        self.assertEqual(
            1, await self.ledger.db.get_txo_count(accounts=[account2]))
        self.assertEqual(
            1, await self.ledger.db.get_utxo_count(accounts=[account3]))
        self.assertEqual(
            1, await self.ledger.db.get_txo_count(accounts=[account3]))
        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet1))
        self.assertEqual(10**8 + 10**8, await
                         self.ledger.db.get_balance(wallet=wallet2))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account1]))
        self.assertEqual(10**8, await
                         self.ledger.db.get_balance(accounts=[account2]))
        self.assertEqual(10**8, await
                         self.ledger.db.get_balance(accounts=[account3]))

        tx3 = await self.create_tx_to_nowhere(tx2.outputs[0], 3)
        self.assertEqual(
            2, await self.ledger.db.get_transaction_count(accounts=[account1]))
        self.assertEqual(
            2, await self.ledger.db.get_transaction_count(accounts=[account2]))
        self.assertEqual(
            0, await self.ledger.db.get_utxo_count(accounts=[account1]))
        self.assertEqual(
            1, await self.ledger.db.get_txo_count(accounts=[account1]))
        self.assertEqual(
            0, await self.ledger.db.get_utxo_count(accounts=[account2]))
        self.assertEqual(
            1, await self.ledger.db.get_txo_count(accounts=[account2]))
        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet1))
        self.assertEqual(10**8, await
                         self.ledger.db.get_balance(wallet=wallet2))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account1]))
        self.assertEqual(0, await
                         self.ledger.db.get_balance(accounts=[account2]))
        self.assertEqual(10**8, await
                         self.ledger.db.get_balance(accounts=[account3]))

        txs = await self.ledger.db.get_transactions(
            accounts=[account1, account2])
        self.assertListEqual([tx3.id, tx2.id, tx1.id], [tx.id for tx in txs])
        self.assertListEqual([3, 2, 1], [tx.height for tx in txs])

        txs = await self.ledger.db.get_transactions(wallet=wallet1,
                                                    accounts=wallet1.accounts)
        self.assertListEqual([tx2.id, tx1.id], [tx.id for tx in txs])
        self.assertEqual(txs[0].inputs[0].is_my_account, True)
        self.assertEqual(txs[0].outputs[0].is_my_account, False)
        self.assertEqual(txs[1].inputs[0].is_my_account, False)
        self.assertEqual(txs[1].outputs[0].is_my_account, True)

        txs = await self.ledger.db.get_transactions(wallet=wallet2,
                                                    accounts=[account2])
        self.assertListEqual([tx3.id, tx2.id], [tx.id for tx in txs])
        self.assertEqual(txs[0].inputs[0].is_my_account, True)
        self.assertEqual(txs[0].outputs[0].is_my_account, False)
        self.assertEqual(txs[1].inputs[0].is_my_account, False)
        self.assertEqual(txs[1].outputs[0].is_my_account, True)
        self.assertEqual(
            2, await self.ledger.db.get_transaction_count(accounts=[account2]))

        tx = await self.ledger.db.get_transaction(txid=tx2.id)
        self.assertEqual(tx.id, tx2.id)
        self.assertFalse(tx.inputs[0].is_my_account)
        self.assertFalse(tx.outputs[0].is_my_account)
        tx = await self.ledger.db.get_transaction(wallet=wallet1, txid=tx2.id)
        self.assertTrue(tx.inputs[0].is_my_account)
        self.assertFalse(tx.outputs[0].is_my_account)
        tx = await self.ledger.db.get_transaction(wallet=wallet2, txid=tx2.id)
        self.assertFalse(tx.inputs[0].is_my_account)
        self.assertTrue(tx.outputs[0].is_my_account)

        # height 0 sorted to the top with the rest in descending order
        tx4 = await self.create_tx_from_nothing(account1, 0)
        txos = await self.ledger.db.get_txos()
        self.assertListEqual([0, 2, 2, 1], [txo.tx_ref.height for txo in txos])
        self.assertListEqual([tx4.id, tx2.id, tx2b.id, tx1.id],
                             [txo.tx_ref.id for txo in txos])
        txs = await self.ledger.db.get_transactions(
            accounts=[account1, account2])
        self.assertListEqual([0, 3, 2, 1], [tx.height for tx in txs])
        self.assertListEqual([tx4.id, tx3.id, tx2.id, tx1.id],
                             [tx.id for tx in txs])

    async def test_empty_history(self):
        self.assertEqual((None, []), await
                         self.ledger.get_local_status_and_history(''))