Add an "everything" option to Transaction.create() and unit test.

This commit is contained in:
Jonathan Moody 2022-04-21 16:40:34 -04:00
parent 8becf1f69f
commit 9cf28f0db5
2 changed files with 131 additions and 2 deletions

View file

@ -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):

View file

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