electrum

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

commit 886192aba7f075cd01278604342c1cb26540972a
parent f90ca2684e3b366c7267a98cdfc5cc3cf78fc2fa
Author: Neil Booth <kyuupichan@gmail.com>
Date:   Wed,  9 Sep 2015 16:24:11 +0900

Make trustedcoin.py multi-window compatible

Diffstat:
Mgui/qt/main_window.py | 2+-
Mlib/wallet.py | 4++--
Mplugins/trustedcoin.py | 319++++++++++++++++++++++++++++++++++++++++---------------------------------------
3 files changed, 163 insertions(+), 162 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -1170,7 +1170,7 @@ class ElectrumWindow(QMainWindow, PrintError): def do_send(self): - if run_hook('before_send', self): + if run_hook('abort_send', self): return r = self.read_send_tab() if not r: diff --git a/lib/wallet.py b/lib/wallet.py @@ -972,7 +972,7 @@ class Abstract_Wallet(PrintError): # Sort the inputs and outputs deterministically tx.BIP_LI01_sort() - run_hook('make_unsigned_transaction', tx) + run_hook('make_unsigned_transaction', self, tx) return tx def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None): @@ -1022,7 +1022,7 @@ class Abstract_Wallet(PrintError): tx.sign(keypairs) # Run hook, and raise if error tx.error = None - run_hook('sign_transaction', tx, password) + run_hook('sign_transaction', self, tx, password) if tx.error: raise BaseException(tx.error) diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import threading +from threading import Thread import socket import os import re @@ -170,7 +170,6 @@ class TrustedCoinCosignerClient(object): server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION) - class Wallet_2fa(Multisig_Wallet): def __init__(self, storage): @@ -197,72 +196,111 @@ class Wallet_2fa(Multisig_Wallet): def estimated_fee(self, tx, fee_per_kb): fee = Multisig_Wallet.estimated_fee(self, tx, fee_per_kb) - x = run_hook('extra_fee', tx) + x = run_hook('extra_fee', self, tx) if x: fee += x return fee def get_tx_fee(self, tx): fee = Multisig_Wallet.get_tx_fee(self, tx) - x = run_hook('extra_fee', tx) + x = run_hook('extra_fee', self, tx) if x: fee += x return fee +# Utility functions +def get_user_id(wallet): + def make_long_id(xpub_hot, xpub_cold): + return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) -class Plugin(BasePlugin): + xpub_hot = wallet.master_public_keys["x1/"] + xpub_cold = wallet.master_public_keys["x2/"] + long_id = make_long_id(xpub_hot, xpub_cold) + short_id = hashlib.sha256(long_id).hexdigest() + return long_id, short_id + +def make_xpub(xpub, s): + _, _, _, c, cK = deserialize_xkey(xpub) + cK2, c2 = bitcoin._CKD_pub(cK, c, s) + xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2 + return EncodeBase58Check(xpub2) + +def restore_third_key(wallet): + long_user_id, short_id = get_user_id(wallet) + xpub3 = make_xpub(signing_xpub, long_user_id) + wallet.add_master_public_key('x3/', xpub3) + +def make_billing_address(wallet, num): + long_id, short_id = get_user_id(wallet) + xpub = make_xpub(billing_xpub, long_id) + _, _, _, c, cK = deserialize_xkey(xpub) + cK, c = bitcoin.CKD_pub(cK, c, num) + address = public_key_to_bc_address( cK ) + return address + +def need_server(wallet, tx): + from electrum.account import BIP32_Account + # Detect if the server is needed + long_id, short_id = get_user_id(wallet) + xpub3 = wallet.master_public_keys['x3/'] + for x in tx.inputs_to_sign(): + if x[0:2] == 'ff': + xpub, sequence = BIP32_Account.parse_xpubkey(x) + if xpub == xpub3: + return True + return False - wallet = None +class Plugin(BasePlugin): def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) self.seed_func = lambda x: bitcoin.is_new_seed(x, SEED_PREFIX) - self.billing_info = None - self.is_billing = False + # Keyed by wallet to handle multiple pertinent windows. Each + # wallet is a 2fa wallet. Each value is a dictionary with + # information about the wallet for the plugin + self.wallets = {} def constructor(self, s): return Wallet_2fa(s) def is_available(self): - if not self.wallet: - return False - if self.wallet.storage.get('wallet_type') == '2fa': - return True - return False + return bool(self.wallets) - def set_enabled(self, enabled): - self.wallet.storage.put('use_' + self.name, enabled) + def set_enabled(self, wallet, enabled): + wallet.storage.put('use_' + self.name, enabled) def is_enabled(self): - if not self.is_available(): - return False - if self.wallet.master_private_keys.get('x2/'): - return False + return (self.is_available() and + not all(wallet.master_private_keys.get('x2/') + for wallet in self.wallets.keys())) + + def on_new_window(self, window): + wallet = window.wallet + if wallet.storage.get('wallet_type') == '2fa': + button = StatusBarButton(QIcon(":icons/trustedcoin.png"), + _("TrustedCoin"), + partial(self.settings_dialog, window)) + window.statusBar().addPermanentWidget(button) + self.wallets[wallet] = { + 'is_billing' : False, + 'billing_info' : None, + 'button' : button, # Avoid loss to GC + } + t = Thread(target=self.request_billing_info, args=(wallet,)) + t.setDaemon(True) + t.start() + + def on_close_window(self, window): + self.wallets.pop(window.wallet, None) + + def request_billing_info(self, wallet): + billing_info = server.get(get_user_id(wallet)[1]) + billing_address = make_billing_address(wallet, billing_info['billing_index']) + assert billing_address == billing_info['billing_address'] + wallet_info = self.wallets[wallet] + wallet_info['billing_info'] = billing_info + wallet_info['price_per_tx'] = dict(billing_info['price_per_tx']) return True - def make_long_id(self, xpub_hot, xpub_cold): - return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) - - def get_user_id(self): - xpub_hot = self.wallet.master_public_keys["x1/"] - xpub_cold = self.wallet.master_public_keys["x2/"] - long_id = self.make_long_id(xpub_hot, xpub_cold) - short_id = hashlib.sha256(long_id).hexdigest() - return long_id, short_id - - def make_xpub(self, xpub, s): - _, _, _, c, cK = deserialize_xkey(xpub) - cK2, c2 = bitcoin._CKD_pub(cK, c, s) - xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2 - return EncodeBase58Check(xpub2) - - def make_billing_address(self, num): - long_id, short_id = self.get_user_id() - xpub = self.make_xpub(billing_xpub, long_id) - _, _, _, c, cK = deserialize_xkey(xpub) - cK, c = bitcoin.CKD_pub(cK, c, num) - address = public_key_to_bc_address( cK ) - return address - def create_extended_seed(self, wallet, window): seed = wallet.make_seed() if not window.show_seed(seed, None): @@ -309,34 +347,12 @@ class Plugin(BasePlugin): icon = QPixmap(':icons/trustedcoin.png') if not window.question(''.join(msg), icon=icon): return False - self.wallet = wallet - self.set_enabled(True) + self.set_enabled(wallet, True) return True - - def restore_third_key(self, wallet): - long_user_id, short_id = self.get_user_id() - xpub3 = self.make_xpub(signing_xpub, long_user_id) - wallet.add_master_public_key('x3/', xpub3) - @hook def do_clear(self, window): - self.is_billing = False - - @hook - def load_wallet(self, wallet, window): - self.wallet = wallet - self.trustedcoin_button = StatusBarButton(QIcon(":icons/trustedcoin.png"), _("TrustedCoin"), partial(self.settings_dialog, window)) - window.statusBar().addPermanentWidget(self.trustedcoin_button) - self.xpub = self.wallet.master_public_keys.get('x1/') - self.user_id = self.get_user_id()[1] - t = threading.Thread(target=self.request_billing_info) - t.setDaemon(True) - t.start() - - @hook - def installwizard_load_wallet(self, wallet, window): - self.wallet = wallet + self.wallets[window.wallet]['is_billing'] = False @hook def get_wizard_action(self, window, wallet, action): @@ -352,7 +368,6 @@ class Plugin(BasePlugin): if not seed: return wallet = Wallet_2fa(storage) - self.wallet = wallet password = window.password_dialog() wallet.add_seed(seed, password) @@ -361,16 +376,14 @@ class Plugin(BasePlugin): wallet.add_cosigner_seed(' '.join(words[0:n]), 'x1/', password) wallet.add_cosigner_seed(' '.join(words[n:]), 'x2/', password) - self.restore_third_key(wallet) + restore_third_key(wallet) wallet.create_main_account(password) # disable plugin - self.set_enabled(False) + self.set_enabled(wallet, False) return wallet def create_remote_key(self, wallet, window): - self.wallet = wallet - if wallet.storage.get('wallet_type') != '2fa': raise return @@ -383,8 +396,8 @@ class Plugin(BasePlugin): xpub_cold = wallet.master_public_keys["x2/"] # Generate third key deterministically. - long_user_id, self.user_id = self.get_user_id() - xpub3 = self.make_xpub(signing_xpub, long_user_id) + long_user_id, short_id = get_user_id(wallet) + xpub3 = make_xpub(signing_xpub, long_user_id) # secret must be sent by the server try: @@ -408,65 +421,72 @@ class Plugin(BasePlugin): _xpub3 = r['xpubkey_cosigner'] _id = r['id'] try: - assert _id == self.user_id, ("user id error", _id, self.user_id) + assert _id == short_id, ("user id error", _id, short_id) assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3) except Exception as e: window.show_message(str(e)) return - if not self.setup_google_auth(window, self.user_id, otp_secret): + if not self.setup_google_auth(window, short_id, otp_secret): return - self.wallet.add_master_public_key('x3/', xpub3) + wallet.add_master_public_key('x3/', xpub3) return True - - - def need_server(self, tx): - from electrum.account import BIP32_Account - # Detect if the server is needed - long_id, short_id = self.get_user_id() - xpub3 = self.wallet.master_public_keys['x3/'] - for x in tx.inputs_to_sign(): - if x[0:2] == 'ff': - xpub, sequence = BIP32_Account.parse_xpubkey(x) - if xpub == xpub3: - return True - return False + def auth_dialog(self, window): + d = QDialog(window) + d.setModal(1) + vbox = QVBoxLayout(d) + pw = AmountEdit(None, is_int = True) + msg = _('Please enter your Google Authenticator code') + vbox.addWidget(QLabel(msg)) + grid = QGridLayout() + grid.setSpacing(8) + grid.addWidget(QLabel(_('Code')), 1, 0) + grid.addWidget(pw, 1, 1) + vbox.addLayout(grid) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + if not d.exec_(): + return + return pw.get_amount() @hook def sign_tx(self, window, tx): self.print_error("twofactor:sign_tx") - if self.wallet.storage.get('wallet_type') != '2fa': - return - - if not self.need_server(tx): - self.print_error("twofactor: xpub3 not needed") - self.auth_code = None - return - - self.auth_code = self.auth_dialog(window) + if window.wallet in self.wallets: + auth_code = None + if need_server(window.wallet, tx): + auth_code = self.auth_dialog(window) + else: + self.print_error("twofactor: xpub3 not needed") + self.wallets[window.wallet]['auth_code'] = auth_code @hook - def before_send(self, window): - # request billing info before forming the transaction - self.billing_info = None - self.waiting_dialog = WaitingDialog(window, 'please wait...', self.request_billing_info) - self.waiting_dialog.start() - self.waiting_dialog.wait() - if self.billing_info is None: - window.show_message('Could not contact server') - return True + def abort_send(self, window): + if window.wallet in self.wallets: + wallet_info = self.wallets[window.wallet] + # request billing info before forming the transaction + wallet_info['billing_info'] = None + task = partial(self.request_billing_info, window.wallet) + waiting_dialog = WaitingDialog(window, 'please wait...', task) + waiting_dialog.start() + waiting_dialog.wait() + if wallet_info['billing_info'] is None: + window.show_message('Could not contact server') + return True return False @hook - def extra_fee(self, tx): - if self.billing_info.get('tx_remaining'): + def extra_fee(self, wallet, tx): + if not wallet in self.wallets: + return 0 + wallet_info = self.wallets[wallet] + if wallet_info['billing_info'].get('tx_remaining'): return 0 - if self.is_billing: + if wallet_info['is_billing']: return 0 # trustedcoin won't charge if the total inputs is lower than their fee - price = int(self.price_per_tx.get(1)) + price = int(wallet_info['price_per_tx'].get(1)) assert price <= 100000 if tx.input_value() < price: self.print_error("not charging for this tx") @@ -474,31 +494,34 @@ class Plugin(BasePlugin): return price @hook - def make_unsigned_transaction(self, tx): - price = self.extra_fee(tx) - if not price: - return - tx.outputs.append(('address', self.billing_info['billing_address'], price)) + def make_unsigned_transaction(self, wallet, tx): + if wallet in self.wallets: + price = self.extra_fee(wallet, tx) + if not price: + return + address = self.wallets[wallet]['billing_info']['billing_address'] + tx.outputs.append(('address', address, price)) @hook - def sign_transaction(self, tx, password): + def sign_transaction(self, wallet, tx, password): self.print_error("twofactor:sign") - if self.wallet.storage.get('wallet_type') != '2fa': - self.print_error("twofactor: aborting") + if not wallet in self.wallets: + self.print_error("not 2fa wallet") return - self.long_user_id, self.user_id = self.get_user_id() - - if not self.auth_code: + auth_code = self.wallets[wallet]['auth_code'] + if not auth_code: + self.print_error("sign_transaction: no auth code") return if tx.is_complete(): return + long_user_id, short_id = get_user_id(wallet) tx_dict = tx.as_dict() raw_tx = tx_dict["hex"] try: - r = server.sign(self.user_id, raw_tx, self.auth_code) + r = server.sign(short_id, raw_tx, auth_code) except Exception as e: tx.error = str(e) return @@ -511,26 +534,9 @@ class Plugin(BasePlugin): tx.update(raw_tx) self.print_error("twofactor: is complete", tx.is_complete()) - - def auth_dialog(self, window): - d = QDialog(window) - d.setModal(1) - vbox = QVBoxLayout(d) - pw = AmountEdit(None, is_int = True) - msg = _('Please enter your Google Authenticator code') - vbox.addWidget(QLabel(msg)) - grid = QGridLayout() - grid.setSpacing(8) - grid.addWidget(QLabel(_('Code')), 1, 0) - grid.addWidget(pw, 1, 1) - vbox.addLayout(grid) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - if not d.exec_(): - return - return pw.get_amount() - def settings_dialog(self, window): - self.waiting_dialog = WaitingDialog(window, 'please wait...', self.request_billing_info, partial(self.show_settings_dialog, window)) + task = partial(self.request_billing_info, window.wallet) + self.waiting_dialog = WaitingDialog(window, 'please wait...', task, partial(self.show_settings_dialog, window)) self.waiting_dialog.start() def show_settings_dialog(self, window, success): @@ -538,6 +544,7 @@ class Plugin(BasePlugin): window.show_message(_('Server not reachable.')) return + wallet = window.wallet d = QDialog(window) d.setWindowTitle("TrustedCoin Information") d.setMinimumSize(500, 200) @@ -569,16 +576,17 @@ class Plugin(BasePlugin): grid = QGridLayout() vbox.addLayout(grid) - v = self.price_per_tx.get(1) + price_per_tx = self.wallets[wallet]['price_per_tx'] + v = price_per_tx.get(1) grid.addWidget(QLabel(_("Price per transaction (not prepaid):")), 0, 0) grid.addWidget(QLabel(window.format_amount(v) + ' ' + window.base_unit()), 0, 1) i = 1 - if 10 not in self.price_per_tx: - self.price_per_tx[10] = 10 * self.price_per_tx.get(1) + if 10 not in price_per_tx: + price_per_tx[10] = 10 * price_per_tx.get(1) - for k, v in sorted(self.price_per_tx.items()): + for k, v in sorted(price_per_tx.items()): if k == 1: continue grid.addWidget(QLabel("Price for %d prepaid transactions:"%k), i, 0) @@ -588,7 +596,7 @@ class Plugin(BasePlugin): grid.addWidget(b, i, 2) i += 1 - n = self.billing_info.get('tx_remaining', 0) + n = self.wallets[wallet]['billing_info'].get('tx_remaining', 0) grid.addWidget(QLabel(_("Your wallet has %d prepaid transactions.")%n), i, 0) # tranfer button @@ -608,21 +616,14 @@ class Plugin(BasePlugin): d.close() if window.pluginsdialog: window.pluginsdialog.close() - uri = "bitcoin:" + self.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) - self.is_billing = True + wallet_info = self.wallets[window.wallet] + uri = "bitcoin:" + wallet_info['billing_info']['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) + wallet_info['is_billing'] = True window.pay_to_URI(uri) window.payto_e.setFrozen(True) window.message_e.setFrozen(True) window.amount_e.setFrozen(True) - def request_billing_info(self): - billing_info = server.get(self.user_id) - billing_address = self.make_billing_address(billing_info['billing_index']) - assert billing_address == billing_info['billing_address'] - self.billing_info = billing_info - self.price_per_tx = dict(self.billing_info['price_per_tx']) - return True - def accept_terms_of_use(self, window): vbox = QVBoxLayout() window.set_layout(vbox) @@ -649,7 +650,7 @@ class Plugin(BasePlugin): tos_e.setText(self.TOS) window.connect(window, SIGNAL('twofactor:TOS'), on_result) - t = threading.Thread(target=request_TOS) + t = Thread(target=request_TOS) t.setDaemon(True) t.start()