Browse Source

Clean up and fix account adding

As per BIP44, 20 addresses are checked for transactions, not just the
first one.
Show the last account only if used or named.
If all accounts are used, prompt for password to create new one.

Fixes #1128
283
Neil Booth 9 years ago
parent
commit
a58c19d7c0
  1. 66
      gui/qt/main_window.py
  2. 36
      lib/account.py
  3. 2
      lib/synchronizer.py
  4. 180
      lib/wallet.py

66
gui/qt/main_window.py

@ -167,6 +167,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.console.showMessage(self.network.banner) self.console.showMessage(self.network.banner)
self.payment_request = None self.payment_request = None
self.checking_accounts = False
self.qr_window = None self.qr_window = None
self.not_enough_funds = False self.not_enough_funds = False
self.pluginsdialog = None self.pluginsdialog = None
@ -263,9 +264,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.need_update.set() self.need_update.set()
# Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
self.notify_transactions() self.notify_transactions()
self.update_account_selector()
# update menus # update menus
self.new_account_menu.setVisible(self.wallet.can_create_accounts()) self.update_new_account_menu()
self.export_menu.setEnabled(not self.wallet.is_watching_only()) self.export_menu.setEnabled(not self.wallet.is_watching_only())
self.password_menu.setEnabled(self.wallet.can_change_password()) self.password_menu.setEnabled(self.wallet.can_change_password())
self.seed_menu.setEnabled(self.wallet.has_seed()) self.seed_menu.setEnabled(self.wallet.has_seed())
@ -511,8 +511,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def timer_actions(self): def timer_actions(self):
if self.need_update.is_set(): if self.need_update.is_set():
self.update_wallet()
self.need_update.clear() self.need_update.clear()
self.update_wallet()
# resolve aliases # resolve aliases
self.payto_e.resolve() self.payto_e.resolve()
# update fee # update fee
@ -589,6 +589,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.update_status() self.update_status()
if self.wallet.up_to_date or not self.network or not self.network.is_connected(): if self.wallet.up_to_date or not self.network or not self.network.is_connected():
self.update_tabs() self.update_tabs()
if self.wallet.up_to_date:
self.check_next_account()
def update_tabs(self): def update_tabs(self):
self.history_list.update() self.history_list.update()
@ -1489,15 +1491,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
menu.addAction(_("Rename"), lambda: self.edit_account_label(k)) menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
if self.wallet.seed_version > 4: if self.wallet.seed_version > 4:
menu.addAction(_("View details"), lambda: self.show_account_details(k)) menu.addAction(_("View details"), lambda: self.show_account_details(k))
if self.wallet.account_is_pending(k):
menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
menu.exec_(self.address_list.viewport().mapToGlobal(position)) menu.exec_(self.address_list.viewport().mapToGlobal(position))
def delete_pending_account(self, k):
self.wallet.delete_pending_account(k)
self.address_list.update()
self.update_account_selector()
def create_receive_menu(self, position): def create_receive_menu(self, position):
selected = self.address_list.selectedItems() selected = self.address_list.selectedItems()
multi_select = len(selected) > 1 multi_select = len(selected) > 1
@ -1933,30 +1928,47 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.set_contact(unicode(line2.text()), str(line1.text())): if self.set_contact(unicode(line2.text()), str(line1.text())):
self.tabs.setCurrentIndex(4) self.tabs.setCurrentIndex(4)
def update_new_account_menu(self):
self.new_account_menu.setVisible(self.wallet.can_create_accounts())
self.new_account_menu.setEnabled(self.wallet.permit_account_naming())
self.update_account_selector()
@protected def new_account_dialog(self):
def new_account_dialog(self, password): dialog = WindowModalDialog(self, _("New Account Name"))
dialog = WindowModalDialog(self, _("New Account"))
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget(QLabel(_('Account name')+':')) msg = _("Enter a name to give the account. You will not be "
"permitted to create further accounts until the new account "
"receives at least one transaction.") + "\n"
label = QLabel(msg)
label.setWordWrap(True)
vbox.addWidget(label)
e = QLineEdit() e = QLineEdit()
vbox.addWidget(e) vbox.addWidget(e)
msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
+ _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
l = QLabel(msg)
l.setWordWrap(True)
vbox.addWidget(l)
vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
dialog.setLayout(vbox) dialog.setLayout(vbox)
r = dialog.exec_() if dialog.exec_():
if not r: self.wallet.set_label(self.wallet.last_account_id(), str(e.text()))
return self.address_list.update()
name = str(e.text()) self.tabs.setCurrentIndex(3)
self.wallet.create_pending_account(name, password) self.update_new_account_menu()
self.address_list.update()
self.update_account_selector()
self.tabs.setCurrentIndex(3)
def check_next_account(self):
if self.wallet.needs_next_account() and not self.checking_accounts:
try:
self.checking_accounts = True
msg = _("All the accounts in your wallet have received "
"transactions. Electrum must check whether more "
"accounts exist; one will only be shown if "
"it has been used or you give it a name.")
self.show_message(msg, title=_("Check Accounts"))
self.create_next_account()
self.update_new_account_menu()
finally:
self.checking_accounts = False
@protected
def create_next_account(self, password):
self.wallet.create_next_account(password)
def show_master_public_keys(self): def show_master_public_keys(self):
dialog = WindowModalDialog(self, "Master Public Keys") dialog = WindowModalDialog(self, "Master Public Keys")

36
lib/account.py

@ -75,6 +75,10 @@ class Account(object):
def redeem_script(self, for_change, n): def redeem_script(self, for_change, n):
return None return None
def is_used(self, wallet):
addresses = self.get_addresses(False)
return any(wallet.address_is_old(a, -1) for a in addresses)
def synchronize_sequence(self, wallet, for_change): def synchronize_sequence(self, wallet, for_change):
limit = wallet.gap_limit_for_change if for_change else wallet.gap_limit limit = wallet.gap_limit_for_change if for_change else wallet.gap_limit
while True: while True:
@ -94,36 +98,6 @@ class Account(object):
self.synchronize_sequence(wallet, True) self.synchronize_sequence(wallet, True)
class PendingAccount(Account):
def __init__(self, v):
self.pending_address = v['address']
self.change_pubkeys = []
self.receiving_pubkeys = [ v['pubkey'] ]
def synchronize(self, wallet):
return
def get_addresses(self, is_change):
return [] if is_change else [self.pending_address]
def has_change(self):
return False
def dump(self):
return {'pending':True, 'address':self.pending_address, 'pubkey':self.receiving_pubkeys[0] }
def get_name(self, k):
return _('Pending account')
def get_master_pubkeys(self):
return []
def get_type(self):
return _('pending')
def get_xpubkeys(self, for_change, n):
return self.get_pubkeys(for_change, n)
class ImportedAccount(Account): class ImportedAccount(Account):
def __init__(self, d): def __init__(self, d):
self.keypairs = d['imported'] self.keypairs = d['imported']
@ -399,5 +373,3 @@ class Multisig_Account(BIP32_Account):
def get_type(self): def get_type(self):
return _('Multisig %d of %d'%(self.m, len(self.xpub_list))) return _('Multisig %d of %d'%(self.m, len(self.xpub_list)))

2
lib/synchronizer.py

@ -178,6 +178,4 @@ class Synchronizer(ThreadJob):
up_to_date = self.is_up_to_date() up_to_date = self.is_up_to_date()
if up_to_date != self.wallet.is_up_to_date(): if up_to_date != self.wallet.is_up_to_date():
self.wallet.set_up_to_date(up_to_date) self.wallet.set_up_to_date(up_to_date)
if up_to_date:
self.wallet.save_transactions(write=True)
self.network.trigger_callback('updated') self.network.trigger_callback('updated')

180
lib/wallet.py

@ -25,6 +25,7 @@ import time
import json import json
import copy import copy
from functools import partial from functools import partial
from i18n import _
from util import NotEnoughFunds, PrintError, profiler from util import NotEnoughFunds, PrintError, profiler
@ -291,6 +292,7 @@ class Abstract_Wallet(PrintError):
def load_accounts(self): def load_accounts(self):
self.accounts = {} self.accounts = {}
d = self.storage.get('accounts', {}) d = self.storage.get('accounts', {})
removed = False
for k, v in d.items(): for k, v in d.items():
if self.wallet_type == 'old' and k in [0, '0']: if self.wallet_type == 'old' and k in [0, '0']:
v['mpk'] = self.storage.get('master_public_key') v['mpk'] = self.storage.get('master_public_key')
@ -300,12 +302,11 @@ class Abstract_Wallet(PrintError):
elif v.get('xpub'): elif v.get('xpub'):
self.accounts[k] = BIP32_Account(v) self.accounts[k] = BIP32_Account(v)
elif v.get('pending'): elif v.get('pending'):
try: removed = True
self.accounts[k] = PendingAccount(v)
except:
pass
else: else:
self.print_error("cannot load account", v) self.print_error("cannot load account", v)
if removed:
self.save_accounts()
def synchronize(self): def synchronize(self):
pass pass
@ -313,8 +314,17 @@ class Abstract_Wallet(PrintError):
def can_create_accounts(self): def can_create_accounts(self):
return False return False
def set_up_to_date(self,b): def needs_next_account(self):
with self.lock: self.up_to_date = b return self.can_create_accounts() and self.accounts_all_used()
def permit_account_naming(self):
return self.can_create_accounts()
def set_up_to_date(self, up_to_date):
with self.lock:
self.up_to_date = up_to_date
if up_to_date:
self.save_transactions(write=True)
def is_up_to_date(self): def is_up_to_date(self):
with self.lock: return self.up_to_date with self.lock: return self.up_to_date
@ -645,15 +655,6 @@ class Abstract_Wallet(PrintError):
amount = max(0, sendable - fee) amount = max(0, sendable - fee)
return amount, fee return amount, fee
def get_account_name(self, k):
return self.labels.get(k, self.accounts[k].get_name(k))
def get_account_names(self):
account_names = {}
for k in self.accounts.keys():
account_names[k] = self.get_account_name(k)
return account_names
def get_account_addresses(self, acc_id, include_change=True): def get_account_addresses(self, acc_id, include_change=True):
if acc_id is None: if acc_id is None:
addr_list = self.addresses(include_change) addr_list = self.addresses(include_change)
@ -1101,7 +1102,6 @@ class Abstract_Wallet(PrintError):
self.storage.write() self.storage.write()
def wait_until_synchronized(self, callback=None): def wait_until_synchronized(self, callback=None):
from i18n import _
def wait_for_wallet(): def wait_for_wallet():
self.set_up_to_date(False) self.set_up_to_date(False)
while not self.is_up_to_date(): while not self.is_up_to_date():
@ -1125,8 +1125,19 @@ class Abstract_Wallet(PrintError):
else: else:
self.synchronize() self.synchronize()
def accounts_to_show(self):
return self.accounts.keys()
def get_accounts(self): def get_accounts(self):
return self.accounts return {a_id: a for a_id, a in self.accounts.items()
if a_id in self.accounts_to_show()}
def get_account_name(self, k):
return self.labels.get(k, self.accounts[k].get_name(k))
def get_account_names(self):
ids = self.accounts_to_show()
return dict(zip(ids, map(self.get_account_name, ids)))
def add_account(self, account_id, account): def add_account(self, account_id, account):
self.accounts[account_id] = account self.accounts[account_id] = account
@ -1632,111 +1643,66 @@ class BIP32_Simple_Wallet(BIP32_Wallet):
class BIP32_HD_Wallet(BIP32_Wallet): class BIP32_HD_Wallet(BIP32_Wallet):
# wallet that can create accounts # wallet that can create accounts
def __init__(self, storage): def __init__(self, storage):
self.next_account = storage.get('next_account2', None)
BIP32_Wallet.__init__(self, storage) BIP32_Wallet.__init__(self, storage)
# Backwards-compatibility. Remove legacy "next_account2" and
# drop unused master public key to avoid duplicate errors
storage.put('next_account2', None)
self.master_public_keys.pop(self.next_derivation()[0], None)
def next_account_number(self):
assert (set(self.accounts.keys()) ==
set(['%d' % n for n in range(len(self.accounts))]))
return len(self.accounts)
def next_derivation(self):
account_id = '%d' % self.next_account_number()
return self.root_name + account_id + "'", account_id
def show_account(self, account_id):
return self.account_is_used(account_id) or account_id in self.labels
def last_account_id(self):
return '%d' % (self.next_account_number() - 1)
def accounts_to_show(self):
# The last account is shown only if named or used
result = list(self.accounts.keys())
last_id = self.last_account_id()
if not self.show_account(last_id):
result.remove(last_id)
return result
def can_create_accounts(self): def can_create_accounts(self):
return self.root_name in self.master_private_keys.keys() return self.root_name in self.master_private_keys.keys()
def addresses(self, b=True): def permit_account_naming(self):
l = BIP32_Wallet.addresses(self, b) return (self.can_create_accounts() and
if self.next_account: not self.show_account(self.last_account_id()))
_, _, _, next_address = self.next_account
if next_address not in l:
l.append(next_address)
return l
def get_address_index(self, address):
if self.next_account:
next_id, next_xpub, next_pubkey, next_address = self.next_account
if address == next_address:
return next_id, (0,0)
return BIP32_Wallet.get_address_index(self, address)
def num_accounts(self):
keys = []
for k, v in self.accounts.items():
if type(v) != BIP32_Account:
continue
keys.append(k)
i = 0
while True:
account_id = '%d'%i
if account_id not in keys:
break
i += 1
return i
def get_next_account(self, password):
account_id = '%d'%self.num_accounts()
derivation = self.root_name + "%d'"%int(account_id)
xpub, xprv = self.derive_xkeys(self.root_name, derivation, password)
self.add_master_public_key(derivation, xpub)
if xprv:
self.add_master_private_key(derivation, xprv, password)
account = BIP32_Account({'xpub':xpub})
addr, pubkey = account.first_address()
self.add_address(addr)
return account_id, xpub, pubkey, addr
def create_main_account(self, password): def create_main_account(self, password):
# First check the password is valid (this raises if it isn't). # First check the password is valid (this raises if it isn't).
self.check_password(password) self.check_password(password)
assert self.num_accounts() == 0 assert self.next_account_number() == 0
self.create_account('Main account', password) self.create_next_account(password, _('Main account'))
self.create_next_account(password)
def create_account(self, name, password): def create_next_account(self, password, label=None):
account_id, xpub, _, _ = self.get_next_account(password) derivation, account_id = self.next_derivation()
xpub, xprv = self.derive_xkeys(self.root_name, derivation, password)
self.add_master_public_key(derivation, xpub)
if xprv:
self.add_master_private_key(derivation, xprv, password)
account = BIP32_Account({'xpub':xpub}) account = BIP32_Account({'xpub':xpub})
self.add_account(account_id, account) self.add_account(account_id, account)
self.set_label(account_id, name) if label:
# add address of the next account self.set_label(account_id, label)
self.next_account = self.get_next_account(password)
self.storage.put('next_account2', self.next_account)
def account_is_pending(self, k):
return type(self.accounts.get(k)) == PendingAccount
def delete_pending_account(self, k):
assert type(self.accounts.get(k)) == PendingAccount
self.accounts.pop(k)
self.save_accounts() self.save_accounts()
def create_pending_account(self, name, password): def account_is_used(self, account_id):
if self.next_account is None: return self.accounts[account_id].is_used(self)
self.next_account = self.get_next_account(password)
self.storage.put('next_account2', self.next_account)
next_id, next_xpub, next_pubkey, next_address = self.next_account
if name:
self.set_label(next_id, name)
self.accounts[next_id] = PendingAccount({'pending':True, 'address':next_address, 'pubkey':next_pubkey})
self.save_accounts()
def synchronize(self):
# synchronize existing accounts
BIP32_Wallet.synchronize(self)
if self.next_account is None and not self.use_encryption:
try:
self.next_account = self.get_next_account(None)
self.storage.put('next_account2', self.next_account)
except:
self.print_error('cannot get next account')
# check pending account
if self.next_account is not None:
next_id, next_xpub, next_pubkey, next_address = self.next_account
if self.address_is_old(next_address):
self.print_error("creating account", next_id)
self.add_account(next_id, BIP32_Account({'xpub':next_xpub}))
# here the user should get a notification
self.next_account = None
self.storage.put('next_account2', self.next_account)
elif self.history.get(next_address, []):
if next_id not in self.accounts:
self.print_error("create pending account", next_id)
self.accounts[next_id] = PendingAccount({'pending':True, 'address':next_address, 'pubkey':next_pubkey})
self.save_accounts()
def accounts_all_used(self):
return all(self.account_is_used(acc_id) for acc_id in self.accounts)
class NewWallet(BIP32_Wallet, Mnemonic): class NewWallet(BIP32_Wallet, Mnemonic):

Loading…
Cancel
Save