URI: 
       tDisentangle plugins and window management; use Qt signals - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 175fdbcac6fc1bd5ed54ceb709200b746877935d
   DIR parent b727824eedd7324133e18f698cf7babc156fd801
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sat, 21 Nov 2015 15:24:38 +0100
       
       Disentangle plugins and window management; use Qt signals
       
       Diffstat:
         M gui/qt/__init__.py                  |       5 +++--
         M gui/qt/main_window.py               |       3 +--
         M lib/plugins.py                      |      23 ++---------------------
         M lib/wallet.py                       |       5 -----
         M plugins/cosigner_pool.py            |      47 ++++++++++++++-----------------
         M plugins/email_requests.py           |       6 ++----
         M plugins/exchange_rate.py            |      98 ++++++++++++++++----------------
         M plugins/labels.py                   |       2 ++
         M plugins/trustedcoin.py              |     199 +++++++++++++------------------
       
       9 files changed, 165 insertions(+), 223 deletions(-)
       ---
   DIR diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py
       t@@ -91,6 +91,7 @@ class ElectrumGui:
                self.build_tray_menu()
                self.tray.show()
                self.app.connect(self.app, QtCore.SIGNAL('new_window'), self.start_new_window)
       +        run_hook('init_qt', self)
        
            def build_tray_menu(self):
                # Avoid immediate GC of old menu when window closed via its action
       t@@ -217,7 +218,7 @@ class ElectrumGui:
                        w.show()
                    self.windows.append(w)
                    self.build_tray_menu()
       -            self.plugins.on_new_window(w)
       +            run_hook('on_new_window', w)
        
                if uri:
                    w.pay_to_URI(uri)
       t@@ -227,7 +228,7 @@ class ElectrumGui:
            def close_window(self, window):
                self.windows.remove(window)
                self.build_tray_menu()
       -        self.plugins.on_close_window(window)
       +        run_hook('on_close_window', window)
        
            def main(self):
                self.timer.start()
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -565,8 +565,6 @@ class ElectrumWindow(QMainWindow, PrintError):
        
            def update_wallet(self):
                self.update_status()
       -        if self.wallet is None:
       -            return
                if self.wallet.up_to_date or not self.network or not self.network.is_connected():
                    self.update_tabs()
        
       t@@ -2868,6 +2866,7 @@ class ElectrumWindow(QMainWindow, PrintError):
                    p = plugins.toggle_enabled(self.config, name)
                    cb.setChecked(bool(p))
                    enable_settings_widget(p, name, i)
       +            run_hook('init_qt', self.gui_object)
        
                for i, descr in enumerate(plugins.descriptions):
                    name = descr['name']
   DIR diff --git a/lib/plugins.py b/lib/plugins.py
       t@@ -39,7 +39,6 @@ class Plugins(PrintError):
                    self.pathname = None
        
                self.plugins = {}
       -        self.windows = []
                self.network = None
                self.descriptions = plugins.descriptions
                for item in self.descriptions:
       t@@ -52,6 +51,7 @@ class Plugins(PrintError):
                    if config.get('use_' + name):
                        self.load_plugin(config, name)
        
       +
            def get(self, name):
                return self.plugins.get(name)
        
       t@@ -67,9 +67,6 @@ class Plugins(PrintError):
                    else:
                        p = __import__(full_name, fromlist=['electrum_plugins'])
                    plugin = p.Plugin(self, config, name)
       -            # Inform the plugin of our windows
       -            for window in self.windows:
       -                plugin.on_new_window(window)
                    if self.network:
                        self.network.add_jobs(plugin.thread_jobs())
                    self.plugins[name] = plugin
       t@@ -80,6 +77,7 @@ class Plugins(PrintError):
                    traceback.print_exc(file=sys.stdout)
                    return None
        
       +
            def close_plugin(self, plugin):
                if self.network:
                    self.network.remove_jobs(plugin.thread_jobs())
       t@@ -132,17 +130,6 @@ class Plugins(PrintError):
                    if network:
                        network.add_jobs(jobs)
        
       -    def trigger(self, event, *args, **kwargs):
       -        for plugin in self.plugins.values():
       -            getattr(plugin, event)(*args, **kwargs)
       -
       -    def on_new_window(self, window):
       -        self.windows.append(window)
       -        self.trigger('on_new_window', window)
       -
       -    def on_close_window(self, window):
       -        self.windows.remove(window)
       -        self.trigger('on_close_window', window)
        
        
        hook_names = set()
       t@@ -226,9 +213,3 @@ class BasePlugin(PrintError):
            def settings_dialog(self):
                pass
        
       -    # Events
       -    def on_close_window(self, window):
       -        pass
       -
       -    def on_new_window(self, window):
       -        pass
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -1046,11 +1046,6 @@ class Abstract_Wallet(PrintError):
                # Sign
                if keypairs:
                    tx.sign(keypairs)
       -        # Run hook, and raise if error
       -        tx.error = None
       -        run_hook('sign_transaction', self, tx, password)
       -        if tx.error:
       -            raise BaseException(tx.error)
        
        
            def sendtx(self, tx):
   DIR diff --git a/plugins/cosigner_pool.py b/plugins/cosigner_pool.py
       t@@ -89,43 +89,38 @@ class Plugin(BasePlugin):
                self.keys = []
                self.cosigner_list = []
        
       +    @hook
            def on_new_window(self, window):
       -        self.update()
       +        self.update(window)
        
       +    @hook
            def on_close_window(self, window):
       -        self.update()
       -
       -    def available_wallets(self):
       -        result = {}
       -        for window in self.parent.windows:
       -            if window.wallet.wallet_type in ['2of2', '2of3']:
       -                result[window.wallet] = window
       -        return result
       +        self.update(window)
        
            def is_available(self):
       -        return bool(self.available_wallets())
       -
       -    def update(self):
       -        wallets = self.available_wallets()
       -        if wallets:
       -            if self.listener is None:
       -                self.print_error("starting listener")
       -                self.listener = Listener(self)
       -                self.listener.start()
       +        return True
       +
       +    def update(self, window):
       +        wallet = window.wallet
       +        if wallet.wallet_type not in ['2of2', '2of3']:
       +            return
       +        if self.listener is None:
       +            self.print_error("starting listener")
       +            self.listener = Listener(self)
       +            self.listener.start()
                elif self.listener:
                    self.print_error("shutting down listener")
                    self.listener.stop()
                    self.listener = None
                self.keys = []
                self.cosigner_list = []
       -        for wallet, window in wallets.items():
       -            for key, xpub in wallet.master_public_keys.items():
       -                K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex')
       -                _hash = bitcoin.Hash(K).encode('hex')
       -                if wallet.master_private_keys.get(key):
       -                    self.keys.append((key, _hash, window))
       -                else:
       -                    self.cosigner_list.append((window, xpub, K, _hash))
       +        for key, xpub in wallet.master_public_keys.items():
       +            K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex')
       +            _hash = bitcoin.Hash(K).encode('hex')
       +            if wallet.master_private_keys.get(key):
       +                self.keys.append((key, _hash, window))
       +            else:
       +                self.cosigner_list.append((window, xpub, K, _hash))
                if self.listener:
                    self.listener.set_keyhashes([t[1] for t in self.keys])
        
   DIR diff --git a/plugins/email_requests.py b/plugins/email_requests.py
       t@@ -129,10 +129,8 @@ class Plugin(BasePlugin):
                self.obj.emit(SIGNAL('email:new_invoice'))
        
            def new_invoice(self):
       -        if self.parent.windows:
       -            window = self.parent.windows[0]
       -            window.invoices.add(self.pr)
       -            window.update_invoices_list()
       +        self.parent.invoices.add(self.pr)
       +        #window.update_invoices_list()
        
            @hook
            def receive_list_menu(self, menu, addr):
   DIR diff --git a/plugins/exchange_rate.py b/plugins/exchange_rate.py
       t@@ -30,10 +30,12 @@ CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
                          'XPF': 0}
        
        class ExchangeBase(PrintError):
       -    def __init__(self, sig):
       +
       +    def __init__(self, on_quotes, on_history):
                self.history = {}
                self.quotes = {}
       -        self.sig = sig
       +        self.on_quotes = on_quotes
       +        self.on_history = on_history
        
            def protocol(self):
                return "https"
       t@@ -59,7 +61,7 @@ class ExchangeBase(PrintError):
                    self.print_error("getting fx quotes for", ccy)
                    self.quotes = self.get_rates(ccy)
                    self.print_error("received fx quotes")
       -            self.sig.emit(SIGNAL('fx_quotes'))
       +            self.on_quotes()
                except Exception, e:
                    self.print_error("failed fx quotes:", e)
        
       t@@ -73,7 +75,7 @@ class ExchangeBase(PrintError):
                    self.print_error("requesting fx history for", ccy)
                    self.history[ccy] = self.historical_rates(ccy)
                    self.print_error("received fx history for", ccy)
       -            self.sig.emit(SIGNAL("fx_history"))
       +            self.on_history()
                except Exception, e:
                    self.print_error("failed fx history:", e)
        
       t@@ -241,16 +243,11 @@ class Plugin(BasePlugin, ThreadJob):
        
            def __init__(self, parent, config, name):
                BasePlugin.__init__(self, parent, config, name)
       -        # Signal object first
       -        self.sig = QObject()
       -        self.sig.connect(self.sig, SIGNAL('fx_quotes'), self.on_fx_quotes)
       -        self.sig.connect(self.sig, SIGNAL('fx_history'), self.on_fx_history)
                self.ccy = self.config_ccy()
                self.history_used_spot = False
                self.ccy_combo = None
                self.hist_checkbox = None
       -        self.windows = dict()
       -
       +        self.app = None
                is_exchange = lambda obj: (inspect.isclass(obj)
                                           and issubclass(obj, ExchangeBase)
                                           and obj != ExchangeBase)
       t@@ -268,7 +265,7 @@ class Plugin(BasePlugin, ThreadJob):
        
            def run(self):
                # This runs from the network thread which catches exceptions
       -        if self.windows and self.timeout <= time.time():
       +        if self.timeout <= time.time():
                    self.timeout = time.time() + 150
                    self.exchange.update(self.ccy)
        
       t@@ -285,24 +282,26 @@ class Plugin(BasePlugin, ThreadJob):
            def show_history(self):
                return self.config_history() and self.exchange.history_ccys()
        
       +
            def set_exchange(self, name):
                class_ = self.exchanges.get(name) or self.exchanges.values()[0]
                name = class_.__name__
                self.print_error("using exchange", name)
                if self.config_exchange() != name:
                    self.config.set_key('use_exchange', name, True)
       -        self.exchange = class_(self.sig)
       +
       +        on_quotes = lambda: self.app.emit(SIGNAL('new_fx_quotes'))
       +        on_history = lambda: self.app.emit(SIGNAL('new_fx_history'))
       +        self.exchange = class_(on_quotes, on_history)
                # A new exchange means new fx quotes, initially empty.  Force
                # a quote refresh
                self.timeout = 0
                self.get_historical_rates()
       -        self.on_fx_quotes()
       +        #self.on_fx_quotes()
        
       -    def update_status_bars(self):
       -        '''Update status bar fiat balance in all windows'''
       -        for window in self.windows:
       -            window.update_status()
        
       +
       +    @hook
            def on_new_window(self, window):
                # Additional send and receive edit boxes
                send_e = AmountEdit(self.config_ccy)
       t@@ -311,20 +310,23 @@ class Plugin(BasePlugin, ThreadJob):
                    lambda: send_e.setFrozen(window.amount_e.isReadOnly()))
                receive_e = AmountEdit(self.config_ccy)
                window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft)
       -
       -        self.windows[window] = {'edits': (send_e, receive_e),
       -                                'last_edited': {}}
       +        window.fiat_send_e = send_e
       +        window.fiat_receive_e = receive_e
                self.connect_fields(window, window.amount_e, send_e, window.fee_e)
                self.connect_fields(window, window.receive_amount_e, receive_e, None)
                window.history_list.refresh_headers()
                window.update_status()
       +        window.connect(window.app, SIGNAL('new_fx_quotes'), lambda: self.on_fx_quotes(window))
       +        window.connect(window.app, SIGNAL('new_fx_history'), lambda: self.on_fx_history(window))
       +        window.connect(window.app, SIGNAL('close_fx_plugin'), lambda: self.restore_window(window))
       +        window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers)
       +
        
            def connect_fields(self, window, btc_e, fiat_e, fee_e):
       -        last_edited = self.windows[window]['last_edited']
        
                def edit_changed(edit):
                    edit.setStyleSheet(BLACK_FG)
       -            last_edited[(fiat_e, btc_e)] = edit
       +            fiat_e.is_last_edited = (edit == fiat_e)
                    amount = edit.get_amount()
                    rate = self.exchange_rate()
                    if rate is None or amount is None:
       t@@ -346,45 +348,43 @@ class Plugin(BasePlugin, ThreadJob):
        
                fiat_e.textEdited.connect(partial(edit_changed, fiat_e))
                btc_e.textEdited.connect(partial(edit_changed, btc_e))
       -        last_edited[(fiat_e, btc_e)] = btc_e
       +        fiat_e.is_last_edited = False
        
            @hook
       -    def do_clear(self, window):
       -        self.windows[window]['edits'][0].setText('')
       +    def init_qt(self, gui):
       +        self.app = gui.app
        
       -    def on_close_window(self, window):
       -        self.windows.pop(window)
       +    @hook
       +    def do_clear(self, window):
       +        window.fiat_send_e.setText('')
        
            def close(self):
                # Get rid of hooks before updating status bars.
                BasePlugin.close(self)
       -        self.update_status_bars()
       -        self.refresh_headers()
       -        for window, data in self.windows.items():
       -            for edit in data['edits']:
       -                edit.hide()
       -            window.update_status()
       -
       -    def refresh_headers(self):
       -        for window in self.windows:
       -            window.history_list.refresh_headers()
       -
       -    def on_fx_history(self):
       +        self.app.emit(SIGNAL('close_fx_plugin'))
       +
       +    def restore_window(self, window):
       +        window.update_status()
       +        window.history_list.refresh_headers()
       +        window.fiat_send_e.hide()
       +        window.fiat_receive_e.hide()
       +
       +    def on_fx_history(self, window):
                '''Called when historical fx quotes are updated'''
       -        for window in self.windows:
       -            window.history_list.update()
       +        window.history_list.update()
        
       -    def on_fx_quotes(self):
       +    def on_fx_quotes(self, window):
                '''Called when fresh spot fx quotes come in'''
       -        self.update_status_bars()
       +        window.update_status()
                self.populate_ccy_combo()
                # Refresh edits with the new rate
       -        for window, data in self.windows.items():
       -            for edit in data['last_edited'].values():
       -                edit.textEdited.emit(edit.text())
       +        edit = window.fiat_send_e if window.fiat_send_e.is_last_edited else window.amount_e
       +        edit.textEdited.emit(edit.text())
       +        edit = window.fiat_receive_e if window.fiat_receive_e.is_last_edited else window.receive_amount_e
       +        edit.textEdited.emit(edit.text())
                # History tab needs updating if it used spot
                if self.history_used_spot:
       -            self.on_fx_history()
       +            self.on_fx_history(window)
        
            def on_ccy_combo_change(self):
                '''Called when the chosen currency changes'''
       t@@ -392,7 +392,7 @@ class Plugin(BasePlugin, ThreadJob):
                if ccy and ccy != self.ccy:
                    self.ccy = ccy
                    self.config.set_key('currency', ccy, True)
       -            self.update_status_bars()
       +            self.app.emit(SIGNAL('new_fx_quotes'))
                    self.get_historical_rates() # Because self.ccy changes
                    self.hist_checkbox_update()
        
       t@@ -503,7 +503,7 @@ class Plugin(BasePlugin, ThreadJob):
                        self.get_historical_rates()
                    else:
                        self.config.set_key('history_rates', 'unchecked')
       -            self.refresh_headers()
       +            self.app.emit(SIGNAL('refresh_headers'))
        
                def ok_clicked():
                    self.timeout = 0
   DIR diff --git a/plugins/labels.py b/plugins/labels.py
       t@@ -35,6 +35,7 @@ class Plugin(BasePlugin):
                self.obj = QObject()
                self.obj.connect(self.obj, SIGNAL('labels:pulled'), self.on_pulled)
        
       +    @hook
            def on_new_window(self, window):
                wallet = window.wallet
                nonce = self.get_nonce(wallet)
       t@@ -53,6 +54,7 @@ class Plugin(BasePlugin):
                t.setDaemon(True)
                t.start()
        
       +    @hook
            def on_close_window(self, window):
                self.wallets.pop(window.wallet)
        
   DIR diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py
       t@@ -177,6 +177,8 @@ class Wallet_2fa(Multisig_Wallet):
                self.wallet_type = '2fa'
                self.m = 2
                self.n = 3
       +        self.is_billing = False
       +        self.billing_info = None
        
            def get_action(self):
                xpub1 = self.master_public_keys.get("x1/")
       t@@ -194,29 +196,68 @@ class Wallet_2fa(Multisig_Wallet):
            def make_seed(self):
                return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
        
       +    def can_sign_without_server(self):
       +        return self.master_private_keys.get('x2/') is not None
       +
       +    def extra_fee(self, tx):
       +        if self.can_sign_without_server():
       +            return 0
       +        if self.billing_info.get('tx_remaining'):
       +            return 0
       +        if self.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))
       +        assert price <= 100000
       +        if tx.input_value() < price:
       +            self.print_error("not charging for this tx")
       +            return 0
       +        return price
       +
            def estimated_fee(self, tx, fee_per_kb):
                fee = Multisig_Wallet.estimated_fee(self, tx, fee_per_kb)
       -        x = run_hook('extra_fee', self, tx)
       -        if x: fee += x
       +        fee += self.extra_fee(tx)
                return fee
        
            def get_tx_fee(self, tx):
                fee = Multisig_Wallet.get_tx_fee(self, tx)
       -        x = run_hook('extra_fee', self, tx)
       -        if x: fee += x
       +        fee += self.extra_fee(tx)
                return fee
        
       -# Utility functions
       +    def make_unsigned_transaction(self, *args):
       +        tx = BIP32_Wallet.make_unsigned_transaction(self, *args)
       +        fee = self.extra_fee(tx)
       +        if fee:
       +            address = self.billing_info['billing_address']
       +            tx.outputs.append(('address', address, fee))
       +        return tx
       +
       +    def sign_transaction(self, tx, password):
       +        BIP32_Wallet.sign_transaction(self, tx, password)
       +        if tx.is_complete():
       +            return
       +        if not self.auth_code:
       +            self.print_error("sign_transaction: no auth code")
       +            return
       +        long_user_id, short_id = self.get_user_id()
       +        tx_dict = tx.as_dict()
       +        raw_tx = tx_dict["hex"]
       +        r = server.sign(short_id, raw_tx, self.auth_code)
       +        if r:
       +            raw_tx = r.get('transaction')
       +            tx.update(raw_tx)
       +        self.print_error("twofactor: is complete", tx.is_complete())
        
       -def get_user_id(wallet):
       -    def make_long_id(xpub_hot, xpub_cold):
       -        return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
       +    def get_user_id(self):
       +        def make_long_id(xpub_hot, xpub_cold):
       +            return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
       +        xpub_hot = self.master_public_keys["x1/"]
       +        xpub_cold = self.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
        
       -    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
       +# Utility functions
        
        def make_xpub(xpub, s):
            _, _, _, c, cK = deserialize_xkey(xpub)
       t@@ -225,12 +266,12 @@ def make_xpub(xpub, s):
            return EncodeBase58Check(xpub2)
        
        def restore_third_key(wallet):
       -    long_user_id, short_id = get_user_id(wallet)
       +    long_user_id, short_id = wallet.get_user_id()
            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)
       +    long_id, short_id = wallet.get_user_id()
            xpub = make_xpub(billing_xpub, long_id)
            _, _, _, c, cK = deserialize_xkey(xpub)
            cK, c = bitcoin.CKD_pub(cK, c, num)
       t@@ -240,7 +281,7 @@ def make_billing_address(wallet, num):
        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)
       +    long_id, short_id = wallet.get_user_id()
            xpub3 = wallet.master_public_keys['x3/']
            for x in tx.inputs_to_sign():
                if x[0:2] == 'ff':
       t@@ -254,25 +295,20 @@ 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)
       -        # 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):
       -        return bool(self.wallets)
       +        return True
        
            def set_enabled(self, wallet, enabled):
                wallet.storage.put('use_' + self.name, enabled)
        
            def is_enabled(self):
       -        return (self.is_available() and
       -                not all(wallet.master_private_keys.get('x2/')
       -                        for wallet in self.wallets.keys()))
       +        return True
        
       +    @hook
            def on_new_window(self, window):
                wallet = window.wallet
                if wallet.storage.get('wallet_type') == '2fa':
       t@@ -280,25 +316,16 @@ class Plugin(BasePlugin):
                                             _("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_info = server.get(wallet.get_user_id()[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'])
       +        wallet.billing_info = billing_info
       +        wallet.price_per_tx = dict(billing_info['price_per_tx'])
                return True
        
            def create_extended_seed(self, wallet, window):
       t@@ -352,7 +379,7 @@ class Plugin(BasePlugin):
        
            @hook
            def do_clear(self, window):
       -        self.wallets[window.wallet]['is_billing'] = False
       +        window.wallet.is_billing = False
        
            @hook
            def get_wizard_action(self, window, wallet, action):
       t@@ -396,7 +423,7 @@ class Plugin(BasePlugin):
                xpub_cold = wallet.master_public_keys["x2/"]
        
                # Generate third key deterministically.
       -        long_user_id, short_id = get_user_id(wallet)
       +        long_user_id, short_id = wallet.get_user_id()
                xpub3 = make_xpub(signing_xpub, long_user_id)
        
                # secret must be sent by the server
       t@@ -453,86 +480,30 @@ class Plugin(BasePlugin):
            @hook
            def sign_tx(self, window, tx):
                self.print_error("twofactor:sign_tx")
       -        if window.wallet in self.wallets:
       +        wallet = window.wallet
       +        if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server():
                    auth_code = None
       -            if need_server(window.wallet, tx):
       +            if need_server(wallet, tx):
                        auth_code = self.auth_dialog(window)
                    else:
                        self.print_error("twofactor: xpub3 not needed")
       -            self.wallets[window.wallet]['auth_code'] = auth_code
       +            window.wallet.auth_code = auth_code
        
            @hook
            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
       +        wallet = window.wallet
       +        if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server():
       +            if wallet.billing_info is None:
       +                # request billing info before forming the transaction
       +                task = partial(self.request_billing_info, wallet)
       +                waiting_dialog = WaitingDialog(window, 'please wait...', task)
       +                waiting_dialog.start()
       +                waiting_dialog.wait()
       +                if wallet.billing_info is None:
       +                    window.show_message('Could not contact server')
       +                    return True
                return False
        
       -    @hook
       -    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 wallet_info['is_billing']:
       -            return 0
       -        # trustedcoin won't charge if the total inputs is lower than their fee
       -        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")
       -            return 0
       -        return price
       -
       -    @hook
       -    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, wallet, tx, password):
       -        self.print_error("twofactor:sign")
       -        if not wallet in self.wallets:
       -            self.print_error("not 2fa wallet")
       -            return
       -
       -        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(short_id, raw_tx, auth_code)
       -        except Exception as e:
       -            tx.error = str(e)
       -            return
       -
       -        self.print_error( "received answer", r)
       -        if not r:
       -            return
       -
       -        raw_tx = r.get('transaction')
       -        tx.update(raw_tx)
       -        self.print_error("twofactor: is complete", tx.is_complete())
        
            def settings_dialog(self, window):
                task = partial(self.request_billing_info, window.wallet)
       t@@ -576,7 +547,7 @@ class Plugin(BasePlugin):
                grid = QGridLayout()
                vbox.addLayout(grid)
        
       -        price_per_tx = self.wallets[wallet]['price_per_tx']
       +        price_per_tx = 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)
       t@@ -596,7 +567,7 @@ class Plugin(BasePlugin):
                    grid.addWidget(b, i, 2)
                    i += 1
        
       -        n = self.wallets[wallet]['billing_info'].get('tx_remaining', 0)
       +        n = wallet.billing_info.get('tx_remaining', 0)
                grid.addWidget(QLabel(_("Your wallet has %d prepaid transactions.")%n), i, 0)
        
                # tranfer button
       t@@ -616,9 +587,9 @@ class Plugin(BasePlugin):
                d.close()
                if window.pluginsdialog:
                    window.pluginsdialog.close()
       -        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
       +        wallet = window.wallet
       +        uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000)
       +        wallet.is_billing = True
                window.pay_to_URI(uri)
                window.payto_e.setFrozen(True)
                window.message_e.setFrozen(True)