Add an "everything" option to Transaction.create() and unit test.
This commit is contained in:
parent
8becf1f69f
commit
9cf28f0db5
2 changed files with 131 additions and 2 deletions
|
@ -793,6 +793,7 @@ class Transaction:
|
|||
@classmethod
|
||||
async def create(cls, inputs: Iterable[Input], outputs: Iterable[Output],
|
||||
funding_accounts: Iterable['Account'], change_account: 'Account',
|
||||
everything: bool = False,
|
||||
sign: bool = True):
|
||||
""" Find optimal set of inputs when only outputs are provided; add change
|
||||
outputs if only inputs are provided or if inputs are greater than outputs. """
|
||||
|
@ -803,6 +804,19 @@ class Transaction:
|
|||
|
||||
ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)
|
||||
|
||||
if everything and not len(tx._inputs):
|
||||
# Spend "everything" requested, but inputs not specified.
|
||||
# Make a set of inputs from all funding accounts.
|
||||
all_utxos = []
|
||||
for a in funding_accounts:
|
||||
utxos = await a.get_utxos()
|
||||
await a.ledger.reserve_outputs(utxos)
|
||||
all_utxos.extend(utxos)
|
||||
if not len(all_utxos):
|
||||
raise InsufficientFundsError()
|
||||
everything_in = [Input.spend(txo) for txo in all_utxos]
|
||||
tx.add_inputs(everything_in)
|
||||
|
||||
# value of the outputs plus associated fees
|
||||
cost = (
|
||||
tx.get_base_fee(ledger) +
|
||||
|
@ -811,6 +825,17 @@ class Transaction:
|
|||
# value of the inputs less the cost to spend those inputs
|
||||
payment = tx.get_effective_input_sum(ledger)
|
||||
|
||||
if everything and len(tx._outputs) and payment > cost:
|
||||
# Distribute the surplus across the known set of outputs.
|
||||
amount = (payment - cost) // len(tx._outputs)
|
||||
for txo in tx._outputs:
|
||||
txo.amount += amount
|
||||
# Recompute: value of the outputs plus associated fees
|
||||
cost = (
|
||||
tx.get_base_fee(ledger) +
|
||||
tx.get_total_output_sum(ledger)
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
for _ in range(5):
|
||||
|
|
|
@ -5,6 +5,7 @@ import shutil
|
|||
from binascii import hexlify, unhexlify
|
||||
from itertools import cycle
|
||||
|
||||
import lbry.error
|
||||
from lbry.testcase import AsyncioTestCase
|
||||
from lbry.wallet.constants import CENT, COIN, NULL_HASH32
|
||||
from lbry.wallet import Wallet, Account, Ledger, Database, Headers, Transaction, Output, Input
|
||||
|
@ -373,8 +374,8 @@ class TransactionIOBalancing(AsyncioTestCase):
|
|||
def txi(self, txo):
|
||||
return Input.spend(txo)
|
||||
|
||||
def tx(self, inputs, outputs):
|
||||
return Transaction.create(inputs, outputs, [self.account], self.account)
|
||||
def tx(self, inputs, outputs, everything: bool = False):
|
||||
return Transaction.create(inputs, outputs, [self.account], self.account, everything=everything)
|
||||
|
||||
async def create_utxos(self, amounts):
|
||||
utxos = [self.txo(amount) for amount in amounts]
|
||||
|
@ -532,3 +533,106 @@ class TransactionIOBalancing(AsyncioTestCase):
|
|||
self.assertListEqual([0.01, 1], self.inputs(tx))
|
||||
# change is now needed to consume extra input
|
||||
self.assertListEqual([0.97], self.outputs(tx))
|
||||
|
||||
async def _test_send_everything_use_cases(self):
|
||||
self.ledger.fee_per_byte = int(.01*CENT)
|
||||
|
||||
# available UTXOs for filling missing inputs
|
||||
avail = [
|
||||
1, 1, 3, 5, 10
|
||||
]
|
||||
utxos = await self.create_utxos(avail)
|
||||
#total = sum(avail)
|
||||
|
||||
# everything: outputs populated via change_account
|
||||
tx = await self.tx(
|
||||
[], # inputs
|
||||
[], # outputs
|
||||
everything=True
|
||||
)
|
||||
self.assertListEqual(self.inputs(tx), avail)
|
||||
self.assertListEqual(self.outputs(tx), [19.92])
|
||||
|
||||
await self.ledger.release_outputs(utxos)
|
||||
|
||||
# everything: one output with initial amount (0.0) bumped
|
||||
tx = await self.tx(
|
||||
[], # inputs
|
||||
[self.txo(0.0)], # outputs
|
||||
everything=True
|
||||
)
|
||||
self.assertListEqual(self.inputs(tx), avail)
|
||||
self.assertListEqual(self.outputs(tx), [19.92])
|
||||
|
||||
await self.ledger.release_outputs(utxos)
|
||||
|
||||
# everything: two outputs with initial amounts bumped
|
||||
tx = await self.tx(
|
||||
[], # inputs
|
||||
[self.txo(1.0), self.txo(4.0)], # outputs
|
||||
everything=True
|
||||
)
|
||||
self.assertListEqual(self.inputs(tx), avail)
|
||||
self.assertListEqual(self.outputs(tx), [8.46, 11.46])
|
||||
|
||||
await self.ledger.release_outputs(utxos)
|
||||
|
||||
# everything: some inputs provided
|
||||
if self.ledger.coin_selection_strategy == 'sqlite':
|
||||
# NOTE: With this strategy, get_spendable_utxos() grabs extra
|
||||
# utxos with the plan that any excess change can be added to
|
||||
# the outputs. Hence the given initial output must be less
|
||||
# than the absolute maximum (19.92).
|
||||
await self.ledger.reserve_outputs(utxos[2:]);
|
||||
tx = await self.tx(
|
||||
[self.txi(self.txo(a)) for a in avail[2:]], # inputs
|
||||
[self.txo(19.0)], # outputs
|
||||
everything=True
|
||||
)
|
||||
self.assertListEqual(self.inputs(tx), avail[2:] + avail[0:2])
|
||||
# NOTE: The maximum amount (19.92) was transferred. But it
|
||||
# was broken into two outputs (19.0 and 0.92).
|
||||
self.assertListEqual(self.outputs(tx), [19.0, 0.92])
|
||||
else:
|
||||
await self.ledger.reserve_outputs(utxos[2:]);
|
||||
tx = await self.tx(
|
||||
[self.txi(self.txo(a)) for a in avail[2:]], # inputs
|
||||
[self.txo(19.92)], # outputs
|
||||
everything=True
|
||||
)
|
||||
self.assertListEqual(self.inputs(tx), avail[2:] + avail[0:2])
|
||||
self.assertListEqual(self.outputs(tx), [19.92])
|
||||
|
||||
await self.ledger.release_outputs(utxos)
|
||||
|
||||
# everything: maximum output already present
|
||||
tx = await self.tx(
|
||||
[], # inputs
|
||||
[self.txo(19.92)], # outputs
|
||||
everything=True
|
||||
)
|
||||
self.assertListEqual(self.inputs(tx), avail)
|
||||
self.assertListEqual(self.outputs(tx), [19.92])
|
||||
|
||||
await self.ledger.release_outputs(utxos)
|
||||
|
||||
# everything: insufficient funds
|
||||
try:
|
||||
tx = await self.tx(
|
||||
[], # inputs
|
||||
[self.txo(19.93)], # outputs
|
||||
everything=True
|
||||
)
|
||||
except lbry.error.InsufficientFundsError:
|
||||
pass
|
||||
else:
|
||||
self.fail("expected InsufficientFunds exception")
|
||||
|
||||
await self.ledger.release_outputs(utxos)
|
||||
|
||||
async def test_send_everything_use_cases(self):
|
||||
await self._test_send_everything_use_cases()
|
||||
|
||||
async def test_send_everything_use_cases_sqlite(self):
|
||||
self.ledger.coin_selection_strategy = 'sqlite'
|
||||
await self._test_send_everything_use_cases()
|
||||
|
|
Loading…
Add table
Reference in a new issue