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
|
@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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Reference in a new issue