From 5e7e002bd8e870a7c606d93c1f322c63a4e24386 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Tue, 12 Mar 2013 21:55:56 +0100 Subject: [PATCH 01/44] remove internal check that was too strong --- lib/wallet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index fbd3408f..5c3a7b87 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -640,7 +640,9 @@ class Wallet: def receive_tx_callback(self, tx_hash, tx, tx_height): if not self.check_new_tx(tx_hash, tx): - raise BaseException("error: received transaction is not consistent with history", tx_hash) + # may happen due to pruning + print_error("received transaction that is no longer referenced in history", tx_hash) + return with self.lock: self.transactions[tx_hash] = tx From 4e4a43b59e9e85b75dd9440749a938c0ef8462cc Mon Sep 17 00:00:00 2001 From: Maran Date: Tue, 12 Mar 2013 22:31:10 +0100 Subject: [PATCH 02/44] Ammended release notes about raw tx --- RELEASE-NOTES | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6accaab1..45f2e5b4 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -35,8 +35,7 @@ For an example, see Gavin's tutorial: https://gist.github.com/gavinandresen/3966 1. user creates an unsigned transaction using the online (watching-only) wallet. 2. unsigned transaction is copied to the offline computer, and signed by the offline wallet. 3. signed transaction is copied to the online computer, broadcasted by the online client. - -* Raw transactions can also be loaded/signed/broadcasted via the GUI. + 4. All these steps can be done via the command line interface or the classic GUI. * Many command line commands have been renamed in order to make the syntax consistent with bitcoind. From 4e3c9de1d0ca298dc8180eaa332c5028bf81fc2d Mon Sep 17 00:00:00 2001 From: ecdsa Date: Tue, 12 Mar 2013 22:56:58 +0100 Subject: [PATCH 03/44] catch http exception --- gui/exchange_rate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/exchange_rate.py b/gui/exchange_rate.py index c9cd7a79..cbfa9ca7 100644 --- a/gui/exchange_rate.py +++ b/gui/exchange_rate.py @@ -36,7 +36,10 @@ class Exchanger(threading.Thread): response = connection.getresponse() if response.reason == httplib.responses[httplib.NOT_FOUND]: return - response = json.loads(response.read()) + try: + response = json.loads(response.read()) + except: + return quote_currencies = {} try: for r in response: From 12d65f5e522a0edff0acc38b0e44d679d8716975 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Tue, 12 Mar 2013 23:10:43 +0100 Subject: [PATCH 04/44] better synchronize method --- lib/wallet.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 5c3a7b87..b315c208 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -566,6 +566,7 @@ class Wallet: if h == ['*']: continue for tx_hash, tx_height in h: tx = self.transactions.get(tx_hash) + if tx is None: raise BaseException("Wallet not synchronized") for output in tx.d.get('outputs'): if output.get('address') != addr: continue key = tx_hash + ":%d" % output.get('index') @@ -1157,22 +1158,6 @@ class WalletSynchronizer(threading.Thread): def is_running(self): with self.lock: return self.running - def synchronize_wallet(self): - new_addresses = self.wallet.synchronize() - if new_addresses: - self.subscribe_to_addresses(new_addresses) - self.wallet.up_to_date = False - return - - if not self.interface.is_up_to_date('synchronizer'): - if self.wallet.is_up_to_date(): - self.wallet.set_up_to_date(False) - self.was_updated = True - return - - self.wallet.set_up_to_date(True) - self.was_updated = True - def subscribe_to_addresses(self, addresses): messages = [] @@ -1204,15 +1189,30 @@ class WalletSynchronizer(threading.Thread): self.subscribe_to_addresses(self.wallet.addresses(True)) while self.is_running(): - # 1. send new requests - self.synchronize_wallet() + # 1. create new addresses + new_addresses = self.wallet.synchronize() + # request missing addresses + if new_addresses: + self.subscribe_to_addresses(new_addresses) + + # request missing transactions for tx_hash, tx_height in missing_tx: if (tx_hash, tx_height) not in requested_tx: self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer') requested_tx.append( (tx_hash, tx_height) ) missing_tx = [] + # detect if situation has changed + if not self.interface.is_up_to_date('synchronizer'): + if self.wallet.is_up_to_date(): + self.wallet.set_up_to_date(False) + self.was_updated = True + else: + if not self.wallet.is_up_to_date(): + self.wallet.set_up_to_date(True) + self.was_updated = True + if self.was_updated: self.interface.trigger_callback('updated') self.was_updated = False From a1be16105b3e1a589ffdfe92893437a91221338f Mon Sep 17 00:00:00 2001 From: ecdsa Date: Tue, 12 Mar 2013 23:53:56 +0100 Subject: [PATCH 05/44] start verifier right after initialization --- electrum | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum b/electrum index 16f94265..a3961e71 100755 --- a/electrum +++ b/electrum @@ -162,6 +162,7 @@ if __name__ == '__main__': gui.show_seed() verifier = WalletVerifier(interface, config) + verifier.start() wallet.set_verifier(verifier) synchronizer = WalletSynchronizer(wallet, config) synchronizer.start() @@ -181,7 +182,6 @@ if __name__ == '__main__': gui.password_dialog() wallet.save() - verifier.start() gui.main(url) wallet.save() @@ -244,6 +244,7 @@ if __name__ == '__main__': interface.start(wait=True) wallet.interface = interface verifier = WalletVerifier(interface, config) + verifier.start() wallet.set_verifier(verifier) print_msg("Recovering wallet...") @@ -361,6 +362,7 @@ if __name__ == '__main__': interface.start() wallet.interface = interface verifier = WalletVerifier(interface, config) + verifier.start() wallet.set_verifier(verifier) synchronizer = WalletSynchronizer(wallet, config) synchronizer.start() From 6a848564fa47de6bdad1aa20bf22bae186406331 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 00:55:08 +0100 Subject: [PATCH 06/44] do not raise exception on strange input scripts --- lib/deserialize.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/deserialize.py b/lib/deserialize.py index 5fe2ee6b..744261ce 100644 --- a/lib/deserialize.py +++ b/lib/deserialize.py @@ -356,7 +356,8 @@ def get_address_from_input_script(bytes): pubkeys = [ dec2[1][1].encode('hex'), dec2[2][1].encode('hex'), dec2[3][1].encode('hex') ] return pubkeys, signatures, hash_160_to_bc_address(hash_160(redeemScript), 5) - raise BaseException("no match for scriptsig") + print_error("cannot find address in input script", bytes.encode('hex')) + return "(None)" From 0aaafe85ad11e116c4b31f2ea255b38b2c7c53fb Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 01:03:51 +0100 Subject: [PATCH 07/44] fix --- lib/deserialize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/deserialize.py b/lib/deserialize.py index 744261ce..ca4c6358 100644 --- a/lib/deserialize.py +++ b/lib/deserialize.py @@ -3,6 +3,7 @@ # from bitcoin import public_key_to_bc_address, hash_160_to_bc_address, hash_encode, hash_160 +from util import print_error #import socket import time import struct From 469d17355d09af8bd17060b92daca6983286e9c7 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 01:08:30 +0100 Subject: [PATCH 08/44] fix --- lib/deserialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/deserialize.py b/lib/deserialize.py index ca4c6358..96972ce3 100644 --- a/lib/deserialize.py +++ b/lib/deserialize.py @@ -358,7 +358,7 @@ def get_address_from_input_script(bytes): return pubkeys, signatures, hash_160_to_bc_address(hash_160(redeemScript), 5) print_error("cannot find address in input script", bytes.encode('hex')) - return "(None)" + return [], [], "(None)" From e3bb6f8879ee03a2f63bfc5042cc0ed1f05fdb0c Mon Sep 17 00:00:00 2001 From: thomasv Date: Wed, 13 Mar 2013 14:23:10 +0100 Subject: [PATCH 09/44] 'import private keys' may import several keys --- gui/gui_classic.py | 77 ++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index dc7bc966..75da35e2 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -226,7 +226,6 @@ class StatusBarButton(QPushButton): - def waiting_dialog(f): s = Timer() @@ -248,19 +247,35 @@ def waiting_dialog(f): w.destroy() -def ok_cancel_buttons(dialog): +def ok_cancel_buttons(dialog, ok_label=_("OK") ): hbox = QHBoxLayout() hbox.addStretch(1) - b = QPushButton("Cancel") + b = QPushButton(_("Cancel")) hbox.addWidget(b) b.clicked.connect(dialog.reject) - b = QPushButton("OK") + b = QPushButton(ok_label) hbox.addWidget(b) b.clicked.connect(dialog.accept) b.setDefault(True) return hbox +def text_dialog(parent, title, label, ok_label): + dialog = QDialog(parent) + dialog.setMinimumWidth(500) + dialog.setWindowTitle(title) + dialog.setModal(1) + l = QVBoxLayout() + dialog.setLayout(l) + l.addWidget(QLabel(label)) + txt = QTextEdit() + l.addWidget(txt) + l.addLayout(ok_cancel_buttons(dialog, ok_label)) + if dialog.exec_(): + return unicode(txt.toPlainText()) + + + default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370],[370,200,130]] } @@ -1729,23 +1744,10 @@ class ElectrumWindow(QMainWindow): self.show_message("There was a problem sending your transaction:\n %s" % (result_message)) def do_process_from_text(self): - dialog = QDialog(self) - dialog.setMinimumWidth(500) - dialog.setWindowTitle(_('Input raw transaction')) - dialog.setModal(1) - l = QVBoxLayout() - dialog.setLayout(l) - l.addWidget(QLabel(_("Transaction:"))) - txt = QTextEdit() - l.addWidget(txt) - - ok_button = QPushButton(_("Load transaction")) - ok_button.setDefault(True) - ok_button.clicked.connect(dialog.accept) - l.addWidget(ok_button) - - dialog.exec_() - tx_dict = self.tx_dict_from_text(unicode(txt.toPlainText())) + text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction")) + if not text: + return + tx_dict = self.tx_dict_from_text(text) if tx_dict: self.create_process_transaction_window(tx_dict) @@ -1858,19 +1860,28 @@ class ElectrumWindow(QMainWindow): + _('Are you sure you understand what you are doing?'), 3, 4) if r == 4: return - text, ok = QInputDialog.getText(self, _('Import private key'), _('Private Key') + ':') - if not ok: return - sec = str(text).strip() - try: - addr = self.wallet.import_key(sec, password) - if not addr: - QMessageBox.critical(None, _("Unable to import key"), "error") + text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import")) + if not text: return + + text = str(text).split() + badkeys = [] + addrlist = [] + for key in text: + try: + addr = self.wallet.import_key(key, password) + except BaseException as e: + badkeys.append(key) + continue + if not addr: + badkeys.append(key) else: - QMessageBox.information(None, _("Key imported"), addr) - self.update_receive_tab() - self.update_history_tab() - except BaseException as e: - QMessageBox.critical(None, _("Unable to import key"), str(e)) + addrlist.append(addr) + if addrlist: + QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist)) + if badkeys: + QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys)) + self.update_receive_tab() + self.update_history_tab() def settings_dialog(self): From c3dc2d52846702c0a0341999ac9043cddbe9715e Mon Sep 17 00:00:00 2001 From: thomasv Date: Wed, 13 Mar 2013 14:29:50 +0100 Subject: [PATCH 10/44] fix: command line with no password --- lib/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/commands.py b/lib/commands.py index c8ac4ae1..85c53ac8 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -82,9 +82,10 @@ class Commands: self.wallet = wallet self.interface = interface self._callback = callback + self.password = None def _run(self, method, args, password_getter): - if method in protected_commands: + if method in protected_commands and self.wallet.use_encryption: self.password = apply(password_getter,()) f = eval('self.'+method) result = apply(f,args) From a4f977190e0a1f64f5badca430c492b2000709a6 Mon Sep 17 00:00:00 2001 From: thomasv Date: Wed, 13 Mar 2013 15:26:29 +0100 Subject: [PATCH 11/44] do not call exit() in the interface module --- electrum | 8 ++++++-- lib/interface.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/electrum b/electrum index a3961e71..dfa71112 100755 --- a/electrum +++ b/electrum @@ -241,7 +241,9 @@ if __name__ == '__main__': if not options.offline: interface = Interface(config) - interface.start(wait=True) + if not interface.start(wait=True): + print_msg("Not connected, aborting.") + sys.exit(1) wallet.interface = interface verifier = WalletVerifier(interface, config) verifier.start() @@ -359,7 +361,9 @@ if __name__ == '__main__': if cmd not in offline_commands and not options.offline: interface = Interface(config) interface.register_callback('connected', lambda: sys.stderr.write("Connected to " + interface.connection_msg + "\n")) - interface.start() + if not interface.start(wait=True): + print_msg("Not connected, aborting.") + sys.exit(1) wallet.interface = interface verifier = WalletVerifier(interface, config) verifier.start() diff --git a/lib/interface.py b/lib/interface.py index eeca9a4a..ea787dda 100644 --- a/lib/interface.py +++ b/lib/interface.py @@ -595,8 +595,8 @@ class Interface(threading.Thread): # wait until connection is established self.connect_event.wait() if not self.is_connected: - print_msg("Not connected, aborting.") - sys.exit(1) + return False + return True def run(self): while True: From 1f1693d29b7d6b50ebdddd08540756f1335c967d Mon Sep 17 00:00:00 2001 From: thomasv Date: Wed, 13 Mar 2013 15:31:24 +0100 Subject: [PATCH 12/44] bug fix: init_seed --- electrum | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum b/electrum index dfa71112..2d5532d6 100755 --- a/electrum +++ b/electrum @@ -236,8 +236,7 @@ if __name__ == '__main__': wallet.seed = None wallet.init_sequence(str(seed)) else: - wallet.seed = str(seed) - wallet.init_mpk( wallet.seed ) + wallet.init_seed( str(seed) ) if not options.offline: interface = Interface(config) From c9a7c583239dd5b15c3f30f01bcdcc8d97e038d1 Mon Sep 17 00:00:00 2001 From: thomasv Date: Wed, 13 Mar 2013 15:33:50 +0100 Subject: [PATCH 13/44] print created address in terminal, as in previous versions --- lib/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wallet.py b/lib/wallet.py index b315c208..4768e4ee 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -302,6 +302,7 @@ class Wallet: address = self.get_new_address( account, for_change, n) self.accounts[account][for_change].append(address) self.history[address] = [] + print_msg(address) return address From 1d3be5fb0b1d54a38b790aa894eb5e5ee6e59162 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 17:52:54 +0100 Subject: [PATCH 14/44] fix: number of arguments --- electrum | 2 +- lib/bitcoin.py | 75 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/electrum b/electrum index 2d5532d6..d85ecaa3 100755 --- a/electrum +++ b/electrum @@ -347,7 +347,7 @@ if __name__ == '__main__': sys.exit(1) if max_args < 0: - if len(args) > min_args: + if len(args) > min_args + 1: message = ' '.join(args[min_args:]) print_msg("Warning: Final argument was reconstructed from several arguments:", repr(message)) args = args[0:min_args] + [ message ] diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 1bfee8c9..1ede17d5 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -415,8 +415,8 @@ def CKD_prime(K, c, n): class ElectrumSequence: """ Privatekey(type,n) = Master_private_key + H(n|S|type) """ - def __init__(self, master_public_key, mpk2 = None, mpk3 = None): - self.master_public_key = master_public_key + def __init__(self, mpk, mpk2 = None, mpk3 = None): + self.mpk = mpk self.mpk2 = mpk2 self.mpk3 = mpk3 @@ -445,7 +445,7 @@ class ElectrumSequence: address = public_key_to_bc_address( pubkey.decode('hex') ) elif not self.mpk3: pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence, use_mpk2=True) + pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"] else: pubkey1 = self.get_pubkey(sequence) @@ -456,7 +456,7 @@ class ElectrumSequence: def get_pubkey(self, sequence, mpk=None): curve = SECP256k1 - if mpk is None: mpk = self.master_public_key + if mpk is None: mpk = self.mpk z = self.get_sequence(sequence, mpk) master_public_key = ecdsa.VerifyingKey.from_string( mpk.decode('hex'), curve = SECP256k1 ) pubkey_point = master_public_key.pubkey.point + z*curve.generator @@ -465,7 +465,7 @@ class ElectrumSequence: def get_private_key_from_stretched_exponent(self, sequence, secexp): order = generator_secp256k1.order() - secexp = ( secexp + self.get_sequence(sequence, self.master_public_key) ) % order + secexp = ( secexp + self.get_sequence(sequence, self.mpk) ) % order pk = number_to_string( secexp, generator_secp256k1.order() ) compressed = False return SecretToASecret( pk, compressed ) @@ -483,7 +483,7 @@ class ElectrumSequence: secexp = self.stretch_key(seed) master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) master_public_key = master_private_key.get_verifying_key().to_string().encode('hex') - if master_public_key != self.master_public_key: + if master_public_key != self.mpk: print_error('invalid password (mpk)') raise BaseException('Invalid password') return True @@ -499,8 +499,8 @@ class ElectrumSequence: redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript'] else: pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence,mpk=self.mpk2) - pubkey3 = self.get_pubkey(sequence,mpk=self.mpk3) + pubkey2 = self.get_pubkey(sequence, mpk=self.mpk2) + pubkey3 = self.get_pubkey(sequence, mpk=self.mpk3) pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key redeemScript = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)['redeemScript'] return pk_addr, redeemScript @@ -510,44 +510,71 @@ class ElectrumSequence: class BIP32Sequence: - def __init__(self, mpkc, mpkc2 = None): - self.master_public_key, self.master_chain = mpkc - if mpkc2: - self.master_public_key2, self.master_chain2 = mpkc2 - self.is_p2sh = True - else: - self.is_p2sh = False + def __init__(self, mpk, mpk2 = None, mpk3 = None): + self.mpk = mpk + self.mpk2 = mpk2 + self.mpk3 = mpk3 @classmethod def mpk_from_seed(klass, seed): master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) return master_public_key.encode('hex'), master_chain.encode('hex') - def get_pubkey(self, sequence, use_mpk2=False): - if not use_mpl2: - K = self.master_public_key.decode('hex') - chain = self.master_chain.decode('hex') - else: - K = self.master_public_key_2.decode('hex') - chain = self.master_chain_2.decode('hex') + def get_pubkey(self, sequence, mpk): + if not mpk: mpk = self.mpk + master_public_key, master_chain = self.mpk + K = master_public_key.decode('hex') + chain = master_chain.decode('hex') for i in sequence: K, K_compressed, chain = CKD_prime(K, chain, i) return K_compressed def get_address(self, sequence): - return hash_160_to_bc_address(hash_160(self.get_pubkey(sequence))) + if not self.mpk2: + pubkey = self.get_pubkey(sequence) + address = public_key_to_bc_address( pubkey.decode('hex') ) + elif not self.mpk3: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) + address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"] + else: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) + pubkey3 = self.get_pubkey(sequence, mpk = self.mpk3) + address = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)["address"] + return address - def get_private_key(self, seed, sequence): + def get_private_key(self, sequence, seed): k = self.master_secret chain = self.master_chain for i in sequence: k, k_compressed, chain = CKD(k, chain, i) return SecretToASecret(k0, True) + def get_private_keys(self, sequence_list, seed): + return [ self.get_private_key( sequence, seed) for sequence in sequence_list] + def check_seed(self, seed): master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) assert self.master_public_key == master_public_key + def get_input_info(self, sequence): + if not self.mpk2: + pk_addr = self.get_address(sequence) + redeemScript = None + elif not self.mpk3: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk=self.mpk2) + pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key + redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript'] + else: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk=self.mpk2) + pubkey3 = self.get_pubkey(sequence, mpk=self.mpk3) + pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key + redeemScript = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)['redeemScript'] + return pk_addr, redeemScript + ################################## transactions From c19e0f0b3fd4cf3a8e21ac7eaefa31474ae299d3 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 18:03:41 +0100 Subject: [PATCH 15/44] bip32 fixes --- lib/bitcoin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 1ede17d5..07a10467 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -545,8 +545,7 @@ class BIP32Sequence: return address def get_private_key(self, sequence, seed): - k = self.master_secret - chain = self.master_chain + k, chain = self.mpk for i in sequence: k, k_compressed, chain = CKD(k, chain, i) return SecretToASecret(k0, True) @@ -556,7 +555,7 @@ class BIP32Sequence: def check_seed(self, seed): master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) - assert self.master_public_key == master_public_key + assert self.mpk == master_public_key, master_chain def get_input_info(self, sequence): if not self.mpk2: From 2f31ca779dd06bb1822a386afd54ff488f09d282 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 18:20:05 +0100 Subject: [PATCH 16/44] fix bip32 get_private_key --- lib/bitcoin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 07a10467..a600505e 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -545,10 +545,12 @@ class BIP32Sequence: return address def get_private_key(self, sequence, seed): - k, chain = self.mpk + master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) + chain = master_chain + k = master_secret for i in sequence: - k, k_compressed, chain = CKD(k, chain, i) - return SecretToASecret(k0, True) + k, chain = CKD(k, chain, i) + return SecretToASecret(k, True) def get_private_keys(self, sequence_list, seed): return [ self.get_private_key( sequence, seed) for sequence in sequence_list] From b955c9ffa1fc58d9e9c588ce55ccf3a3bb5ab7eb Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 18:51:05 +0100 Subject: [PATCH 17/44] more bip32 related fixes --- lib/bitcoin.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index a600505e..1d297bac 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -364,10 +364,9 @@ random_seed = lambda n: "%032x"%ecdsa.util.randrange( pow(2,n) ) def bip32_init(seed): import hmac - + seed = seed.decode('hex') I = hmac.new("Bitcoin seed", seed, hashlib.sha512).digest() - print "seed", seed.encode('hex') master_secret = I[0:32] master_chain = I[32:] @@ -520,14 +519,14 @@ class BIP32Sequence: master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) return master_public_key.encode('hex'), master_chain.encode('hex') - def get_pubkey(self, sequence, mpk): + def get_pubkey(self, sequence, mpk = None): if not mpk: mpk = self.mpk master_public_key, master_chain = self.mpk K = master_public_key.decode('hex') chain = master_chain.decode('hex') for i in sequence: K, K_compressed, chain = CKD_prime(K, chain, i) - return K_compressed + return K_compressed.encode('hex') def get_address(self, sequence): if not self.mpk2: @@ -557,7 +556,7 @@ class BIP32Sequence: def check_seed(self, seed): master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) - assert self.mpk == master_public_key, master_chain + assert self.mpk == (master_public_key.encode('hex'), master_chain.encode('hex')) def get_input_info(self, sequence): if not self.mpk2: @@ -870,7 +869,7 @@ class Transaction: def test_bip32(): - seed = "ff000000000000000000000000000000".decode('hex') + seed = "ff000000000000000000000000000000" master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) print "secret key", master_secret.encode('hex') From 1d66deba693c6f87faabfd922d3e93d46ba5b484 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 18:51:38 +0100 Subject: [PATCH 18/44] parent->self in password dialog --- gui/gui_classic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 75da35e2..a56df6e6 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -1299,7 +1299,7 @@ class ElectrumWindow(QMainWindow): try: seed = self.wallet.decode_seed(password) except: - QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK')) + QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK')) return self.show_seed(seed, self) From f537f02e1f9ea7b1ea1e505f08e49b25a7bd7b9d Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 21:00:29 +0100 Subject: [PATCH 19/44] better seed dialog --- gui/gui_classic.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index a56df6e6..8f0477af 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -1606,20 +1606,22 @@ class ElectrumWindow(QMainWindow): d.setModal(1) vbox = QVBoxLayout() - msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.") + msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + '\n') vbox.addWidget(QLabel(msg)) grid = QGridLayout() grid.setSpacing(8) seed_e = QLineEdit() - grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0) + grid.addWidget(QLabel(_('Seed or master public key')), 1, 0) grid.addWidget(seed_e, 1, 1) + grid.addWidget(HelpButton(_("Your seed can be entered as a mnemonic (sequence of words), or as a hexadecimal string.")), 1, 3) gap_e = QLineEdit() gap_e.setText("5") grid.addWidget(QLabel(_('Gap limit')), 2, 0) grid.addWidget(gap_e, 2, 1) + grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3) gap_e.textChanged.connect(lambda: numbify(gap_e,True)) vbox.addLayout(grid) From 11552c2f23d5b02e14c79f6e34818fcedcbba89b Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 22:05:02 +0100 Subject: [PATCH 20/44] use an abstract class SequenceClass --- lib/wallet.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 4768e4ee..fb038fbc 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -93,8 +93,9 @@ class Wallet: self.tx_height = config.get('tx_height',{}) self.accounts = config.get('accounts', {}) # this should not include public keys + self.SequenceClass = ElectrumSequence self.sequences = {} - self.sequences[0] = ElectrumSequence(self.config.get('master_public_key')) + self.sequences[0] = self.SequenceClass(self.config.get('master_public_key')) if self.accounts.get(0) is None: self.accounts[0] = { 0:[], 1:[], 'name':'Main account' } @@ -161,13 +162,13 @@ class Wallet: self.seed = seed self.config.set_key('seed', self.seed, True) self.config.set_key('seed_version', self.seed_version, True) - mpk = ElectrumSequence.mpk_from_seed(self.seed) + mpk = self.SequenceClass.mpk_from_seed(self.seed) self.init_sequence(mpk) def init_sequence(self, mpk): self.config.set_key('master_public_key', mpk, True) - self.sequences[0] = ElectrumSequence(mpk) + self.sequences[0] = self.SequenceClass(mpk) self.accounts[0] = { 0:[], 1:[], 'name':'Main account' } self.config.set_key('accounts', self.accounts, True) From b7dd1699ad0a0577e2b12f7af784855eeee01ff0 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Wed, 13 Mar 2013 22:52:43 +0100 Subject: [PATCH 21/44] missing dirs in MANIFEST --- MANIFEST.in | 2 ++ make_packages | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index cff9c6dc..9ad40988 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,3 +22,5 @@ include scripts/servers include scripts/validate_tx include scripts/watch_address recursive-include data * +recursive-include locale *.mo +recursive-include docs * diff --git a/make_packages b/make_packages index 936c48a9..5085cff4 100755 --- a/make_packages +++ b/make_packages @@ -10,7 +10,7 @@ if __name__ == '__main__': sys.exit() os.system("python mki18n.py") - os.system("pyrcc4 icons.qrc -o lib/icons_rc.py") + os.system("pyrcc4 icons.qrc -o gui/icons_rc.py") os.system("python setup.py sdist --format=zip,gztar") _tgz="Electrum-%s.tar.gz"%version From bdb515dabdaacc56a4e2ee7183ce1100de5db622 Mon Sep 17 00:00:00 2001 From: thomasv Date: Thu, 14 Mar 2013 10:49:00 +0100 Subject: [PATCH 22/44] print error received by verifier, and continue --- lib/verifier.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/verifier.py b/lib/verifier.py index 1b80d6b0..75773576 100644 --- a/lib/verifier.py +++ b/lib/verifier.py @@ -145,6 +145,10 @@ class WalletVerifier(threading.Thread): continue if not r: continue + if r.get('error'): + print_error('Verifier received an error:', r) + continue + # 3. handle response method = r['method'] params = r['params'] From 3b80ef7c6046398d7d086e567cef4a0b38a1a7bd Mon Sep 17 00:00:00 2001 From: thomasv Date: Thu, 14 Mar 2013 12:22:06 +0100 Subject: [PATCH 23/44] rely only on the verifier to get the height of transactions --- lib/verifier.py | 30 +++++++++++++++++++----------- lib/wallet.py | 29 ++--------------------------- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/lib/verifier.py b/lib/verifier.py index 75773576..81a76619 100644 --- a/lib/verifier.py +++ b/lib/verifier.py @@ -49,14 +49,10 @@ class WalletVerifier(threading.Thread): def get_confirmations(self, tx): """ return the number of confirmations of a monitored transaction. """ with self.lock: - if tx in self.transactions.keys(): - if tx in self.verified_tx: - height, timestamp = self.verified_tx[tx] - conf = (self.local_height - height + 1) - else: - conf = -1 + if tx in self.verified_tx: + height, timestamp = self.verified_tx[tx] + conf = (self.local_height - height + 1) else: - #print "verifier: tx not in list", tx conf = 0 if conf <= 0: @@ -65,6 +61,13 @@ class WalletVerifier(threading.Thread): return conf, timestamp + def get_height(self, tx_hash): + with self.lock: + v = self.verified_tx.get(tx_hash) + height = v[0] if v else None + return height + + def add(self, tx_hash, tx_height): """ add a transaction to the list of monitored transactions. """ assert tx_height > 0 @@ -187,7 +190,8 @@ class WalletVerifier(threading.Thread): # we passed all the tests header = self.read_header(tx_height) timestamp = header.get('timestamp') - self.verified_tx[tx_hash] = (tx_height, timestamp) + with self.lock: + self.verified_tx[tx_hash] = (tx_height, timestamp) print_error("verified %s"%tx_hash) self.config.set_key('verified_tx2', self.verified_tx, True) self.interface.trigger_callback('updated') @@ -245,12 +249,16 @@ class WalletVerifier(threading.Thread): # this can be caused by a reorg. print_error("verify header failed"+ repr(header)) # undo verifications - for tx_hash, item in self.verified_tx.items(): + with self.lock: + items = self.verified_tx.items()[:] + for tx_hash, item in items: tx_height, timestamp = item if tx_height >= height: print_error("redoing", tx_hash) - self.verified_tx.pop(tx_hash) - if tx_hash in self.merkle_roots: self.merkle_roots.pop(tx_hash) + with self.lock: + self.verified_tx.pop(tx_hash) + if tx_hash in self.merkle_roots: + self.merkle_roots.pop(tx_hash) # return False to request previous header. return False diff --git a/lib/wallet.py b/lib/wallet.py index fb038fbc..070a337f 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -90,7 +90,6 @@ class Wallet: self.addressbook = config.get('contacts', []) self.imported_keys = config.get('imported_keys',{}) self.history = config.get('addr_history',{}) # address -> list(txid, height) - self.tx_height = config.get('tx_height',{}) self.accounts = config.get('accounts', {}) # this should not include public keys self.SequenceClass = ElectrumSequence @@ -649,7 +648,6 @@ class Wallet: with self.lock: self.transactions[tx_hash] = tx - self.tx_height[tx_hash] = tx_height #tx_height = tx.get('height') if self.verifier and tx_height>0: @@ -674,17 +672,12 @@ class Wallet: if tx_height>0: # add it in case it was previously unconfirmed if self.verifier: self.verifier.add(tx_hash, tx_height) - # set the height in case it changed - txh = self.tx_height.get(tx_hash) - if txh is not None and txh != tx_height: - print_error( "changing height for tx", tx_hash ) - self.tx_height[tx_hash] = tx_height def get_tx_history(self): with self.lock: history = self.transactions.items() - history.sort(key = lambda x: self.tx_height.get(x[0]) if self.tx_height.get(x[0]) else 1e12) + history.sort(key = lambda x: self.verifier.get_height(x[0]) if self.verifier.get_height(x[0]) else 1e12) result = [] balance = 0 @@ -1020,7 +1013,6 @@ class Wallet: 'prioritized_addresses': self.prioritized_addresses, 'gap_limit': self.gap_limit, 'transactions': tx, - 'tx_height': self.tx_height, } for k, v in s.items(): self.config.set_key(k,v) @@ -1029,17 +1021,6 @@ class Wallet: def set_verifier(self, verifier): self.verifier = verifier - # review stored transactions and send them to the verifier - # (they are not necessarily in the history, because history items might have have been pruned) - for tx_hash, tx in self.transactions.items(): - tx_height = self.tx_height[tx_hash] - if tx_height <1: - print_error( "skipping", tx_hash, tx_height ) - continue - - if tx_height>0: - self.verifier.add(tx_hash, tx_height) - # review transactions that are in the history for addr, hist in self.history.items(): if hist == ['*']: continue @@ -1047,11 +1028,6 @@ class Wallet: if tx_height>0: # add it in case it was previously unconfirmed self.verifier.add(tx_hash, tx_height) - # set the height in case it changed - txh = self.tx_height.get(tx_hash) - if txh is not None and txh != tx_height: - print_error( "changing height for tx", tx_hash ) - self.tx_height[tx_hash] = tx_height @@ -1087,7 +1063,7 @@ class Wallet: if not tx: continue # already verified? - if self.tx_height.get(tx_hash): + if self.verifier.get_height(tx_hash): continue # unconfirmed tx print_error("new history is orphaning transaction:", tx_hash) @@ -1104,7 +1080,6 @@ class Wallet: for item in h: if item.get('tx_hash') == tx_hash: height = item.get('height') - self.tx_height[tx_hash] = height if height: print_error("found height for", tx_hash, height) self.verifier.add(tx_hash, height) From 8c71c544875c9c3d6f04cfa8464ed51f04324f76 Mon Sep 17 00:00:00 2001 From: thomasv Date: Thu, 14 Mar 2013 13:08:50 +0100 Subject: [PATCH 24/44] add label to internal transactions --- lib/wallet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/wallet.py b/lib/wallet.py index 070a337f..3814cd2d 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -722,6 +722,9 @@ class Wallet: default_label = self.labels[o_addr] except KeyError: default_label = o_addr + break + else: + default_label = '(internal)' else: for o in tx.outputs: o_addr, _ = o From 2b3b7d7c38ee37ffca1212a8503b5a3cd1bb718e Mon Sep 17 00:00:00 2001 From: thomasv Date: Thu, 14 Mar 2013 17:05:50 +0100 Subject: [PATCH 25/44] use proper syntax for variable args --- gui/gui_classic.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 8f0477af..b86926e6 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -375,7 +375,7 @@ class ElectrumWindow(QMainWindow): if callback in h: h.remove(callback) self.plugin_hooks[name] = h - def run_hook(self, name, args = ()): + def run_hook(self, name, *args): args = (self,) + args for cb in self.plugin_hooks.get(name,[]): apply(cb, args) @@ -392,7 +392,7 @@ class ElectrumWindow(QMainWindow): if old_text: self.wallet.labels.pop(name) changed = True - self.run_hook('set_label', (name, text, changed)) + self.run_hook('set_label', name, text, changed) return changed @@ -618,11 +618,11 @@ class ElectrumWindow(QMainWindow): self.current_item_changed(item) - self.run_hook('item_changed', (item, column)) + self.run_hook('item_changed', item, column) def current_item_changed(self, a): - self.run_hook('current_item_changed', (a,)) + self.run_hook('current_item_changed', a) @@ -768,7 +768,7 @@ class ElectrumWindow(QMainWindow): self.amount_e.textChanged.connect(lambda: entry_changed(False) ) self.fee_e.textChanged.connect(lambda: entry_changed(True) ) - self.run_hook('create_send_tab', (grid,)) + self.run_hook('create_send_tab', grid) return w2 @@ -828,7 +828,7 @@ class ElectrumWindow(QMainWindow): self.show_message(str(e)) return - self.run_hook('send_tx', (tx,)) + self.run_hook('send_tx', tx) if label: self.set_label(tx.hash(), label) @@ -1024,7 +1024,7 @@ class ElectrumWindow(QMainWindow): t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize") menu.addAction(t, lambda: self.toggle_priority(addr)) - self.run_hook('receive_menu', (menu,)) + self.run_hook('receive_menu', menu) menu.exec_(self.receive_list.viewport().mapToGlobal(position)) @@ -1081,7 +1081,7 @@ class ElectrumWindow(QMainWindow): label = self.wallet.labels.get(address,'') item.setData(1,0,label) - self.run_hook('update_receive_item', (address, item)) + self.run_hook('update_receive_item', address, item) c, u = self.wallet.get_addr_balance(address) balance = format_satoshis( c + u, False, self.wallet.num_zeros ) From e3677eb0a0d591b91f1ead98f003b86d877f0a14 Mon Sep 17 00:00:00 2001 From: thomasv Date: Fri, 15 Mar 2013 10:49:08 +0100 Subject: [PATCH 26/44] simplification --- electrum | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/electrum b/electrum index d85ecaa3..53942030 100755 --- a/electrum +++ b/electrum @@ -39,11 +39,8 @@ is_android = 'ANDROID_DATA' in os.environ # load local module as electrum if os.path.exists("lib") or is_android: import imp - fp, pathname, description = imp.find_module('lib') - imp.load_module('electrum', fp, pathname, description) - fp, pathname, description = imp.find_module('gui') - imp.load_module('electrum_gui', fp, pathname, description) - + imp.load_module('electrum', *imp.find_module('lib')) + imp.load_module('electrum_gui', *imp.find_module('gui')) from electrum import * From 45c0880195d2013d7cf64d7d683eb771f9d27a7b Mon Sep 17 00:00:00 2001 From: thomasv Date: Fri, 15 Mar 2013 13:00:59 +0100 Subject: [PATCH 27/44] global switch use_local_modules --- electrum | 7 +++++-- gui/gui_classic.py | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/electrum b/electrum index 53942030..28e30533 100755 --- a/electrum +++ b/electrum @@ -34,17 +34,20 @@ except ImportError: sys.exit("Error: AES does not seem to be installed. Try 'sudo pip install slowaes'") +is_local = os.path.dirname(os.path.realpath(__file__)) == os.getcwd() is_android = 'ANDROID_DATA' in os.environ +import __builtin__ +__builtin__.use_local_modules = is_local or is_android + # load local module as electrum -if os.path.exists("lib") or is_android: +if __builtin__.use_local_modules: import imp imp.load_module('electrum', *imp.find_module('lib')) imp.load_module('electrum_gui', *imp.find_module('gui')) from electrum import * - # get password routine def prompt_password(prompt, confirm=True): import getpass diff --git a/gui/gui_classic.py b/gui/gui_classic.py index b86926e6..ea1ae249 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -346,11 +346,11 @@ class ElectrumWindow(QMainWindow): # plugins def init_plugins(self): - import imp, pkgutil - if os.path.exists("plugins"): + import imp, pkgutil, __builtin__ + if __builtin__.use_local_modules: fp, pathname, description = imp.find_module('plugins') + plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])] imp.load_module('electrum_plugins', fp, pathname, description) - plugin_names = [name for a, name, b in pkgutil.iter_modules(['plugins'])] self.plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names) else: import electrum_plugins From 76f045f61641c3e01516eb40d6226db22cecc316 Mon Sep 17 00:00:00 2001 From: Maran Date: Fri, 15 Mar 2013 13:03:20 +0100 Subject: [PATCH 28/44] Update osx build scripts --- setup-release.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup-release.py b/setup-release.py index 5884cb6e..a61c71b0 100644 --- a/setup-release.py +++ b/setup-release.py @@ -28,6 +28,8 @@ if sys.platform == 'darwin': setup_requires=['py2app'], app=[mainscript], options=dict(py2app=dict(argv_emulation=True, + includes = ['PyQt4.QtCore','PyQt4.QtGui', 'sip'], + packages = ['lib', 'gui', 'plugins'], iconfile='electrum.icns', resources=["data", "icons"])), ) From 6662c1dc53a84675ca88c64294ebbf5258e971fb Mon Sep 17 00:00:00 2001 From: thomasv Date: Fri, 15 Mar 2013 13:16:41 +0100 Subject: [PATCH 29/44] filter plugins that do not have a .py source --- gui/gui_classic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index ea1ae249..9e8139e5 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -350,6 +350,7 @@ class ElectrumWindow(QMainWindow): if __builtin__.use_local_modules: fp, pathname, description = imp.find_module('plugins') plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])] + plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names) imp.load_module('electrum_plugins', fp, pathname, description) self.plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names) else: From 8d4f409dd704c6efefdb974d4127d536c53b8e64 Mon Sep 17 00:00:00 2001 From: thomasv Date: Fri, 15 Mar 2013 14:15:32 +0100 Subject: [PATCH 30/44] fix qrscanner module --- plugins/qrscanner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/qrscanner.py b/plugins/qrscanner.py index b13e0d51..f21c8369 100644 --- a/plugins/qrscanner.py +++ b/plugins/qrscanner.py @@ -81,7 +81,7 @@ def parse_uri(uri): def fill_from_qr(self): - qrcode = qrscanner.scan_qr() + qrcode = scan_qr() if 'address' in qrcode: self.payto_e.setText(qrcode['address']) if 'amount' in qrcode: @@ -93,7 +93,7 @@ def fill_from_qr(self): def create_send_tab(gui, grid): - if qrscanner.is_available(): + if is_available(): b = QPushButton(_("Scan QR code")) b.clicked.connect(lambda: fill_from_qr(gui)) grid.addWidget(b, 1, 5) From b6afa2455c62a91df7e00ee92d203ef72036bf8e Mon Sep 17 00:00:00 2001 From: slush Date: Fri, 15 Mar 2013 14:19:36 +0100 Subject: [PATCH 31/44] Fixes Qt imports --- plugins/qrscanner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/qrscanner.py b/plugins/qrscanner.py index f21c8369..cc164843 100644 --- a/plugins/qrscanner.py +++ b/plugins/qrscanner.py @@ -1,5 +1,7 @@ from electrum.util import print_error from urlparse import urlparse, parse_qs +from PyQt4.QtGui import QPushButton +from electrum_gui.i18n import _ try: import zbar From 93b98e117654866024a8b850e3a4efc0bfd007d3 Mon Sep 17 00:00:00 2001 From: thomasv Date: Thu, 14 Mar 2013 16:32:05 +0100 Subject: [PATCH 32/44] move http aliases to separate plugin --- gui/gui_classic.py | 141 ++++++++++----------------- gui/gui_gtk.py | 76 ++++++++++----- lib/util.py | 35 ++++++- lib/wallet.py | 181 ----------------------------------- plugins/aliases.py | 231 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 295 deletions(-) create mode 100644 plugins/aliases.py diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 9e8139e5..1d801325 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -61,8 +61,6 @@ elif platform.system() == 'Darwin': else: MONOSPACE_FONT = 'monospace' -ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$' - from electrum import ELECTRUM_VERSION import re @@ -426,23 +424,6 @@ class ElectrumWindow(QMainWindow): def timer_actions(self): self.run_hook('timer_actions') - if self.payto_e.hasFocus(): - return - r = unicode( self.payto_e.text() ) - if r != self.previous_payto_e: - self.previous_payto_e = r - r = r.strip() - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): - try: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - except: - return - if to_address: - s = r + ' <' + to_address + '>' - self.payto_e.setText(s) - - - def update_status(self): if self.wallet.interface and self.wallet.interface.is_connected: if not self.wallet.up_to_date: @@ -591,10 +572,11 @@ class ElectrumWindow(QMainWindow): def address_label_clicked(self, item, column, l, column_addr, column_label): if column == column_label and item.isSelected(): + is_editable = item.data(0, 32).toBool() + if not is_editable: + return addr = unicode( item.text(column_addr) ) label = unicode( item.text(column_label) ) - if label in self.wallet.aliases.keys(): - return item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) l.editItem( item, column ) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) @@ -605,17 +587,14 @@ class ElectrumWindow(QMainWindow): if column == column_label: addr = unicode( item.text(column_addr) ) text = unicode( item.text(column_label) ) - changed = False + is_editable = item.data(0, 32).toBool() + if not is_editable: + return - if text in self.wallet.aliases.keys(): - print_error("Error: This is one of your aliases") - label = self.wallet.labels.get(addr,'') - item.setText(column_label, QString(label)) - else: - changed = self.set_label(addr, text) - if changed: - self.update_history_tab() - self.update_completions() + changed = self.set_label(addr, text) + if changed: + self.update_history_tab() + self.update_completions() self.current_item_changed(item) @@ -778,8 +757,8 @@ class ElectrumWindow(QMainWindow): for addr,label in self.wallet.labels.items(): if addr in self.wallet.addressbook: l.append( label + ' <' + addr + '>') - l = l + self.wallet.aliases.keys() + self.run_hook('update_completions', l) self.completions.setStringList(l) @@ -794,19 +773,9 @@ class ElectrumWindow(QMainWindow): r = unicode( self.payto_e.text() ) r = r.strip() - # alias - m1 = re.match(ALIAS_REGEXP, r) # label or alias, with address in brackets - m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) - - if m1: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - if not to_address: - return - elif m2: - to_address = m2.group(2) - else: - to_address = r + m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + to_address = m.group(2) if m else r if not is_valid(to_address): QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK')) @@ -858,10 +827,19 @@ class ElectrumWindow(QMainWindow): def set_url(self, url): - payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) + address, amount, label, message, signature, identity, url = util.parse_url(url) + + if label and self.wallet.labels.get(address) != label: + if self.question('Give label "%s" to address %s ?'%(label,address)): + if address not in self.wallet.addressbook and not self.wallet.is_mine(address): + self.wallet.addressbook.append(address) + self.set_label(address, label) + + self.run_hook('set_url', url, self.show_message, self.question) + self.tabs.setCurrentIndex(1) - label = self.wallet.labels.get(payto) - m_addr = label + ' <'+ payto+'>' if label else payto + label = self.wallet.labels.get(address) + m_addr = label + ' <'+ address +'>' if label else address self.payto_e.setText(m_addr) self.message_e.setText(message) @@ -1029,50 +1007,41 @@ class ElectrumWindow(QMainWindow): menu.exec_(self.receive_list.viewport().mapToGlobal(position)) - def payto(self, x, is_alias): - if not x: return - if is_alias: - label = x - m_addr = label - else: - addr = x - label = self.wallet.labels.get(addr) - m_addr = label + ' <' + addr + '>' if label else addr + def payto(self, addr): + if not addr: return + label = self.wallet.labels.get(addr) + m_addr = label + ' <' + addr + '>' if label else addr self.tabs.setCurrentIndex(1) self.payto_e.setText(m_addr) self.amount_e.setFocus() - def delete_contact(self, x, is_alias): + + def delete_contact(self, x): if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")): - if not is_alias and x in self.wallet.addressbook: + if x in self.wallet.addressbook: self.wallet.addressbook.remove(x) self.set_label(x, None) - elif is_alias and x in self.wallet.aliases: - self.wallet.aliases.pop(x) - self.update_history_tab() - self.update_contacts_tab() - self.update_completions() + self.update_history_tab() + self.update_contacts_tab() + self.update_completions() + def create_contact_menu(self, position): - # fixme: this function apparently has a side effect. - # if it is not called the menu pops up several times - #self.contacts_list.selectedIndexes() - item = self.contacts_list.itemAt(position) if not item: return addr = unicode(item.text(0)) label = unicode(item.text(1)) - is_alias = label in self.wallet.aliases.keys() - x = label if is_alias else addr + is_editable = item.data(0,32).toBool() + payto_addr = item.data(0,33).toString() menu = QMenu() menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr)) - menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias)) + menu.addAction(_("Pay to"), lambda: self.payto(payto_addr)) menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address"))) - if not is_alias: + if is_editable: menu.addAction(_("Edit label"), lambda: self.edit_label(False)) - else: - menu.addAction(_("View alias details"), lambda: self.show_contact_details(label)) - menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias)) + menu.addAction(_("Delete"), lambda: self.delete_contact(addr)) + + self.run_hook('create_contact_menu', menu, item) menu.exec_(self.contacts_list.viewport().mapToGlobal(position)) @@ -1157,31 +1126,13 @@ class ElectrumWindow(QMainWindow): # we use column 1 because column 0 may be hidden l.setCurrentItem(l.topLevelItem(0),1) - def show_contact_details(self, m): - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0] - QMessageBox.information(self, 'Alias', msg, 'OK') def update_contacts_tab(self): l = self.contacts_list l.clear() - alias_targets = [] - for alias, v in self.wallet.aliases.items(): - s, target = v - alias_targets.append(target) - item = QTreeWidgetItem( [ target, alias, '-'] ) - item.setBackgroundColor(0, QColor('lightgray')) - l.addTopLevelItem(item) - for address in self.wallet.addressbook: - if address in alias_targets: continue label = self.wallet.labels.get(address,'') n = 0 for tx in self.wallet.transactions.values(): @@ -1189,11 +1140,17 @@ class ElectrumWindow(QMainWindow): item = QTreeWidgetItem( [ address, label, "%d"%n] ) item.setFont(0, QFont(MONOSPACE_FONT)) + # 32 = label can be edited (bool) + item.setData(0,32, True) + # 33 = payto string + item.setData(0,33, address) l.addTopLevelItem(item) + self.run_hook('update_contacts_tab', l) l.setCurrentItem(l.topLevelItem(0)) + def create_console_tab(self): from qt_console import Console self.console = console = Console() diff --git a/gui/gui_gtk.py b/gui/gui_gtk.py index bbab63d2..55b93423 100644 --- a/gui/gui_gtk.py +++ b/gui/gui_gtk.py @@ -868,14 +868,14 @@ class ElectrumWindow: self.show_message(tx_details) elif treeview == self.contacts_treeview: m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) + #a = self.wallet.aliases.get(m) + #if a: + # if a[0] in self.wallet.authorities.keys(): + # s = self.wallet.authorities.get(a[0]) + # else: + # s = "self-signed" + # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] + # self.show_message(msg) def treeview_key_press(self, treeview, event): @@ -890,14 +890,14 @@ class ElectrumWindow: self.show_message(tx_details) elif treeview == self.contacts_treeview: m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self" - msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) + #a = self.wallet.aliases.get(m) + #if a: + # if a[0] in self.wallet.authorities.keys(): + # s = self.wallet.authorities.get(a[0]) + # else: + # s = "self" + # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] + # self.show_message(msg) return False @@ -1145,10 +1145,10 @@ class ElectrumWindow: def update_sending_tab(self): # detect addresses that are not mine in history, add them here... self.addressbook_list.clear() - for alias, v in self.wallet.aliases.items(): - s, target = v - label = self.wallet.labels.get(alias) - self.addressbook_list.append((alias, label, '-')) + #for alias, v in self.wallet.aliases.items(): + # s, target = v + # label = self.wallet.labels.get(alias) + # self.addressbook_list.append((alias, label, '-')) for address in self.wallet.addressbook: label = self.wallet.labels.get(address) @@ -1176,7 +1176,7 @@ class ElectrumWindow: label, is_default_label = self.wallet.get_label(tx_hash) tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else '' - details = self.wallet.get_tx_details(tx_hash) + details = self.get_tx_details(tx_hash) self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, format_satoshis(value,True,self.wallet.num_zeros), @@ -1184,6 +1184,40 @@ class ElectrumWindow: if cursor: self.history_treeview.set_cursor( cursor ) + def get_tx_details(self, tx_hash): + import datetime + if not tx_hash: return '' + tx = self.wallet.transactions.get(tx_hash) + is_mine, v, fee = self.wallet.get_tx_value(tx) + conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash) + + if timestamp: + time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] + else: + time_str = 'pending' + + inputs = map(lambda x: x.get('address'), tx.inputs) + outputs = map(lambda x: x.get('address'), tx.d['outputs']) + tx_details = "Transaction Details" +"\n\n" \ + + "Transaction ID:\n" + tx_hash + "\n\n" \ + + "Status: %d confirmations\n"%conf + if is_mine: + if fee: + tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \ + + "Transaction fee: %s\n"% format_satoshis(fee, False) + else: + tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \ + + "Transaction fee: unknown\n" + else: + tx_details += "Amount received: %s\n"% format_satoshis(v, False) \ + + tx_details += "Date: %s\n\n"%time_str \ + + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \ + + "Outputs:\n-"+ '\n-'.join(outputs) + + return tx_details + + def newaddress_dialog(self, w): diff --git a/lib/util.py b/lib/util.py index a76be6be..50d8ec34 100644 --- a/lib/util.py +++ b/lib/util.py @@ -1,4 +1,4 @@ -import os, sys +import os, sys, re import platform import shutil from datetime import datetime @@ -147,3 +147,36 @@ def age(from_date, since_date = None, target_tz=None, include_seconds=False): return "about 1 year ago" else: return "over %d years ago" % (round(distance_in_minutes / 525600)) + + + + +# URL decode +_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) +urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) + +def parse_url(url): + o = url[8:].split('?') + address = o[0] + if len(o)>1: + params = o[1].split('&') + else: + params = [] + + amount = label = message = signature = identity = '' + for p in params: + k,v = p.split('=') + uv = urldecode(v) + if k == 'amount': amount = uv + elif k == 'message': message = uv + elif k == 'label': label = uv + elif k == 'signature': + identity, signature = uv.split(':') + url = url.replace('&%s=%s'%(k,v),'') + else: + print k,v + + return address, amount, label, message, signature, identity, url + + + diff --git a/lib/wallet.py b/lib/wallet.py index 3814cd2d..0383e4a2 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -33,9 +33,6 @@ import time from util import print_msg, print_error, user_dir, format_satoshis from bitcoin import * -# URL decode -_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) -urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) # AES encryption EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s)) @@ -82,11 +79,8 @@ class Wallet: self.use_encryption = config.get('use_encryption', False) self.seed = config.get('seed', '') # encrypted self.labels = config.get('labels', {}) - self.aliases = config.get('aliases', {}) # aliases for addresses - self.authorities = config.get('authorities', {}) # trusted addresses self.frozen_addresses = config.get('frozen_addresses',[]) self.prioritized_addresses = config.get('prioritized_addresses',[]) - self.receipts = config.get('receipts',{}) # signed URIs self.addressbook = config.get('contacts', []) self.imported_keys = config.get('imported_keys',{}) self.history = config.get('addr_history',{}) # address -> list(txid, height) @@ -425,46 +419,6 @@ class Wallet: return tx.get_value(addresses, self.prevout_values) - def get_tx_details(self, tx_hash): - import datetime - if not tx_hash: return '' - tx = self.transactions.get(tx_hash) - is_mine, v, fee = self.get_tx_value(tx) - conf, timestamp = self.verifier.get_confirmations(tx_hash) - - if timestamp: - time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] - else: - time_str = 'pending' - - inputs = map(lambda x: x.get('address'), tx.inputs) - outputs = map(lambda x: x.get('address'), tx.d['outputs']) - tx_details = "Transaction Details" +"\n\n" \ - + "Transaction ID:\n" + tx_hash + "\n\n" \ - + "Status: %d confirmations\n"%conf - if is_mine: - if fee: - tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \ - + "Transaction fee: %s\n"% format_satoshis(fee, False) - else: - tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \ - + "Transaction fee: unknown\n" - else: - tx_details += "Amount received: %s\n"% format_satoshis(v, False) \ - - tx_details += "Date: %s\n\n"%time_str \ - + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \ - + "Outputs:\n-"+ '\n-'.join(outputs) - - r = self.receipts.get(tx_hash) - if r: - tx_details += "\n_______________________________________" \ - + '\n\nSigned URI: ' + r[2] \ - + "\n\nSigned by: " + r[0] \ - + '\n\nSignature: ' + r[1] - - return tx_details - def update_tx_outputs(self, tx_hash): tx = self.transactions.get(tx_hash) @@ -813,48 +767,6 @@ class Wallet: return True, out - def read_alias(self, alias): - # this might not be the right place for this function. - import urllib - - m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) - m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) - if m1: - url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) - elif m2: - url = 'https://' + alias + '/bitcoin.id' - else: - return '' - try: - lines = urllib.urlopen(url).readlines() - except: - return '' - - # line 0 - line = lines[0].strip().split(':') - if len(line) == 1: - auth_name = None - target = signing_addr = line[0] - else: - target, auth_name, signing_addr, signature = line - msg = "alias:%s:%s:%s"%(alias,target,auth_name) - print msg, signature - EC_KEY.verify_message(signing_addr, signature, msg) - - # other lines are signed updates - for line in lines[1:]: - line = line.strip() - if not line: continue - line = line.split(':') - previous = target - print repr(line) - target, signature = line - EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) - - if not is_valid(target): - raise ValueError("Invalid bitcoin address") - - return target, signing_addr, auth_name def update_password(self, seed, old_password, new_password): if new_password == '': new_password = None @@ -868,96 +780,6 @@ class Wallet: self.imported_keys[k] = c self.save() - def get_alias(self, alias, interactive = False, show_message=None, question = None): - try: - target, signing_address, auth_name = self.read_alias(alias) - except BaseException, e: - # raise exception if verify fails (verify the chain) - if interactive: - show_message("Alias error: " + str(e)) - return - - print target, signing_address, auth_name - - if auth_name is None: - a = self.aliases.get(alias) - if not a: - msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) - if interactive and question( msg ): - self.aliases[alias] = (signing_address, target) - else: - target = None - else: - if signing_address != a[0]: - msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias - if interactive and question( msg ): - self.aliases[alias] = (signing_address, target) - else: - target = None - else: - if signing_address not in self.authorities.keys(): - msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) - if interactive and question( msg ): - self.authorities[signing_address] = auth_name - else: - target = None - - if target: - self.aliases[alias] = (signing_address, target) - - return target - - - def parse_url(self, url, show_message, question): - o = url[8:].split('?') - address = o[0] - if len(o)>1: - params = o[1].split('&') - else: - params = [] - - amount = label = message = signature = identity = '' - for p in params: - k,v = p.split('=') - uv = urldecode(v) - if k == 'amount': amount = uv - elif k == 'message': message = uv - elif k == 'label': label = uv - elif k == 'signature': - identity, signature = uv.split(':') - url = url.replace('&%s=%s'%(k,v),'') - else: - print k,v - - if label and self.labels.get(address) != label: - if question('Give label "%s" to address %s ?'%(label,address)): - if address not in self.addressbook and not self.is_mine(address): - self.addressbook.append(address) - self.labels[address] = label - - if signature: - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): - signing_address = self.get_alias(identity, True, show_message, question) - elif is_valid(identity): - signing_address = identity - else: - signing_address = None - if not signing_address: - return - try: - EC_KEY.verify_message(signing_address, signature, url ) - self.receipt = (signing_address, signature, url) - except: - show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') - address = amount = label = identity = message = '' - - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): - payto_address = self.get_alias(address, True, show_message, question) - if payto_address: - address = address + ' <' + payto_address + '>' - - return address, amount, label, message, signature, identity, url - def freeze(self,addr): @@ -1008,9 +830,6 @@ class Wallet: 'labels': self.labels, 'contacts': self.addressbook, 'imported_keys': self.imported_keys, - 'aliases': self.aliases, - 'authorities': self.authorities, - 'receipts': self.receipts, 'num_zeros': self.num_zeros, 'frozen_addresses': self.frozen_addresses, 'prioritized_addresses': self.prioritized_addresses, diff --git a/plugins/aliases.py b/plugins/aliases.py new file mode 100644 index 00000000..f8a3a5b2 --- /dev/null +++ b/plugins/aliases.py @@ -0,0 +1,231 @@ +import re +import platform +from decimal import Decimal + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui + +from electrum_gui.qrcodewidget import QRCodeWidget +from electrum_gui import bmp, pyqrnative +from electrum_gui.i18n import _ + + +ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$' + + +config = {} + +def get_info(): + return 'Aliases', _('Retrieve aliases using http.') + +def init(self): + global config + config = self.config + self.aliases = config.get('aliases', {}) # aliases for addresses + self.authorities = config.get('authorities', {}) # trusted addresses + self.receipts = config.get('receipts',{}) # signed URIs + do_enable(self, is_enabled()) + +def is_enabled(): + return config.get('use_aliases') is True + +def is_available(): + return True + + +def toggle(gui): + enabled = not is_enabled() + config.set_key('use_aliases', enabled, True) + do_enable(gui, enabled) + return enabled + + +def do_enable(gui, enabled): + if enabled: + gui.set_hook('timer_actions', timer_actions) + gui.set_hook('set_url', set_url_hook) + gui.set_hook('update_contacts_tab', update_contacts_tab_hook) + gui.set_hook('update_completions', update_completions_hook) + gui.set_hook('create_contact_menu', create_contact_menu_hook) + else: + gui.unset_hook('timer_actions', timer_actions) + gui.unset_hook('set_url', set_url_hook) + gui.unset_hook('update_contacts_tab', update_contacts_tab_hook) + gui.unset_hook('update_completions', update_completions_hook) + gui.unset_hook('create_contact_menu', create_contact_menu_hook) + + +def timer_actions(self): + if self.payto_e.hasFocus(): + return + r = unicode( self.payto_e.text() ) + if r != self.previous_payto_e: + self.previous_payto_e = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = get_alias(self, r, True, self.show_message, self.question) + except: + return + if to_address: + s = r + ' <' + to_address + '>' + self.payto_e.setText(s) + + +def get_alias(self, alias, interactive = False, show_message=None, question = None): + try: + target, signing_address, auth_name = read_alias(self, alias) + except BaseException, e: + # raise exception if verify fails (verify the chain) + if interactive: + show_message("Alias error: " + str(e)) + return + + print target, signing_address, auth_name + + if auth_name is None: + a = self.aliases.get(alias) + if not a: + msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) + if interactive and question( msg ): + self.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address != a[0]: + msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias + if interactive and question( msg ): + self.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address not in self.authorities.keys(): + msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) + if interactive and question( msg ): + self.authorities[signing_address] = auth_name + else: + target = None + + if target: + self.aliases[alias] = (signing_address, target) + + return target + + + +def read_alias(self, alias): + import urllib + + m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) + m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) + if m1: + url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) + elif m2: + url = 'https://' + alias + '/bitcoin.id' + else: + return '' + try: + lines = urllib.urlopen(url).readlines() + except: + return '' + + # line 0 + line = lines[0].strip().split(':') + if len(line) == 1: + auth_name = None + target = signing_addr = line[0] + else: + target, auth_name, signing_addr, signature = line + msg = "alias:%s:%s:%s"%(alias,target,auth_name) + print msg, signature + EC_KEY.verify_message(signing_addr, signature, msg) + + # other lines are signed updates + for line in lines[1:]: + line = line.strip() + if not line: continue + line = line.split(':') + previous = target + print repr(line) + target, signature = line + EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) + + if not is_valid(target): + raise ValueError("Invalid bitcoin address") + + return target, signing_addr, auth_name + + +def set_url_hook(self, url, show_message, question): + payto, amount, label, message, signature, identity, url = util.parse_url(url) + if signature: + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): + signing_address = get_alias(identity, True, show_message, question) + elif is_valid(identity): + signing_address = identity + else: + signing_address = None + if not signing_address: + return + try: + EC_KEY.verify_message(signing_address, signature, url ) + self.receipt = (signing_address, signature, url) + except: + show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') + address = amount = label = identity = message = '' + + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): + payto_address = get_alias(address, True, show_message, question) + if payto_address: + address = address + ' <' + payto_address + '>' + + return address, amount, label, message, signature, identity, url + + + +def update_contacts_tab_hook(self, l): + alias_targets = [] + for alias, v in self.aliases.items(): + s, target = v + alias_targets.append(target) + item = QTreeWidgetItem( [ target, alias, '-'] ) + item.setBackgroundColor(0, QColor('lightgray')) + l.insertTopLevelItem(0,item) + item.setData(0,32,False) + item.setData(0,33,alias + ' <' + target + '>') + + + +def update_completions_hook(self, l): + l[:] = l + self.aliases.keys() + + +def create_contact_menu_hook(self, menu, item): + label = unicode(item.text(1)) + if label in self.aliases.keys(): + addr = unicode(item.text(0)) + label = unicode(item.text(1)) + menu.addAction(_("View alias details"), lambda: show_contact_details(self, label)) + menu.addAction(_("Delete alias"), lambda: delete_alias(self, label)) + + +def show_contact_details(self, m): + a = self.aliases.get(m) + if a: + if a[0] in self.authorities.keys(): + s = self.authorities.get(a[0]) + else: + s = "self-signed" + msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0] + QMessageBox.information(self, 'Alias', msg, 'OK') + + +def delete_alias(self, x): + if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")): + if x in self.aliases: + self.aliases.pop(x) + self.update_history_tab() + self.update_contacts_tab() + self.update_completions() From bd1cdc9bfb6b82281414609068f17587389a1f9f Mon Sep 17 00:00:00 2001 From: thomasv Date: Fri, 15 Mar 2013 09:58:05 +0100 Subject: [PATCH 33/44] derive plugins from BasePlugin class --- gui/__init__.py | 1 + gui/gui_classic.py | 37 ++--- gui/plugins.py | 32 ++++ plugins/aliases.py | 344 +++++++++++++++++++---------------------- plugins/pointofsale.py | 212 +++++++++++-------------- plugins/qrscanner.py | 105 ++++++------- 6 files changed, 342 insertions(+), 389 deletions(-) create mode 100644 gui/plugins.py diff --git a/gui/__init__.py b/gui/__init__.py index afa8ca43..9bccd2ad 100644 --- a/gui/__init__.py +++ b/gui/__init__.py @@ -1 +1,2 @@ # do not remove this file +from plugins import BasePlugin diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 1d801325..49944383 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -339,7 +339,7 @@ class ElectrumWindow(QMainWindow): self.console.showMessage(self.wallet.banner) # plugins that need to change the GUI do it here - self.run_hook('init') + self.run_hook('init_gui') # plugins @@ -350,36 +350,33 @@ class ElectrumWindow(QMainWindow): plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])] plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names) imp.load_module('electrum_plugins', fp, pathname, description) - self.plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names) + plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names) else: import electrum_plugins plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)] - self.plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names] + plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names] - self.plugin_hooks = {} - for p in self.plugins: + self.plugins = [] + for p in plugins: try: - p.init(self) + self.plugins.append( p.Plugin(self) ) except: print_msg("Error:cannot initialize plugin",p) traceback.print_exc(file=sys.stdout) - def set_hook(self, name, callback): - h = self.plugin_hooks.get(name, []) - h.append(callback) - self.plugin_hooks[name] = h - - def unset_hook(self, name, callback): - h = self.plugin_hooks.get(name,[]) - if callback in h: h.remove(callback) - self.plugin_hooks[name] = h def run_hook(self, name, *args): - args = (self,) + args - for cb in self.plugin_hooks.get(name,[]): - apply(cb, args) - + for p in self.plugins: + if not p.is_enabled(): + continue + try: + f = eval('p.'+name) + except: + continue + apply(f, args) + return + def set_label(self, name, text = None): changed = False old_text = self.wallet.labels.get(name) @@ -2002,7 +1999,7 @@ class ElectrumWindow(QMainWindow): grid_plugins.setColumnStretch(0,1) tabs.addTab(tab5, _('Plugins') ) def mk_toggle(cb, p): - return lambda: cb.setChecked(p.toggle(self)) + return lambda: cb.setChecked(p.toggle()) for i, p in enumerate(self.plugins): try: name, description = p.get_info() diff --git a/gui/plugins.py b/gui/plugins.py new file mode 100644 index 00000000..bb00babf --- /dev/null +++ b/gui/plugins.py @@ -0,0 +1,32 @@ + + +class BasePlugin: + + def get_info(self): + return self.fullname, self.description + + def __init__(self, gui, name, fullname, description): + self.name = name + self.fullname = fullname + self.description = description + self.gui = gui + self.config = gui.config + + def toggle(self): + enabled = not self.is_enabled() + self.set_enabled(enabled) + self.init_gui() + return enabled + + def init_gui(self): + pass + + def is_enabled(self): + return self.is_available() and self.config.get('use_'+self.name) is True + + def is_available(self): + return True + + def set_enabled(self, enabled): + self.config.set_key('use_'+self.name, enabled, True) + diff --git a/plugins/aliases.py b/plugins/aliases.py index f8a3a5b2..c44df3ff 100644 --- a/plugins/aliases.py +++ b/plugins/aliases.py @@ -15,217 +15,185 @@ from electrum_gui.i18n import _ ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$' -config = {} -def get_info(): - return 'Aliases', _('Retrieve aliases using http.') +from electrum_gui import BasePlugin +class Plugin(BasePlugin): -def init(self): - global config - config = self.config - self.aliases = config.get('aliases', {}) # aliases for addresses - self.authorities = config.get('authorities', {}) # trusted addresses - self.receipts = config.get('receipts',{}) # signed URIs - do_enable(self, is_enabled()) - -def is_enabled(): - return config.get('use_aliases') is True - -def is_available(): - return True + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'aliases', 'Aliases', _('Retrieve aliases using http.')) + self.aliases = self.config.get('aliases', {}) # aliases for addresses + self.authorities = self.config.get('authorities', {}) # trusted addresses + self.receipts = self.config.get('receipts',{}) # signed URIs -def toggle(gui): - enabled = not is_enabled() - config.set_key('use_aliases', enabled, True) - do_enable(gui, enabled) - return enabled + def timer_actions(self): + if self.gui.payto_e.hasFocus(): + return + r = unicode( self.gui.payto_e.text() ) + if r != self.gui.previous_payto_e: + self.gui.previous_payto_e = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = self.get_alias(r, True, self.gui.show_message, self.gui.question) + except: + return + if to_address: + s = r + ' <' + to_address + '>' + self.gui.payto_e.setText(s) -def do_enable(gui, enabled): - if enabled: - gui.set_hook('timer_actions', timer_actions) - gui.set_hook('set_url', set_url_hook) - gui.set_hook('update_contacts_tab', update_contacts_tab_hook) - gui.set_hook('update_completions', update_completions_hook) - gui.set_hook('create_contact_menu', create_contact_menu_hook) - else: - gui.unset_hook('timer_actions', timer_actions) - gui.unset_hook('set_url', set_url_hook) - gui.unset_hook('update_contacts_tab', update_contacts_tab_hook) - gui.unset_hook('update_completions', update_completions_hook) - gui.unset_hook('create_contact_menu', create_contact_menu_hook) + def get_alias(self, alias, interactive = False, show_message=None, question = None): + try: + target, signing_address, auth_name = read_alias(self, alias) + except BaseException, e: + # raise exception if verify fails (verify the chain) + if interactive: + show_message("Alias error: " + str(e)) + return + print target, signing_address, auth_name -def timer_actions(self): - if self.payto_e.hasFocus(): - return - r = unicode( self.payto_e.text() ) - if r != self.previous_payto_e: - self.previous_payto_e = r - r = r.strip() - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): - try: - to_address = get_alias(self, r, True, self.show_message, self.question) - except: - return - if to_address: - s = r + ' <' + to_address + '>' - self.payto_e.setText(s) - - -def get_alias(self, alias, interactive = False, show_message=None, question = None): - try: - target, signing_address, auth_name = read_alias(self, alias) - except BaseException, e: - # raise exception if verify fails (verify the chain) - if interactive: - show_message("Alias error: " + str(e)) - return - - print target, signing_address, auth_name - - if auth_name is None: - a = self.aliases.get(alias) - if not a: - msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) - if interactive and question( msg ): - self.aliases[alias] = (signing_address, target) - else: - target = None - else: - if signing_address != a[0]: - msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias + if auth_name is None: + a = self.aliases.get(alias) + if not a: + msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) if interactive and question( msg ): self.aliases[alias] = (signing_address, target) else: target = None - else: - if signing_address not in self.authorities.keys(): - msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) - if interactive and question( msg ): - self.authorities[signing_address] = auth_name else: - target = None + if signing_address != a[0]: + msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias + if interactive and question( msg ): + self.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address not in self.authorities.keys(): + msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) + if interactive and question( msg ): + self.authorities[signing_address] = auth_name + else: + target = None - if target: - self.aliases[alias] = (signing_address, target) + if target: + self.aliases[alias] = (signing_address, target) - return target + return target -def read_alias(self, alias): - import urllib + def read_alias(self, alias): + import urllib - m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) - m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) - if m1: - url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) - elif m2: - url = 'https://' + alias + '/bitcoin.id' - else: - return '' - try: - lines = urllib.urlopen(url).readlines() - except: - return '' - - # line 0 - line = lines[0].strip().split(':') - if len(line) == 1: - auth_name = None - target = signing_addr = line[0] - else: - target, auth_name, signing_addr, signature = line - msg = "alias:%s:%s:%s"%(alias,target,auth_name) - print msg, signature - EC_KEY.verify_message(signing_addr, signature, msg) - - # other lines are signed updates - for line in lines[1:]: - line = line.strip() - if not line: continue - line = line.split(':') - previous = target - print repr(line) - target, signature = line - EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) - - if not is_valid(target): - raise ValueError("Invalid bitcoin address") - - return target, signing_addr, auth_name - - -def set_url_hook(self, url, show_message, question): - payto, amount, label, message, signature, identity, url = util.parse_url(url) - if signature: - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): - signing_address = get_alias(identity, True, show_message, question) - elif is_valid(identity): - signing_address = identity + m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) + m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) + if m1: + url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) + elif m2: + url = 'https://' + alias + '/bitcoin.id' else: - signing_address = None - if not signing_address: - return + return '' try: - EC_KEY.verify_message(signing_address, signature, url ) - self.receipt = (signing_address, signature, url) + lines = urllib.urlopen(url).readlines() except: - show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') - address = amount = label = identity = message = '' + return '' - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): - payto_address = get_alias(address, True, show_message, question) - if payto_address: - address = address + ' <' + payto_address + '>' - - return address, amount, label, message, signature, identity, url - - - -def update_contacts_tab_hook(self, l): - alias_targets = [] - for alias, v in self.aliases.items(): - s, target = v - alias_targets.append(target) - item = QTreeWidgetItem( [ target, alias, '-'] ) - item.setBackgroundColor(0, QColor('lightgray')) - l.insertTopLevelItem(0,item) - item.setData(0,32,False) - item.setData(0,33,alias + ' <' + target + '>') - - - -def update_completions_hook(self, l): - l[:] = l + self.aliases.keys() - - -def create_contact_menu_hook(self, menu, item): - label = unicode(item.text(1)) - if label in self.aliases.keys(): - addr = unicode(item.text(0)) - label = unicode(item.text(1)) - menu.addAction(_("View alias details"), lambda: show_contact_details(self, label)) - menu.addAction(_("Delete alias"), lambda: delete_alias(self, label)) - - -def show_contact_details(self, m): - a = self.aliases.get(m) - if a: - if a[0] in self.authorities.keys(): - s = self.authorities.get(a[0]) + # line 0 + line = lines[0].strip().split(':') + if len(line) == 1: + auth_name = None + target = signing_addr = line[0] else: - s = "self-signed" - msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0] - QMessageBox.information(self, 'Alias', msg, 'OK') + target, auth_name, signing_addr, signature = line + msg = "alias:%s:%s:%s"%(alias,target,auth_name) + print msg, signature + EC_KEY.verify_message(signing_addr, signature, msg) + + # other lines are signed updates + for line in lines[1:]: + line = line.strip() + if not line: continue + line = line.split(':') + previous = target + print repr(line) + target, signature = line + EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) + + if not is_valid(target): + raise ValueError("Invalid bitcoin address") + + return target, signing_addr, auth_name -def delete_alias(self, x): - if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")): - if x in self.aliases: - self.aliases.pop(x) - self.update_history_tab() - self.update_contacts_tab() - self.update_completions() + def set_url(self, url, show_message, question): + payto, amount, label, message, signature, identity, url = util.parse_url(url) + if signature: + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): + signing_address = get_alias(identity, True, show_message, question) + elif is_valid(identity): + signing_address = identity + else: + signing_address = None + if not signing_address: + return + try: + EC_KEY.verify_message(signing_address, signature, url ) + self.receipt = (signing_address, signature, url) + except: + show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') + address = amount = label = identity = message = '' + + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): + payto_address = get_alias(address, True, show_message, question) + if payto_address: + address = address + ' <' + payto_address + '>' + + return address, amount, label, message, signature, identity, url + + + + def update_contacts_tab(self, l): + alias_targets = [] + for alias, v in self.aliases.items(): + s, target = v + alias_targets.append(target) + item = QTreeWidgetItem( [ target, alias, '-'] ) + item.setBackgroundColor(0, QColor('lightgray')) + item.setData(0,32,False) + item.setData(0,33,alias + ' <' + target + '>') + l.insertTopLevelItem(0,item) + + + def update_completions(self, l): + l[:] = l + self.aliases.keys() + + + def create_contact_menu(self, menu, item): + label = unicode(item.text(1)) + if label in self.aliases.keys(): + addr = unicode(item.text(0)) + label = unicode(item.text(1)) + menu.addAction(_("View alias details"), lambda: self.show_contact_details(label)) + menu.addAction(_("Delete alias"), lambda: delete_alias(self, label)) + + + def show_contact_details(self, m): + a = self.aliases.get(m) + if a: + if a[0] in self.authorities.keys(): + s = self.authorities.get(a[0]) + else: + s = "self-signed" + msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0] + QMessageBox.information(self.gui, 'Alias', msg, 'OK') + + + def delete_alias(self, x): + if self.gui.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")): + if x in self.aliases: + self.aliases.pop(x) + self.update_history_tab() + self.update_contacts_tab() + self.update_completions() diff --git a/plugins/pointofsale.py b/plugins/pointofsale.py index 29dedeb9..7cc8c778 100644 --- a/plugins/pointofsale.py +++ b/plugins/pointofsale.py @@ -8,8 +8,7 @@ import PyQt4.QtCore as QtCore import PyQt4.QtGui as QtGui from electrum_gui.qrcodewidget import QRCodeWidget -from electrum_gui import bmp, pyqrnative - +from electrum_gui import bmp, pyqrnative, BasePlugin from electrum_gui.i18n import _ @@ -89,98 +88,95 @@ class QR_Window(QWidget): self.qrw.set_addr( msg ) - - - -config = {} - -def get_info(): - return 'Point of Sale', _('Show QR code window and amounts requested for each address. Add menu item to request amount.') - -def init(gui): - global config - config = gui.config - gui.requested_amounts = config.get('requested_amounts',{}) - gui.merchant_name = config.get('merchant_name', 'Invoice') - gui.qr_window = None - do_enable(gui, is_enabled()) - -def is_enabled(): - return config.get('pointofsale') is True - -def is_available(): - return True - - -def toggle(gui): - enabled = not is_enabled() - config.set_key('pointofsale', enabled, True) - do_enable(gui, enabled) - update_gui(gui) - return enabled - - -def do_enable(gui, enabled): - if enabled: - gui.expert_mode = True - gui.set_hook('item_changed', item_changed) - gui.set_hook('current_item_changed', recv_changed) - gui.set_hook('receive_menu', receive_menu) - gui.set_hook('update_receive_item', update_receive_item) - gui.set_hook('timer_actions', timer_actions) - gui.set_hook('close_main_window', close_main_window) - gui.set_hook('init', update_gui) - else: - gui.unset_hook('item_changed', item_changed) - gui.unset_hook('current_item_changed', recv_changed) - gui.unset_hook('receive_menu', receive_menu) - gui.unset_hook('update_receive_item', update_receive_item) - gui.unset_hook('timer_actions', timer_actions) - gui.unset_hook('close_main_window', close_main_window) - gui.unset_hook('init', update_gui) -def update_gui(gui): - enabled = is_enabled() - if enabled: - gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Request')]) - else: - gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Tx')]) +class Plugin(BasePlugin): - toggle_QR_window(gui, enabled) + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'pointofsale', 'Point of Sale', + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') ) + self.qr_window = None + self.requested_amounts = self.config.get('requested_amounts',{}) + self.merchant_name = self.config.get('merchant_name', 'Invoice') + + + def init_gui(self): + enabled = self.is_enabled() + if enabled: + self.gui.expert_mode = True + self.gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Request')]) + else: + self.gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Tx')]) + + self.toggle_QR_window(enabled) + def close_main_window(self): + if self.qr_window: + self.qr_window.close() + self.qr_window = None -def toggle_QR_window(self, show): - if show and not self.qr_window: - self.qr_window = QR_Window(self.exchanger) - self.qr_window.setVisible(True) - self.qr_window_geometry = self.qr_window.geometry() - item = self.receive_list.currentItem() - if item: - address = str(item.text(1)) - label = self.wallet.labels.get(address) + + def timer_actions(self): + if self.qr_window: + self.qr_window.qrw.update_qr() + + + def toggle_QR_window(self, show): + if show and not self.qr_window: + self.qr_window = QR_Window(self.gui.exchanger) + self.qr_window.setVisible(True) + self.qr_window_geometry = self.qr_window.geometry() + item = self.gui.receive_list.currentItem() + if item: + address = str(item.text(1)) + label = self.gui.wallet.labels.get(address) + amount, currency = self.requested_amounts.get(address, (None, None)) + self.qr_window.set_content( address, label, amount, currency ) + + elif show and self.qr_window and not self.qr_window.isVisible(): + self.qr_window.setVisible(True) + self.qr_window.setGeometry(self.qr_window_geometry) + + elif not show and self.qr_window and self.qr_window.isVisible(): + self.qr_window_geometry = self.qr_window.geometry() + self.qr_window.setVisible(False) + + + + def update_receive_item(self, address, item): + try: amount, currency = self.requested_amounts.get(address, (None, None)) + except: + print "cannot get requested amount", address, self.requested_amounts.get(address) + amount, currency = None, None + self.requested_amounts.pop(address) + + amount_str = amount + (' ' + currency if currency else '') if amount is not None else '' + item.setData(column_index,0,amount_str) + + + + def current_item_changed(self, a): + if a is not None and self.qr_window and self.qr_window.isVisible(): + address = str(a.text(0)) + label = self.gui.wallet.labels.get(address) + try: + amount, currency = self.requested_amounts.get(address, (None, None)) + except: + amount, currency = None, None self.qr_window.set_content( address, label, amount, currency ) - elif show and self.qr_window and not self.qr_window.isVisible(): - self.qr_window.setVisible(True) - self.qr_window.setGeometry(self.qr_window_geometry) - elif not show and self.qr_window and self.qr_window.isVisible(): - self.qr_window_geometry = self.qr_window.geometry() - self.qr_window.setVisible(False) - - - - -def item_changed(self, item, column): - if column == column_index: + + def item_changed(self, item, column): + if column != column_index: + return address = str( item.text(0) ) text = str( item.text(column) ) try: - seq = self.wallet.get_address_index(address) + seq = self.gui.wallet.get_address_index(address) index = seq[-1] except: print "cannot get index" @@ -198,9 +194,9 @@ def item_changed(self, item, column): currency = currency.upper() self.requested_amounts[address] = (amount, currency) - self.wallet.config.set_key('requested_amounts', self.requested_amounts, True) + self.gui.wallet.config.set_key('requested_amounts', self.requested_amounts, True) - label = self.wallet.labels.get(address) + label = self.gui.wallet.labels.get(address) if label is None: label = self.merchant_name + ' - %04d'%(index+1) self.wallet.labels[address] = label @@ -213,50 +209,20 @@ def item_changed(self, item, column): if address in self.requested_amounts: self.requested_amounts.pop(address) - self.update_receive_item(self.receive_list.currentItem()) - - -def recv_changed(self, a): - if a is not None and self.qr_window and self.qr_window.isVisible(): - address = str(a.text(0)) - label = self.wallet.labels.get(address) - try: - amount, currency = self.requested_amounts.get(address, (None, None)) - except: - amount, currency = None, None - self.qr_window.set_content( address, label, amount, currency ) + self.gui.update_receive_item(self.gui.receive_list.currentItem()) -def edit_amount(self): - l = self.receive_list - item = l.currentItem() - item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - l.editItem( item, column_index ) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) -def receive_menu(self, menu): - menu.addAction(_("Request amount"), lambda: edit_amount(self)) + def edit_amount(self): + l = self.gui.receive_list + item = l.currentItem() + item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + l.editItem( item, column_index ) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + + + def receive_menu(self, menu): + menu.addAction(_("Request amount"), self.edit_amount) -def update_receive_item(self, address, item): - try: - amount, currency = self.requested_amounts.get(address, (None, None)) - except: - print "cannot get requested amount", address, self.requested_amounts.get(address) - amount, currency = None, None - self.requested_amounts.pop(address) - - amount_str = amount + (' ' + currency if currency else '') if amount is not None else '' - item.setData(column_index,0,amount_str) - - -def close_main_window(self): - if self.qr_window: - self.qr_window.close() - self.qr_window = None - - -def timer_actions(self): - if self.qr_window: - self.qr_window.qrw.update_qr() diff --git a/plugins/qrscanner.py b/plugins/qrscanner.py index cc164843..f66e60b0 100644 --- a/plugins/qrscanner.py +++ b/plugins/qrscanner.py @@ -8,55 +8,62 @@ try: except ImportError: zbar = None +from electrum_gui import BasePlugin +class Plugin(BasePlugin): + + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'qrscans', 'QR scans', "QR Scans.\nInstall the zbar package to enable this plugin") + + def is_available(self): + if not zbar: + return False + try: + proc = zbar.Processor() + proc.init() + except zbar.SystemError: + # Cannot open video device + return False + return True -def init(gui): - if is_enabled(): - gui.set_hook('create_send_tab', create_send_tab) - else: - gui.unset_hook('create_send_tab', create_send_tab) - -def get_info(): - return 'QR scans', "QR Scans.\nInstall the zbar package to enable this plugin" - -def is_enabled(): - return is_available() - -def toggle(gui): - return is_enabled() + def create_send_tab(self, grid): + b = QPushButton(_("Scan QR code")) + b.clicked.connect(self.fill_from_qr) + grid.addWidget(b, 1, 5) -def is_available(): - if not zbar: - return False - - try: + def scan_qr(self): proc = zbar.Processor() proc.init() - except zbar.SystemError: - # Cannot open video device - return False + proc.visible = True - return True + while True: + try: + proc.process_one() + except: + # User closed the preview window + return {} -def scan_qr(): - proc = zbar.Processor() - proc.init() - proc.visible = True - - while True: - try: - proc.process_one() - except: - # User closed the preview window - return {} - - for r in proc.results: - if str(r.type) != 'QRCODE': - continue - - return parse_uri(r.data) + for r in proc.results: + if str(r.type) != 'QRCODE': + continue + return parse_uri(r.data) + + def fill_from_qr(self): + qrcode = self.scan_qr() + if 'address' in qrcode: + self.gui.payto_e.setText(qrcode['address']) + if 'amount' in qrcode: + self.gui.amount_e.setText(str(qrcode['amount'])) + if 'label' in qrcode: + self.gui.message_e.setText(qrcode['label']) + if 'message' in qrcode: + self.gui.message_e.setText("%s (%s)" % (self.gui.message_e.text(), qrcode['message'])) + + + + def parse_uri(uri): if ':' not in uri: # It's just an address (not BIP21) @@ -82,24 +89,6 @@ def parse_uri(uri): -def fill_from_qr(self): - qrcode = scan_qr() - if 'address' in qrcode: - self.payto_e.setText(qrcode['address']) - if 'amount' in qrcode: - self.amount_e.setText(str(qrcode['amount'])) - if 'label' in qrcode: - self.message_e.setText(qrcode['label']) - if 'message' in qrcode: - self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message'])) - - -def create_send_tab(gui, grid): - if is_available(): - b = QPushButton(_("Scan QR code")) - b.clicked.connect(lambda: fill_from_qr(gui)) - grid.addWidget(b, 1, 5) - if __name__ == '__main__': From 24db3c9aee570929b85c32a6b1189d658aeecc80 Mon Sep 17 00:00:00 2001 From: thomasv Date: Fri, 15 Mar 2013 18:35:05 +0100 Subject: [PATCH 34/44] virtual keyboard plugin --- gui/gui_classic.py | 3 +- plugins/virtualkeyboard.py | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 plugins/virtualkeyboard.py diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 49944383..14b75c4b 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -1484,8 +1484,9 @@ class ElectrumWindow(QMainWindow): vbox.addLayout(grid) vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) + d.setLayout(vbox) + self.run_hook('password_dialog', pw, grid, 1) if not d.exec_(): return return unicode(pw.text()) diff --git a/plugins/virtualkeyboard.py b/plugins/virtualkeyboard.py new file mode 100644 index 00000000..f150e29f --- /dev/null +++ b/plugins/virtualkeyboard.py @@ -0,0 +1,65 @@ +from PyQt4.QtGui import * +from electrum_gui import BasePlugin +from electrum_gui.i18n import _ + +class Plugin(BasePlugin): + + + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'virtualkeyboard', 'Virtual Keyboard', + _("Add an optional, mouse keyboard to the password dialog.\nWarning: do not use this if it makes you pick a weaker password.")) + self.vkb = None + self.vkb_index = 0 + + + def password_dialog(self, pw, grid, pos): + vkb_button = QPushButton(_("+")) + vkb_button.setFixedWidth(20) + vkb_button.clicked.connect(lambda: self.toggle_vkb(grid, pw)) + grid.addWidget(vkb_button, pos, 2) + self.kb_pos = 2 + + + def toggle_vkb(self, grid, pw): + if self.vkb: grid.removeItem(self.vkb) + self.vkb = self.virtual_keyboard(self.vkb_index, pw) + grid.addLayout(self.vkb, self.kb_pos, 0, 1, 3) + self.vkb_index += 1 + + + def virtual_keyboard(self, i, pw): + import random + i = i%3 + if i == 0: + chars = 'abcdefghijklmnopqrstuvwxyz ' + elif i == 1: + chars = 'ABCDEFGHIJKLMNOPQRTSUVWXYZ ' + elif i == 2: + chars = '1234567890!?.,;:/%&()[]{}+-' + + n = len(chars) + s = [] + for i in xrange(n): + while True: + k = random.randint(0,n-1) + if k not in s: + s.append(k) + break + + def add_target(t): + return lambda: pw.setText(str( pw.text() ) + t) + + vbox = QVBoxLayout() + grid = QGridLayout() + grid.setSpacing(2) + for i in range(n): + l_button = QPushButton(chars[s[i]]) + l_button.setFixedWidth(25) + l_button.setFixedHeight(25) + l_button.clicked.connect(add_target(chars[s[i]]) ) + grid.addWidget(l_button, i/6, i%6) + + vbox.addLayout(grid) + + return vbox + From 5eaa9238240cd353550631532aa99e4e5a936d0c Mon Sep 17 00:00:00 2001 From: Maran Date: Fri, 15 Mar 2013 23:30:44 +0100 Subject: [PATCH 35/44] Make windows build scripts work for Electrum 1.7 --- contrib/build-wine/electrum.nsi | 104 ++++++++++++++++++++++++++++++++ icons/electrum.ico | Bin 0 -> 34276 bytes 2 files changed, 104 insertions(+) create mode 100644 contrib/build-wine/electrum.nsi create mode 100644 icons/electrum.ico diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi new file mode 100644 index 00000000..9b2aab7b --- /dev/null +++ b/contrib/build-wine/electrum.nsi @@ -0,0 +1,104 @@ +;-------------------------------- +;Include Modern UI + + !include "MUI2.nsh" + +;-------------------------------- +;General + + ;Name and file + Name "Electrum" + OutFile "dist/electrum-setup.exe" + + ;Default installation folder + InstallDir "$PROGRAMFILES\Electrum" + + ;Get installation folder from registry if available + InstallDirRegKey HKCU "Software\Electrum" "" + + ;Request application privileges for Windows Vista + RequestExecutionLevel admin + +;-------------------------------- +;Variables + +;-------------------------------- +;Interface Settings + + !define MUI_ABORTWARNING + +;-------------------------------- +;Pages + + ;!insertmacro MUI_PAGE_LICENSE "tmp/LICENCE" + ;!insertmacro MUI_PAGE_COMPONENTS + !insertmacro MUI_PAGE_DIRECTORY + + ;Start Menu Folder Page Configuration + !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" + !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Electrum" + !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + + ;!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder + + !insertmacro MUI_PAGE_INSTFILES + + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +;Installer Sections + +Section + + SetOutPath "$INSTDIR" + + ;ADD YOUR OWN FILES HERE... + file /r dist\electrum\*.* + + ;Store installation folder + WriteRegStr HKCU "Software\Electrum" "" $INSTDIR + + ;Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + + CreateShortCut "$DESKTOP\Electrum.lnk" "$INSTDIR\electrum.exe" "" + + ;create start-menu items + CreateDirectory "$SMPROGRAMS\Electrum" + CreateShortCut "$SMPROGRAMS\Electrum\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\Electrum\Electrum.lnk" "$INSTDIR\electrum.exe" "" "$INSTDIR\electrum.exe" 0 + +SectionEnd + +;-------------------------------- +;Descriptions + + ;Assign language strings to sections + ;!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + ; !insertmacro MUI_DESCRIPTION_TEXT ${SecDummy} $(DESC_SecDummy) + ;!insertmacro MUI_FUNCTION_DESCRIPTION_END + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + + ;ADD YOUR OWN FILES HERE... + RMDir /r "$INSTDIR\*.*" + + RMDir "$INSTDIR" + + Delete "$DESKTOP\Electrum.lnk" + Delete "$SMPROGRAMS\Electrum\*.*" + RmDir "$SMPROGRAMS\Electrum" + + DeleteRegKey /ifempty HKCU "Software\Electrum" + +SectionEnd diff --git a/icons/electrum.ico b/icons/electrum.ico new file mode 100644 index 0000000000000000000000000000000000000000..db45c1a90341900ae76761d2b44e73306afa7c4d GIT binary patch literal 34276 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 rcAmPzI-JiijUD~@>yvLN9 Date: Sat, 16 Mar 2013 12:02:34 +0100 Subject: [PATCH 36/44] modify warning displayed when keys are imported --- gui/gui_classic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 14b75c4b..2ac67c52 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -1813,8 +1813,9 @@ class ElectrumWindow(QMainWindow): @protected def do_import_privkey(self, password): if not self.wallet.imported_keys: - r = QMessageBox.question(None, _('Warning'), _('Warning: Imported keys are not recoverable from seed.') + ' ' \ - + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '\n\n' \ + r = QMessageBox.question(None, _('Warning'), ''+_('Warning') +':\n
'+ _('Imported keys are not recoverable from seed.') + ' ' \ + + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '

' \ + + _('In addition, when you send bitcoins from one of your imported addresses, the "change" will be sent to an address derived from your seed, unless you disabled this option.') + '

' \ + _('Are you sure you understand what you are doing?'), 3, 4) if r == 4: return From 3d745637a257cfa4d30b58218ef8516fd3350974 Mon Sep 17 00:00:00 2001 From: thomasv Date: Sat, 16 Mar 2013 13:09:48 +0100 Subject: [PATCH 37/44] update pyinstaller url --- contrib/build-wine/prepare-wine.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index ae71321e..6764ae7b 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -4,7 +4,7 @@ PYTHON_URL=http://www.python.org/ftp/python/2.6.6/python-2.6.6.msi PYQT4_URL=http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.9.5/PyQt-Py2.6-x86-gpl-4.9.5-1.exe PYWIN32_URL=http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win32-py2.6.exe/download -PYINSTALLER_URL=https://github.com/downloads/pyinstaller/pyinstaller/pyinstaller-2.0.zip +PYINSTALLER_URL=http://downloads.sourceforge.net/project/pyinstaller/2.0/pyinstaller-2.0.zip NSIS_URL=http://prdownloads.sourceforge.net/nsis/nsis-2.46-setup.exe?download #ZBAR_URL=http://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download From 1adbef4b25f7ba0273eeeff5802a74c301c9c4d7 Mon Sep 17 00:00:00 2001 From: thomasv Date: Sat, 16 Mar 2013 13:34:51 +0100 Subject: [PATCH 38/44] fee may be zero (tx details) --- gui/gui_classic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 2ac67c52..88cff2b6 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -512,7 +512,7 @@ class ElectrumWindow(QMainWindow): vbox.addWidget(QLabel("Date: %s"%time_str)) vbox.addWidget(QLabel("Status: %d confirmations"%conf)) if is_mine: - if fee: + if fee is not None: vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v-fee, False))) vbox.addWidget(QLabel("Transaction fee: %s"% format_satoshis(fee, False))) else: From 4b74faea1e66bbbe00a5be7aef69e629408e2182 Mon Sep 17 00:00:00 2001 From: thomasv Date: Sat, 16 Mar 2013 14:04:46 +0100 Subject: [PATCH 39/44] typo: where->were --- gui/gui_classic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 88cff2b6..ef2958b2 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -1788,7 +1788,7 @@ class ElectrumWindow(QMainWindow): for key, value in json.loads(data).items(): self.wallet.labels[key] = value self.wallet.save() - QMessageBox.information(None, _("Labels imported"), _("Your labels where imported from")+" '%s'" % str(labelsFile)) + QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile)) except (IOError, os.error), reason: QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason)) From fefb88479462b8d782f92e0efe799c4c0497ccdc Mon Sep 17 00:00:00 2001 From: ecdsa Date: Sat, 16 Mar 2013 17:51:58 +0100 Subject: [PATCH 40/44] fix wallet.is_change() method --- lib/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 0383e4a2..9e3b5197 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -178,8 +178,8 @@ class Wallet: return address in self.addresses(True) def is_change(self, address): - #return address in self.change_addresses - return False + acct, s = self.get_address_index(address) + return s[0] == 1 def get_master_public_key(self): return self.sequences[0].master_public_key From d6952228be6d269a81025397eaddef0ed502338d Mon Sep 17 00:00:00 2001 From: ecdsa Date: Sat, 16 Mar 2013 18:17:50 +0100 Subject: [PATCH 41/44] define wallet.get_num_tx() --- gui/gui_classic.py | 5 +---- gui/gui_gtk.py | 5 +---- lib/wallet.py | 6 ++++++ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index ef2958b2..0b28f4e7 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -1131,10 +1131,7 @@ class ElectrumWindow(QMainWindow): for address in self.wallet.addressbook: label = self.wallet.labels.get(address,'') - n = 0 - for tx in self.wallet.transactions.values(): - if address in map(lambda x: x[0], tx.outputs): n += 1 - + n = self.wallet.get_num_tx(address) item = QTreeWidgetItem( [ address, label, "%d"%n] ) item.setFont(0, QFont(MONOSPACE_FONT)) # 32 = label can be edited (bool) diff --git a/gui/gui_gtk.py b/gui/gui_gtk.py index 55b93423..88de6ec5 100644 --- a/gui/gui_gtk.py +++ b/gui/gui_gtk.py @@ -1152,10 +1152,7 @@ class ElectrumWindow: for address in self.wallet.addressbook: label = self.wallet.labels.get(address) - n = 0 - for tx in self.wallet.transactions.values(): - if address in map(lambda x:x[0], tx.outputs): n += 1 - + n = self.wallet.get_num_tx(address) self.addressbook_list.append((address, label, "%d"%n)) def update_history_tab(self): diff --git a/lib/wallet.py b/lib/wallet.py index 9e3b5197..b39770ee 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -407,6 +407,12 @@ class Wallet: # redo labels # self.update_tx_labels() + def get_num_tx(self, address): + n = 0 + for tx in self.transactions.values(): + if address in map(lambda x:x[0], tx.outputs): n += 1 + return n + def get_address_flags(self, addr): flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-" From cce4a6c001c49724eb852d9a74c4f19dab64b795 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Sat, 16 Mar 2013 18:24:45 +0100 Subject: [PATCH 42/44] detect gaps for change too --- gui/gui_classic.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 0b28f4e7..8037d920 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -1094,13 +1094,12 @@ class ElectrumWindow(QMainWindow): for address in account[is_change]: h = self.wallet.history.get(address,[]) - if not is_change: - if h == []: - gap += 1 - if gap > self.wallet.gap_limit: - is_red = True - else: - gap = 0 + if h == []: + gap += 1 + if gap > self.wallet.gap_limit: + is_red = True + else: + gap = 0 num_tx = '*' if h == ['*'] else "%d"%len(h) item = QTreeWidgetItem( [ address, '', '', num_tx] ) From a2ecc0e7bbbb7aaf9ea53fa73bb4ad574bb73b18 Mon Sep 17 00:00:00 2001 From: ecdsa Date: Sat, 16 Mar 2013 20:37:49 +0100 Subject: [PATCH 43/44] allow manual setting when disconnected --- gui/gui_classic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 8037d920..159417a0 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -2179,7 +2179,7 @@ class ElectrumWindow(QMainWindow): for p in protocol_letters: i = protocol_letters.index(p) j = server_protocol.model().index(i,0) - if p not in pp.keys(): + if p not in pp.keys() and interface.is_connected: server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1) else: server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1) From 070d497afbee28b237d2051381b18d264697ca14 Mon Sep 17 00:00:00 2001 From: Maran Date: Sat, 16 Mar 2013 21:13:55 +0100 Subject: [PATCH 44/44] Somehow forgot to push my windows build script changes in all my blurness last night --- contrib/build-wine/build-electrum-git.sh | 2 +- contrib/build-wine/build-electrum.sh | 2 +- contrib/build-wine/deterministic.spec | 60 ++++++++++--- contrib/build-wine/electrum.nsis | 104 ----------------------- 4 files changed, 52 insertions(+), 116 deletions(-) delete mode 100644 contrib/build-wine/electrum.nsis diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index bafa2277..2abb575f 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -58,7 +58,7 @@ $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w --onefile "C:/ele $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w deterministic.spec # For building NSIS installer, run: -wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsis +wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsi #wine $WINEPREFIX/drive_c/Program\ Files\ \(x86\)/NSIS/makensis.exe electrum.nsis DATE=`date +"%Y%m%d"` diff --git a/contrib/build-wine/build-electrum.sh b/contrib/build-wine/build-electrum.sh index a54e29a5..f4d91d73 100755 --- a/contrib/build-wine/build-electrum.sh +++ b/contrib/build-wine/build-electrum.sh @@ -40,7 +40,7 @@ $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w --onefile "C:/ele $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w deterministic.spec # For building NSIS installer, run: -wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsis +wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsi #wine $WINEPREFIX/drive_c/Program\ Files\ \(x86\)/NSIS/makensis.exe electrum.nsis cd dist diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 5293cbfe..db3eca3d 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -1,24 +1,64 @@ # -*- mode: python -*- -a = Analysis(['C:/electrum/electrum'], - pathex=['Z:\\electrum-wine'], - hiddenimports=[], - excludes=['Tkinter'], + +# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports +a = Analysis(['electrum', 'gui/gui_classic.py', 'gui/gui_lite.py', 'gui/gui_text.py', + 'lib/util.py', 'lib/wallet.py', 'lib/simple_config.py', + 'lib/bitcoin.py', 'lib/deserialize.py' + ], + hiddenimports=["lib","gui"], + pathex=['lib:gui:plugins'], hookspath=None) -pyz = PYZ(a.pure, level=0) + +##### include mydir in distribution ####### +def extra_datas(mydir): + def rec_glob(p, files): + import os + import glob + for d in glob.glob(p): + if os.path.isfile(d): + files.append(d) + rec_glob("%s/*" % d, files) + files = [] + rec_glob("%s/*" % mydir, files) + extra_datas = [] + for f in files: + extra_datas.append((f, f, 'DATA')) + + return extra_datas +########################################### + +# append dirs + +# Theme data +a.datas += extra_datas('data') + +# Localization +a.datas += extra_datas('locale') + +# Py folders that are needed because of the magic import finding +a.datas += extra_datas('gui') +a.datas += extra_datas('lib') +a.datas += extra_datas('plugins') + +pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, exclude_binaries=1, name=os.path.join('build\\pyi.win32\\electrum', 'electrum.exe'), - debug=False, + debug=True, strip=None, - upx=True, - console=False ) + upx=False, + icon='icons/electrum.ico', + console=True) + # The console True makes an annoying black box pop up, but it does make Electrum accept command line options. + coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=None, upx=True, + debug=False, + icon='icons/electrum.ico', + console=True, name=os.path.join('dist', 'electrum')) -app = BUNDLE(coll, - name=os.path.join('dist', 'electrum.app')) diff --git a/contrib/build-wine/electrum.nsis b/contrib/build-wine/electrum.nsis deleted file mode 100644 index 46945557..00000000 --- a/contrib/build-wine/electrum.nsis +++ /dev/null @@ -1,104 +0,0 @@ -;-------------------------------- -;Include Modern UI - - !include "MUI2.nsh" - -;-------------------------------- -;General - - ;Name and file - Name "Electrum" - OutFile "dist/electrum-setup.exe" - - ;Default installation folder - InstallDir "$PROGRAMFILES\Electrum" - - ;Get installation folder from registry if available - InstallDirRegKey HKCU "Software\Electrum" "" - - ;Request application privileges for Windows Vista - RequestExecutionLevel admin - -;-------------------------------- -;Variables - -;-------------------------------- -;Interface Settings - - !define MUI_ABORTWARNING - -;-------------------------------- -;Pages - - !insertmacro MUI_PAGE_LICENSE "tmp/LICENCE" - ;!insertmacro MUI_PAGE_COMPONENTS - !insertmacro MUI_PAGE_DIRECTORY - - ;Start Menu Folder Page Configuration - !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" - !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Electrum" - !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" - - ;!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder - - !insertmacro MUI_PAGE_INSTFILES - - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - -;-------------------------------- -;Languages - - !insertmacro MUI_LANGUAGE "English" - -;-------------------------------- -;Installer Sections - -Section - - SetOutPath "$INSTDIR" - - ;ADD YOUR OWN FILES HERE... - file /r dist\electrum\*.* - - ;Store installation folder - WriteRegStr HKCU "Software\Electrum" "" $INSTDIR - - ;Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" - - - CreateShortCut "$DESKTOP\Electrum.lnk" "$INSTDIR\electrum.exe" "" - - ;create start-menu items - CreateDirectory "$SMPROGRAMS\Electrum" - CreateShortCut "$SMPROGRAMS\Electrum\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 - CreateShortCut "$SMPROGRAMS\Electrum\Electrum.lnk" "$INSTDIR\electrum.exe" "" "$INSTDIR\electrum.exe" 0 - -SectionEnd - -;-------------------------------- -;Descriptions - - ;Assign language strings to sections - ;!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - ; !insertmacro MUI_DESCRIPTION_TEXT ${SecDummy} $(DESC_SecDummy) - ;!insertmacro MUI_FUNCTION_DESCRIPTION_END - -;-------------------------------- -;Uninstaller Section - -Section "Uninstall" - - ;ADD YOUR OWN FILES HERE... - RMDir /r "$INSTDIR\*.*" - - RMDir "$INSTDIR" - - Delete "$DESKTOP\Electrum.lnk" - Delete "$SMPROGRAMS\Electrum\*.*" - RmDir "$SMPROGRAMS\Electrum" - - DeleteRegKey /ifempty HKCU "Software\Electrum" - -SectionEnd