From 4b9a1499879d537fdca9c124529ed9c68abf4f45 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 9 Aug 2015 13:17:04 +0200 Subject: [PATCH 01/47] recently visited wallets: increase shortcut index --- gui/qt/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index df85a19d..cfc355c4 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -386,7 +386,7 @@ class ElectrumWindow(QMainWindow): b = os.path.basename(k) def loader(k): return lambda: self.load_wallet_file(k) - self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%i)) + self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1))) self.recently_visited_menu.setEnabled(len(recent)) def init_menubar(self): From a3a01be8cef5aa7f023834d3d45eafb1426bb038 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 9 Aug 2015 19:00:15 +0300 Subject: [PATCH 02/47] remove unused code --- gui/qt/main_window.py | 13 --------- lib/x509.py | 65 ------------------------------------------- 2 files changed, 78 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index df85a19d..85223d9a 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2915,16 +2915,3 @@ class ElectrumWindow(QMainWindow): text.setText(mpk_text) vbox.addLayout(Buttons(CloseButton(d))) d.exec_() - - @protected - def create_csr(self, alias, challenge, password): - from electrum import x509 - import tlslite - xprv = self.wallet.get_master_private_key(self.wallet.root_name, password) - _, _, _, c, k = bitcoin.deserialize_xkey(xprv) - csr = x509.create_csr(alias, challenge, k) - csr = tlslite.utils.pem.pem(bytearray(csr), "CERTIFICATE REQUEST") - with open('test.csr', 'w') as f: - f.write(csr) - #os.system('openssl asn1parse -i -in test.csr') - return 'test.csr' diff --git a/lib/x509.py b/lib/x509.py index 3f722a16..11716022 100644 --- a/lib/x509.py +++ b/lib/x509.py @@ -190,68 +190,3 @@ def load_certificates(ca_path): ca_keyID[x.get_keyID()] = fp return ca_list, ca_keyID - - -def int_to_bytestr(i): - s = chr(i % 256) - while i > 256: - i >>= 8 - s = chr(i % 256) + s - return s - -def create_csr(commonName, challenge, k): - from bitcoin import point_to_ser - private_key = ecdsa.SigningKey.from_string(k, curve = ecdsa.SECP256k1) - public_key = private_key.get_verifying_key() - pubkey = point_to_ser(public_key.pubkey.point, False) - asn1_type_table = { - 'BOOLEAN': 0x01, 'INTEGER': 0x02, - 'BIT STRING': 0x03, 'OCTET STRING': 0x04, - 'NULL': 0x05, 'OBJECT IDENTIFIER': 0x06, - 'SEQUENCE': 0x30, 'SET': 0x31, - 'PrintableString': 0x13, 'IA5String': 0x16, - 'UTCTime': 0x17, 'ENUMERATED': 0x0A, - 'UTF8String': 0x0C, 'PrintableString': 0x13, - } - def x(t, s): - c = asn1_type_table[t] & 0x3f if type(t) == str else t - l = len(s) - if l < 128: - ls = chr(l) - else: - n = int_to_bytestr(l) - ls = chr(len(n) + 128) + n - return chr(c) + ls + s - x_int = lambda i: x('INTEGER', int_to_bytestr(i)) - x_seq = lambda *items: x('SEQUENCE', ''.join(items)) - x_bitstring = lambda s: x('BIT STRING', s) - x_utf8 = lambda s: x('UTF8String', s) - x_set = lambda *items: x('SET', ''.join(items)) - x_printable = lambda s: x('PrintableString', s) - x_obj = lambda oid: x('OBJECT IDENTIFIER', encode_OID(oid)) - body = x_seq( - x_int(0), - x_seq( - x_set(x_seq(x_obj('2.5.4.3'), x_utf8(commonName))) - ), - x_seq( - x_seq( - x_obj('1.2.840.10045.2.1'), - x_obj('1.3.132.0.10') - ), - x_bitstring(chr(0) + pubkey) - ), - x(0xa0, x_seq(x_obj('1.2.840.113549.1.9.7'), x_set(x_utf8(challenge))) - ) - ) - signature = private_key.sign_deterministic(body, hashfunc=hashlib.sha256, sigencode = ecdsa.util.sigencode_der) - assert public_key.verify(signature, body, hashfunc=hashlib.sha256, sigdecode = ecdsa.util.sigdecode_der) - csr = x_seq( - body, - x_seq(x_obj(ALGO_ECDSA_SHA256)), - x_bitstring(chr(0) + signature) - ) - return csr - - - From b5854b6e8ba638a338603f9740387a8afb990684 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 9 Aug 2015 19:22:26 +0300 Subject: [PATCH 03/47] account: remove unused variables --- lib/account.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/account.py b/lib/account.py index a50d3f03..705052b5 100644 --- a/lib/account.py +++ b/lib/account.py @@ -187,7 +187,6 @@ class OldAccount(Account): @classmethod def mpk_from_seed(klass, seed): - curve = SECP256k1 secexp = klass.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') @@ -211,7 +210,6 @@ class OldAccount(Account): @classmethod def get_pubkey_from_mpk(self, mpk, for_change, n): - curve = SECP256k1 z = self.get_sequence(mpk, for_change, n) master_public_key = ecdsa.VerifyingKey.from_string( mpk, curve = SECP256k1 ) pubkey_point = master_public_key.pubkey.point + z*curve.generator @@ -239,7 +237,6 @@ class OldAccount(Account): def check_seed(self, seed): - curve = SECP256k1 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() From abc7429b0ffc6d3fdadbf78cc6272891a8e5e1e9 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 9 Aug 2015 19:27:26 +0300 Subject: [PATCH 04/47] bitcoin: remove multiple imports --- lib/bitcoin.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 96b61b42..5e3f6723 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -604,8 +604,6 @@ def CKD_priv(k, c, n): return _CKD_priv(k, c, rev_hex(int_to_hex(n,4)).decode('hex'), is_prime) def _CKD_priv(k, c, s, is_prime): - import hmac - from ecdsa.util import string_to_number, number_to_string order = generator_secp256k1.order() keypair = EC_KEY(k) cK = GetPubKey(keypair.pubkey,True) @@ -627,8 +625,6 @@ def CKD_pub(cK, c, n): # helper function, callable with arbitrary string def _CKD_pub(cK, c, s): - import hmac - from ecdsa.util import string_to_number, number_to_string order = generator_secp256k1.order() I = hmac.new(c, cK + s, hashlib.sha512).digest() curve = SECP256k1 @@ -706,7 +702,6 @@ def xpub_from_xprv(xprv, testnet=False): def bip32_root(seed, testnet=False): - import hmac header_pub, header_priv = _get_headers(testnet) I = hmac.new("Bitcoin seed", seed, hashlib.sha512).digest() master_k = I[0:32] From 94ecf8dd7d0197646e6bc2b7d47c256a3132855b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Aug 2015 15:51:31 +0200 Subject: [PATCH 05/47] fix trezor statusbar --- plugins/trezor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/trezor.py b/plugins/trezor.py index 1ab45d12..854191f1 100644 --- a/plugins/trezor.py +++ b/plugins/trezor.py @@ -22,7 +22,7 @@ from electrum.util import print_error, print_msg from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root from electrum_gui.qt.util import * -from electrum_gui.qt.main_window import StatusBarButton +from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow try: from trezorlib.client import types @@ -130,7 +130,8 @@ class Plugin(BasePlugin): self.window = window self.wallet.plugin = self self.trezor_button = StatusBarButton(QIcon(":icons/trezor.png"), _("Trezor"), self.settings_dialog) - self.window.statusBar().addPermanentWidget(self.trezor_button) + if type(window) is ElectrumWindow: + self.window.statusBar().addPermanentWidget(self.trezor_button) if self.handler is None: self.handler = TrezorQtHandler(self.window.app) try: @@ -145,7 +146,8 @@ class Plugin(BasePlugin): @hook def close_wallet(self): - self.window.statusBar().removeWidget(self.trezor_button) + if type(self.window) is ElectrumWindow: + self.window.statusBar().removeWidget(self.trezor_button) @hook def installwizard_load_wallet(self, wallet, window): From 1a9e768e318b9aff6261763b03f665a04edf1a0c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 13 Aug 2015 15:19:34 +0200 Subject: [PATCH 06/47] fix dynamic fee slider behaviour in OSX --- gui/qt/main_window.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index cfc355c4..074cb3d3 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2597,15 +2597,16 @@ class ElectrumWindow(QMainWindow): gui_widgets.append((nz_label, nz)) msg = _('Fee per kilobyte of transaction.') + '\n' \ - + _('If you enable dynamic fees, and this parameter will be used as upper bound.') + + _('If you enable dynamic fees, this parameter will be used as upper bound.') fee_label = HelpLabel(_('Transaction fee per kb') + ':', msg) fee_e = BTCkBEdit(self.get_decimal_point) fee_e.setAmount(self.config.get('fee_per_kb', bitcoin.RECOMMENDED_FEE)) def on_fee(is_done): + if self.config.get('dynamic_fees'): + return v = fee_e.get_amount() or 0 self.config.set_key('fee_per_kb', v, is_done) - if not is_done: - self.update_fee() + self.update_fee() fee_e.editingFinished.connect(lambda: on_fee(True)) fee_e.textEdited.connect(lambda: on_fee(False)) tx_widgets.append((fee_label, fee_e)) From 59e999d3a558f91e5ad7b8605c6787b87ee0bec7 Mon Sep 17 00:00:00 2001 From: BTChip Date: Thu, 13 Aug 2015 23:28:36 +0200 Subject: [PATCH 07/47] Update installwizard init --- plugins/btchipwallet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/btchipwallet.py b/plugins/btchipwallet.py index beb4cde3..d10d2e74 100644 --- a/plugins/btchipwallet.py +++ b/plugins/btchipwallet.py @@ -94,6 +94,10 @@ class Plugin(BasePlugin): QMessageBox.information(self.window, _('Error'), _("BTChip device not detected.\nContinuing in watching-only mode."), _('OK')) self.wallet.force_watching_only = True + @hook + def installwizard_load_wallet(self, wallet, window): + self.load_wallet(wallet, window) + @hook def installwizard_restore(self, wizard, storage): if storage.get('wallet_type') != 'btchip': From 22c602a61c767c34ecd3b2b5cd384801d4ef6ac5 Mon Sep 17 00:00:00 2001 From: BTChip Date: Fri, 14 Aug 2015 13:34:22 +0200 Subject: [PATCH 08/47] Rename BTChip -> Ledger --- plugins/__init__.py | 8 ++++---- plugins/btchipwallet.py | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/__init__.py b/plugins/__init__.py index 5b6eb7ec..072f633a 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -29,11 +29,11 @@ descriptions = [ }, { 'name': 'btchipwallet', - 'fullname': _('BTChip Wallet'), - 'description': _('Provides support for BTChip hardware wallet'), - 'requires': [('btchip', 'github.com/btchip/btchip-python')], + 'fullname': _('Ledger Wallet'), + 'description': _('Provides support for Ledger hardware wallet'), + 'requires': [('btchip', 'github.com/ledgerhq/btchip-python')], 'requires_wallet_type': ['btchip'], - 'registers_wallet_type': ('hardware', 'btchip', _("BTChip wallet")), + 'registers_wallet_type': ('hardware', 'btchip', _("Ledger wallet")), 'available_for': ['qt', 'cmdline'], }, { diff --git a/plugins/btchipwallet.py b/plugins/btchipwallet.py index beb4cde3..c198b982 100644 --- a/plugins/btchipwallet.py +++ b/plugins/btchipwallet.py @@ -88,10 +88,10 @@ class Plugin(BasePlugin): self.handler = BTChipQTHandler(self.window.app) if self.btchip_is_connected(): if not self.wallet.check_proper_device(): - QMessageBox.information(self.window, _('Error'), _("This wallet does not match your BTChip device"), _('OK')) + QMessageBox.information(self.window, _('Error'), _("This wallet does not match your Ledger device"), _('OK')) self.wallet.force_watching_only = True else: - QMessageBox.information(self.window, _('Error'), _("BTChip device not detected.\nContinuing in watching-only mode."), _('OK')) + QMessageBox.information(self.window, _('Error'), _("Ledger device not detected.\nContinuing in watching-only mode."), _('OK')) self.wallet.force_watching_only = True @hook @@ -196,9 +196,9 @@ class BTChipWallet(BIP32_HD_Wallet): # Immediately prompts for the PIN remaining_attempts = self.client.getVerifyPinRemainingAttempts() if remaining_attempts <> 1: - msg = "Enter your BTChip PIN - remaining attempts : " + str(remaining_attempts) + msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) else: - msg = "Enter your BTChip PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." + msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." confirmed, p, pin = self.password_dialog(msg) if not confirmed: aborted = True @@ -224,7 +224,7 @@ class BTChipWallet(BIP32_HD_Wallet): pass self.client = None if not aborted: - raise Exception("Could not connect to your BTChip dongle. Please verify access permissions, PIN, or unplug the dongle and plug it again") + raise Exception("Could not connect to your Ledger wallet. Please verify access permissions, PIN, or unplug the dongle and plug it again") else: raise e self.client.bad = False @@ -320,7 +320,7 @@ class BTChipWallet(BIP32_HD_Wallet): signature = self.get_client().signMessageSign(pin) except BTChipException, e: if e.sw == 0x6a80: - self.give_error("Unfortunately, this message cannot be signed by BTChip. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") + self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") else: self.give_error(e, True) except Exception, e: @@ -419,7 +419,7 @@ class BTChipWallet(BIP32_HD_Wallet): pin2 = "" for keycardIndex in range(len(outputData['keycardData'])): msg = "Do not enter your device PIN here !\r\n\r\n" + \ - "Your BTChip wants to talk to you and tell you a unique second factor code.\r\n" + \ + "Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" + \ "For this to work, please match the character between stars of the output address using your security card\r\n\r\n" + \ "Output address : " for index in range(len(output)): @@ -494,8 +494,8 @@ class BTChipWallet(BIP32_HD_Wallet): def password_dialog(self, msg=None): if not msg: msg = _("Do not enter your device PIN here !\r\n\r\n" \ - "Your BTChip wants to talk to you and tell you a unique second factor code.\r\n" \ - "For this to work, please open a text editor (on a different computer / device if you believe this computer is compromised) and put your cursor into it, unplug your BTChip and plug it back in.\r\n" \ + "Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" \ + "For this to work, please open a text editor (on a different computer / device if you believe this computer is compromised) and put your cursor into it, unplug your Ledger Wallet and plug it back in.\r\n" \ "It should show itself to your computer as a keyboard and output the second factor along with a summary of the transaction it is signing into the text-editor.\r\n\r\n" \ "Check that summary and then enter the second factor code here.\r\n" \ "Before clicking OK, re-plug the device once more (unplug it and plug it again if you read the second factor code on the same computer)") @@ -528,7 +528,7 @@ class BTChipQTHandler: return self.response def auth_dialog(self): - response = QInputDialog.getText(None, "BTChip Authentication", self.message, QLineEdit.Password) + response = QInputDialog.getText(None, "Ledger Wallet Authentication", self.message, QLineEdit.Password) if not response[1]: self.response = None else: @@ -538,7 +538,7 @@ class BTChipQTHandler: def message_dialog(self): self.d = QDialog() self.d.setModal(1) - self.d.setWindowTitle('BTChip') + self.d.setWindowTitle('Ledger') self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) l = QLabel(self.message) vbox = QVBoxLayout(self.d) From e9b346ed29fc963c000e3380e4781d613610fc3a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Aug 2015 15:23:50 +0200 Subject: [PATCH 09/47] add command for master private key --- lib/commands.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/commands.py b/lib/commands.py index 4ae6e476..28da62da 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -322,6 +322,11 @@ class Commands: """Get master public key. Return your wallet\'s master public key(s)""" return self.wallet.get_master_public_keys() + @command('wp') + def getmasterprivate(self): + """Get master private key. Return your wallet\'s master private key""" + return str(self.wallet.get_master_private_key(self.wallet.root_name, self.password)) + @command('wp') def getseed(self): """Get seed phrase. Print the generation seed of your wallet.""" From d69da1ef437499c121293a97441186ba05a895d9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Aug 2015 15:30:00 +0200 Subject: [PATCH 10/47] add/remove transaction: remove unused tx_height parameter --- lib/wallet.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 8489d2dd..3960b9ee 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -251,7 +251,7 @@ class Abstract_Wallet(object): tx = self.transactions.get(tx_hash) if tx is not None: tx.deserialize() - self.add_transaction(tx_hash, tx, tx_height) + self.add_transaction(tx_hash, tx) if save: self.storage.put('addr_history', self.history, True) @@ -693,7 +693,7 @@ class Abstract_Wallet(object): print_error("found pay-to-pubkey address:", addr) return addr - def add_transaction(self, tx_hash, tx, tx_height): + def add_transaction(self, tx_hash, tx): is_coinbase = tx.inputs[0].get('is_coinbase') == True with self.transaction_lock: # add inputs @@ -744,7 +744,7 @@ class Abstract_Wallet(object): # save self.transactions[tx_hash] = tx - def remove_transaction(self, tx_hash, tx_height): + def remove_transaction(self, tx_hash): with self.transaction_lock: print_error("removing tx from history", tx_hash) #tx = self.transactions.pop(tx_hash) @@ -770,7 +770,7 @@ class Abstract_Wallet(object): def receive_tx_callback(self, tx_hash, tx, tx_height): - self.add_transaction(tx_hash, tx, tx_height) + self.add_transaction(tx_hash, tx) #self.network.pending_transactions_for_notifications.append(tx) self.add_unverified_tx(tx_hash, tx_height) @@ -784,7 +784,7 @@ class Abstract_Wallet(object): # remove tx if it's not referenced in histories self.tx_addr_hist[tx_hash].remove(addr) if not self.tx_addr_hist[tx_hash]: - self.remove_transaction(tx_hash, height) + self.remove_transaction(tx_hash) self.history[addr] = hist self.storage.put('addr_history', self.history, True) @@ -800,7 +800,7 @@ class Abstract_Wallet(object): tx = self.transactions.get(tx_hash) if tx is not None and self.txi.get(tx_hash, {}).get(addr) is None and self.txo.get(tx_hash, {}).get(addr) is None: tx.deserialize() - self.add_transaction(tx_hash, tx, tx_height) + self.add_transaction(tx_hash, tx) def get_history(self, domain=None): From 175fef2d566696e11dad43a31ba9e8ce397a99db Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 15 Aug 2015 09:18:29 +0300 Subject: [PATCH 11/47] test_bitcoin: add test case for seeds --- lib/tests/test_bitcoin.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py index a6cb38bf..1c4d73c8 100644 --- a/lib/tests/test_bitcoin.py +++ b/lib/tests/test_bitcoin.py @@ -6,7 +6,7 @@ from lib.bitcoin import ( generator_secp256k1, point_to_ser, public_key_to_bc_address, EC_KEY, bip32_root, bip32_public_derivation, bip32_private_derivation, pw_encode, pw_decode, Hash, public_key_from_private_key, address_from_private_key, - is_valid, is_private_key, xpub_from_xprv) + is_valid, is_private_key, xpub_from_xprv, is_new_seed, is_old_seed) try: import ecdsa @@ -161,3 +161,21 @@ class Test_keyImport(unittest.TestCase): self.assertFalse(is_private_key(self.public_key_hex)) +class Test_seeds(unittest.TestCase): + """ Test old and new seeds. """ + + def test_new_seed(self): + seed = "cram swing cover prefer miss modify ritual silly deliver chunk behind inform able" + self.assertTrue(is_new_seed(seed)) + + seed = "cram swing cover prefer miss modify ritual silly deliver chunk behind inform" + self.assertFalse(is_new_seed(seed)) + + def test_old_seed(self): + self.assertTrue(is_old_seed(" ".join(["like"] * 12))) + self.assertFalse(is_old_seed(" ".join(["like"] * 18))) + self.assertTrue(is_old_seed(" ".join(["like"] * 24))) + self.assertFalse(is_old_seed("not a seed")) + + self.assertTrue(is_old_seed("0123456789ABCDEF" * 2)) + self.assertTrue(is_old_seed("0123456789ABCDEF" * 4)) From fa412c3a86fe1e916cad8f921a059624f6c4f532 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 15 Aug 2015 09:41:20 +0300 Subject: [PATCH 12/47] test_bitcoin: add tests for int packing --- lib/tests/test_bitcoin.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py index 1c4d73c8..9a73b8e3 100644 --- a/lib/tests/test_bitcoin.py +++ b/lib/tests/test_bitcoin.py @@ -6,7 +6,8 @@ from lib.bitcoin import ( generator_secp256k1, point_to_ser, public_key_to_bc_address, EC_KEY, bip32_root, bip32_public_derivation, bip32_private_derivation, pw_encode, pw_decode, Hash, public_key_from_private_key, address_from_private_key, - is_valid, is_private_key, xpub_from_xprv, is_new_seed, is_old_seed) + is_valid, is_private_key, xpub_from_xprv, is_new_seed, is_old_seed, + var_int, op_push) try: import ecdsa @@ -135,6 +136,35 @@ class Test_bitcoin(unittest.TestCase): result = xpub_from_xprv(xprv, testnet=True) self.assertEqual(result, xpub) + def test_var_int(self): + for i in range(0xfd): + self.assertEqual(var_int(i), "{:02x}".format(i) ) + + self.assertEqual(var_int(0xfd), "fdfd00") + self.assertEqual(var_int(0xfe), "fdfe00") + self.assertEqual(var_int(0xff), "fdff00") + self.assertEqual(var_int(0x1234), "fd3412") + self.assertEqual(var_int(0xffff), "fdffff") + self.assertEqual(var_int(0x10000), "fe00000100") + self.assertEqual(var_int(0x12345678), "fe78563412") + self.assertEqual(var_int(0xffffffff), "feffffffff") + self.assertEqual(var_int(0x100000000), "ff0000000001000000") + self.assertEqual(var_int(0x0123456789abcdef), "ffefcdab8967452301") + + def test_op_push(self): + self.assertEqual(op_push(0x00), '00') + self.assertEqual(op_push(0x12), '12') + self.assertEqual(op_push(0x4b), '4b') + self.assertEqual(op_push(0x4c), '4c4c') + self.assertEqual(op_push(0xfe), '4cfe') + self.assertEqual(op_push(0xff), '4dff00') + self.assertEqual(op_push(0x100), '4d0001') + self.assertEqual(op_push(0x1234), '4d3412') + self.assertEqual(op_push(0xfffe), '4dfeff') + self.assertEqual(op_push(0xffff), '4effff0000') + self.assertEqual(op_push(0x10000), '4e00000100') + self.assertEqual(op_push(0x12345678), '4e78563412') + class Test_keyImport(unittest.TestCase): """ The keys used in this class are TEST keys from From ac8e792c5842d5aae20bd97b1f51cbe8066e9276 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 15 Aug 2015 09:52:27 +0300 Subject: [PATCH 13/47] test_ripemd: add simple test case for ripemd module --- lib/tests/test_ripemd.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 lib/tests/test_ripemd.py diff --git a/lib/tests/test_ripemd.py b/lib/tests/test_ripemd.py new file mode 100644 index 00000000..cc832608 --- /dev/null +++ b/lib/tests/test_ripemd.py @@ -0,0 +1,16 @@ +import unittest +import random +import hashlib + +from lib import ripemd + +class Test_RIPEMD160(unittest.TestCase): + """ Test pure Python implementation against standard library. """ + + def test_ripemd(self): + r = random.Random(0) + for i in range(128): + blob = bytearray([r.randrange(0, 256) for j in range(1024)]) + h = hashlib.new('ripemd160') + h.update(blob) + self.assertEqual(h.hexdigest(), ripemd.new(blob).hexdigest()) From 6232a0b76cc041126af62b628394020115cbace0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Aug 2015 10:02:47 +0200 Subject: [PATCH 14/47] speedup fee computation when collecting small inputs --- lib/wallet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 3960b9ee..b32774c1 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -893,9 +893,10 @@ class Abstract_Wallet(object): fee_per_kb = self.fee_per_kb(config) amount = sum(map(lambda x:x[2], outputs)) - total = fee = 0 + total = 0 inputs = [] tx = Transaction.from_io(inputs, outputs) + fee = fixed_fee if fixed_fee is not None else 0 # add old inputs first for item in coins: v = item.get('value') @@ -903,7 +904,7 @@ class Abstract_Wallet(object): self.add_input_info(item) tx.add_input(item) # no need to estimate fee until we have reached desired amount - if total < amount: + if total < amount + fee: continue fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb) if total >= amount + fee: From 98930575546d9a18df646e72198deb22a750992a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Aug 2015 12:17:43 +0200 Subject: [PATCH 15/47] unicode conversion --- gui/qt/paytoedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/paytoedit.py b/gui/qt/paytoedit.py index 1316f543..c7f1214a 100644 --- a/gui/qt/paytoedit.py +++ b/gui/qt/paytoedit.py @@ -160,7 +160,7 @@ class PayToEdit(ScanQRTextEdit): def lines(self): - return str(self.toPlainText()).split('\n') + return unicode(self.toPlainText()).split('\n') def is_multiline(self): From e0939348c3e5d1aac24a1d53931b5fd26ebd35cc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Aug 2015 12:31:57 +0200 Subject: [PATCH 16/47] minimize calls to estimated_fee --- lib/wallet.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index b32774c1..5de73b88 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -878,6 +878,7 @@ class Abstract_Wallet(object): # this method can be overloaded return tx.get_fee() + @profiler def estimated_fee(self, tx, fee_per_kb): estimated_size = len(tx.serialize(-1))/2 fee = int(fee_per_kb * estimated_size / 1000.) @@ -897,7 +898,7 @@ class Abstract_Wallet(object): inputs = [] tx = Transaction.from_io(inputs, outputs) fee = fixed_fee if fixed_fee is not None else 0 - # add old inputs first + # add inputs, sorted by age for item in coins: v = item.get('value') total += v @@ -911,15 +912,27 @@ class Abstract_Wallet(object): break else: raise NotEnoughFunds() - # remove unneeded inputs + # remove unneeded inputs. + removed = False for item in sorted(tx.inputs, key=itemgetter('value')): v = item.get('value') if total - v >= amount + fee: tx.inputs.remove(item) total -= v - fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb) + removed = True + continue else: break + if removed: + fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb) + for item in sorted(tx.inputs, key=itemgetter('value')): + v = item.get('value') + if total - v >= amount + fee: + tx.inputs.remove(item) + total -= v + fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb) + continue + break print_error("using %d inputs"%len(tx.inputs)) # change address From 717a8a5ca7a1584a069b440acfe90282fe7e5812 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Aug 2015 13:32:59 +0200 Subject: [PATCH 17/47] perform fee computation as a timer action --- gui/qt/main_window.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 70735178..0ce64b8c 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -189,6 +189,7 @@ class ElectrumWindow(QMainWindow): self.not_enough_funds = False self.pluginsdialog = None self.fetch_alias() + self.require_fee_update = False def fetch_alias(self): self.alias_info = None @@ -524,6 +525,10 @@ class ElectrumWindow(QMainWindow): self.need_update.clear() # resolve aliases self.payto_e.resolve() + # update fee + if self.require_fee_update: + self.do_update_fee() + self.require_fee_update = False run_hook('timer_actions') def format_amount(self, x, is_diff=False, whitespaces=False): @@ -1001,24 +1006,24 @@ class ElectrumWindow(QMainWindow): def on_shortcut(): sendable = self.get_sendable_balance() inputs = self.get_coins() - for i in inputs: self.wallet.add_input_info(i) + for i in inputs: + self.wallet.add_input_info(i) addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address output = ('address', addr, sendable) dummy_tx = Transaction.from_io(inputs, [output]) - if not self.fee_e.isModified(): + if self.fee_e.get_amount() is None: fee_per_kb = self.wallet.fee_per_kb(self.config) self.fee_e.setAmount(self.wallet.estimated_fee(dummy_tx, fee_per_kb)) self.amount_e.setAmount(max(0, sendable - self.fee_e.get_amount())) - self.amount_e.textEdited.emit("") self.amount_e.shortcut.connect(on_shortcut) - self.payto_e.textChanged.connect(lambda: self.update_fee()) - self.amount_e.textEdited.connect(lambda: self.update_fee()) - self.fee_e.textEdited.connect(lambda: self.update_fee()) + self.payto_e.textChanged.connect(self.update_fee) + self.amount_e.textEdited.connect(self.update_fee) + self.fee_e.textEdited.connect(self.update_fee) # This is so that when the user blanks the fee and moves on, # we go back to auto-calculate mode and put a fee back. - self.fee_e.editingFinished.connect(lambda: self.update_fee()) + self.fee_e.editingFinished.connect(self.update_fee) def entry_changed(): text = "" @@ -1070,6 +1075,9 @@ class ElectrumWindow(QMainWindow): return w def update_fee(self): + self.require_fee_update = True + + def do_update_fee(self): '''Recalculate the fee. If the fee was manually input, retain it, but still build the TX to see if there are enough funds. ''' From c5ee6bf57c79b5f8950a123902cb1d1ed2e2a8dd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Aug 2015 13:39:11 +0200 Subject: [PATCH 18/47] emit signal for fiat_amount update --- gui/qt/main_window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 0ce64b8c..72027205 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1015,6 +1015,8 @@ class ElectrumWindow(QMainWindow): fee_per_kb = self.wallet.fee_per_kb(self.config) self.fee_e.setAmount(self.wallet.estimated_fee(dummy_tx, fee_per_kb)) self.amount_e.setAmount(max(0, sendable - self.fee_e.get_amount())) + # emit signal for fiat_amount update + self.amount_e.textEdited.emit("") self.amount_e.shortcut.connect(on_shortcut) From 438bc94dcedcede8aa657d0b162a888f78f7ea01 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 16 Aug 2015 11:25:23 +0200 Subject: [PATCH 19/47] fix #1399 --- lib/account.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/account.py b/lib/account.py index 705052b5..bae7fc12 100644 --- a/lib/account.py +++ b/lib/account.py @@ -211,9 +211,9 @@ class OldAccount(Account): @classmethod def get_pubkey_from_mpk(self, mpk, for_change, n): z = self.get_sequence(mpk, for_change, n) - master_public_key = ecdsa.VerifyingKey.from_string( mpk, curve = SECP256k1 ) - pubkey_point = master_public_key.pubkey.point + z*curve.generator - public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 ) + master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1) + pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator + public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1) return '04' + public_key2.to_string().encode('hex') def derive_pubkeys(self, for_change, n): From bfae04e6f00579645a8f970bc4604fdc400b48ca Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 16 Aug 2015 11:35:39 +0200 Subject: [PATCH 20/47] Improved in network callbacks: * Pass arguments * Don't redraw history when a tx is verified. * Fix new tx notifications. --- gui/qt/history_widget.py | 51 ++++++++++++++++++++++++---------------- gui/qt/lite_window.py | 6 ++--- gui/qt/main_window.py | 49 +++++++++++++++++++++----------------- lib/network_proxy.py | 10 ++++---- lib/synchronizer.py | 8 +++---- lib/wallet.py | 6 ++--- 6 files changed, 73 insertions(+), 57 deletions(-) diff --git a/gui/qt/history_widget.py b/gui/qt/history_widget.py index 94acd43e..847c728a 100644 --- a/gui/qt/history_widget.py +++ b/gui/qt/history_widget.py @@ -28,10 +28,27 @@ from electrum.plugins import run_hook class HistoryWidget(MyTreeWidget): def __init__(self, parent=None): - MyTreeWidget.__init__(self, parent, self.create_menu, [ '', _('Date'), _('Description') , _('Amount'), _('Balance')], 2) + MyTreeWidget.__init__(self, parent, self.create_menu, ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')], 3) + self.setColumnHidden(1, True) self.config = self.parent.config self.setSortingEnabled(False) + def get_icon(self, conf, timestamp): + time_str = _("unknown") + if conf > 0: + time_str = format_time(timestamp) + if conf == -1: + time_str = 'unverified' + icon = QIcon(":icons/unconfirmed.png") + elif conf == 0: + time_str = 'pending' + icon = QIcon(":icons/unconfirmed.png") + elif conf < 6: + icon = QIcon(":icons/clock%d.png"%conf) + else: + icon = QIcon(":icons/confirmed.png") + return icon, time_str + def update(self, h): self.wallet = self.parent.wallet item = self.currentItem() @@ -39,41 +56,35 @@ class HistoryWidget(MyTreeWidget): self.clear() for item in h: tx_hash, conf, value, timestamp, balance = item - time_str = _("unknown") if conf is None and timestamp is None: continue # skip history in offline mode - if conf > 0: - time_str = format_time(timestamp) - if conf == -1: - time_str = 'unverified' - icon = QIcon(":icons/unconfirmed.png") - elif conf == 0: - time_str = 'pending' - icon = QIcon(":icons/unconfirmed.png") - elif conf < 6: - icon = QIcon(":icons/clock%d.png"%conf) - else: - icon = QIcon(":icons/confirmed.png") + icon, time_str = self.get_icon(conf, timestamp) v_str = self.parent.format_amount(value, True, whitespaces=True) balance_str = self.parent.format_amount(balance, whitespaces=True) label, is_default_label = self.wallet.get_label(tx_hash) - item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] ) - item.setFont(2, QFont(MONOSPACE_FONT)) + item = QTreeWidgetItem(['', tx_hash, time_str, label, v_str, balance_str]) + item.setIcon(0, icon) item.setFont(3, QFont(MONOSPACE_FONT)) item.setFont(4, QFont(MONOSPACE_FONT)) + item.setFont(5, QFont(MONOSPACE_FONT)) if value < 0: - item.setForeground(3, QBrush(QColor("#BC1E1E"))) + item.setForeground(4, QBrush(QColor("#BC1E1E"))) if tx_hash: item.setData(0, Qt.UserRole, tx_hash) if is_default_label: - item.setForeground(2, QBrush(QColor('grey'))) - item.setIcon(0, icon) + item.setForeground(3, QBrush(QColor('grey'))) self.insertTopLevelItem(0, item) if current_tx == tx_hash: self.setCurrentItem(item) - run_hook('history_tab_update') + def update_item(self, tx_hash, conf, timestamp): + icon, time_str = self.get_icon(conf, timestamp) + items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1) + if items: + item = items[0] + item.setIcon(0, icon) + item.setText(2, time_str) def create_menu(self, position): self.selectedIndexes() diff --git a/gui/qt/lite_window.py b/gui/qt/lite_window.py index 153049dd..30009864 100644 --- a/gui/qt/lite_window.py +++ b/gui/qt/lite_window.py @@ -792,9 +792,9 @@ class MiniDriver(QObject): self.network = main_window.network self.window = mini_window - if self.network: - self.network.register_callback('updated',self.update_callback) - self.network.register_callback('status', self.update_callback) + #if self.network: + # self.network.register_callback('updated',self.update_callback) + # self.network.register_callback('status', self.update_callback) self.state = None diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 72027205..21ec426e 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -163,23 +163,19 @@ class ElectrumWindow(QMainWindow): for i in range(tabs.count()): QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i)) - self.connect(self, QtCore.SIGNAL('stop'), self.close) - self.connect(self, QtCore.SIGNAL('update_status'), self.update_status) - self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) ) - self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() ) self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok) self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error) self.labelsChanged.connect(self.update_tabs) - self.history_list.setFocus(True) # network callbacks if self.network: self.network.register_callback('updated', lambda: self.need_update.set()) - self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal'))) - self.network.register_callback('status', lambda: self.emit(QtCore.SIGNAL('update_status'))) - self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal'))) - self.network.register_callback('stop', lambda: self.emit(QtCore.SIGNAL('stop'))) + self.network.register_callback('new_transaction', self.new_transaction) + self.register_callback('status', self.update_status) + self.register_callback('close', self.close) + self.register_callback('banner', self.console.showMessage) + self.register_callback('verified', self.history_list.update_item) # set initial message self.console.showMessage(self.network.banner) @@ -190,6 +186,14 @@ class ElectrumWindow(QMainWindow): self.pluginsdialog = None self.fetch_alias() self.require_fee_update = False + self.tx_notifications = [] + + + def register_callback(self, name, method): + """ run callback in the qt thread """ + self.connect(self, QtCore.SIGNAL(name), method) + self.network.register_callback(name, lambda *params: self.emit(QtCore.SIGNAL(name), *params)) + def fetch_alias(self): self.alias_info = None @@ -461,30 +465,31 @@ class ElectrumWindow(QMainWindow): _("Please report any bugs as issues on github:")+" https://github.com/spesmilo/electrum/issues") + def new_transaction(self, tx): + print "new tx", tx + self.tx_notifications.append(tx) + def notify_transactions(self): if not self.network or not self.network.is_connected(): return - print_error("Notifying GUI") - if len(self.network.pending_transactions_for_notifications) > 0: + if len(self.tx_notifications) > 0: # Combine the transactions if there are more then three - tx_amount = len(self.network.pending_transactions_for_notifications) + tx_amount = len(self.tx_notifications) if(tx_amount >= 3): total_amount = 0 - for tx in self.network.pending_transactions_for_notifications: - is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) + for tx in self.tx_notifications: + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) if(v > 0): total_amount += v - self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \ - % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()}) - - self.network.pending_transactions_for_notifications = [] + % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()}) + self.tx_notifications = [] else: - for tx in self.network.pending_transactions_for_notifications: + for tx in self.tx_notifications: if tx: - self.network.pending_transactions_for_notifications.remove(tx) - is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) + self.tx_notifications.remove(tx) + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) if(v > 0): self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()}) @@ -1858,7 +1863,7 @@ class ElectrumWindow(QMainWindow): def do_search(self, t): i = self.tabs.currentIndex() if i == 0: - self.history_list.filter(t, [1, 2, 3]) # Date, Description, Amount + self.history_list.filter(t, [2, 3, 4]) # Date, Description, Amount elif i == 1: self.invoices_list.filter(t, [0, 1, 2, 3]) # Date, Requestor, Description, Amount elif i == 2: diff --git a/lib/network_proxy.py b/lib/network_proxy.py index 4a4f6e4e..950f7a49 100644 --- a/lib/network_proxy.py +++ b/lib/network_proxy.py @@ -40,7 +40,6 @@ class NetworkProxy(util.DaemonThread): self.subscriptions = {} self.debug = False self.lock = threading.Lock() - self.pending_transactions_for_notifications = [] self.callbacks = {} if socket: @@ -100,7 +99,10 @@ class NetworkProxy(util.DaemonThread): self.servers = value elif key == 'interfaces': self.interfaces = value - self.trigger_callback(key) + if key in ['status', 'updated']: + self.trigger_callback(key) + else: + self.trigger_callback(key, (value,)) return msg_id = response.get('id') @@ -227,8 +229,8 @@ class NetworkProxy(util.DaemonThread): self.callbacks[event] = [] self.callbacks[event].append(callback) - def trigger_callback(self, event): + def trigger_callback(self, event, params=()): with self.lock: callbacks = self.callbacks.get(event,[])[:] if callbacks: - [callback() for callback in callbacks] + [callback(*params) for callback in callbacks] diff --git a/lib/synchronizer.py b/lib/synchronizer.py index 5464299b..08f96fdc 100644 --- a/lib/synchronizer.py +++ b/lib/synchronizer.py @@ -126,16 +126,14 @@ class WalletSynchronizer(): except Exception: self.print_msg("cannot deserialize transaction, skipping", tx_hash) return - self.wallet.receive_tx_callback(tx_hash, tx, tx_height) self.requested_tx.remove((tx_hash, tx_height)) self.print_error("received tx:", tx_hash, len(tx.raw)) + # callbacks + self.network.trigger_callback('new_transaction', (tx,)) if not self.requested_tx: self.network.trigger_callback('updated') - # Updated gets called too many times from other places as - # well; if we used that signal we get the notification - # three times - self.network.trigger_callback("new_transaction") + def request_missing_txs(self, hist): # "hist" is a list of [tx_hash, tx_height] lists diff --git a/lib/wallet.py b/lib/wallet.py index 5de73b88..367162e9 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -424,7 +424,9 @@ class Abstract_Wallet(object): with self.lock: self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) self.storage.put('verified_tx3', self.verified_tx, True) - self.network.trigger_callback('updated') + + conf, timestamp = self.get_confirmations(tx_hash) + self.network.trigger_callback('verified', (tx_hash, conf, timestamp)) def get_unverified_txs(self): '''Returns a list of tuples (tx_hash, height) that are unverified and not beyond local height''' @@ -771,12 +773,10 @@ class Abstract_Wallet(object): def receive_tx_callback(self, tx_hash, tx, tx_height): self.add_transaction(tx_hash, tx) - #self.network.pending_transactions_for_notifications.append(tx) self.add_unverified_tx(tx_hash, tx_height) def receive_history_callback(self, addr, hist): - with self.lock: old_hist = self.history.get(addr, []) for tx_hash, height in old_hist: From 1b3f450267892fee5a5d78fdc2aee20650d28da2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 16 Aug 2015 11:43:59 +0200 Subject: [PATCH 21/47] fix #1401 --- gui/qt/main_window.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 21ec426e..b60e8f29 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1239,9 +1239,10 @@ class ElectrumWindow(QMainWindow): outputs, fee, tx_desc, coins = r try: tx = self.wallet.make_unsigned_transaction(coins, outputs, self.config, fee) - if not tx: - raise BaseException(_("Insufficient funds")) - except Exception as e: + except NotEnoughFunds: + self.show_message(_("Insufficient funds")) + return + except BaseException as e: traceback.print_exc(file=sys.stdout) self.show_message(str(e)) return From 5686499eae5d1c410371cf9cf5226cdb2d83774b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 16 Aug 2015 13:50:03 +0200 Subject: [PATCH 22/47] fix #1400 --- lib/transaction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/transaction.py b/lib/transaction.py index 7368cba1..3fb5381c 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -541,6 +541,7 @@ class Transaction: @classmethod def sweep(klass, privkeys, network, to_address, fee): inputs = [] + keypairs = {} for privkey in privkeys: pubkey = public_key_from_private_key(privkey) address = address_from_private_key(privkey) @@ -557,6 +558,7 @@ class Transaction: item['signatures'] = [None] item['num_sig'] = 1 inputs += u + keypairs[pubkey] = privkey if not inputs: return @@ -564,7 +566,7 @@ class Transaction: total = sum(i.get('value') for i in inputs) - fee outputs = [('address', to_address, total)] self = klass.from_io(inputs, outputs) - self.sign({ pubkey:privkey }) + self.sign(keypairs) return self @classmethod From 02e81a365522912ee4766529677cc8b02ce45b0e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 16 Aug 2015 16:11:52 +0200 Subject: [PATCH 23/47] read command line arguments from stdin if '-' is passed --- electrum | 37 ++++++++++++++++++++++++------------- lib/commands.py | 17 ++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/electrum b/electrum index 0850c47c..7c5df7e1 100755 --- a/electrum +++ b/electrum @@ -80,14 +80,11 @@ from electrum.commands import get_parser, known_commands, Commands, config_varia # get password routine def prompt_password(prompt, confirm=True): import getpass - if sys.stdin.isatty(): - password = getpass.getpass(prompt) - if password and confirm: - password2 = getpass.getpass("Confirm: ") - if password != password2: - sys.exit("Error: Passwords do not match.") - else: - password = raw_input(prompt) + password = getpass.getpass(prompt) + if password and confirm: + password2 = getpass.getpass("Confirm: ") + if password != password2: + sys.exit("Error: Passwords do not match.") if not password: password = None return password @@ -204,6 +201,20 @@ def run_cmdline(config): # options args += map(lambda x: config.get(x), cmd.options) + # pipe data + for i, arg in enumerate(args): + if arg == '-': + if not sys.stdin.isatty(): + pipe_data = sys.stdin.read() + try: + pipe_data = json.loads(pipe_data) + except: + pass + args[i] = pipe_data + break + else: + raise BaseException('Cannot get argument from stdin') + # instanciate wallet for command-line storage = WalletStorage(config.get_wallet_path()) @@ -272,14 +283,16 @@ def run_cmdline(config): always_hook('cmdline_load_wallet', wallet) # important warning - if cmd.name in ['dumpprivkey', 'dumpprivkeys']: + if cmd.name in ['getprivatekeys', 'dumpprivkeys']: print_stderr("WARNING: ALL your private keys are secret.") print_stderr("Exposing a single private key can compromise your entire wallet!") print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.") # commands needing password - if cmd.requires_password: - if wallet.use_encryption: + if cmd.requires_password and wallet.use_encryption: + if config.get('password'): + password = config.get('password') + else: password = prompt_password('Password:', False) if not password: print_msg("Error: Password required") @@ -290,8 +303,6 @@ def run_cmdline(config): except InvalidPassword: print_msg("Error: This password does not decode this wallet.") sys.exit(1) - else: - password = None else: password = None diff --git a/lib/commands.py b/lib/commands.py index 28da62da..d7f49b98 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -239,8 +239,11 @@ class Commands: @command('wp') def getprivatekeys(self, address): - """Get the private keys of an address. Address must be in wallet.""" - return self.wallet.get_private_key(address, self.password) + """Get the private keys of a wallet address, or list of wallet addresses.""" + is_list = type(address) is list + domain = address if is_list else [address] + out = [self.wallet.get_private_key(address, self.password) for address in domain] + return out if is_list else out[0] @command('w') def ismine(self, address): @@ -248,10 +251,9 @@ class Commands: return self.wallet.is_mine(address) @command('wp') - def dumpprivkeys(self, domain=None): + def dumpprivkeys(self): """Dump private keys from your wallet""" - if domain is None: - domain = self.wallet.addresses(True) + domain = self.wallet.addresses(True) return [self.wallet.get_private_key(address, self.password) for address in domain] @command('') @@ -353,15 +355,16 @@ class Commands: @command('n') def sweep(self, privkey, destination, tx_fee=None, nocheck=False): - """Sweep private key. Returns a transaction that spends UTXOs from + """Sweep private keys. Returns a transaction that spends UTXOs from privkey to a destination address. The transaction is not broadcasted.""" + privkeys = privkey if type(privkey) is list else [privkey] self.nocheck = nocheck dest = self._resolver(destination) if tx_fee is None: tx_fee = 0.0001 fee = int(Decimal(tx_fee)*COIN) - return Transaction.sweep([privkey], self.network, dest, fee) + return Transaction.sweep(privkeys, self.network, dest, fee) @command('wp') def signmessage(self, address, message): From fa9ce51685dda32b2e78444d8414946f6f0f8b93 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 16 Aug 2015 16:30:55 +0200 Subject: [PATCH 24/47] deprecate dumpprivkeys --- electrum | 2 +- lib/commands.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/electrum b/electrum index 7c5df7e1..a7ec0910 100755 --- a/electrum +++ b/electrum @@ -283,7 +283,7 @@ def run_cmdline(config): always_hook('cmdline_load_wallet', wallet) # important warning - if cmd.name in ['getprivatekeys', 'dumpprivkeys']: + if cmd.name in ['getprivatekeys']: print_stderr("WARNING: ALL your private keys are secret.") print_stderr("Exposing a single private key can compromise your entire wallet!") print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.") diff --git a/lib/commands.py b/lib/commands.py index d7f49b98..dedfc9d6 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -239,7 +239,7 @@ class Commands: @command('wp') def getprivatekeys(self, address): - """Get the private keys of a wallet address, or list of wallet addresses.""" + """Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.""" is_list = type(address) is list domain = address if is_list else [address] out = [self.wallet.get_private_key(address, self.password) for address in domain] @@ -250,11 +250,10 @@ class Commands: """Check if address is in wallet. Return true if and only address is in wallet""" return self.wallet.is_mine(address) - @command('wp') + @command('') def dumpprivkeys(self): - """Dump private keys from your wallet""" - domain = self.wallet.addresses(True) - return [self.wallet.get_private_key(address, self.password) for address in domain] + """Deprecated.""" + return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '" @command('') def validateaddress(self, address): From 89677c47f78c0b762106c575df952376328805e1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Aug 2015 09:46:50 +0200 Subject: [PATCH 25/47] cmdline: read all arguments before parsing --- electrum | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/electrum b/electrum index a7ec0910..e0f5dd6f 100755 --- a/electrum +++ b/electrum @@ -201,20 +201,6 @@ def run_cmdline(config): # options args += map(lambda x: config.get(x), cmd.options) - # pipe data - for i, arg in enumerate(args): - if arg == '-': - if not sys.stdin.isatty(): - pipe_data = sys.stdin.read() - try: - pipe_data = json.loads(pipe_data) - except: - pass - args[i] = pipe_data - break - else: - raise BaseException('Cannot get argument from stdin') - # instanciate wallet for command-line storage = WalletStorage(config.get_wallet_path()) @@ -383,6 +369,23 @@ if __name__ == '__main__': sys.argv.remove('help') sys.argv.append('-h') + # read arguments from stdin pipe and prompt + for i, arg in enumerate(sys.argv): + if arg == '-': + if not sys.stdin.isatty(): + pipe_data = sys.stdin.read() + try: + pipe_data = json.loads(pipe_data) + except: + pass + sys.argv[i] = pipe_data + break + else: + raise BaseException('Cannot get argument from stdin') + elif arg == '?': + sys.argv[i] = prompt_password('Enter argument (will not echo):', False) + + # parse cmd line parser = get_parser(run_gui, run_daemon, run_cmdline) args = parser.parse_args() @@ -404,10 +407,6 @@ if __name__ == '__main__': if config_options.get('portable'): config_options['electrum_path'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data') - # If private key is passed on the command line, '?' triggers prompt. - if config_options.get('privkey') and config_options['privkey'] == '?': - config_options['privkey'] = prompt_password('Enter PrivateKey (will not echo):', False) - set_verbosity(config_options.get('verbose')) config = SimpleConfig(config_options) From e9523f231ba4e14d0e04a530936a92359d7b3343 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Aug 2015 12:49:04 +0200 Subject: [PATCH 26/47] fix account renaming issues #1158 #1299 --- gui/qt/main_window.py | 2 +- gui/qt/util.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index b60e8f29..9cca30ce 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1706,9 +1706,9 @@ class ElectrumWindow(QMainWindow): name = self.wallet.get_account_name(k) c, u, x = self.wallet.get_account_balance(k) account_item = QTreeWidgetItem([ name, '', self.format_amount(c + u + x), '']) - l.addTopLevelItem(account_item) account_item.setExpanded(self.accounts_expanded.get(k, True)) account_item.setData(0, Qt.UserRole, k) + l.addTopLevelItem(account_item) else: account_item = l sequences = [0,1] if account.has_change() else [0] diff --git a/gui/qt/util.py b/gui/qt/util.py index cb78c41d..bd2e8863 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -335,6 +335,8 @@ class MyTreeWidget(QTreeWidget): self.is_edit = False def label_changed(self, item, column): + if column != self.edit_column: + return if self.is_edit: return self.is_edit = True From 4a514c7638ce627fe6bd8aad1084b0910d7a5cd1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Aug 2015 13:20:45 +0200 Subject: [PATCH 27/47] version 2.4.2 and release notes --- RELEASE-NOTES | 5 +++++ lib/version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 7f8534ce..fa5bfa56 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,8 @@ +# Release 2.4.2 + * Command line can read arguments from stdin (pipe) + * Speedup fee computation for large transactions + * Various bugfixes + # Release 2.4.1 * Use ssl.PROTOCOL_TLSv1 * Fix DNSSEC issues with ECDSA signatures diff --git a/lib/version.py b/lib/version.py index b1943c26..b06549ad 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,4 +1,4 @@ -ELECTRUM_VERSION = "2.4.1" # version of the client package +ELECTRUM_VERSION = "2.4.2" # version of the client package PROTOCOL_VERSION = '0.10' # protocol version requested NEW_SEED_VERSION = 11 # electrum versions >= 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0 From 3f1e4ae3b1a8ced14c0e87355af2d365b2ff47f0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 10:17:53 +0200 Subject: [PATCH 28/47] strip spaces from raw tx --- lib/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transaction.py b/lib/transaction.py index 3fb5381c..ae0c609b 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -481,7 +481,7 @@ class Transaction: return self.raw def __init__(self, raw): - self.raw = raw + self.raw = raw.strip() self.inputs = None def update(self, raw): From f583fcfedbd6e9b7a00caec9f88452f6d61938fd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 12:15:56 +0200 Subject: [PATCH 29/47] do not import dns on android --- electrum | 3 ++- lib/bitcoin.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum b/electrum index e0f5dd6f..338f8c1d 100755 --- a/electrum +++ b/electrum @@ -37,7 +37,8 @@ elif is_bundle and sys.platform=='darwin': # pure-python dependencies need to be imported here for pyinstaller try: - import dns + if not is_android: + import dns import aes import ecdsa import requests diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 5e3f6723..1cbbf462 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -201,6 +201,7 @@ def hash_160(public_key): md.update(sha256(public_key)) return md.digest() except Exception: + # not available in Android SL4a import ripemd md = ripemd.new(sha256(public_key)) return md.digest() From 064f0b71e27967f6f956e7869940450a6e5fd5b6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 12:36:12 +0200 Subject: [PATCH 30/47] dns is used by android now --- contrib/make_android | 2 -- electrum | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/contrib/make_android b/contrib/make_android index 11fa70f1..cfc796fb 100755 --- a/contrib/make_android +++ b/contrib/make_android @@ -21,8 +21,6 @@ if __name__ == '__main__': shutil.copyfile('scripts/authenticator.py', target + '/authenticator.py') shutil.copytree("packages",'dist/e4a-%s/packages'%version, ignore=shutil.ignore_patterns('*.pyc')) shutil.copytree("lib",'dist/e4a-%s/lib'%version, ignore=shutil.ignore_patterns('*.pyc')) - # dns is not used by android app - os.system('rm -rf %s/packages/dns'%target) os.mkdir(target + '/gui') shutil.copyfile('gui/android.py', target + '/gui/android.py') open(target + '/gui/__init__.py','w').close() diff --git a/electrum b/electrum index 338f8c1d..e0f5dd6f 100755 --- a/electrum +++ b/electrum @@ -37,8 +37,7 @@ elif is_bundle and sys.platform=='darwin': # pure-python dependencies need to be imported here for pyinstaller try: - if not is_android: - import dns + import dns import aes import ecdsa import requests From 93b674982e03b24d90ac36c6c1f986c81b9440b9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 16:33:51 +0200 Subject: [PATCH 31/47] fix tx strip --- lib/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transaction.py b/lib/transaction.py index ae0c609b..f0632cfa 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -481,7 +481,7 @@ class Transaction: return self.raw def __init__(self, raw): - self.raw = raw.strip() + self.raw = raw.strip() if raw else None self.inputs = None def update(self, raw): From b7b86481c1725f34e48c6e33afbab64f1fb59d50 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 16:34:10 +0200 Subject: [PATCH 32/47] android fixes --- gui/android.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/gui/android.py b/gui/android.py index 875a9872..537adf28 100644 --- a/gui/android.py +++ b/gui/android.py @@ -607,10 +607,13 @@ def payto_loop(): if data: print "data", data if re.match('^bitcoin:', data): - payto, amount, label, message, _ = util.parse_URI(data) + rr = util.parse_URI(data) + amount = rr.get('amount') + address = rr.get('address') + message = rr.get('message', '') if amount: - amount = str(amount / COIN) - droid.fullSetProperty("recipient", "text", payto) + amount = str(Decimal(amount)/COIN) + droid.fullSetProperty("recipient", "text", address) droid.fullSetProperty("amount", "text", amount) droid.fullSetProperty("message", "text", message) elif is_address(data): @@ -771,7 +774,7 @@ def settings_loop(): def set_listview(): host, port, p, proxy_config, auto_connect = network.get_parameters() - fee = str(Decimal(wallet.fee_per_kb) / COIN) + fee = str(Decimal(wallet.fee_per_kb(config)) / COIN) is_encrypted = 'yes' if wallet.use_encryption else 'no' protocol = protocol_name(p) droid.fullShow(settings_layout) @@ -818,8 +821,10 @@ def settings_loop(): network_changed = True elif pos == "3": #fee - fee = modal_input('Transaction fee', 'The fee will be this amount multiplied by the number of inputs in your transaction. ', - str(Decimal(wallet.fee_per_kb) / COIN), "numberDecimal") + fee = modal_input( + 'Transaction fee', + 'The fee will be this amount multiplied by the number of inputs in your transaction. ', + str(Decimal(wallet.fee_per_kb(config)) / COIN), "numberDecimal") if fee: try: fee = int(COIN * Decimal(fee)) @@ -900,7 +905,7 @@ config = None class ElectrumGui: def __init__(self, _config, _network): - global wallet, network, contacts + global wallet, network, contacts, config network = _network config = _config network.register_callback('updated', update_callback) From c9b3185724f93be0777e0f676bd346da20f1b0de Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 17:05:02 +0200 Subject: [PATCH 33/47] copy mpk to clipboard --- scripts/authenticator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/authenticator.py b/scripts/authenticator.py index 9eed5dd7..fcb5add8 100644 --- a/scripts/authenticator.py +++ b/scripts/authenticator.py @@ -290,6 +290,8 @@ class Authenticator: mpk = wallet.get_master_public_key() self.show_qr(mpk) self.show_title('master public key') + droid.setClipboard(mpk) + droid.makeToast("Master public key copied to clipboard") elif event["name"] == "scan": r = droid.scanBarcode() From 6f474eea3b570abf9e21e9d651001a0927c095e9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 17:07:32 +0200 Subject: [PATCH 34/47] remove deprecated docs --- docs/android.html | 60 --------------------------------------- docs/cold_storage | 31 --------------------- docs/console.html | 71 ----------------------------------------------- 3 files changed, 162 deletions(-) delete mode 100644 docs/android.html delete mode 100644 docs/cold_storage delete mode 100644 docs/console.html diff --git a/docs/android.html b/docs/android.html deleted file mode 100644 index 1aed1a47..00000000 --- a/docs/android.html +++ /dev/null @@ -1,60 +0,0 @@ - - -
-

Electrum for Android

- -This page explains how to install Electrum on Android devices.

- -Please note that Electrum is not distributed as a binary package, but -as python source code; this gives users the possibility to see what -the code is doing, and to check that it does not contain malware. The -downside is that installation is slightly more complicated than -downloading an app on the Android market, but it remains very -simple.

- -It is possible to print this page on paper and to install everything from -QR codes. If you encounter problems, you may find help at - -this link. - - -

1. Download and install Google Scripting Layer for Android

- -You can get -it here, -or by scanning the following qr code:
- - - -

2. Download and install Python for Android

- -You can get -it here, -or by scanning the following qr code:
- -
-Once you have installed the apk, launch the Python for Android application and click 'install' - -

3. Download the Electrum install script

- -Download e4a_install.py and install it in your sl4a/scripts directory. -You can do it manually, or from QR code, as follows: -
-1. Launch SL4A.
-2. Press the Menu button.
-3. Tap Add.
-4. Tap Scan Barcode.
-5. Scan the following QRcode:
-
- -
This will install a script named e4a_install.py
- -

4. Download and install Electrum

-
-1. Tap e4a_install.py: it will download and install a directory named "Electrum-0.43d"
-2. To launch Electrum, visit the "Electrum-0.43d" directory and tap 'electrum4a.py'
-
- - diff --git a/docs/cold_storage b/docs/cold_storage deleted file mode 100644 index f94a524e..00000000 --- a/docs/cold_storage +++ /dev/null @@ -1,31 +0,0 @@ -Here is how to sign a transaction with an offline Electrum wallet. - -1. With your online (seedless) wallet, create the transaction using 'mktx': - -./electrum -w seedless_wallet mktx 1Cpf9zb5Rm5Z5qmmGezn6ERxFWvwuZ6UCx 0.1 -{ - "complete": false, - "hex": "01000000015a92850cc5dc7bb7c1a711c1ce0c1658596c085d49c17fce68c641cce0bdd188010000004801ff45fe197f1a7a7779f58690c3100364d7ce596bf47bb52e88e617e22940bf54a8f139194652584b0d357eb95defb8b4911b0a53118b8afecb96aedb1334e772df350901002800ffffffff02b1f0f65d000000001976a9147ea19cc36d846e2ce81762def3cb9299bed0847188ac80969800000000001976a91451e814c0f7637ba9a59bc11628337a2df6559a5088ac00000000" -} - -Electrum returns an unsigned transaction. Note that the serialization -format contains the master public key needed and key derivation, used -by the offline wallet to sign the transaction. - - -2. Sign the transaction with your offline wallet, using 'signrawtransaction': - -./electrum -w wallet_with_seed signrawtransaction 01000000015a92850cc5dc7bb7c1a711c1ce0c1658596c085d49c17fce68c641cce0bdd188010000004801ff45fe197f1a7a7779f58690c3100364d7ce596bf47bb52e88e617e22940bf54a8f139194652584b0d357eb95defb8b4911b0a53118b8afecb96aedb1334e772df350901002800ffffffff02b1f0f65d000000001976a9147ea19cc36d846e2ce81762def3cb9299bed0847188ac80969800000000001976a91451e814c0f7637ba9a59bc11628337a2df6559a5088ac00000000 -Password: -{ - "complete": true, - "hex": "01000000015a92850cc5dc7bb7c1a711c1ce0c1658596c085d49c17fce68c641cce0bdd188010000008b483045022100c65dd8899d4e1d12b1ebaa0ea15835f9a158343733fbe990cdfebde2164d89c802201a5a8fe737b07daf700aeecf3b6a4111c563ebc181da75b1f264883060c273da0141040beb415f075a532982fe982d01736453d4e3413566c79a39d16679474c7ab94022269b9f726edc152a89dfcf18cd3dd2a38fc5e442f24d22a51545ca42beb7b5ffffffff02b1f0f65d000000001976a9147ea19cc36d846e2ce81762def3cb9299bed0847188ac80969800000000001976a91451e814c0f7637ba9a59bc11628337a2df6559a5088ac00000000" -} - -The result is a fully signed transaction, as indicated by the "complete" field. - - -3. Broadcast the transaction to the Bitcoin network, using 'sendrawtransaction': - - ./electrum sendrawtransaction 01000000015a92850cc5dc7bb7c1a711c1ce0c1658596c085d49c17fce68c641cce0bdd188010000008b483045022100c65dd8899d4e1d12b1ebaa0ea15835f9a158343733fbe990cdfebde2164d89c802201a5a8fe737b07daf700aeecf3b6a4111c563ebc181da75b1f264883060c273da0141040beb415f075a532982fe982d01736453d4e3413566c79a39d16679474c7ab94022269b9f726edc152a89dfcf18cd3dd2a38fc5e442f24d22a51545ca42beb7b5ffffffff02b1f0f65d000000001976a9147ea19cc36d846e2ce81762def3cb9299bed0847188ac80969800000000001976a91451e814c0f7637ba9a59bc11628337a2df6559a5088ac00000000 -"ef6b561232f3c507219ab7d2a79f8849e14ed7e926e77546c2d9e751905b825b" diff --git a/docs/console.html b/docs/console.html deleted file mode 100644 index e8430f02..00000000 --- a/docs/console.html +++ /dev/null @@ -1,71 +0,0 @@ - - -This is the documentation for the Electrum Console.
- - -
-
-Most Electrum command-line commands are also available in the console.
-The results are Python objects, even though they are -sometimes rendered as JSON for clarity.
-
-Let us call listunspent(), to see the list of unspent outputs in the wallet: -
->> listunspent()
-[
-    {
-        "address": "12cmY5RHRgx8KkUKASDcDYRotget9FNso3", 
-        "index": 0, 
-        "raw_output_script": "76a91411bbdc6e3a27c44644d83f783ca7df3bdc2778e688ac", 
-        "tx_hash": "e7029df9ac8735b04e8e957d0ce73987b5c9c5e920ec4a445130cdeca654f096", 
-        "value": 0.01
-    }, 
-    {
-        "address": "1GavSCND6TB7HuCnJSTEbHEmCctNGeJwXF", 
-        "index": 0, 
-        "raw_output_script": "76a914aaf437e25805f288141bfcdc27887ee5492bd13188ac", 
-        "tx_hash": "b30edf57ca2a31560b5b6e8dfe567734eb9f7d3259bb334653276efe520735df", 
-        "value": 9.04735316
-    }
-]
-
-Note that the result is rendered as JSON.
-However, if we save it to a Python variable, it is rendered as a Python object: -
->> u = listunspent()
->> u
-[{'tx_hash': u'e7029df9ac8735b04e8e957d0ce73987b5c9c5e920ec4a445130cdeca654f096', 'index': 0, 'raw_output_script': '76a91411bbdc6e3a27c44644d83f783ca7df3bdc2778e688ac', 'value': 0.01, 'address': '12cmY5RHRgx8KkUKASDcDYRotget9FNso3'}, {'tx_hash': u'b30edf57ca2a31560b5b6e8dfe567734eb9f7d3259bb334653276efe520735df', 'index': 0, 'raw_output_script': '76a914aaf437e25805f288141bfcdc27887ee5492bd13188ac', 'value': 9.04735316, 'address': '1GavSCND6TB7HuCnJSTEbHEmCctNGeJwXF'}]
-
-
-This makes it possible to combine Electrum commands with Python.
-For example, let us pick only the addresses in the previous result: -
->> map(lambda x:x.get('address'), listunspent())
-[
-    "12cmY5RHRgx8KkUKASDcDYRotget9FNso3", 
-    "1GavSCND6TB7HuCnJSTEbHEmCctNGeJwXF"
-]
-
-Here we combine two commands, listunspent -and dumpprivkeys, in order to dump the private keys of all adresses that have unspent outputs: -
->> dumpprivkeys( map(lambda x:x.get('address'), listunspent()) )
-{
-    "12cmY5RHRgx8KkUKASDcDYRotget9FNso3": "***************************************************", 
-    "1GavSCND6TB7HuCnJSTEbHEmCctNGeJwXF": "***************************************************"
-}
-
-Note that dumpprivkey will ask for your password if your -wallet is encrypted. -
-The GUI methods can be accessed through the gui variable. -For example, you can display a QR code from a string using -gui.show_qrcode. -Example: -
-gui.show_qrcode(dumpprivkey(listunspent()[0]['address']))
-
- -
- - From bba9da91dea39dbba2c88f41601cbaa4ac90d922 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 17:16:15 +0200 Subject: [PATCH 35/47] rm debugging statement --- gui/qt/main_window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 9cca30ce..45e17ba4 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -466,7 +466,6 @@ class ElectrumWindow(QMainWindow): def new_transaction(self, tx): - print "new tx", tx self.tx_notifications.append(tx) def notify_transactions(self): From 2af670ea2b92e835919b745d94afcb8b4ec32fda Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 17:36:22 +0200 Subject: [PATCH 36/47] add packages directory to osx setup --- setup-release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup-release.py b/setup-release.py index de778f83..3f946334 100644 --- a/setup-release.py +++ b/setup-release.py @@ -1,5 +1,5 @@ """ -py2app/py2exe build script for Electrum Litecoin +py2app/py2exe build script for Electrum Usage (Mac OS X): python setup.py py2app @@ -37,7 +37,7 @@ if sys.platform == 'darwin': app=[mainscript], options=dict(py2app=dict(argv_emulation=False, includes=['PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtWebKit', 'PyQt4.QtNetwork', 'sip'], - packages=['lib', 'gui', 'plugins'], + packages=['lib', 'gui', 'plugins', 'packages'], iconfile='electrum.icns', plist=plist, resources=["icons"])), From dfb3d7b119ab43f6936d130c98a8cc20d90ca8e3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Aug 2015 18:35:52 +0200 Subject: [PATCH 37/47] show number of inputs/outputs --- gui/qt/transaction_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 525ca791..a69ecfac 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -234,7 +234,7 @@ class TxDialog(QDialog): if self.tx.locktime > 0: vbox.addWidget(QLabel("LockTime: %d\n" % self.tx.locktime)) - vbox.addWidget(QLabel(_("Inputs"))) + vbox.addWidget(QLabel(_("Inputs") + ' (%d)'%len(self.tx.inputs))) ext = QTextCharFormat() rec = QTextCharFormat() @@ -273,7 +273,7 @@ class TxDialog(QDialog): cursor.insertBlock() vbox.addWidget(i_text) - vbox.addWidget(QLabel(_("Outputs"))) + vbox.addWidget(QLabel(_("Outputs") + ' (%d)'%len(self.tx.outputs))) o_text = QTextEdit() o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setReadOnly(True) From 2ffd419e66ac4d2cedae1dd5e91d11203101f4ec Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 09:12:46 +0200 Subject: [PATCH 38/47] possibly fix #1403 --- gui/qt/paytoedit.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/gui/qt/paytoedit.py b/gui/qt/paytoedit.py index c7f1214a..ded2b950 100644 --- a/gui/qt/paytoedit.py +++ b/gui/qt/paytoedit.py @@ -75,39 +75,34 @@ class PayToEdit(ScanQRTextEdit): x, y = line.split(',') n = re.match('^SCRIPT\s+([0-9a-fA-F]+)$', x.strip()) if n: - _type = 'script' - address = n.group(1).decode('hex') + script = str(n.group(1)).decode('hex') amount = self.parse_amount(y) + return 'script', script, amount else: - _type = 'address' address = self.parse_address(x) amount = self.parse_amount(y) - return _type, address, amount - + return 'address', address, amount def parse_amount(self, x): p = pow(10, self.amount_edit.decimal_point()) - return int( p * Decimal(x.strip())) - + return int(p * Decimal(x.strip())) def parse_address(self, line): r = line.strip() m = re.match('^'+RE_ALIAS+'$', r) - address = m.group(2) if m else r + address = str(m.group(2)) if m else r assert bitcoin.is_address(address) return address - def check_text(self): self.errors = [] if self.is_pr: return # filter out empty lines - lines = filter( lambda x: x, self.lines()) + lines = filter(lambda x: x, self.lines()) outputs = [] total = 0 self.payto_address = None - if len(lines) == 1: data = lines[0] if data.startswith("bitcoin:"): @@ -123,12 +118,12 @@ class PayToEdit(ScanQRTextEdit): for i, line in enumerate(lines): try: - type, to_address, amount = self.parse_address_and_amount(line) + _type, to_address, amount = self.parse_address_and_amount(line) except: self.errors.append((i, line.strip())) continue - outputs.append((type, to_address, amount)) + outputs.append((_type, to_address, amount)) total += amount self.outputs = outputs @@ -158,11 +153,9 @@ class PayToEdit(ScanQRTextEdit): return self.outputs[:] - def lines(self): return unicode(self.toPlainText()).split('\n') - def is_multiline(self): return len(self.lines()) > 1 From 2996ec927f9313bc4a603b50cf38b6fb53998718 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 10:15:36 +0200 Subject: [PATCH 39/47] fix #862: ask passphrase only once --- plugins/trezor.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/plugins/trezor.py b/plugins/trezor.py index 854191f1..89c9b592 100644 --- a/plugins/trezor.py +++ b/plugins/trezor.py @@ -23,6 +23,7 @@ from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root from electrum_gui.qt.util import * from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow +from electrum_gui.qt.installwizard import InstallWizard try: from trezorlib.client import types @@ -133,7 +134,7 @@ class Plugin(BasePlugin): if type(window) is ElectrumWindow: self.window.statusBar().addPermanentWidget(self.trezor_button) if self.handler is None: - self.handler = TrezorQtHandler(self.window.app) + self.handler = TrezorQtHandler(self.window) try: self.get_client().ping('t') except BaseException as e: @@ -625,18 +626,23 @@ class TrezorQtHandler: self.done.set() def passphrase_dialog(self): - from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog - d = QDialog() - d.setModal(1) - d.setLayout(make_password_dialog(d, None, self.message, False)) - confirmed, p, passphrase = run_password_dialog(d, None, None) - if not confirmed: - QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) - self.passphrase = None - else: - if passphrase is None: - passphrase = '' # Even blank string is valid Trezor passphrase + if type(self.win) is ElectrumWindow: + passphrase = self.win.password_dialog(_("Please enter your Trezor passphrase")) self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) + else: + assert type(self.win) is InstallWizard + from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog + d = QDialog() + d.setModal(1) + d.setLayout(make_password_dialog(d, None, self.message, False)) + confirmed, p, passphrase = run_password_dialog(d, None, None) + if not confirmed: + QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) + self.passphrase = None + else: + if passphrase is None: + passphrase = '' # Even blank string is valid Trezor passphrase + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) self.done.set() def message_dialog(self): From 425cc4d258ef994db72adcfbb4a66b9922325019 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 10:20:33 +0200 Subject: [PATCH 40/47] fix: handle blank passphrase properly --- plugins/trezor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/trezor.py b/plugins/trezor.py index 89c9b592..386602d8 100644 --- a/plugins/trezor.py +++ b/plugins/trezor.py @@ -628,7 +628,7 @@ class TrezorQtHandler: def passphrase_dialog(self): if type(self.win) is ElectrumWindow: passphrase = self.win.password_dialog(_("Please enter your Trezor passphrase")) - self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' else: assert type(self.win) is InstallWizard from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog @@ -640,9 +640,7 @@ class TrezorQtHandler: QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) self.passphrase = None else: - if passphrase is None: - passphrase = '' # Even blank string is valid Trezor passphrase - self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase emse '' self.done.set() def message_dialog(self): From ed6c6bae2488d7f29b6c4f876d69bb09c93479cc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 10:24:11 +0200 Subject: [PATCH 41/47] fix typo --- plugins/trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/trezor.py b/plugins/trezor.py index 386602d8..ad46679c 100644 --- a/plugins/trezor.py +++ b/plugins/trezor.py @@ -640,7 +640,7 @@ class TrezorQtHandler: QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) self.passphrase = None else: - self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase emse '' + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' self.done.set() def message_dialog(self): From 6bbfef51989ad9a9cac3513c6595bb7c725828dc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 11:04:06 +0200 Subject: [PATCH 42/47] add timestamp to history command output, convert value to float --- lib/commands.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index dedfc9d6..dd008f32 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -453,13 +453,18 @@ class Commands: for item in self.wallet.get_history(): tx_hash, conf, value, timestamp, balance = item try: - time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3] + time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] except Exception: time_str = "----" - label, is_default_label = self.wallet.get_label(tx_hash) - - out.append({'txid':tx_hash, 'date':"%16s"%time_str, 'label':label, 'value':format_satoshis(value), 'confirmations':conf}) + out.append({ + 'txid':tx_hash, + 'timestamp':timestamp, + 'date':"%16s"%time_str, + 'label':label, + 'value':float(format_satoshis(value)) if value is not None else None, + 'confirmations':conf} + ) return out @command('w') From 37034bdf7f64509998cc26959bb656d32fa416ae Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 11:10:55 +0200 Subject: [PATCH 43/47] listunspent: convert value to float --- lib/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index dd008f32..5e793ae6 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -155,7 +155,9 @@ class Commands: """List unspent outputs. Returns the list of unspent transaction outputs in your wallet.""" l = copy.deepcopy(self.wallet.get_spendable_coins(exclude_frozen = False)) - for i in l: i["value"] = str(Decimal(i["value"])/COIN) + for i in l: + v = i["value"] + i["value"] = float(v)/COIN if v is not None else None return l @command('n') @@ -462,7 +464,7 @@ class Commands: 'timestamp':timestamp, 'date':"%16s"%time_str, 'label':label, - 'value':float(format_satoshis(value)) if value is not None else None, + 'value':float(value)/COIN if value is not None else None, 'confirmations':conf} ) return out From df206719b74a2109be3144ba5dc62c039207df2b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 13:33:00 +0200 Subject: [PATCH 44/47] should fix #1403 --- gui/qt/paytoedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/paytoedit.py b/gui/qt/paytoedit.py index ded2b950..c1da6e0c 100644 --- a/gui/qt/paytoedit.py +++ b/gui/qt/paytoedit.py @@ -90,7 +90,7 @@ class PayToEdit(ScanQRTextEdit): def parse_address(self, line): r = line.strip() m = re.match('^'+RE_ALIAS+'$', r) - address = str(m.group(2)) if m else r + address = str(m.group(2) if m else r) assert bitcoin.is_address(address) return address From b2e2218556cba29114c33eeada4888d159da5ad7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 18:33:49 +0200 Subject: [PATCH 45/47] fix: remove deprecated parameter --- gui/qt/main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 45e17ba4..6bca31fc 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2260,7 +2260,7 @@ class ElectrumWindow(QMainWindow): tx = self.tx_from_text(data) if not tx: return - self.show_transaction(tx, prompt_if_unsaved=True) + self.show_transaction(tx) def read_tx_from_file(self): @@ -2282,7 +2282,7 @@ class ElectrumWindow(QMainWindow): return tx = self.tx_from_text(text) if tx: - self.show_transaction(tx, prompt_if_unsaved=True) + self.show_transaction(tx) def do_process_from_file(self): tx = self.read_tx_from_file() From 53b1ce2c5003fd29fb27c95e83cddb8982b911ff Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Aug 2015 19:25:05 +0200 Subject: [PATCH 46/47] restore 'save' menu. fixes #1407 --- gui/qt/main_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 6bca31fc..ae3e7c9c 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -401,6 +401,7 @@ class ElectrumWindow(QMainWindow): self.recently_visited_menu = file_menu.addMenu(_("&Recently open")) file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open) file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New) + file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs) file_menu.addSeparator() file_menu.addAction(_("&Quit"), self.close) self.update_recently_visited() From 678ec84267638e2edcb6a75650a248c12efae59b Mon Sep 17 00:00:00 2001 From: dabura667 Date: Thu, 20 Aug 2015 23:18:47 +0900 Subject: [PATCH 47/47] Add Chinese Simplified wordlist --- lib/mnemonic.py | 1 + lib/wordlist/chinese_simplified.txt | 2048 +++++++++++++++++++++++++++ 2 files changed, 2049 insertions(+) create mode 100644 lib/wordlist/chinese_simplified.txt diff --git a/lib/mnemonic.py b/lib/mnemonic.py index 0b8a5d15..14d5a8a5 100644 --- a/lib/mnemonic.py +++ b/lib/mnemonic.py @@ -90,6 +90,7 @@ filenames = { 'es':'spanish.txt', 'ja':'japanese.txt', 'pt':'portuguese.txt', + 'zh':'chinese_simplified.txt' } diff --git a/lib/wordlist/chinese_simplified.txt b/lib/wordlist/chinese_simplified.txt new file mode 100644 index 00000000..b90f1ed8 --- /dev/null +++ b/lib/wordlist/chinese_simplified.txt @@ -0,0 +1,2048 @@ +的 +一 +是 +在 +不 +了 +有 +和 +人 +这 +中 +大 +为 +上 +个 +国 +我 +以 +要 +他 +时 +来 +用 +们 +生 +到 +作 +地 +于 +出 +就 +分 +对 +成 +会 +可 +主 +发 +年 +动 +同 +工 +也 +能 +下 +过 +子 +说 +产 +种 +面 +而 +方 +后 +多 +定 +行 +学 +法 +所 +民 +得 +经 +十 +三 +之 +进 +着 +等 +部 +度 +家 +电 +力 +里 +如 +水 +化 +高 +自 +二 +理 +起 +小 +物 +现 +实 +加 +量 +都 +两 +体 +制 +机 +当 +使 +点 +从 +业 +本 +去 +把 +性 +好 +应 +开 +它 +合 +还 +因 +由 +其 +些 +然 +前 +外 +天 +政 +四 +日 +那 +社 +义 +事 +平 +形 +相 +全 +表 +间 +样 +与 +关 +各 +重 +新 +线 +内 +数 +正 +心 +反 +你 +明 +看 +原 +又 +么 +利 +比 +或 +但 +质 +气 +第 +向 +道 +命 +此 +变 +条 +只 +没 +结 +解 +问 +意 +建 +月 +公 +无 +系 +军 +很 +情 +者 +最 +立 +代 +想 +已 +通 +并 +提 +直 +题 +党 +程 +展 +五 +果 +料 +象 +员 +革 +位 +入 +常 +文 +总 +次 +品 +式 +活 +设 +及 +管 +特 +件 +长 +求 +老 +头 +基 +资 +边 +流 +路 +级 +少 +图 +山 +统 +接 +知 +较 +将 +组 +见 +计 +别 +她 +手 +角 +期 +根 +论 +运 +农 +指 +几 +九 +区 +强 +放 +决 +西 +被 +干 +做 +必 +战 +先 +回 +则 +任 +取 +据 +处 +队 +南 +给 +色 +光 +门 +即 +保 +治 +北 +造 +百 +规 +热 +领 +七 +海 +口 +东 +导 +器 +压 +志 +世 +金 +增 +争 +济 +阶 +油 +思 +术 +极 +交 +受 +联 +什 +认 +六 +共 +权 +收 +证 +改 +清 +美 +再 +采 +转 +更 +单 +风 +切 +打 +白 +教 +速 +花 +带 +安 +场 +身 +车 +例 +真 +务 +具 +万 +每 +目 +至 +达 +走 +积 +示 +议 +声 +报 +斗 +完 +类 +八 +离 +华 +名 +确 +才 +科 +张 +信 +马 +节 +话 +米 +整 +空 +元 +况 +今 +集 +温 +传 +土 +许 +步 +群 +广 +石 +记 +需 +段 +研 +界 +拉 +林 +律 +叫 +且 +究 +观 +越 +织 +装 +影 +算 +低 +持 +音 +众 +书 +布 +复 +容 +儿 +须 +际 +商 +非 +验 +连 +断 +深 +难 +近 +矿 +千 +周 +委 +素 +技 +备 +半 +办 +青 +省 +列 +习 +响 +约 +支 +般 +史 +感 +劳 +便 +团 +往 +酸 +历 +市 +克 +何 +除 +消 +构 +府 +称 +太 +准 +精 +值 +号 +率 +族 +维 +划 +选 +标 +写 +存 +候 +毛 +亲 +快 +效 +斯 +院 +查 +江 +型 +眼 +王 +按 +格 +养 +易 +置 +派 +层 +片 +始 +却 +专 +状 +育 +厂 +京 +识 +适 +属 +圆 +包 +火 +住 +调 +满 +县 +局 +照 +参 +红 +细 +引 +听 +该 +铁 +价 +严 +首 +底 +液 +官 +德 +随 +病 +苏 +失 +尔 +死 +讲 +配 +女 +黄 +推 +显 +谈 +罪 +神 +艺 +呢 +席 +含 +企 +望 +密 +批 +营 +项 +防 +举 +球 +英 +氧 +势 +告 +李 +台 +落 +木 +帮 +轮 +破 +亚 +师 +围 +注 +远 +字 +材 +排 +供 +河 +态 +封 +另 +施 +减 +树 +溶 +怎 +止 +案 +言 +士 +均 +武 +固 +叶 +鱼 +波 +视 +仅 +费 +紧 +爱 +左 +章 +早 +朝 +害 +续 +轻 +服 +试 +食 +充 +兵 +源 +判 +护 +司 +足 +某 +练 +差 +致 +板 +田 +降 +黑 +犯 +负 +击 +范 +继 +兴 +似 +余 +坚 +曲 +输 +修 +故 +城 +夫 +够 +送 +笔 +船 +占 +右 +财 +吃 +富 +春 +职 +觉 +汉 +画 +功 +巴 +跟 +虽 +杂 +飞 +检 +吸 +助 +升 +阳 +互 +初 +创 +抗 +考 +投 +坏 +策 +古 +径 +换 +未 +跑 +留 +钢 +曾 +端 +责 +站 +简 +述 +钱 +副 +尽 +帝 +射 +草 +冲 +承 +独 +令 +限 +阿 +宣 +环 +双 +请 +超 +微 +让 +控 +州 +良 +轴 +找 +否 +纪 +益 +依 +优 +顶 +础 +载 +倒 +房 +突 +坐 +粉 +敌 +略 +客 +袁 +冷 +胜 +绝 +析 +块 +剂 +测 +丝 +协 +诉 +念 +陈 +仍 +罗 +盐 +友 +洋 +错 +苦 +夜 +刑 +移 +频 +逐 +靠 +混 +母 +短 +皮 +终 +聚 +汽 +村 +云 +哪 +既 +距 +卫 +停 +烈 +央 +察 +烧 +迅 +境 +若 +印 +洲 +刻 +括 +激 +孔 +搞 +甚 +室 +待 +核 +校 +散 +侵 +吧 +甲 +游 +久 +菜 +味 +旧 +模 +湖 +货 +损 +预 +阻 +毫 +普 +稳 +乙 +妈 +植 +息 +扩 +银 +语 +挥 +酒 +守 +拿 +序 +纸 +医 +缺 +雨 +吗 +针 +刘 +啊 +急 +唱 +误 +训 +愿 +审 +附 +获 +茶 +鲜 +粮 +斤 +孩 +脱 +硫 +肥 +善 +龙 +演 +父 +渐 +血 +欢 +械 +掌 +歌 +沙 +刚 +攻 +谓 +盾 +讨 +晚 +粒 +乱 +燃 +矛 +乎 +杀 +药 +宁 +鲁 +贵 +钟 +煤 +读 +班 +伯 +香 +介 +迫 +句 +丰 +培 +握 +兰 +担 +弦 +蛋 +沉 +假 +穿 +执 +答 +乐 +谁 +顺 +烟 +缩 +征 +脸 +喜 +松 +脚 +困 +异 +免 +背 +星 +福 +买 +染 +井 +概 +慢 +怕 +磁 +倍 +祖 +皇 +促 +静 +补 +评 +翻 +肉 +践 +尼 +衣 +宽 +扬 +棉 +希 +伤 +操 +垂 +秋 +宜 +氢 +套 +督 +振 +架 +亮 +末 +宪 +庆 +编 +牛 +触 +映 +雷 +销 +诗 +座 +居 +抓 +裂 +胞 +呼 +娘 +景 +威 +绿 +晶 +厚 +盟 +衡 +鸡 +孙 +延 +危 +胶 +屋 +乡 +临 +陆 +顾 +掉 +呀 +灯 +岁 +措 +束 +耐 +剧 +玉 +赵 +跳 +哥 +季 +课 +凯 +胡 +额 +款 +绍 +卷 +齐 +伟 +蒸 +殖 +永 +宗 +苗 +川 +炉 +岩 +弱 +零 +杨 +奏 +沿 +露 +杆 +探 +滑 +镇 +饭 +浓 +航 +怀 +赶 +库 +夺 +伊 +灵 +税 +途 +灭 +赛 +归 +召 +鼓 +播 +盘 +裁 +险 +康 +唯 +录 +菌 +纯 +借 +糖 +盖 +横 +符 +私 +努 +堂 +域 +枪 +润 +幅 +哈 +竟 +熟 +虫 +泽 +脑 +壤 +碳 +欧 +遍 +侧 +寨 +敢 +彻 +虑 +斜 +薄 +庭 +纳 +弹 +饲 +伸 +折 +麦 +湿 +暗 +荷 +瓦 +塞 +床 +筑 +恶 +户 +访 +塔 +奇 +透 +梁 +刀 +旋 +迹 +卡 +氯 +遇 +份 +毒 +泥 +退 +洗 +摆 +灰 +彩 +卖 +耗 +夏 +择 +忙 +铜 +献 +硬 +予 +繁 +圈 +雪 +函 +亦 +抽 +篇 +阵 +阴 +丁 +尺 +追 +堆 +雄 +迎 +泛 +爸 +楼 +避 +谋 +吨 +野 +猪 +旗 +累 +偏 +典 +馆 +索 +秦 +脂 +潮 +爷 +豆 +忽 +托 +惊 +塑 +遗 +愈 +朱 +替 +纤 +粗 +倾 +尚 +痛 +楚 +谢 +奋 +购 +磨 +君 +池 +旁 +碎 +骨 +监 +捕 +弟 +暴 +割 +贯 +殊 +释 +词 +亡 +壁 +顿 +宝 +午 +尘 +闻 +揭 +炮 +残 +冬 +桥 +妇 +警 +综 +招 +吴 +付 +浮 +遭 +徐 +您 +摇 +谷 +赞 +箱 +隔 +订 +男 +吹 +园 +纷 +唐 +败 +宋 +玻 +巨 +耕 +坦 +荣 +闭 +湾 +键 +凡 +驻 +锅 +救 +恩 +剥 +凝 +碱 +齿 +截 +炼 +麻 +纺 +禁 +废 +盛 +版 +缓 +净 +睛 +昌 +婚 +涉 +筒 +嘴 +插 +岸 +朗 +庄 +街 +藏 +姑 +贸 +腐 +奴 +啦 +惯 +乘 +伙 +恢 +匀 +纱 +扎 +辩 +耳 +彪 +臣 +亿 +璃 +抵 +脉 +秀 +萨 +俄 +网 +舞 +店 +喷 +纵 +寸 +汗 +挂 +洪 +贺 +闪 +柬 +爆 +烯 +津 +稻 +墙 +软 +勇 +像 +滚 +厘 +蒙 +芳 +肯 +坡 +柱 +荡 +腿 +仪 +旅 +尾 +轧 +冰 +贡 +登 +黎 +削 +钻 +勒 +逃 +障 +氨 +郭 +峰 +币 +港 +伏 +轨 +亩 +毕 +擦 +莫 +刺 +浪 +秘 +援 +株 +健 +售 +股 +岛 +甘 +泡 +睡 +童 +铸 +汤 +阀 +休 +汇 +舍 +牧 +绕 +炸 +哲 +磷 +绩 +朋 +淡 +尖 +启 +陷 +柴 +呈 +徒 +颜 +泪 +稍 +忘 +泵 +蓝 +拖 +洞 +授 +镜 +辛 +壮 +锋 +贫 +虚 +弯 +摩 +泰 +幼 +廷 +尊 +窗 +纲 +弄 +隶 +疑 +氏 +宫 +姐 +震 +瑞 +怪 +尤 +琴 +循 +描 +膜 +违 +夹 +腰 +缘 +珠 +穷 +森 +枝 +竹 +沟 +催 +绳 +忆 +邦 +剩 +幸 +浆 +栏 +拥 +牙 +贮 +礼 +滤 +钠 +纹 +罢 +拍 +咱 +喊 +袖 +埃 +勤 +罚 +焦 +潜 +伍 +墨 +欲 +缝 +姓 +刊 +饱 +仿 +奖 +铝 +鬼 +丽 +跨 +默 +挖 +链 +扫 +喝 +袋 +炭 +污 +幕 +诸 +弧 +励 +梅 +奶 +洁 +灾 +舟 +鉴 +苯 +讼 +抱 +毁 +懂 +寒 +智 +埔 +寄 +届 +跃 +渡 +挑 +丹 +艰 +贝 +碰 +拔 +爹 +戴 +码 +梦 +芽 +熔 +赤 +渔 +哭 +敬 +颗 +奔 +铅 +仲 +虎 +稀 +妹 +乏 +珍 +申 +桌 +遵 +允 +隆 +螺 +仓 +魏 +锐 +晓 +氮 +兼 +隐 +碍 +赫 +拨 +忠 +肃 +缸 +牵 +抢 +博 +巧 +壳 +兄 +杜 +讯 +诚 +碧 +祥 +柯 +页 +巡 +矩 +悲 +灌 +龄 +伦 +票 +寻 +桂 +铺 +圣 +恐 +恰 +郑 +趣 +抬 +荒 +腾 +贴 +柔 +滴 +猛 +阔 +辆 +妻 +填 +撤 +储 +签 +闹 +扰 +紫 +砂 +递 +戏 +吊 +陶 +伐 +喂 +疗 +瓶 +婆 +抚 +臂 +摸 +忍 +虾 +蜡 +邻 +胸 +巩 +挤 +偶 +弃 +槽 +劲 +乳 +邓 +吉 +仁 +烂 +砖 +租 +乌 +舰 +伴 +瓜 +浅 +丙 +暂 +燥 +橡 +柳 +迷 +暖 +牌 +秧 +胆 +详 +簧 +踏 +瓷 +谱 +呆 +宾 +糊 +洛 +辉 +愤 +竞 +隙 +怒 +粘 +乃 +绪 +肩 +籍 +敏 +涂 +熙 +皆 +侦 +悬 +掘 +享 +纠 +醒 +狂 +锁 +淀 +恨 +牲 +霸 +爬 +赏 +逆 +玩 +陵 +祝 +秒 +浙 +貌 +役 +彼 +悉 +鸭 +趋 +凤 +晨 +畜 +辈 +秩 +卵 +署 +梯 +炎 +滩 +棋 +驱 +筛 +峡 +冒 +啥 +寿 +译 +浸 +泉 +帽 +迟 +硅 +疆 +贷 +漏 +稿 +冠 +嫩 +胁 +芯 +牢 +叛 +蚀 +奥 +鸣 +岭 +羊 +凭 +串 +塘 +绘 +酵 +融 +盆 +锡 +庙 +筹 +冻 +辅 +摄 +袭 +筋 +拒 +僚 +旱 +钾 +鸟 +漆 +沈 +眉 +疏 +添 +棒 +穗 +硝 +韩 +逼 +扭 +侨 +凉 +挺 +碗 +栽 +炒 +杯 +患 +馏 +劝 +豪 +辽 +勃 +鸿 +旦 +吏 +拜 +狗 +埋 +辊 +掩 +饮 +搬 +骂 +辞 +勾 +扣 +估 +蒋 +绒 +雾 +丈 +朵 +姆 +拟 +宇 +辑 +陕 +雕 +偿 +蓄 +崇 +剪 +倡 +厅 +咬 +驶 +薯 +刷 +斥 +番 +赋 +奉 +佛 +浇 +漫 +曼 +扇 +钙 +桃 +扶 +仔 +返 +俗 +亏 +腔 +鞋 +棱 +覆 +框 +悄 +叔 +撞 +骗 +勘 +旺 +沸 +孤 +吐 +孟 +渠 +屈 +疾 +妙 +惜 +仰 +狠 +胀 +谐 +抛 +霉 +桑 +岗 +嘛 +衰 +盗 +渗 +脏 +赖 +涌 +甜 +曹 +阅 +肌 +哩 +厉 +烃 +纬 +毅 +昨 +伪 +症 +煮 +叹 +钉 +搭 +茎 +笼 +酷 +偷 +弓 +锥 +恒 +杰 +坑 +鼻 +翼 +纶 +叙 +狱 +逮 +罐 +络 +棚 +抑 +膨 +蔬 +寺 +骤 +穆 +冶 +枯 +册 +尸 +凸 +绅 +坯 +牺 +焰 +轰 +欣 +晋 +瘦 +御 +锭 +锦 +丧 +旬 +锻 +垄 +搜 +扑 +邀 +亭 +酯 +迈 +舒 +脆 +酶 +闲 +忧 +酚 +顽 +羽 +涨 +卸 +仗 +陪 +辟 +惩 +杭 +姚 +肚 +捉 +飘 +漂 +昆 +欺 +吾 +郎 +烷 +汁 +呵 +饰 +萧 +雅 +邮 +迁 +燕 +撒 +姻 +赴 +宴 +烦 +债 +帐 +斑 +铃 +旨 +醇 +董 +饼 +雏 +姿 +拌 +傅 +腹 +妥 +揉 +贤 +拆 +歪 +葡 +胺 +丢 +浩 +徽 +昂 +垫 +挡 +览 +贪 +慰 +缴 +汪 +慌 +冯 +诺 +姜 +谊 +凶 +劣 +诬 +耀 +昏 +躺 +盈 +骑 +乔 +溪 +丛 +卢 +抹 +闷 +咨 +刮 +驾 +缆 +悟 +摘 +铒 +掷 +颇 +幻 +柄 +惠 +惨 +佳 +仇 +腊 +窝 +涤 +剑 +瞧 +堡 +泼 +葱 +罩 +霍 +捞 +胎 +苍 +滨 +俩 +捅 +湘 +砍 +霞 +邵 +萄 +疯 +淮 +遂 +熊 +粪 +烘 +宿 +档 +戈 +驳 +嫂 +裕 +徙 +箭 +捐 +肠 +撑 +晒 +辨 +殿 +莲 +摊 +搅 +酱 +屏 +疫 +哀 +蔡 +堵 +沫 +皱 +畅 +叠 +阁 +莱 +敲 +辖 +钩 +痕 +坝 +巷 +饿 +祸 +丘 +玄 +溜 +曰 +逻 +彭 +尝 +卿 +妨 +艇 +吞 +韦 +怨 +矮 +歇