From 48d2497eb2e38d6ee3fa945623ed5df0c8c85f49 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Thu, 26 Mar 2020 22:16:05 -0400 Subject: [PATCH] added txo_spend command to support liquidating large number of txos (eg. tips) --- lbry/extras/daemon/daemon.py | 52 +++++++++++++++++++ lbry/testcase.py | 8 +++ lbry/wallet/ledger.py | 2 +- .../blockchain/test_claim_commands.py | 15 ++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 0411e99a5..ff372f68c 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -4248,6 +4248,58 @@ class Daemon(metaclass=JSONRPCServerType): self._constrain_txo_from_kwargs(constraints, **kwargs) return paginate_rows(claims, None if no_totals else claim_count, page, page_size, **constraints) + @requires(WALLET_COMPONENT) + async def jsonrpc_txo_spend( + self, account_id=None, wallet_id=None, batch_size=1000, + preview=False, blocking=False, **kwargs): + """ + Spend transaction outputs, batching into multiple transactions as necessary. + + Usage: + txo_spend [--account_id=] [--type=...] [--txid=...] + [--claim_id=...] [--channel_id=...] [--name=...] + [--is_my_input | --is_not_my_input] + [--exclude_internal_transfers] [--wallet_id=] + [--batch_size=] + + Options: + --type= : (str or list) claim type: stream, channel, support, + purchase, collection, repost, other + --txid= : (str or list) transaction id of outputs + --claim_id= : (str or list) claim id + --channel_id= : (str or list) claims in this channel + --name= : (str or list) claim name + --is_my_input : (bool) show outputs created by you + --is_not_my_input : (bool) show outputs not created by you + --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination: + "--is_my_input --is_my_output --type=other" + this allows to exclude "change" payments, this + flag can be used in combination with any of the other flags + --account_id= : (str) id of the account to query + --wallet_id= : (str) restrict results to specific wallet + --batch_size= : (int) number of txos to spend per transactions + + Returns: {List[Transaction]} + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + accounts = [wallet.get_account_or_error(account_id)] if account_id else wallet.accounts + txos = await self.ledger.get_txos( + wallet=wallet, accounts=accounts, read_only=True, + **self._constrain_txo_from_kwargs({}, unspent=True, is_my_output=True, **kwargs) + ) + txs = [] + while txos: + txs.append( + await Transaction.create( + [Input.spend(txos.pop()) for _ in range(min(len(txos), batch_size))], + [], accounts, accounts[0] + ) + ) + if not preview: + for tx in txs: + await self.broadcast_or_release(tx, blocking) + return txs + @requires(WALLET_COMPONENT) def jsonrpc_txo_sum(self, account_id=None, wallet_id=None, **kwargs): """ diff --git a/lbry/testcase.py b/lbry/testcase.py index c0f8fa449..dcdaa83e5 100644 --- a/lbry/testcase.py +++ b/lbry/testcase.py @@ -565,6 +565,14 @@ class CommandTestCase(IntegrationTestCase): self.daemon.jsonrpc_wallet_send(*args, **kwargs), confirm ) + async def txo_spend(self, *args, confirm=True, **kwargs): + txs = await self.daemon.jsonrpc_txo_spend(*args, **kwargs) + if confirm: + await asyncio.wait([self.ledger.wait(tx) for tx in txs]) + await self.generate(1) + await asyncio.wait([self.ledger.wait(tx, self.blockchain.block_expected) for tx in txs]) + return self.sout(txs) + async def resolve(self, uri, **kwargs): return (await self.out(self.daemon.jsonrpc_resolve(uri, **kwargs)))[uri] diff --git a/lbry/wallet/ledger.py b/lbry/wallet/ledger.py index 27a0ed992..e5eeac41e 100644 --- a/lbry/wallet/ledger.py +++ b/lbry/wallet/ledger.py @@ -266,7 +266,7 @@ class Ledger(metaclass=LedgerRegistry): self.constraint_spending_utxos(constraints) return self.db.get_utxo_count(**constraints) - async def get_txos(self, resolve=False, **constraints): + async def get_txos(self, resolve=False, **constraints) -> List[Output]: txos = await self.db.get_txos(**constraints) if resolve: return await self._resolve_for_local_results(constraints.get('accounts', []), txos) diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py index 38c00591d..84d17da94 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -610,6 +610,21 @@ class TransactionOutputCommands(ClaimTestCase): {'day': '2016-06-25', 'total': '0.6'}, ], plot) + async def test_txo_spend(self): + stream_id = self.get_claim_id(await self.stream_create()) + for _ in range(10): + await self.support_create(stream_id, '0.1') + await self.assertBalance(self.account, '7.978478') + self.assertEqual('1.0', lbc(await self.txo_sum(type='support', unspent=True))) + txs = await self.txo_spend(type='support', batch_size=3) + self.assertEqual(4, len(txs)) + self.assertEqual(3, len(txs[0]['inputs'])) + self.assertEqual(3, len(txs[1]['inputs'])) + self.assertEqual(3, len(txs[2]['inputs'])) + self.assertEqual(1, len(txs[3]['inputs'])) + self.assertEqual('0.0', lbc(await self.txo_sum(type='support', unspent=True))) + await self.assertBalance(self.account, '8.977606') + class ClaimCommands(ClaimTestCase):