electrum

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

commit 0025073b24cf168e2ec29a00640f9280b18f2877
parent 780b2d067c1ade3575e9511e9e3c71a4a39f50c2
Author: ThomasV <thomasv@electrum.org>
Date:   Thu, 19 Jul 2018 10:15:22 +0200

move more methods from wallet to address_synchronizer

Diffstat:
Melectrum/address_synchronizer.py | 255++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Melectrum/wallet.py | 266+------------------------------------------------------------------------------
2 files changed, 255 insertions(+), 266 deletions(-)

diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py @@ -25,6 +25,8 @@ import threading import itertools from collections import defaultdict +from . import bitcoin +from .bitcoin import COINBASE_MATURITY from .util import PrintError, profiler from .transaction import Transaction from .synchronizer import Synchronizer @@ -66,9 +68,50 @@ class AddressSynchronizer(PrintError): self.up_to_date = False self.load_transactions() self.load_local_history() + self.check_history() self.load_unverified_transactions() self.remove_local_transactions_we_dont_have() + def is_mine(self, address): + return address in self.history + + def get_addresses(self): + return sorted(self.history.keys()) + + def get_address_history(self, addr): + h = [] + # we need self.transaction_lock but get_tx_height will take self.lock + # so we need to take that too here, to enforce order of locks + with self.lock, self.transaction_lock: + related_txns = self._history_local.get(addr, set()) + for tx_hash in related_txns: + tx_height = self.get_tx_height(tx_hash)[0] + h.append((tx_hash, tx_height)) + return h + + def get_txin_address(self, txi): + addr = txi.get('address') + if addr and addr != "(pubkey)": + return addr + prevout_hash = txi.get('prevout_hash') + prevout_n = txi.get('prevout_n') + dd = self.txo.get(prevout_hash, {}) + for addr, l in dd.items(): + for n, v, is_cb in l: + if n == prevout_n: + 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)) + else: + addr = None + return addr + def load_unverified_transactions(self): # review transactions that are in the history for addr, hist in self.history.items(): @@ -322,6 +365,26 @@ class AddressSynchronizer(PrintError): for txid in itertools.chain(self.txi, self.txo): self._add_tx_to_local_history(txid) + @profiler + def check_history(self): + save = False + hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.history.keys())) + hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.history.keys())) + for addr in hist_addrs_not_mine: + self.history.pop(addr) + save = True + for addr in hist_addrs_mine: + hist = self.history[addr] + for tx_hash, tx_height in hist: + if self.txi.get(tx_hash) or self.txo.get(tx_hash): + continue + tx = self.transactions.get(tx_hash) + if tx is not None: + self.add_transaction(tx_hash, tx, allow_unrelated=True) + save = True + if save: + self.save_transactions() + def remove_local_transactions_we_dont_have(self): txid_set = set(self.txi) | set(self.txo) for txid in txid_set: @@ -362,10 +425,22 @@ class AddressSynchronizer(PrintError): self.transactions = {} self.save_transactions() + def get_txpos(self, tx_hash): + "return position, even if the tx is unverified" + with self.lock: + if tx_hash in self.verified_tx: + height, timestamp, pos = self.verified_tx[tx_hash] + return height, pos + elif tx_hash in self.unverified_tx: + height = self.unverified_tx[tx_hash] + return (height, 0) if height > 0 else ((1e9 - height), 0) + else: + return (1e9+1, 0) + def get_history(self, domain=None): # get domain if domain is None: - domain = self.get_addresses() + domain = self.history.keys() domain = set(domain) # 1. Get the history of each address in the domain, maintain the # delta of a tx as the sum of its deltas on domain addresses @@ -492,3 +567,181 @@ class AddressSynchronizer(PrintError): def is_up_to_date(self): with self.lock: return self.up_to_date + + def get_num_tx(self, address): + """ return number of transactions where address is involved """ + return len(self.history.get(address, [])) + + def get_tx_delta(self, tx_hash, address): + "effect of tx on address" + delta = 0 + # substract the value of coins sent from address + d = self.txi.get(tx_hash, {}).get(address, []) + for n, v in d: + delta -= v + # add the value of the coins received at address + d = self.txo.get(tx_hash, {}).get(address, []) + for n, v, cb in d: + delta += v + return delta + + def get_tx_value(self, txid): + " effect of tx on the entire domain" + delta = 0 + for addr, d in self.txi.get(txid, {}).items(): + for n, v in d: + delta -= v + for addr, d in self.txo.get(txid, {}).items(): + for n, v, cb in d: + delta += v + return delta + + def get_wallet_delta(self, tx): + """ effect of tx on wallet """ + is_relevant = False # "related to wallet?" + is_mine = False + is_pruned = False + is_partial = False + v_in = v_out = v_out_mine = 0 + for txin in tx.inputs(): + addr = self.get_txin_address(txin) + if self.is_mine(addr): + is_mine = True + is_relevant = True + d = self.txo.get(txin['prevout_hash'], {}).get(addr, []) + for n, v, cb in d: + if n == txin['prevout_n']: + value = v + break + else: + value = None + if value is None: + is_pruned = True + else: + v_in += value + else: + is_partial = True + if not is_mine: + is_partial = False + for addr, value in tx.get_outputs(): + v_out += value + if self.is_mine(addr): + v_out_mine += value + is_relevant = True + if is_pruned: + # some inputs are mine: + fee = None + if is_mine: + v = v_out_mine - v_out + else: + # no input is mine + v = v_out_mine + else: + v = v_out_mine - v_in + if is_partial: + # some inputs are mine, but not all + fee = None + else: + # all inputs are mine + fee = v_in - v_out + if not is_mine: + fee = None + return is_relevant, is_mine, v, fee + + def get_addr_io(self, address): + h = self.get_address_history(address) + received = {} + sent = {} + for tx_hash, height in h: + l = self.txo.get(tx_hash, {}).get(address, []) + for n, v, is_cb in l: + received[tx_hash + ':%d'%n] = (height, v, is_cb) + for tx_hash, height in h: + l = self.txi.get(tx_hash, {}).get(address, []) + for txi, v in l: + sent[txi] = height + return received, sent + + def get_addr_utxo(self, address): + coins, spent = self.get_addr_io(address) + for txi in spent: + coins.pop(txi) + out = {} + for txo, v in coins.items(): + tx_height, value, is_cb = v + prevout_hash, prevout_n = txo.split(':') + x = { + 'address':address, + 'value':value, + 'prevout_n':int(prevout_n), + 'prevout_hash':prevout_hash, + 'height':tx_height, + 'coinbase':is_cb + } + out[txo] = x + return out + + # return the total amount ever received by an address + def get_addr_received(self, address): + received, sent = self.get_addr_io(address) + return sum([v for height, v, is_cb in received.values()]) + + # return the balance of a bitcoin address: confirmed and matured, unconfirmed, unmatured + def get_addr_balance(self, address): + received, sent = self.get_addr_io(address) + c = u = x = 0 + local_height = self.get_local_height() + for txo, (tx_height, v, is_cb) in received.items(): + if is_cb and tx_height + COINBASE_MATURITY > local_height: + x += v + elif tx_height > 0: + c += v + else: + u += v + if txo in sent: + if sent[txo] > 0: + c -= v + else: + u -= v + return c, u, x + + def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False): + coins = [] + if domain is None: + domain = self.get_addresses() + domain = set(domain) + if excluded: + domain = set(domain) - excluded + for addr in domain: + utxos = self.get_addr_utxo(addr) + for x in utxos.values(): + if confirmed_only and x['height'] <= 0: + continue + if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height(): + continue + coins.append(x) + continue + return coins + + def get_balance(self, domain=None): + if domain is None: + domain = self.get_addresses() + domain = set(domain) + cc = uu = xx = 0 + for addr in domain: + c, u, x = self.get_addr_balance(addr) + cc += c + uu += u + xx += x + return cc, uu, xx + + def is_used(self, address): + h = self.history.get(address,[]) + if len(h) == 0: + return False + c, u, x = self.get_addr_balance(address) + return c + u + x == 0 + + def is_empty(self, address): + c, u, x = self.get_addr_balance(address) + return c+u+x == 0 diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -182,8 +182,6 @@ class Abstract_Wallet(AddressSynchronizer): self.load_addresses() self.test_addresses_sanity() - self.check_history() - # save wallet type the first time if self.storage.get('wallet_type') is None: self.storage.put('wallet_type', self.wallet_type) @@ -203,26 +201,6 @@ class Abstract_Wallet(AddressSynchronizer): def get_master_public_key(self): return None - @profiler - def check_history(self): - save = False - hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.history.keys())) - hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.history.keys())) - for addr in hist_addrs_not_mine: - self.history.pop(addr) - save = True - for addr in hist_addrs_mine: - hist = self.history[addr] - for tx_hash, tx_height in hist: - if self.txi.get(tx_hash) or self.txo.get(tx_hash): - continue - tx = self.transactions.get(tx_hash) - if tx is not None: - self.add_transaction(tx_hash, tx, allow_unrelated=True) - save = True - if save: - self.save_transactions() - def basename(self): return os.path.basename(self.storage.path) @@ -290,9 +268,6 @@ class Abstract_Wallet(AddressSynchronizer): except: return - def is_mine(self, address): - return address in self.get_addresses() - def is_change(self, address): if not self.is_mine(address): return False @@ -317,101 +292,9 @@ class Abstract_Wallet(AddressSynchronizer): def get_public_keys(self, address): return [self.get_public_key(address)] - def get_txpos(self, tx_hash): - "return position, even if the tx is unverified" - with self.lock: - if tx_hash in self.verified_tx: - height, timestamp, pos = self.verified_tx[tx_hash] - return height, pos - elif tx_hash in self.unverified_tx: - height = self.unverified_tx[tx_hash] - return (height, 0) if height > 0 else ((1e9 - height), 0) - else: - return (1e9+1, 0) - def is_found(self): return self.history.values() != [[]] * len(self.history) - def get_num_tx(self, address): - """ return number of transactions where address is involved """ - return len(self.history.get(address, [])) - - def get_tx_delta(self, tx_hash, address): - "effect of tx on address" - delta = 0 - # substract the value of coins sent from address - d = self.txi.get(tx_hash, {}).get(address, []) - for n, v in d: - delta -= v - # add the value of the coins received at address - d = self.txo.get(tx_hash, {}).get(address, []) - for n, v, cb in d: - delta += v - return delta - - def get_tx_value(self, txid): - " effect of tx on the entire domain" - delta = 0 - for addr, d in self.txi.get(txid, {}).items(): - for n, v in d: - delta -= v - for addr, d in self.txo.get(txid, {}).items(): - for n, v, cb in d: - delta += v - return delta - - def get_wallet_delta(self, tx): - """ effect of tx on wallet """ - is_relevant = False # "related to wallet?" - is_mine = False - is_pruned = False - is_partial = False - v_in = v_out = v_out_mine = 0 - for txin in tx.inputs(): - addr = self.get_txin_address(txin) - if self.is_mine(addr): - is_mine = True - is_relevant = True - d = self.txo.get(txin['prevout_hash'], {}).get(addr, []) - for n, v, cb in d: - if n == txin['prevout_n']: - value = v - break - else: - value = None - if value is None: - is_pruned = True - else: - v_in += value - else: - is_partial = True - if not is_mine: - is_partial = False - for addr, value in tx.get_outputs(): - v_out += value - if self.is_mine(addr): - v_out_mine += value - is_relevant = True - if is_pruned: - # some inputs are mine: - fee = None - if is_mine: - v = v_out_mine - v_out - else: - # no input is mine - v = v_out_mine - else: - v = v_out_mine - v_in - if is_partial: - # some inputs are mine, but not all - fee = None - else: - # all inputs are mine - fee = v_in - v_out - if not is_mine: - fee = None - return is_relevant, is_mine, v, fee - def get_tx_info(self, tx): is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) exp_n = None @@ -461,143 +344,16 @@ class Abstract_Wallet(AddressSynchronizer): return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n - def get_addr_io(self, address): - h = self.get_address_history(address) - received = {} - sent = {} - for tx_hash, height in h: - l = self.txo.get(tx_hash, {}).get(address, []) - for n, v, is_cb in l: - received[tx_hash + ':%d'%n] = (height, v, is_cb) - for tx_hash, height in h: - l = self.txi.get(tx_hash, {}).get(address, []) - for txi, v in l: - sent[txi] = height - return received, sent - - def get_addr_utxo(self, address): - coins, spent = self.get_addr_io(address) - for txi in spent: - coins.pop(txi) - out = {} - for txo, v in coins.items(): - tx_height, value, is_cb = v - prevout_hash, prevout_n = txo.split(':') - x = { - 'address':address, - 'value':value, - 'prevout_n':int(prevout_n), - 'prevout_hash':prevout_hash, - 'height':tx_height, - 'coinbase':is_cb - } - out[txo] = x - return out - - # return the total amount ever received by an address - def get_addr_received(self, address): - received, sent = self.get_addr_io(address) - return sum([v for height, v, is_cb in received.values()]) - - # return the balance of a bitcoin address: confirmed and matured, unconfirmed, unmatured - def get_addr_balance(self, address): - received, sent = self.get_addr_io(address) - c = u = x = 0 - local_height = self.get_local_height() - for txo, (tx_height, v, is_cb) in received.items(): - if is_cb and tx_height + COINBASE_MATURITY > local_height: - x += v - elif tx_height > 0: - c += v - else: - u += v - if txo in sent: - if sent[txo] > 0: - c -= v - else: - u -= v - return c, u, x - def get_spendable_coins(self, domain, config): confirmed_only = config.get('confirmed_only', False) - return self.get_utxos(domain, exclude_frozen=True, mature=True, confirmed_only=confirmed_only) - - def get_utxos(self, domain = None, exclude_frozen = False, mature = False, confirmed_only = False): - coins = [] - if domain is None: - domain = self.get_addresses() - domain = set(domain) - if exclude_frozen: - domain = set(domain) - self.frozen_addresses - for addr in domain: - utxos = self.get_addr_utxo(addr) - for x in utxos.values(): - if confirmed_only and x['height'] <= 0: - continue - if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height(): - continue - coins.append(x) - continue - return coins + return self.get_utxos(domain, excluded=self.frozen_addresses, mature=True, confirmed_only=confirmed_only) def dummy_address(self): return self.get_receiving_addresses()[0] - def get_addresses(self): - out = [] - out += self.get_receiving_addresses() - out += self.get_change_addresses() - return out - def get_frozen_balance(self): return self.get_balance(self.frozen_addresses) - def get_balance(self, domain=None): - if domain is None: - domain = self.get_addresses() - domain = set(domain) - cc = uu = xx = 0 - for addr in domain: - c, u, x = self.get_addr_balance(addr) - cc += c - uu += u - xx += x - return cc, uu, xx - - def get_address_history(self, addr): - h = [] - # we need self.transaction_lock but get_tx_height will take self.lock - # so we need to take that too here, to enforce order of locks - with self.lock, self.transaction_lock: - related_txns = self._history_local.get(addr, set()) - for tx_hash in related_txns: - tx_height = self.get_tx_height(tx_hash)[0] - h.append((tx_hash, tx_height)) - return h - - def get_txin_address(self, txi): - addr = txi.get('address') - if addr and addr != "(pubkey)": - return addr - prevout_hash = txi.get('prevout_hash') - prevout_n = txi.get('prevout_n') - dd = self.txo.get(prevout_hash, {}) - for addr, l in dd.items(): - for n, v, is_cb in l: - if n == prevout_n: - 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)) - else: - addr = None - return addr - def balance_at_timestamp(self, domain, target_timestamp): h = self.get_history(domain) for tx_hash, height, conf, timestamp, value, balance in h: @@ -884,17 +640,6 @@ class Abstract_Wallet(AddressSynchronizer): def can_export(self): return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key') - def is_used(self, address): - h = self.history.get(address,[]) - if len(h) == 0: - return False - c, u, x = self.get_addr_balance(address) - return c + u + x == 0 - - def is_empty(self, address): - c, u, x = self.get_addr_balance(address) - return c+u+x == 0 - def address_is_old(self, address, age_limit=2): age = -1 h = self.history.get(address, []) @@ -1458,15 +1203,9 @@ class Imported_Wallet(Simple_Wallet): def is_beyond_limit(self, address): return False - def is_mine(self, address): - return address in self.addresses - def get_fingerprint(self): return '' - def get_addresses(self, include_change=False): - return sorted(self.addresses.keys()) - def get_receiving_addresses(self): return self.get_addresses() @@ -1699,9 +1438,6 @@ class Deterministic_Wallet(Abstract_Wallet): return False return True - def is_mine(self, address): - return address in self._addr_to_addr_index - def get_address_index(self, address): return self._addr_to_addr_index[address]