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))
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(''))