URI: 
       tMake trustedcoin.py multi-window compatible - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 886192aba7f075cd01278604342c1cb26540972a
   DIR parent f90ca2684e3b366c7267a98cdfc5cc3cf78fc2fa
  HTML Author: Neil Booth <kyuupichan@gmail.com>
       Date:   Wed,  9 Sep 2015 16:24:11 +0900
       
       Make trustedcoin.py multi-window compatible
       
       Diffstat:
         M gui/qt/main_window.py               |       2 +-
         M lib/wallet.py                       |       4 ++--
         M plugins/trustedcoin.py              |     319 ++++++++++++++++---------------
       
       3 files changed, 163 insertions(+), 162 deletions(-)
       ---
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -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:
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -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):
       t@@ -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)
        
   DIR diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py
       t@@ -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
       t@@ -170,7 +170,6 @@ class TrustedCoinCosignerClient(object):
        
        server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION)
        
       -
        class Wallet_2fa(Multisig_Wallet):
        
            def __init__(self, storage):
       t@@ -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):
       t@@ -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):
       t@@ -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)
       t@@ -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
       t@@ -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:
       t@@ -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")
       t@@ -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
       t@@ -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):
       t@@ -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)
       t@@ -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)
       t@@ -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
       t@@ -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)
       t@@ -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()