From ae906fb17cbe6899af1713f3816d68b2d16af481 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 24 Apr 2025 10:09:48 +0200 Subject: [PATCH] wallet: imports, whitespace, typing hints --- electrum/wallet.py | 140 ++++++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 64 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 12f3e407e..12e2df1b5 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -44,45 +44,40 @@ import asyncio import electrum_ecc as ecc from aiorpcx import ignore_after, run_in_thread +from . import util, keystore, transaction, bitcoin, coinchooser, bip32, descriptor from .i18n import _ from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath -from . import util -from .lntransport import extract_nodeid +from .logging import get_logger, Logger from .util import ( NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, format_fee_satoshis, WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, - Satoshis, Fiat, TxMinedInfo, quantize_feerate, OrderedDictWithIndex + Satoshis, Fiat, TxMinedInfo, quantize_feerate, OrderedDictWithIndex, multisig_type, parse_max_spend, + OnchainHistoryItem, read_json_file, write_json_file, UserFacingException, FileImportFailed, EventListener, + event_listener +) +from .bitcoin import COIN, is_address, is_minikey, relayfee, dust_threshold, DummyAddress, DummyAddressUsedInTxException +from .keystore import ( + load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric, CannotDerivePubkey ) from .simple_config import SimpleConfig from .fee_policy import FeePolicy, FixedFeePolicy, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE -from .lnutil import MIN_FUNDING_SAT -from .bitcoin import COIN, is_address, is_minikey, relayfee, dust_threshold -from .bitcoin import DummyAddress, DummyAddressUsedInTxException -from . import keystore -from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, - AddressIndexGeneric, CannotDerivePubkey) -from .util import multisig_type, parse_max_spend from .storage import StorageEncryptionVersion, WalletStorage from .wallet_db import WalletDB -from . import transaction, bitcoin, coinchooser, bip32 from .transaction import ( - Transaction, TxInput, TxOutput, PartialTransaction, PartialTxInput, - PartialTxOutput, TxOutpoint, Sighash + Transaction, TxInput, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint, Sighash ) from .plugin import run_hook -from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, - TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, TX_TIMESTAMP_INF) -from .invoices import BaseInvoice, Invoice, Request -from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_INFLIGHT +from .address_synchronizer import ( + AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, + TX_TIMESTAMP_INF +) +from .invoices import BaseInvoice, Invoice, Request, PR_PAID, PR_UNPAID, PR_EXPIRED, PR_UNCONFIRMED from .contacts import Contacts from .mnemonic import Mnemonic -from .logging import get_logger, Logger from .lnworker import LNWallet -from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed -from .util import EventListener, event_listener -from . import descriptor +from .lnutil import MIN_FUNDING_SAT +from .lntransport import extract_nodeid from .descriptor import Descriptor -from .util import OnchainHistoryItem from .txbatcher import TxBatcher if TYPE_CHECKING: @@ -177,17 +172,18 @@ async def sweep( fee_policy: FeePolicy, imax=100, locktime=None, - tx_version=None) -> PartialTransaction: - + tx_version=None +) -> PartialTransaction: inputs, keypairs = await sweep_preparations(privkeys, network, imax) total = sum(txin.value_sats() for txin in inputs) outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total)] tx = PartialTransaction.from_io(inputs, outputs) fee = fee_policy.estimate_fee(tx.estimated_size(), network=network) if total - fee < 0: - raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee)) + raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d' % (total, fee)) 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))) + raise Exception(_('Not enough funds on address.') + + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d' % (total, fee, dust_threshold(network))) outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total - fee)] if locktime is None: locktime = get_locktime_for_new_transaction(network) @@ -228,6 +224,8 @@ def get_locktime_for_new_transaction( class CannotRBFTx(Exception): pass +class TransactionPotentiallyDangerousException(Exception): pass +class TransactionDangerousException(TransactionPotentiallyDangerousException): pass class CannotBumpFee(CannotRBFTx): @@ -251,12 +249,6 @@ class InternalAddressCorruption(Exception): "Please restore your wallet from seed, and compare the addresses in both files") -class TransactionPotentiallyDangerousException(Exception): pass - - -class TransactionDangerousException(TransactionPotentiallyDangerousException): pass - - class TxSighashRiskLevel(enum.IntEnum): # higher value -> more risk SAFE = 0 @@ -359,6 +351,7 @@ class TxWalletDelta(NamedTuple): delta: int fee: Optional[int] + class TxWalletDetails(NamedTuple): txid: Optional[str] status: str @@ -412,19 +405,19 @@ class Abstract_Wallet(ABC, Logger, EventListener): self._last_full_history = None self._tx_parents_cache = {} self._default_labels = {} - self._accounting_addresses = set() # addresses counted as ours after successful sweep + self._accounting_addresses = set() # addresses counted as ours after successful sweep self.taskgroup = OldTaskGroup() # saved fields self.use_change = db.get('use_change', True) self.multiple_change = db.get('multiple_change', False) - self._labels = db.get_dict('labels') - self._frozen_addresses = set(db.get('frozen_addresses', [])) - self._frozen_coins = db.get_dict('frozen_coins') # type: Dict[str, bool] + self._labels = db.get_dict('labels') + self._frozen_addresses = set(db.get('frozen_addresses', [])) + self._frozen_coins = db.get_dict('frozen_coins') # type: Dict[str, bool] self.fiat_value = db.get_dict('fiat_value') - self._receive_requests = db.get_dict('payment_requests') # type: Dict[str, Request] - self._invoices = db.get_dict('invoices') # type: Dict[str, Invoice] + self._receive_requests = db.get_dict('payment_requests') # type: Dict[str, Request] + self._invoices = db.get_dict('invoices') # type: Dict[str, Invoice] self._reserved_addresses = set(db.get('reserved_addresses', [])) self._num_parents = db.get_dict('num_parents') @@ -977,7 +970,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): else: assert isinstance(tx, PartialTransaction) s, r = tx.signature_count() - status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)'%(s,r) + status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)' % (s, r) if is_relevant: if tx_wallet_delta.is_all_input_ismine: @@ -1276,7 +1269,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def get_invoices(self) -> List[Invoice]: out = list(self._invoices.values()) - out.sort(key=lambda x:x.time) + out.sort(key=lambda x: x.time) return out def get_unpaid_invoices(self) -> List[Invoice]: @@ -1644,10 +1637,10 @@ class Abstract_Wallet(ABC, Logger, EventListener): label = request.get_message() return label - def set_default_label(self, key:str, value:str): + def set_default_label(self, key: str, value: str): self._default_labels[key] = value - def get_label_for_outpoint(self, outpoint:str) -> str: + def get_label_for_outpoint(self, outpoint: str) -> str: return self._labels.get(outpoint) or self._get_default_label_for_outpoint(outpoint) def _get_default_label_for_outpoint(self, outpoint: str) -> str: @@ -1742,7 +1735,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): time_str = format_time(timestamp) if timestamp else _("unknown") status_str = TX_STATUS[status] if status < 4 else time_str if extra: - status_str += ' [%s]'%(', '.join(extra)) + status_str += ' [%s]' % (', '.join(extra)) return status, status_str def relayfee(self): @@ -2015,10 +2008,10 @@ class Abstract_Wallet(ABC, Logger, EventListener): val = int((amount/i_max_sum) * weight) outputs[i].value = val distr_amount += val - (x,i) = i_max[-1] + (x, i) = i_max[-1] outputs[i].value += (amount - distr_amount) - tx_inputs = inputs + coins # these do not overlap, see above + tx_inputs = inputs + coins # these do not overlap, see above distribute_amount(0) tx = PartialTransaction.from_io(list(tx_inputs), list(outputs)) fee = fee_estimator(tx.estimated_size()) @@ -2032,7 +2025,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): self.logger.info(f'Adding change output to meet utxo reserve requirements') change_addr = self.get_change_addresses_for_new_transaction(change_addr)[0] change = PartialTxOutput.from_address_and_value(change_addr, self.config.LN_UTXO_RESERVE) - change.is_utxo_reserve = True # for GUI + change.is_utxo_reserve = True # for GUI outputs.append(change) to_distribute -= change.value tx = PartialTransaction.from_io(list(tx_inputs), list(outputs)) @@ -2455,7 +2448,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): else: raise CannotCPFP(_("Could not find suitable output")) coins = self.adb.get_addr_utxo(address) - item = coins.get(TxOutpoint.from_str(txid+':%d'%i)) + item = coins.get(TxOutpoint.from_str(txid + ':%d' % i)) if not item: raise CannotCPFP(_("Could not find coins for output")) inputs = [item] @@ -2663,7 +2656,13 @@ class Abstract_Wallet(ABC, Logger, EventListener): txout.is_change = self.is_change(address) self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) - def sign_transaction(self, tx: Transaction, password, *, ignore_warnings: bool = False) -> Optional[PartialTransaction]: + def sign_transaction( + self, + tx: Transaction, + password, + *, + ignore_warnings: bool = False + ) -> Optional[PartialTransaction]: """ returns tx if successful else None """ if self.is_watching_only(): return @@ -2785,7 +2784,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): paid, conf = self.is_onchain_invoice_paid(invoice) if not paid: if isinstance(invoice, Invoice): - if status:=invoice.get_broadcasting_status(): + if status := invoice.get_broadcasting_status(): return status status = PR_UNPAID elif conf == 0: @@ -2877,7 +2876,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): with self.lock, self.transaction_lock: for txo in tx.outputs(): addr = txo.address - if request:=self.get_request_by_addr(addr): + if request := self.get_request_by_addr(addr): request_keys.add(request.get_id()) for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()): invoice_keys.add(invoice_key) @@ -2957,7 +2956,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def add_payment_request(self, req: Request, *, write_to_disk: bool = True): request_id = req.get_id() self._receive_requests[request_id] = req - if addr:=req.get_address(): + if addr := req.get_address(): self._requests_addr_to_key[addr].add(request_id) if write_to_disk: self.save_db() @@ -2969,7 +2968,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): if req is None: return self._receive_requests.pop(request_id, None) - if addr:=req.get_address(): + if addr := req.get_address(): self._requests_addr_to_key[addr].discard(request_id) if req.is_lightning() and self.lnworker: self.lnworker.delete_payment_info(req.rhash) @@ -3341,7 +3340,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): except Exception: zeroconf_nodeid = None can_get_zeroconf_channel = (self.lnworker and self.config.ACCEPT_ZEROCONF_CHANNELS - and zeroconf_nodeid in self.lnworker.peers) + and zeroconf_nodeid in self.lnworker.peers) status = self.get_invoice_status(req) if status == PR_EXPIRED: @@ -3381,7 +3380,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): if amount_sat < MIN_FUNDING_SAT: ln_is_error = True ln_help = (_('Cannot receive this payment. Request at least {} ' - 'to purchase a Lightning channel from your service provider.') + 'to purchase a Lightning channel from your service provider.') .format(self.config.format_amount_and_units(amount_sat=MIN_FUNDING_SAT))) else: ln_zeroconf_suggestion = True @@ -3410,7 +3409,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): ln_zeroconf_suggestion=ln_zeroconf_suggestion ) - def synchronize(self) -> int: """Returns the number of new addresses we generated.""" return 0 @@ -3602,7 +3600,7 @@ class Imported_Wallet(Simple_Wallet): for tx_hash in transactions_to_remove: self.adb._remove_transaction(tx_hash) self.set_label(address, None) - if req:= self.get_request_by_addr(address): + if req := self.get_request_by_addr(address): self.delete_request(req.get_id()) self.set_frozen_state_of_addresses([address], False, write_to_disk=False) pubkey = self.get_public_key(address) @@ -3639,7 +3637,8 @@ class Imported_Wallet(Simple_Wallet): return unused_addrs def is_mine(self, address) -> bool: - if not address: return False + if not address: + return False return self.db.has_imported_address(address) def get_address_index(self, address) -> Optional[str]: @@ -3668,7 +3667,7 @@ class Imported_Wallet(Simple_Wallet): continue addr = bitcoin.pubkey_to_address(txin_type, pubkey) good_addr.append(addr) - self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey}) + self.db.add_imported_address(addr, {'type': txin_type, 'pubkey': pubkey}) self.adb.add_address(addr) self.save_keystore() if write_to_disk: @@ -3788,7 +3787,7 @@ class Deterministic_Wallet(Abstract_Wallet): return self.keystore.get_seed_type() def change_gap_limit(self, value): - '''This method is not called in the code, it is kept for console use''' + """This method is not called in the code, it is kept for console use""" value = int(value) if value >= self.min_acceptable_gap(): self.gap_limit = value @@ -3924,7 +3923,8 @@ class Deterministic_Wallet(Abstract_Wallet): if der_suffix is not None: # note: we already know the pubkey belongs to the keystore, # but the script template might be different - if len(der_suffix) != 2: continue + if len(der_suffix) != 2: + continue try: my_address = self.derive_address(*der_suffix) except CannotDerivePubkey: @@ -4015,7 +4015,7 @@ class Multisig_Wallet(Deterministic_Wallet): def load_keystore(self): self.keystores = {} for i in range(self.n): - name = 'x%d'%(i+1) + name = 'x%d' % (i+1) self.keystores[name] = load_keystore(self.db, name) self.keystore = self.keystores['x1'] xtype = bip32.xpub_type(self.keystore.xpub) @@ -4069,9 +4069,11 @@ class Multisig_Wallet(Deterministic_Wallet): wallet_types = ['standard', 'multisig', 'imported'] + def register_wallet_type(category): wallet_types.append(category) + wallet_constructors = { 'standard': Standard_Wallet, 'old': Standard_Wallet, @@ -4079,16 +4081,18 @@ wallet_constructors = { 'imported': Imported_Wallet } + def register_constructor(wallet_type, constructor): wallet_constructors[wallet_type] = constructor + # former WalletFactory class Wallet(object): """The main wallet "entry point". This class is actually a factory that will return a wallet of the correct type when passed a WalletStorage instance.""" - def __new__(self, db: 'WalletDB', *, config: SimpleConfig): + def __new__(cls, db: 'WalletDB', *, config: SimpleConfig): wallet_type = db.get('wallet_type') WalletClass = Wallet.wallet_class(wallet_type) wallet = WalletClass(db, config=config) @@ -4103,8 +4107,16 @@ class Wallet(object): raise WalletFileException("Unknown wallet type: " + str(wallet_type)) -def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=None, - encrypt_file=True, seed_type=None, gap_limit=None) -> dict: +def create_new_wallet( + *, + path, + config: SimpleConfig, + passphrase: Optional[str] = None, + password: Optional[str] = None, + encrypt_file: bool = True, + seed_type: Optional[str] = None, + gap_limit: Optional[int] = None +) -> dict: """Create a new wallet""" storage = WalletStorage(path) if storage.file_exists():