electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 194ee395e70ad44d19732acd69fe8eec2693f968
parent f64062b6f13dfc340fedcdfdf24156ae21414c1c
Author: ThomasV <thomasv@electrum.org>
Date:   Thu,  2 Aug 2018 12:49:51 +0200

Merge pull request #4596 from SomberNight/txoutput_namedtuple

transaction: introduce TxOutput namedtuple
Diffstat:
Melectrum/address_synchronizer.py | 13++++++-------
Melectrum/coinchooser.py | 4++--
Melectrum/commands.py | 6+++---
Melectrum/gui/kivy/main_window.py | 3++-
Melectrum/gui/kivy/uix/dialogs/__init__.py | 6+++---
Melectrum/gui/kivy/uix/screens.py | 3++-
Melectrum/gui/qt/main_window.py | 12++++++------
Melectrum/gui/qt/paytoedit.py | 13+++++++------
Melectrum/gui/stdio.py | 4+++-
Melectrum/gui/text.py | 4+++-
Melectrum/paymentrequest.py | 7++++---
Melectrum/plugins/digitalbitbox/digitalbitbox.py | 6+++---
Melectrum/plugins/hw_wallet/plugin.py | 12++++++------
Melectrum/plugins/keepkey/keepkey.py | 5+++--
Melectrum/plugins/ledger/ledger.py | 10+++++-----
Melectrum/plugins/safe_t/safe_t.py | 5+++--
Melectrum/plugins/trezor/trezor.py | 5+++--
Melectrum/plugins/trustedcoin/trustedcoin.py | 9+++++----
Melectrum/tests/test_wallet_vertical.py | 45+++++++++++++++++++++++----------------------
Melectrum/transaction.py | 26+++++++++++++++-----------
Melectrum/wallet.py | 33+++++++++++++++------------------
21 files changed, 122 insertions(+), 109 deletions(-)

diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py @@ -28,7 +28,7 @@ from collections import defaultdict from . import bitcoin from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus -from .transaction import Transaction +from .transaction import Transaction, TxOutput from .synchronizer import Synchronizer from .verifier import SPV from .blockchain import hash_header @@ -112,12 +112,11 @@ class AddressSynchronizer(PrintError): return addr return None - def get_txout_address(self, txo): - _type, x, v = txo - if _type == TYPE_ADDRESS: - addr = x - elif _type == TYPE_PUBKEY: - addr = bitcoin.public_key_to_p2pkh(bfh(x)) + def get_txout_address(self, txo: TxOutput): + if txo.type == TYPE_ADDRESS: + addr = txo.address + elif txo.type == TYPE_PUBKEY: + addr = bitcoin.public_key_to_p2pkh(bfh(txo.address)) else: addr = None return addr diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py @@ -26,7 +26,7 @@ from collections import defaultdict, namedtuple from math import floor, log10 from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address -from .transaction import Transaction +from .transaction import Transaction, TxOutput from .util import NotEnoughFunds, PrintError @@ -178,7 +178,7 @@ class CoinChooserBase(PrintError): # size of the change output, add it to the transaction. dust = sum(amount for amount in amounts if amount < dust_threshold) amounts = [amount for amount in amounts if amount >= dust_threshold] - change = [(TYPE_ADDRESS, addr, amount) + change = [TxOutput(TYPE_ADDRESS, addr, amount) for addr, amount in zip(change_addrs, amounts)] self.print_error('change:', change) if dust: diff --git a/electrum/commands.py b/electrum/commands.py @@ -38,7 +38,7 @@ from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_enc from . import bitcoin from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .i18n import _ -from .transaction import Transaction, multisig_script +from .transaction import Transaction, multisig_script, TxOutput from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .plugin import run_hook @@ -226,7 +226,7 @@ class Commands: txin['signatures'] = [None] txin['num_sig'] = 1 - outputs = [(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs] + outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs] tx = Transaction.from_io(inputs, outputs, locktime=locktime) tx.sign(keypairs) return tx.as_dict() @@ -415,7 +415,7 @@ class Commands: for address, amount in outputs: address = self._resolver(address) amount = satoshis(amount) - final_outputs.append((TYPE_ADDRESS, address, amount)) + final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount)) coins = self.wallet.get_spendable_coins(domain, self.config) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -713,13 +713,14 @@ class ElectrumWindow(App): self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy def get_max_amount(self): + from electrum.transaction import TxOutput if run_hook('abort_send', self): return '' inputs = self.wallet.get_spendable_coins(None, self.electrum_config) if not inputs: return '' addr = str(self.send_screen.screen.address) or self.wallet.dummy_address() - outputs = [(TYPE_ADDRESS, addr, '!')] + outputs = [TxOutput(TYPE_ADDRESS, addr, '!')] try: tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) except NoDynamicFeeEstimates as e: diff --git a/electrum/gui/kivy/uix/dialogs/__init__.py b/electrum/gui/kivy/uix/dialogs/__init__.py @@ -206,9 +206,9 @@ class OutputList(RecycleView): def update(self, outputs): res = [] - for (type, address, amount) in outputs: - value = self.app.format_amount_and_units(amount) - res.append({'address': address, 'value': value}) + for o in outputs: + value = self.app.format_amount_and_units(o.value) + res.append({'address': o.address, 'value': value}) self.data = res diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py @@ -20,6 +20,7 @@ from kivy.utils import platform from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat from electrum import bitcoin +from electrum.transaction import TxOutput from electrum.util import timestamp_to_datetime from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum.plugin import run_hook @@ -256,7 +257,7 @@ class SendScreen(CScreen): except: self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount) return - outputs = [(bitcoin.TYPE_ADDRESS, address, amount)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, address, amount)] message = self.screen.message amount = sum(map(lambda x:x[2], outputs)) if self.app.electrum_config.get('use_rbf'): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -50,7 +50,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, export_meta, import_meta, bh2u, bfh, InvalidPassword, base_units, base_units_list, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, quantize_feerate) -from electrum.transaction import Transaction +from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException from electrum.wallet import Multisig_Wallet, CannotBumpFee @@ -1306,7 +1306,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): outputs = self.payto_e.get_outputs(self.is_max) if not outputs: _type, addr = self.get_payto_or_dummy() - outputs = [(_type, addr, amount)] + outputs = [TxOutput(_type, addr, amount)] is_sweep = bool(self.tx_external_keypairs) make_tx = lambda fee_est: \ self.wallet.make_unsigned_transaction( @@ -1485,14 +1485,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.show_error(_('No outputs')) return - for _type, addr, amount in outputs: - if addr is None: + for o in outputs: + if o.address is None: self.show_error(_('Bitcoin Address is None')) return - if _type == TYPE_ADDRESS and not bitcoin.is_address(addr): + if o.type == TYPE_ADDRESS and not bitcoin.is_address(o.address): self.show_error(_('Invalid Bitcoin Address')) return - if amount is None: + if o.value is None: self.show_error(_('Invalid Amount')) return diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py @@ -29,6 +29,7 @@ from decimal import Decimal from electrum import bitcoin from electrum.util import bfh +from electrum.transaction import TxOutput from .qrtextedit import ScanQRTextEdit from .completion_text_edit import CompletionTextEdit @@ -77,7 +78,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit): x, y = line.split(',') out_type, out = self.parse_output(x) amount = self.parse_amount(y) - return out_type, out, amount + return TxOutput(out_type, out, amount) def parse_output(self, x): try: @@ -139,16 +140,16 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit): is_max = False for i, line in enumerate(lines): try: - _type, to_address, amount = self.parse_address_and_amount(line) + output = self.parse_address_and_amount(line) except: self.errors.append((i, line.strip())) continue - outputs.append((_type, to_address, amount)) - if amount == '!': + outputs.append(output) + if output.value == '!': is_max = True else: - total += amount + total += output.value self.win.is_max = is_max self.outputs = outputs @@ -174,7 +175,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit): amount = self.amount_edit.get_amount() _type, addr = self.payto_address - self.outputs = [(_type, addr, amount)] + self.outputs = [TxOutput(_type, addr, amount)] return self.outputs[:] diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py @@ -4,6 +4,7 @@ _ = lambda x:x from electrum import WalletStorage, Wallet from electrum.util import format_satoshis, set_verbosity from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS +from electrum.transaction import TxOutput import getpass, datetime # minimal fdisk like gui for console usage @@ -189,7 +190,8 @@ class ElectrumGui: if c == "n": return try: - tx = self.wallet.mktx([(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee) + tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], + password, self.config, fee) except Exception as e: print(str(e)) return diff --git a/electrum/gui/text.py b/electrum/gui/text.py @@ -6,6 +6,7 @@ import getpass import electrum from electrum.util import format_satoshis, set_verbosity from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS +from electrum.transaction import TxOutput from .. import Wallet, WalletStorage _ = lambda x:x @@ -340,7 +341,8 @@ class ElectrumGui: else: password = None try: - tx = self.wallet.mktx([(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee) + tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], + password, self.config, fee) except Exception as e: self.show_message(str(e)) return diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py @@ -42,6 +42,7 @@ from .util import print_error, bh2u, bfh from .util import export_meta, import_meta from .bitcoin import TYPE_ADDRESS +from .transaction import TxOutput REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'} ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'} @@ -123,7 +124,7 @@ class PaymentRequest: self.outputs = [] for o in self.details.outputs: addr = transaction.get_address_from_output_script(o.script)[1] - self.outputs.append((TYPE_ADDRESS, addr, o.amount)) + self.outputs.append(TxOutput(TYPE_ADDRESS, addr, o.amount)) self.memo = self.details.memo self.payment_url = self.details.payment_url @@ -225,8 +226,8 @@ class PaymentRequest: def get_address(self): o = self.outputs[0] - assert o[0] == TYPE_ADDRESS - return o[1] + assert o.type == TYPE_ADDRESS + return o.address def get_requestor(self): return self.requestor if self.requestor else self.get_address() diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -534,9 +534,9 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): self.give_error("No matching x_key for sign_transaction") # should never happen # Build pubkeyarray from outputs - for _type, address, amount in tx.outputs(): - assert _type == TYPE_ADDRESS - info = tx.output_info.get(address) + for o in tx.outputs(): + assert o.type == TYPE_ADDRESS + info = tx.output_info.get(o.address) if info is not None: index, xpubs, m = info changePath = self.get_derivation() + "/%d/%d" % index diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py @@ -28,7 +28,7 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.bitcoin import is_address, TYPE_SCRIPT from electrum.util import bfh -from electrum.transaction import opcodes +from electrum.transaction import opcodes, TxOutput class HW_PluginBase(BasePlugin): @@ -91,13 +91,13 @@ def is_any_tx_output_on_change_branch(tx): return False -def trezor_validate_op_return_output_and_get_data(_type, address, amount): - if _type != TYPE_SCRIPT: - raise Exception("Unexpected output type: {}".format(_type)) - script = bfh(address) +def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: + if output.type != TYPE_SCRIPT: + raise Exception("Unexpected output type: {}".format(output.type)) + script = bfh(output.address) if not (script[0] == opcodes.OP_RETURN and script[1] == len(script) - 2 and script[1] <= 75): raise Exception(_("Only OP_RETURN scripts, with one constant push, are supported.")) - if amount != 0: + if output.value != 0: raise Exception(_("Amount for OP_RETURN output must be zero.")) return script[2:] diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py @@ -382,7 +382,7 @@ class KeepKeyPlugin(HW_PluginBase): txoutputtype.amount = amount if _type == TYPE_SCRIPT: txoutputtype.script_type = self.types.PAYTOOPRETURN - txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(_type, address, amount) + txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) elif _type == TYPE_ADDRESS: if is_segwit_address(address): txoutputtype.script_type = self.types.PAYTOWITNESS @@ -401,7 +401,8 @@ class KeepKeyPlugin(HW_PluginBase): has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for _type, address, amount in tx.outputs(): + for o in tx.outputs(): + _type, address, amount = o.type, o.address, o.value use_create_by_derivation = False info = tx.output_info.get(address) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py @@ -394,9 +394,9 @@ class Ledger_KeyStore(Hardware_KeyStore): self.give_error("Transaction with more than 2 outputs not supported") has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for _type, address, amount in tx.outputs(): - assert _type == TYPE_ADDRESS - info = tx.output_info.get(address) + for o in tx.outputs(): + assert o.type == TYPE_ADDRESS + info = tx.output_info.get(o.address) if (info is not None) and len(tx.outputs()) > 1 \ and not has_change: index, xpubs, m = info @@ -407,9 +407,9 @@ class Ledger_KeyStore(Hardware_KeyStore): changePath = self.get_derivation()[2:] + "/%d/%d"%index has_change = True else: - output = address + output = o.address else: - output = address + output = o.address self.handler.show_message(_("Confirm Transaction on your Ledger device...")) try: diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py @@ -453,7 +453,7 @@ class SafeTPlugin(HW_PluginBase): txoutputtype.amount = amount if _type == TYPE_SCRIPT: txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN - txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(_type, address, amount) + txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) elif _type == TYPE_ADDRESS: txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS txoutputtype.address = address @@ -463,7 +463,8 @@ class SafeTPlugin(HW_PluginBase): has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for _type, address, amount in tx.outputs(): + for o in tx.outputs(): + _type, address, amount = o.type, o.address, o.value use_create_by_derivation = False info = tx.output_info.get(address) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py @@ -464,7 +464,7 @@ class TrezorPlugin(HW_PluginBase): txoutputtype.amount = amount if _type == TYPE_SCRIPT: txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN - txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(_type, address, amount) + txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) elif _type == TYPE_ADDRESS: txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS txoutputtype.address = address @@ -474,7 +474,8 @@ class TrezorPlugin(HW_PluginBase): has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for _type, address, amount in tx.outputs(): + for o in tx.outputs(): + _type, address, amount = o.type, o.address, o.value use_create_by_derivation = False info = tx.output_info.get(address) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py @@ -33,6 +33,7 @@ from urllib.parse import quote from electrum import bitcoin, ecc, constants, keystore, version from electrum.bitcoin import * +from electrum.transaction import TxOutput from electrum.mnemonic import Mnemonic from electrum.wallet import Multisig_Wallet, Deterministic_Wallet from electrum.i18n import _ @@ -273,7 +274,7 @@ class Wallet_2fa(Multisig_Wallet): fee = self.extra_fee(config) if not is_sweep else 0 if fee: address = self.billing_info['billing_address'] - fee_output = (TYPE_ADDRESS, address, fee) + fee_output = TxOutput(TYPE_ADDRESS, address, fee) try: tx = mk_tx(outputs + [fee_output]) except NotEnoughFunds: @@ -395,9 +396,9 @@ class TrustedCoinPlugin(BasePlugin): def get_tx_extra_fee(self, wallet, tx): if type(wallet) != Wallet_2fa: return - for _type, addr, amount in tx.outputs(): - if _type == TYPE_ADDRESS and wallet.is_billing_address(addr): - return addr, amount + for o in tx.outputs(): + if o.type == TYPE_ADDRESS and wallet.is_billing_address(o.address): + return o.address, o.value def finish_requesting(func): def f(self, *args, **kwargs): diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py @@ -10,6 +10,7 @@ from electrum import SimpleConfig from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet from electrum.util import bfh, bh2u +from electrum.transaction import TxOutput from electrum.plugins.trustedcoin import trustedcoin @@ -532,7 +533,7 @@ class TestWalletSending(TestCaseForTestnet): wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)] tx = wallet1.mktx(outputs=outputs, password=None, config=self.config, fee=5000) self.assertTrue(tx.is_complete()) @@ -552,7 +553,7 @@ class TestWalletSending(TestCaseForTestnet): wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)] tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) self.assertTrue(tx.is_complete()) @@ -605,7 +606,7 @@ class TestWalletSending(TestCaseForTestnet): wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)] tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners self.assertFalse(tx.is_complete()) @@ -628,7 +629,7 @@ class TestWalletSending(TestCaseForTestnet): wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) self.assertTrue(tx.is_complete()) @@ -696,7 +697,7 @@ class TestWalletSending(TestCaseForTestnet): wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)] tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) txid = tx.txid() tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners @@ -722,7 +723,7 @@ class TestWalletSending(TestCaseForTestnet): wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] tx = wallet2a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) txid = tx.txid() tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners @@ -776,7 +777,7 @@ class TestWalletSending(TestCaseForTestnet): wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)] tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) self.assertTrue(tx.is_complete()) @@ -796,7 +797,7 @@ class TestWalletSending(TestCaseForTestnet): wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)] tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) self.assertTrue(tx.is_complete()) @@ -832,7 +833,7 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] coins = wallet.get_spendable_coins(domain=None, config=self.config) tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) tx.set_rbf(True) @@ -918,7 +919,7 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] coins = wallet.get_spendable_coins(domain=None, config=self.config) tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) tx.set_rbf(True) @@ -1048,7 +1049,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1088,7 +1089,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325341 @@ -1129,7 +1130,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325341 @@ -1165,7 +1166,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1199,7 +1200,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1233,7 +1234,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1270,7 +1271,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1307,7 +1308,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1344,7 +1345,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1393,7 +1394,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325503 @@ -1450,7 +1451,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325504 @@ -1509,7 +1510,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx.set_rbf(True) tx.locktime = 1325505 diff --git a/electrum/transaction.py b/electrum/transaction.py @@ -27,7 +27,7 @@ # Note: The deserialization code originally comes from ABE. -from typing import Sequence, Union +from typing import Sequence, Union, NamedTuple from .util import print_error, profiler @@ -59,6 +59,10 @@ class NotRecognizedRedeemScript(Exception): pass +TxOutput = NamedTuple("TxOutput", [('type', int), ('address', str), ('value', Union[int, str])]) +# ^ value is str when the output is set to max: '!' + + class BCDataStream(object): def __init__(self): self.input = None @@ -721,7 +725,7 @@ class Transaction: return d = deserialize(self.raw, force_full_parse) self._inputs = d['inputs'] - self._outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']] + self._outputs = [TxOutput(x['type'], x['address'], x['value']) for x in d['outputs']] self.locktime = d['lockTime'] self.version = d['version'] self.is_partial_originally = d['partial'] @@ -1180,17 +1184,17 @@ class Transaction: def get_outputs(self): """convert pubkeys to addresses""" - o = [] - for type, x, v in self.outputs(): - if type == TYPE_ADDRESS: - addr = x - elif type == TYPE_PUBKEY: + outputs = [] + for o in self.outputs(): + if o.type == TYPE_ADDRESS: + addr = o.address + elif o.type == TYPE_PUBKEY: # TODO do we really want this conversion? it's not really that address after all - addr = bitcoin.public_key_to_p2pkh(bfh(x)) + addr = bitcoin.public_key_to_p2pkh(bfh(o.address)) else: - addr = 'SCRIPT ' + x - o.append((addr,v)) # consider using yield (addr, v) - return o + addr = 'SCRIPT ' + o.address + outputs.append((addr, o.value)) # consider using yield (addr, v) + return outputs def get_output_addresses(self): return [addr for addr, val in self.get_outputs()] diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -51,7 +51,7 @@ from .keystore import load_keystore, Hardware_KeyStore from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW from . import transaction, bitcoin, coinchooser, paymentrequest, contacts -from .transaction import Transaction +from .transaction import Transaction, TxOutput from .plugin import run_hook from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) @@ -133,7 +133,7 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100): inputs, keypairs = sweep_preparations(privkeys, network, imax) total = sum(i.get('value') for i in inputs) if fee is None: - outputs = [(TYPE_ADDRESS, recipient, total)] + outputs = [TxOutput(TYPE_ADDRESS, recipient, total)] tx = Transaction.from_io(inputs, outputs) fee = config.estimate_fee(tx.estimated_size()) if total - fee < 0: @@ -141,7 +141,7 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100): if total - fee < dust_threshold(network): raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network))) - outputs = [(TYPE_ADDRESS, recipient, total - fee)] + outputs = [TxOutput(TYPE_ADDRESS, recipient, total - fee)] locktime = network.get_local_height() tx = Transaction.from_io(inputs, outputs, locktime=locktime) @@ -538,11 +538,10 @@ class Abstract_Wallet(AddressSynchronizer): # check outputs i_max = None for i, o in enumerate(outputs): - _type, data, value = o - if _type == TYPE_ADDRESS: - if not is_address(data): - raise Exception("Invalid bitcoin address: {}".format(data)) - if value == '!': + if o.type == TYPE_ADDRESS: + if not is_address(o.address): + raise Exception("Invalid bitcoin address: {}".format(o.address)) + if o.value == '!': if i_max is not None: raise Exception("More than one output set to spend max") i_max = i @@ -593,14 +592,13 @@ class Abstract_Wallet(AddressSynchronizer): else: # FIXME?? this might spend inputs with negative effective value... sendable = sum(map(lambda x:x['value'], inputs)) - _type, data, value = outputs[i_max] - outputs[i_max] = (_type, data, 0) + outputs[i_max] = outputs[i_max]._replace(value=0) tx = Transaction.from_io(inputs, outputs[:]) fee = fee_estimator(tx.estimated_size()) amount = sendable - tx.output_value() - fee if amount < 0: raise NotEnoughFunds() - outputs[i_max] = (_type, data, amount) + outputs[i_max] = outputs[i_max]._replace(value=amount) tx = Transaction.from_io(inputs, outputs[:]) # Sort the inputs and outputs deterministically @@ -694,14 +692,13 @@ class Abstract_Wallet(AddressSynchronizer): s = sorted(s, key=lambda x: x[2]) for o in s: i = outputs.index(o) - otype, address, value = o - if value - delta >= self.dust_threshold(): - outputs[i] = otype, address, value - delta + if o.value - delta >= self.dust_threshold(): + outputs[i] = o._replace(value=o.value-delta) delta = 0 break else: del outputs[i] - delta -= value + delta -= o.value if delta > 0: continue if delta > 0: @@ -714,8 +711,8 @@ class Abstract_Wallet(AddressSynchronizer): def cpfp(self, tx, fee): txid = tx.txid() for i, o in enumerate(tx.outputs()): - otype, address, value = o - if otype == TYPE_ADDRESS and self.is_mine(address): + address, value = o.address, o.value + if o.type == TYPE_ADDRESS and self.is_mine(address): break else: return @@ -725,7 +722,7 @@ class Abstract_Wallet(AddressSynchronizer): return self.add_input_info(item) inputs = [item] - outputs = [(TYPE_ADDRESS, address, value - fee)] + outputs = [TxOutput(TYPE_ADDRESS, address, value - fee)] locktime = self.get_local_height() # note: no need to call tx.BIP_LI01_sort() here - single input/output return Transaction.from_io(inputs, outputs, locktime=locktime)