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 @classmethod
async def create(cls, inputs: Iterable[Input], outputs: Iterable[Output], async def create(cls, inputs: Iterable[Input], outputs: Iterable[Output],
funding_accounts: Iterable['Account'], change_account: 'Account', funding_accounts: Iterable['Account'], change_account: 'Account',
everything: bool = False,
sign: bool = True): sign: bool = True):
""" Find optimal set of inputs when only outputs are provided; add change """ 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. """ 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) 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 # value of the outputs plus associated fees
cost = ( cost = (
tx.get_base_fee(ledger) + tx.get_base_fee(ledger) +
@ -811,6 +825,17 @@ class Transaction:
# value of the inputs less the cost to spend those inputs # value of the inputs less the cost to spend those inputs
payment = tx.get_effective_input_sum(ledger) 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: try:
for _ in range(5): for _ in range(5):

View file

@ -5,6 +5,7 @@ import shutil
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from itertools import cycle from itertools import cycle
import lbry.error
from lbry.testcase import AsyncioTestCase from lbry.testcase import AsyncioTestCase
from lbry.wallet.constants import CENT, COIN, NULL_HASH32 from lbry.wallet.constants import CENT, COIN, NULL_HASH32
from lbry.wallet import Wallet, Account, Ledger, Database, Headers, Transaction, Output, Input from lbry.wallet import Wallet, Account, Ledger, Database, Headers, Transaction, Output, Input
@ -373,8 +374,8 @@ class TransactionIOBalancing(AsyncioTestCase):
def txi(self, txo): def txi(self, txo):
return Input.spend(txo) return Input.spend(txo)
def tx(self, inputs, outputs): def tx(self, inputs, outputs, everything: bool = False):
return Transaction.create(inputs, outputs, [self.account], self.account) return Transaction.create(inputs, outputs, [self.account], self.account, everything=everything)
async def create_utxos(self, amounts): async def create_utxos(self, amounts):
utxos = [self.txo(amount) for amount in amounts] utxos = [self.txo(amount) for amount in amounts]
@ -532,3 +533,106 @@ class TransactionIOBalancing(AsyncioTestCase):
self.assertListEqual([0.01, 1], self.inputs(tx)) self.assertListEqual([0.01, 1], self.inputs(tx))
# change is now needed to consume extra input # change is now needed to consume extra input
self.assertListEqual([0.97], self.outputs(tx)) 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()