From 04c7bdf63925a77f89a1add9fb7558364090952b Mon Sep 17 00:00:00 2001 From: Jonathan Moody <103143855+moodyjon@users.noreply.github.com> Date: Wed, 8 Jun 2022 11:49:41 -0400 Subject: [PATCH] Convert --amount, --amount_everything to generic amount (int or str) in daemon.py. Unpack the generic amount in transaction.py and account.py. Eliminate separate "everything" keyword args. --- lbry/extras/daemon/daemon.py | 97 +++++++++++---------------- lbry/wallet/account.py | 10 +-- lbry/wallet/dewies.py | 18 +++++ lbry/wallet/transaction.py | 65 +++++++++++------- tests/unit/wallet/test_transaction.py | 4 +- 5 files changed, 106 insertions(+), 88 deletions(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 87362f5719..9dfeafed85 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -11,7 +11,7 @@ import tracemalloc from decimal import Decimal from urllib.parse import urlencode, quote -from typing import Callable, Optional, List +from typing import Callable, Optional, List, Union from binascii import hexlify, unhexlify from traceback import format_exc from functools import wraps, partial @@ -25,7 +25,7 @@ Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic, Transaction, Output, Input, Account, database ) -from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc +from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc, AMOUNT_EVERYTHING from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES from lbry.wallet.bip32 import PrivateKey from lbry.crypto.base58 import Base58 @@ -1569,34 +1569,17 @@ async def jsonrpc_wallet_send( account = wallet.get_account_or_default(change_account_id) accounts = wallet.get_accounts_or_all(funding_account_ids) - amount = self.get_dewies_or_error('amount', amount, everything=amount_everything) + amount = self.get_amount_or_error('amount', amount, everything=amount_everything) if addresses is None: raise InputValueIsNoneError('addresses') if addresses and not isinstance(addresses, list): addresses = [addresses] - outputs = [] for address in addresses: self.valid_address_or_error(address, allow_script_address=True) - if self.ledger.is_pubkey_address(address): - outputs.append( - Output.pay_pubkey_hash( - amount, self.ledger.address_to_hash160(address) - ) - ) - elif self.ledger.is_script_address(address): - outputs.append( - Output.pay_script_hash( - amount, self.ledger.address_to_hash160(address) - ) - ) - else: - raise ValueError(f"Unsupported address: '{address}'") # TODO: use error from lbry.error - tx = await Transaction.create( - [], outputs, accounts, account, everything=amount_everything - ) + tx = await Transaction.pay(amount, addresses, accounts, account) if not preview: await self.broadcast_or_release(tx, blocking) self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent()) @@ -1868,7 +1851,7 @@ def jsonrpc_account_fund(self, to_account=None, from_account=None, amount=None, wallet = self.wallet_manager.get_wallet_or_default(wallet_id) to_account = wallet.get_account_or_default(to_account) from_account = wallet.get_account_or_default(from_account) - amount = self.get_dewies_or_error('amount', amount, everything=everything, + amount = self.get_amount_or_error('amount', amount, everything=everything, default_value=0, argument_everything='everything') if not isinstance(outputs, int): # TODO: use error from lbry.error @@ -1877,8 +1860,7 @@ def jsonrpc_account_fund(self, to_account=None, from_account=None, amount=None, # TODO: use error from lbry.error raise ValueError("Using --everything along with --outputs is not supported.") return from_account.fund( - to_account=to_account, amount=amount, everything=everything, - outputs=outputs, broadcast=broadcast + to_account=to_account, amount=amount, outputs=outputs, broadcast=broadcast ) @requires("wallet") @@ -2770,7 +2752,7 @@ async def jsonrpc_channel_create( account = wallet.get_account_or_default(account_id) funding_accounts = wallet.get_accounts_or_all(funding_account_ids) self.valid_channel_name_or_error(name) - amount = self.get_dewies_or_error('bid', bid, positive_value=True, everything=bid_everything) + amount = self.get_amount_or_error('bid', bid, positive_value=True, everything=bid_everything) claim_address = await self.get_receiving_address(claim_address, account) existing_channels = await self.ledger.get_channels(accounts=wallet.accounts, claim_name=name) @@ -2785,8 +2767,7 @@ async def jsonrpc_channel_create( claim = Claim() claim.channel.update(**kwargs) tx = await Transaction.claim_create( - name, claim, amount, claim_address, funding_accounts, funding_accounts[0], - everything=bid_everything + name, claim, amount, claim_address, funding_accounts, funding_accounts[0] ) txo = tx.outputs[0] txo.set_channel_private_key( @@ -2925,7 +2906,7 @@ async def jsonrpc_channel_update( f"A claim with id '{claim_id}' was found but it is not a channel." ) - amount = self.get_dewies_or_error('bid', bid, positive_value=True, everything=bid_everything, + amount = self.get_amount_or_error('bid', bid, positive_value=True, everything=bid_everything, default_value=old_txo.amount) if claim_address is not None: @@ -2940,8 +2921,7 @@ async def jsonrpc_channel_update( claim = Claim.from_bytes(old_txo.claim.to_bytes()) claim.channel.update(**kwargs) tx = await Transaction.claim_update( - old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], - everything=bid_everything + old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0] ) new_txo = tx.outputs[0] @@ -3364,7 +3344,7 @@ async def jsonrpc_stream_repost(self, name, bid=None, claim_id=None, allow_dupli account = wallet.get_account_or_default(account_id) funding_accounts = wallet.get_accounts_or_all(funding_account_ids) channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True) - amount = self.get_dewies_or_error('bid', bid, positive_value=True, everything=bid_everything) + amount = self.get_amount_or_error('bid', bid, positive_value=True, everything=bid_everything) claim_address = await self.get_receiving_address(claim_address, account) claims = await account.get_claims(claim_name=name) if len(claims) > 0: @@ -3383,8 +3363,7 @@ async def jsonrpc_stream_repost(self, name, bid=None, claim_id=None, allow_dupli claim = Claim() claim.repost.reference.claim_id = claim_id tx = await Transaction.claim_create( - name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel, - everything=bid_everything + name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel ) new_txo = tx.outputs[0] @@ -3519,7 +3498,7 @@ async def jsonrpc_stream_create( account = wallet.get_account_or_default(account_id) funding_accounts = wallet.get_accounts_or_all(funding_account_ids) channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True) - amount = self.get_dewies_or_error('bid', bid, positive_value=True, everything=bid_everything) + amount = self.get_amount_or_error('bid', bid, positive_value=True, everything=bid_everything) claim_address = await self.get_receiving_address(claim_address, account) kwargs['fee_address'] = self.get_fee_address(kwargs, claim_address) @@ -3544,8 +3523,7 @@ async def jsonrpc_stream_create( else: claim.stream.update(**kwargs) tx = await Transaction.claim_create( - name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel, - everything=bid_everything + name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel ) new_txo = tx.outputs[0] @@ -3729,7 +3707,7 @@ async def jsonrpc_stream_update( f"A claim with id '{claim_id}' was found but it is not a stream or repost claim." ) - amount = self.get_dewies_or_error('bid', bid, positive_value=True, everything=bid_everything, + amount = self.get_amount_or_error('bid', bid, positive_value=True, everything=bid_everything, default_value=old_txo.amount) if claim_address is not None: @@ -3775,8 +3753,7 @@ async def jsonrpc_stream_update( claim.clear_signature() tx = await Transaction.claim_update( old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], - channel if not clear_channel else None, - everything=bid_everything + channel if not clear_channel else None ) new_txo = tx.outputs[0] @@ -4020,7 +3997,7 @@ async def jsonrpc_collection_create( funding_accounts = wallet.get_accounts_or_all(funding_account_ids) self.valid_collection_name_or_error(name) channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True) - amount = self.get_dewies_or_error('bid', bid, positive_value=True, everything=bid_everything) + amount = self.get_amount_or_error('bid', bid, positive_value=True, everything=bid_everything) claim_address = await self.get_receiving_address(claim_address, account) @@ -4038,8 +4015,7 @@ async def jsonrpc_collection_create( claim = Claim() claim.collection.update(claims=claims, **kwargs) tx = await Transaction.claim_create( - name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel, - everything=bid_everything + name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel ) new_txo = tx.outputs[0] @@ -4168,7 +4144,7 @@ async def jsonrpc_collection_update( f"A claim with id '{claim_id}' was found but it is not a collection." ) - amount = self.get_dewies_or_error('bid', bid, positive_value=True, everything=bid_everything, + amount = self.get_amount_or_error('bid', bid, positive_value=True, everything=bid_everything, default_value=old_txo.amount) if claim_address is not None: @@ -4190,8 +4166,7 @@ async def jsonrpc_collection_update( claim = Claim.from_bytes(old_txo.claim.to_bytes()) claim.collection.update(**kwargs) tx = await Transaction.claim_update( - old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel, - everything=bid_everything + old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel ) new_txo = tx.outputs[0] @@ -4354,7 +4329,7 @@ async def jsonrpc_support_create( assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first." funding_accounts = wallet.get_accounts_or_all(funding_account_ids) channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True) - amount = self.get_dewies_or_error('amount', amount, everything=amount_everything) + amount = self.get_amount_or_error('amount', amount, everything=amount_everything) claim = await self.ledger.get_claim_by_claim_id(claim_id) claim_address = claim.get_address(self.ledger) @@ -4364,7 +4339,7 @@ async def jsonrpc_support_create( tx = await Transaction.support( claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0], channel, - comment=comment, everything=amount_everything + comment=comment ) new_txo = tx.outputs[0] @@ -5485,25 +5460,29 @@ async def get_channel_or_error( raise ValueError(f"Couldn't find channel with channel_{key} '{value}'.") @staticmethod - def get_dewies_or_error(argument: str, lbc: Optional[str], + def get_dewies_or_error(argument: str, lbc: str, positive_value: bool = False) -> int: + try: + dewies = lbc_to_dewies(lbc) + if positive_value and dewies <= 0: + # TODO: use error from lbry.error + raise ValueError(f"'{argument}' value must be greater than 0.0") + return dewies + except ValueError as e: + # TODO: use error from lbry.error + raise ValueError(f"Invalid value for '{argument}': {e.args[0]}") + + @staticmethod + def get_amount_or_error(argument: str, lbc: Optional[str], positive_value: bool = False, everything: bool = False, default_value: Optional[int] = None, - argument_everything: Optional[str] = None) -> int: + argument_everything: Optional[str] = None) -> Union[int, str]: if everything: if lbc is not None: argument_everything = argument_everything or argument + '_everything' raise ConflictingInputValueError(argument, argument_everything) - return 0 + return AMOUNT_EVERYTHING elif lbc is not None: - try: - dewies = lbc_to_dewies(lbc) - if positive_value and dewies <= 0: - # TODO: use error from lbry.error - raise ValueError(f"'{argument}' value must be greater than 0.0") - return dewies - except ValueError as e: - # TODO: use error from lbry.error - raise ValueError(f"Invalid value for '{argument}': {e.args[0]}") + return Daemon.get_dewies_or_error(argument, lbc, positive_value=positive_value) elif default_value is not None: return default_value else: diff --git a/lbry/wallet/account.py b/lbry/wallet/account.py index c939d7ee91..490c91ef06 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -7,10 +7,11 @@ import random from hashlib import sha256 from string import hexdigits -from typing import Type, Dict, Tuple, Optional, Any, List +from typing import Type, Dict, Tuple, Optional, Any, List, Union from lbry.error import InvalidPasswordError from lbry.crypto.crypt import aes_encrypt, aes_decrypt +from lbry.wallet.dewies import amount_to_dewies from .bip32 import PrivateKey, PublicKey, KeyPath, from_extended_key_string from .mnemonic import Mnemonic @@ -526,9 +527,10 @@ def get_transactions(self, **constraints): def get_transaction_count(self, **constraints): return self.ledger.get_transaction_count(wallet=self.wallet, accounts=[self], **constraints) - async def fund(self, to_account, amount=None, everything=False, + async def fund(self, to_account, amount: Union[int, str], outputs=1, broadcast=False, **constraints): assert self.ledger == to_account.ledger, 'Can only transfer between accounts of the same ledger.' + dewies, everything = amount_to_dewies(amount) if everything: utxos = await self.get_utxos(**constraints) await self.ledger.reserve_outputs(utxos) @@ -538,13 +540,13 @@ async def fund(self, to_account, amount=None, everything=False, funding_accounts=[self], change_account=to_account ) - elif amount > 0: + elif dewies > 0: to_address = await to_account.change.get_or_create_usable_address() to_hash160 = to_account.ledger.address_to_hash160(to_address) tx = await Transaction.create( inputs=[], outputs=[ - Output.pay_pubkey_hash(amount//outputs, to_hash160) + Output.pay_pubkey_hash(dewies//outputs, to_hash160) for _ in range(outputs) ], funding_accounts=[self], diff --git a/lbry/wallet/dewies.py b/lbry/wallet/dewies.py index 8244712b5d..0861713da6 100644 --- a/lbry/wallet/dewies.py +++ b/lbry/wallet/dewies.py @@ -1,6 +1,24 @@ import textwrap +from typing import Tuple, Union from .util import coins_to_satoshis, satoshis_to_coins +# Symbolic amount EVERYTHING +AMOUNT_EVERYTHING = "EVERYTHING" + +def amount_is_everything(amount: Union[int, str]) -> bool: + if isinstance(amount, str): + if amount != AMOUNT_EVERYTHING: + raise ValueError(f"The value '{amount}' for argument 'amount' is invalid.") + return True + elif isinstance(amount, int): + return False + else: + raise ValueError(f"The value '{amount}' for argument 'amount' is invalid.") + +def amount_to_dewies(amount: Union[int, str]) -> Tuple[int, bool]: + everything = amount_is_everything(amount) + dewies = 0 if everything else amount + return dewies, everything def lbc_to_dewies(lbc: str) -> int: try: diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index 308ed66094..6b78ce3af4 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -2,7 +2,7 @@ import logging import typing from binascii import hexlify, unhexlify -from typing import List, Iterable, Optional, Tuple +from typing import List, Iterable, Optional, Tuple, Union from lbry.error import InsufficientFundsError from lbry.crypto.hash import hash160, sha256 @@ -12,6 +12,7 @@ from lbry.schema.base import Signable from lbry.schema.purchase import Purchase from lbry.schema.support import Support +from lbry.wallet.dewies import amount_to_dewies from .script import InputScript, OutputScript from .constants import COIN, DUST, NULL_HASH32 @@ -793,8 +794,8 @@ def ensure_all_have_same_ledger_and_wallet( @classmethod async def create(cls, inputs: Iterable[Input], outputs: Iterable[Output], funding_accounts: Iterable['Account'], change_account: 'Account', - everything: bool = False, - sign: bool = True): + sign: bool = True, + *, everything: bool = False): """ 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. """ @@ -915,34 +916,50 @@ async def sign(self, funding_accounts: Iterable['Account'], extra_keys: dict = N self._reset() @classmethod - def pay(cls, amount: int, address: bytes, funding_accounts: List['Account'], change_account: 'Account', - everything: bool = False): + def pay(cls, amount: Union[int, str], addresses: List[bytes], + funding_accounts: List['Account'], change_account: 'Account'): ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) - output = Output.pay_pubkey_hash(amount, ledger.address_to_hash160(address)) - return cls.create([], [output], funding_accounts, change_account, everything=everything) + dewies, everything = amount_to_dewies(amount) + outputs = [] + for address in addresses: + if ledger.is_pubkey_address(address): + outputs.append( + Output.pay_pubkey_hash( + dewies, ledger.address_to_hash160(address) + ) + ) + elif ledger.is_script_address(address): + outputs.append( + Output.pay_script_hash( + dewies, ledger.address_to_hash160(address) + ) + ) + else: + raise ValueError(f"Unsupported address: '{address}'") # TODO: use error from lbry.error + return cls.create([], outputs, funding_accounts, change_account, everything=everything) @classmethod def claim_create( - cls, name: str, claim: Claim, amount: int, holding_address: str, - funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None, - everything: bool = False): + cls, name: str, claim: Claim, amount: Union[int, str], holding_address: str, + funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None): ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) + dewies, everything = amount_to_dewies(amount) claim_output = Output.pay_claim_name_pubkey_hash( - amount, name, claim, ledger.address_to_hash160(holding_address) + dewies, name, claim, ledger.address_to_hash160(holding_address) ) if signing_channel is not None: claim_output.sign(signing_channel, b'placeholder txid:nout') return cls.create([], [claim_output], funding_accounts, change_account, - everything=everything, sign=False) + sign=False, everything=everything) @classmethod def claim_update( - cls, previous_claim: Output, claim: Claim, amount: int, holding_address: str, - funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None, - everything: bool = False): + cls, previous_claim: Output, claim: Claim, amount: Union[int, str], holding_address: str, + funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None): ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) + dewies, everything = amount_to_dewies(amount) updated_claim = Output.pay_update_claim_pubkey_hash( - amount, previous_claim.claim_name, previous_claim.claim_id, + dewies, previous_claim.claim_name, previous_claim.claim_id, claim, ledger.address_to_hash160(holding_address) ) if signing_channel is not None: @@ -951,35 +968,37 @@ def claim_update( updated_claim.clear_signature() return cls.create( [Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, - everything=everything, sign=False + sign=False, everything=everything ) @classmethod - def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str, + def support(cls, claim_name: str, claim_id: str, amount: Union[int, str], holding_address: str, funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None, - comment: str = None, everything: bool = False): + comment: str = None): ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) + dewies, everything = amount_to_dewies(amount) if signing_channel is not None or comment is not None: support = Support() if comment is not None: support.comment = comment support_output = Output.pay_support_data_pubkey_hash( - amount, claim_name, claim_id, support, ledger.address_to_hash160(holding_address) + dewies, claim_name, claim_id, support, ledger.address_to_hash160(holding_address) ) if signing_channel is not None: support_output.sign(signing_channel, b'placeholder txid:nout') else: support_output = Output.pay_support_pubkey_hash( - amount, claim_name, claim_id, ledger.address_to_hash160(holding_address) + dewies, claim_name, claim_id, ledger.address_to_hash160(holding_address) ) return cls.create([], [support_output], funding_accounts, change_account, - everything=everything, sign=False) + sign=False, everything=everything) @classmethod def purchase(cls, claim_id: str, amount: int, merchant_address: bytes, funding_accounts: List['Account'], change_account: 'Account'): ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) - payment = Output.pay_pubkey_hash(amount, ledger.address_to_hash160(merchant_address)) + dewies, _ = amount_to_dewies(amount) + payment = Output.pay_pubkey_hash(dewies, ledger.address_to_hash160(merchant_address)) data = Output.add_purchase_data(Purchase(claim_id)) return cls.create([], [payment, data], funding_accounts, change_account) diff --git a/tests/unit/wallet/test_transaction.py b/tests/unit/wallet/test_transaction.py index e12b0e7566..434fac7b22 100644 --- a/tests/unit/wallet/test_transaction.py +++ b/tests/unit/wallet/test_transaction.py @@ -374,8 +374,8 @@ def txo(self, amount, address=None): def txi(self, txo): return Input.spend(txo) - def tx(self, inputs, outputs, everything: bool = False): - return Transaction.create(inputs, outputs, [self.account], self.account, everything=everything) + def tx(self, inputs, outputs, **kwargs): + return Transaction.create(inputs, outputs, [self.account], self.account, **kwargs) async def create_utxos(self, amounts): utxos = [self.txo(amount) for amount in amounts]