commit 3f919e193b15a6096d120828fd742188b6578497
parent 2959a29134f87298a64a66536e7b4bb4601774b3
Author: slush0 <info@bitcoin.cz>
Date: Fri, 18 Jan 2013 13:10:02 +0000
Merge branch 'master' of git://github.com/spesmilo/electrum
Diffstat:
32 files changed, 1211 insertions(+), 455 deletions(-)
diff --git a/README b/README
@@ -1,9 +1,9 @@
Electrum - lightweight Bitcoin client
Licence: GNU GPL v3
-Author: thomasv@gitorious
+Author: thomasv@bitcointalk.org
Language: Python
-Homepage: http://electrum.ecdsa.org/
+Homepage: http://electrum.org/
== INSTALL ==
@@ -42,4 +42,4 @@ On Mac OS X:
== BROWSER CONFIGURATION ==
-see http://ecdsa.org/bitcoin_URIs.html
+See http://electrum.org/bitcoin_URIs.html
diff --git a/RELEASE-NOTES b/RELEASE-NOTES
@@ -1,3 +1,55 @@
+# Release 1.6.2 (Not released)
+
+== Classic GUI
+* Added new version notification
+
+# Release 1.6.1 (11-01-2013)
+
+== Core
+* It is now possible to restore a wallet from MPK (this will create a watching-only wallet)
+* A switch button allows to easily switch between Lite and Classic GUI.
+
+== Classic GUI
+* Seed and MPK help dialogs were rewritten
+* Point of Sale: requested amounts can be expressed in other currencies and are converted to bitcoin.
+
+== Lite GUI
+* The receiving button was removed in favor of a menu item to keep it consistent with the history toggle.
+
+# Release 1.6.0 (07-01-2013)
+
+== Core
+* (Feature) Add support for importing, signing and verifiying compressed keys
+* (Feature) Auto reconnect to random server on disconnect
+* (Feature) Ultimate fallback to HTTP port 80 if TCP doesn't work on any server
+* (Bug) Under rare circumstances changing password with incorrect password could damage wallet
+
+== Lite GUI
+* (Chore) Use blockchain.info for exchange rate data
+* (Feature) added currency conversion for BRL, CNY, RUB
+* (Feature) Saraha theme
+* (Feature) csv import/export for transactions including labels
+
+== Classic GUI
+* (Chore) pruning servers now called "p", full servers "f" to avoid confusion with terms
+* (Feature) Debits in history shown in red
+* (Feature) csv import/export for transactions including labels
+
+# Release 1.5.8 (02-01-2013)
+
+== Core
+* (Bug) Fix pending address balance on received coins for pruning servers
+* (Bug) Fix history command line option to show output again (regression by SPV)
+* (Chore) Add timeout to blockchain headers file download by HTTP
+* (Feature) new option: -L, --language: default language used in GUI.
+
+== Lite GUI
+* (Bug) Sending to auto-completed contacts works again
+* (Chore) Added version number to title bar
+
+== Classic GUI
+* (Feature) Language selector in options.
+
# Release 1.5.7 (18-12-2012)
== Core
diff --git a/TODOLIST b/TODOLIST
@@ -0,0 +1,23 @@
+
+security:
+ - check that we are on the longest chain after a reorg
+ - check SSL cerfificates of servers
+
+
+wallet, transactions :
+ - dust sweeping
+ - transactions with multiple outputs
+ - BIP 32
+
+
+code improvements:
+ - qrcode and bmp patches are on github (they are incompatible with android)
+
+
+android:
+ - kivy-based gui
+
+
+protocol:
+ - add client authentication, to make paying servers possible
+
diff --git a/app.fil b/app.fil
@@ -1,2 +1,3 @@
lib/gui_qt.py
lib/gui.py
+lib/gui_lite.py
diff --git a/contrib/build-wine/README b/contrib/build-wine/README
@@ -1,7 +1,7 @@
These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine.
Usage:
-1. Copy content of this directory to /electrum-wine.
+1. Copy content of this directory to /build-wine.
2. Install Wine (version 1.4 or 1.5+ works fine, 1.4.1 has bug).
3. Run "./prepare-wine.sh", it will download all dependencies. When you'll be asked, always leave default settings and press "Next >".
4. By running "./build-electrum.sh", sources will be packed into three separate versions to dist/ directory:
diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh
@@ -6,7 +6,7 @@ BRANCH=master
NAME_ROOT=electrum
# These settings probably don't need any change
-export WINEPREFIX=~/.wine-electrum
+export WINEPREFIX=/opt/wine-electrum
PYHOME=c:/python26
PYTHON="wine $PYHOME/python.exe -OO -B"
diff --git a/contrib/build-wine/build-electrum.sh b/contrib/build-wine/build-electrum.sh
@@ -1,11 +1,11 @@
#!/bin/bash
# You probably need to update only this link
-ELECTRUM_URL=https://github.com/downloads/spesmilo/electrum/Electrum-1.5.6.tar.gz
-NAME_ROOT=electrum-1.5.6
+ELECTRUM_URL=https://github.com/downloads/spesmilo/electrum/Electrum-1.6.1.tar.gz
+NAME_ROOT=electrum-1.6.1
# These settings probably don't need any change
-export WINEPREFIX=~/.wine-electrum
+export WINEPREFIX=/opt/wine-electrum
PYHOME=c:/python26
PYTHON="wine $PYHOME/python.exe -OO -B"
diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh
@@ -9,7 +9,7 @@ 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
# These settings probably don't need change
-export WINEPREFIX=~/.wine-electrum
+export WINEPREFIX=/opt/wine-electrum
PYHOME=c:/python26
PYTHON="wine $PYHOME/python.exe -OO -B"
diff --git a/data/cleanlook/style.css b/data/cleanlook/style.css
@@ -9,19 +9,18 @@ MiniWindow QPushButton {
border-radius: 0px;
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 white, stop: 1 #E6E6E6);
- min-height: 25px;
+ min-height: 30px;
min-width: 30px;
}
#send_button{
- color: #E5F2FF;
+ color: #FFF;
border: 1px solid #3786E6;
border-radius: 4px;
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #72B2F8, stop: 1 #3484E6);
- min-width: 80px;
- min-height: 23px;
padding: 2px;
+ width: 20px;
}
#send_button:disabled{
@@ -30,71 +29,33 @@ MiniWindow QPushButton {
border-radius: 4px;
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #A5CFFA, stop: 1 #72B2F8);
- min-width: 80px;
- min-height: 23px;
- padding: 2px;
}
-#receive_button
-{
- color: #777;
- border: 1px solid #CCC;
- border-radius: 0px;
- background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
- stop: 0 white, stop: 1 #E6E6E6);
- min-height: 25px;
- min-width: 30px;
-}
-#receive_button[isActive=true]
-{
- color: #575757;
- background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
- stop: 0 white, stop: 1 #D1D1D1);
-}
-
#address_input, #amount_input, #label_input
{
color: #000;
padding: 5px;
- border-radius: 4px;
+ border-radius: 5px;
+ min-height: 23px;
border: 1px solid #AAA9A9;
- width: 225px;
+ width: 200px;
}
#address_input[isValid=true]
{
color: #4D9948;
- padding: 5px;
- border-radius: 4px;
- border: 1px solid #AAA9A9;
- width: 225px;
- margin-top: 4px;
}
#address_input[isValid=false]
{
color: #CE4141;
- padding: 5px;
- border-radius: 4px;
- border: 1px solid #AAA9A9;
- width: 225px;
- margin-top: 4px;
}
-#address_input[isValid=placeholder]
-{
- color: blue;
- padding: 5px;
- border-radius: 4px;
- border: 1px solid #AAA9A9;
- width: 225px;
- margin-top: 4px;
-}
#balance_label
{
color: #333;
}
-#history::item
+#history
{
color: #888;
}
diff --git a/data/sahara/name.cfg b/data/sahara/name.cfg
@@ -0,0 +1 @@
+Sahara
diff --git a/data/sahara/style.css b/data/sahara/style.css
@@ -0,0 +1,102 @@
+#main_window
+{
+ background: qlineargradient(x1: 0, y1: 0, x2:0,y2:1, stop: 0 white , stop: 1 #F2E3BE);
+}
+
+MiniWindow QPushButton {
+ color: #C1A76D;
+ border: 1px solid #A7811C;
+ border-radius: 0px;
+ background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+ stop: 0 white, stop: 1 #F2E3BE);
+ min-height: 25px;
+ min-width: 30px;
+}
+
+#send_button{
+ color: #FEEBA7;
+ border: 1px solid #AD8B35;
+ border-radius: 4px;
+ background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+ stop: 0 #E0A035, stop: 1 #AD8B35);
+ min-width: 80px;
+ min-height: 23px;
+ padding: 2px;
+}
+
+#send_button:disabled{
+ color: #FEEDD3;
+ border: 1px solid #F7D46D;
+ border-radius: 4px;
+ background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+ stop: 0 #FAEEA5, stop: 1 #DBC050);
+ min-width: 80px;
+ min-height: 23px;
+ padding: 2px;
+}
+
+#receive_button
+{
+ color: #FEEBA7;
+ border: 1px solid #AD8B35;
+ border-radius: 4px;
+ background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+ stop: 0 #E0A035, stop: 1 #AD8B35);
+ min-height: 25px;
+ min-width: 30px;
+}
+#receive_button[isActive=true]
+{
+ color: #FEEBA7;
+ background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+ stop: 0 #E0A035, stop: 1 #987620);
+}
+
+#address_input, #amount_input
+{
+ color: #000;
+ padding: 5px;
+ border-radius: 4px;
+ border: 1px solid #CBAE69;
+ width: 225px;
+}
+
+#address_input[isValid=true]
+{
+ color: #4D9948;
+ padding: 5px;
+ border-radius: 4px;
+ border: 1px solid #CBAE69;
+ width: 225px;
+ margin-top: 4px;
+}
+
+#address_input[isValid=false]
+{
+ color: #CE4141;
+ padding: 5px;
+ border-radius: 4px;
+ border: 1px solid #CBAE69;
+ width: 225px;
+ margin-top: 4px;
+}
+
+#address_input[isValid=placeholder]
+{
+ color: #DEC58D;
+ padding: 5px;
+ border-radius: 4px;
+ border: 1px solid #CBAE69;
+ width: 225px;
+ margin-top: 4px;
+}
+#balance_label
+{
+ color: #7E5907;
+}
+
+#history
+{
+ color: #8B6914;
+}
+
diff --git a/electrum b/electrum
@@ -70,8 +70,8 @@ options:\n --fee, -f: set transaction fee\n --fromaddr, -s: send from address
'signtx':"Sign an unsigned transaction created by a deseeded wallet\nSyntax: signtx <filename>",
'seed':
"Print the generation seed of your wallet.",
- 'import':
- 'Imports a key pair\nSyntax: import <address>:<privatekey>',
+ 'importprivkey':
+ 'Import a private key\nSyntax: importprivkey <privatekey>',
'signmessage':
'Signs a message with a key\nSyntax: signmessage <address> <message>\nIf you want to lead or end a message with spaces, or want double spaces inside the message make sure you quote the string. I.e. " Hello This is a weird String "',
'verifymessage':
@@ -99,13 +99,13 @@ offline_commands = [ 'password', 'mktx', 'signtx',
'help', 'validateaddress',
'signmessage', 'verifymessage',
'eval', 'set', 'get', 'create', 'addresses',
- 'import', 'seed',
+ 'importprivkey', 'seed',
'deseed','reseed',
'freeze','unfreeze',
'prioritize','unprioritize']
-protected_commands = ['payto', 'password', 'mktx', 'signtx', 'seed', 'import','signmessage' ]
+protected_commands = ['payto', 'password', 'mktx', 'signtx', 'seed', 'importprivkey','signmessage' ]
# get password routine
def prompt_password(prompt, confirm=True):
@@ -138,6 +138,8 @@ def arg_parser():
parser.add_option("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="show debugging information")
parser.add_option("-P", "--portable", action="store_true", dest="portable", default=False, help="portable wallet")
+ parser.add_option("-L", "--lang", dest="language", default=None, help="defaut language used in GUI")
+ parser.add_option("-u", "--usb", dest="bitkey", action="store_true", help="Turn on support for hardware wallets (EXPERIMENTAL)")
return parser
@@ -204,8 +206,10 @@ if __name__ == '__main__':
interface = Interface(config, True)
wallet.interface = interface
interface.start()
- interface.send([('server.peers.subscribe',[])])
+ if interface.is_connected:
+ interface.send([('server.peers.subscribe',[])])
+ set_language(config.get('language'))
gui = gui.ElectrumGui(wallet, config)
found = config.wallet_file_exists
@@ -220,8 +224,18 @@ if __name__ == '__main__':
wallet.init_mpk( wallet.seed )
else:
# ask for seed and gap.
- if not gui.seed_dialog(): exit()
- wallet.init_mpk( wallet.seed )
+ sg = gui.seed_dialog()
+ if not sg: exit()
+ seed, gap = sg
+ if not seed: exit()
+ wallet.gap_limit = gap
+ if len(seed) == 128:
+ wallet.seed = None
+ wallet.master_public_key = seed
+ else:
+ wallet.seed = str(seed)
+ wallet.init_mpk( wallet.seed )
+
# generate the first addresses, in case we are offline
if s is None or a == 'create':
@@ -301,10 +315,14 @@ if __name__ == '__main__':
if not seed:
sys.exit("Error: No seed")
- wallet.seed = str(seed)
- wallet.init_mpk( wallet.seed )
- if not options.offline:
+ if len(seed) == 128:
+ wallet.seed = None
+ wallet.master_public_key = seed
+ else:
+ wallet.seed = str(seed)
+ wallet.init_mpk( wallet.seed )
+ if not options.offline:
interface = Interface(config)
interface.start()
wallet.interface = interface
@@ -378,24 +396,31 @@ if __name__ == '__main__':
# commands needing password
if cmd in protected_commands or ( cmd=='addresses' and options.show_keys):
- password = prompt_password('Password:', False) if wallet.use_encryption and not is_temporary else None
- # check password
- try:
- wallet.pw_decode( wallet.seed, password)
- except:
- print_msg("Error: This password does not decode this wallet.")
- exit(1)
+ if wallet.use_encryption and not is_temporary:
+ password = prompt_password('Password:', False)
+ if not password:
+ print_msg("Error: Password required")
+ exit(1)
+ # check password
+ try:
+ seed = wallet.decode_seed(password)
+ except:
+ print_msg("Error: This password does not decode this wallet.")
+ exit(1)
+ else:
+ password = None
+ seed = wallet.seed
- if cmd == 'import':
+ if cmd == 'importprivkey':
# See if they specificed a key on the cmd line, if not prompt
if len(args) > 1:
- keypair = args[1]
+ sec = args[1]
else:
- keypair = prompt_password('Enter Address:PrivateKey (will not echo):', False)
+ sec = prompt_password('Enter PrivateKey (will not echo):', False)
try:
- wallet.import_key(keypair,password)
+ addr = wallet.import_key(sec,password)
wallet.save()
- print_msg("Keypair imported")
+ print_msg("Keypair imported: ", addr)
except BaseException as e:
print_msg("Error: Keypair import failed: " + str(e))
@@ -410,7 +435,6 @@ if __name__ == '__main__':
print_msg(known_commands[cmd2])
elif cmd == 'seed':
- seed = wallet.pw_decode( wallet.seed, password)
print_msg(seed + ' "' + ' '.join(mnemonic_encode(seed)) + '"')
elif cmd == 'deseed':
@@ -613,11 +637,6 @@ if __name__ == '__main__':
print_msg(h)
elif cmd == 'password':
- try:
- seed = wallet.pw_decode( wallet.seed, password)
- except ValueError:
- sys.exit("Error: Password does not decrypt this wallet.")
-
new_password = prompt_password('New password:')
wallet.update_password(seed, password, new_password)
diff --git a/icons.qrc b/icons.qrc
@@ -13,6 +13,7 @@
<file>icons/status_connected.png</file>
<file>icons/status_disconnected.png</file>
<file>icons/status_waiting.png</file>
+ <file>icons/switchgui.png</file>
<file>icons/unconfirmed.png</file>
<file>icons/network.png</file>
</qresource>
diff --git a/icons/switchgui.png b/icons/switchgui.png
Binary files differ.
diff --git a/lib/__init__.py b/lib/__init__.py
@@ -1,6 +1,8 @@
from version import ELECTRUM_VERSION
from util import format_satoshis, print_msg, print_error, set_verbosity
-from wallet import Wallet, WalletSynchronizer
+from i18n import set_language
+from wallet import WalletSynchronizer
+from wallet_factory import WalletFactory as Wallet
from verifier import WalletVerifier
from interface import Interface, pick_random_server, DEFAULT_SERVERS
from simple_config import SimpleConfig
diff --git a/lib/bitcoin.py b/lib/bitcoin.py
@@ -43,8 +43,57 @@ Hash = lambda x: hashlib.sha256(hashlib.sha256(x).digest()).digest()
hash_encode = lambda x: x[::-1].encode('hex')
hash_decode = lambda x: x.decode('hex')[::-1]
-############ functions from pywallet #####################
+# pywallet openssl private key implementation
+
+def i2d_ECPrivateKey(pkey, compressed=False):
+ if compressed:
+ key = '3081d30201010420' + \
+ '%064x' % pkey.secret + \
+ 'a081a53081a2020101302c06072a8648ce3d0101022100' + \
+ '%064x' % _p + \
+ '3006040100040107042102' + \
+ '%064x' % _Gx + \
+ '022100' + \
+ '%064x' % _r + \
+ '020101a124032200'
+ else:
+ key = '308201130201010420' + \
+ '%064x' % pkey.secret + \
+ 'a081a53081a2020101302c06072a8648ce3d0101022100' + \
+ '%064x' % _p + \
+ '3006040100040107044104' + \
+ '%064x' % _Gx + \
+ '%064x' % _Gy + \
+ '022100' + \
+ '%064x' % _r + \
+ '020101a144034200'
+
+ return key.decode('hex') + i2o_ECPublicKey(pkey, compressed)
+
+def i2o_ECPublicKey(pkey, compressed=False):
+ # public keys are 65 bytes long (520 bits)
+ # 0x04 + 32-byte X-coordinate + 32-byte Y-coordinate
+ # 0x00 = point at infinity, 0x02 and 0x03 = compressed, 0x04 = uncompressed
+ # compressed keys: <sign> <x> where <sign> is 0x02 if y is even and 0x03 if y is odd
+ if compressed:
+ if pkey.pubkey.point.y() & 1:
+ key = '03' + '%064x' % pkey.pubkey.point.x()
+ else:
+ key = '02' + '%064x' % pkey.pubkey.point.x()
+ else:
+ key = '04' + \
+ '%064x' % pkey.pubkey.point.x() + \
+ '%064x' % pkey.pubkey.point.y()
+
+ return key.decode('hex')
+
+# end pywallet openssl private key implementation
+
+
+
+############ functions from pywallet #####################
+
addrtype = 0
def hash_160(public_key):
@@ -151,17 +200,39 @@ def DecodeBase58Check(psz):
def PrivKeyToSecret(privkey):
return privkey[9:9+32]
-def SecretToASecret(secret):
- vchIn = chr(addrtype+128) + secret
+def SecretToASecret(secret, compressed=False):
+ vchIn = chr((addrtype+128)&255) + secret
+ if compressed: vchIn += '\01'
return EncodeBase58Check(vchIn)
def ASecretToSecret(key):
vch = DecodeBase58Check(key)
- if vch and vch[0] == chr(addrtype+128):
+ if vch and vch[0] == chr((addrtype+128)&255):
return vch[1:]
else:
return False
+def regenerate_key(sec):
+ b = ASecretToSecret(sec)
+ if not b:
+ return False
+ b = b[0:32]
+ secret = int('0x' + b.encode('hex'), 16)
+ return EC_KEY(secret)
+
+def GetPubKey(pkey, compressed=False):
+ return i2o_ECPublicKey(pkey, compressed)
+
+def GetPrivKey(pkey, compressed=False):
+ return i2d_ECPrivateKey(pkey, compressed)
+
+def GetSecret(pkey):
+ return ('%064x' % pkey.secret).decode('hex')
+
+def is_compressed(sec):
+ b = ASecretToSecret(sec)
+ return len(b) == 33
+
########### end pywallet functions #######################
# secp256k1, http://www.oid-info.com/get/1.3.132.0.10
@@ -176,6 +247,13 @@ generator_secp256k1 = ecdsa.ellipticcurve.Point( curve_secp256k1, _Gx, _Gy, _r )
oid_secp256k1 = (1,3,132,0,10)
SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1, oid_secp256k1 )
+class EC_KEY(object):
+ def __init__( self, secret ):
+ self.pubkey = ecdsa.ecdsa.Public_key( generator_secp256k1, generator_secp256k1 * secret )
+ self.privkey = ecdsa.ecdsa.Private_key( self.pubkey, secret )
+ self.secret = secret
+
+
def filter(s):
out = re.sub('( [^\n]*|)\n','',s)
@@ -195,7 +273,6 @@ def raw_tx( inputs, outputs, for_sig = None ):
sig = sig + chr(1) # hashtype
script = int_to_hex( len(sig)) + ' push %d bytes\n'%len(sig)
script += sig.encode('hex') + ' sig\n'
- pubkey = chr(4) + pubkey
script += int_to_hex( len(pubkey)) + ' push %d bytes\n'%len(pubkey)
script += pubkey.encode('hex') + ' pubkey\n'
elif for_sig==i:
diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
@@ -28,31 +28,32 @@ class Exchanger(threading.Thread):
self.discovery()
def discovery(self):
- connection = httplib.HTTPSConnection('intersango.com')
- connection.request("GET", "/api/ticker.php")
+ try:
+ connection = httplib.HTTPSConnection('blockchain.info')
+ connection.request("GET", "/ticker")
+ except:
+ return
response = connection.getresponse()
if response.reason == httplib.responses[httplib.NOT_FOUND]:
return
response = json.loads(response.read())
- # 1 = BTC:GBP
- # 2 = BTC:EUR
- # 3 = BTC:USD
- # 4 = BTC:PLN
quote_currencies = {}
try:
- quote_currencies["GBP"] = self._lookup_rate(response, 1)
- quote_currencies["EUR"] = self._lookup_rate(response, 2)
- quote_currencies["USD"] = self._lookup_rate(response, 3)
+ for r in response:
+ quote_currencies[r] = self._lookup_rate(response, r)
with self.lock:
self.quote_currencies = quote_currencies
self.parent.emit(SIGNAL("refresh_balance()"))
except KeyError:
pass
+
+ def get_currencies(self):
+ return [] if self.quote_currencies == None else sorted(self.quote_currencies.keys())
def _lookup_rate(self, response, quote_id):
- return decimal.Decimal(response[str(quote_id)]["last"])
+ return decimal.Decimal(str(response[str(quote_id)]["15m"]))
if __name__ == "__main__":
- exch = Exchanger(("EUR", "USD", "GBP"))
+ exch = Exchanger(("BRL", "CNY", "EUR", "GBP", "RUB", "USD"))
print exch.exchange(1, "EUR")
diff --git a/lib/gui.py b/lib/gui.py
@@ -65,7 +65,7 @@ def show_seed_dialog(wallet, password, parent):
show_message("No seed")
return
try:
- seed = wallet.pw_decode( wallet.seed, password)
+ seed = wallet.decode_seed(password)
except:
show_message("Incorrect password")
return
@@ -164,10 +164,7 @@ def run_recovery_dialog(wallet):
show_message("no seed")
return False
- wallet.seed = seed
- wallet.gap_limit = gap
- wallet.save()
- return True
+ return seed, gap
@@ -477,7 +474,7 @@ def change_password_dialog(wallet, parent, icon):
return
try:
- seed = wallet.pw_decode( wallet.seed, password)
+ seed = wallet.decode_seed(password)
except:
show_message("Incorrect password")
return
diff --git a/lib/gui_android.py b/lib/gui_android.py
@@ -709,7 +709,7 @@ def seed_dialog():
password = None
try:
- seed = wallet.pw_decode( wallet.seed, password)
+ seed = wallet.decode_seed(password)
except:
modal_dialog('error','incorrect password')
return
@@ -725,7 +725,7 @@ def change_password_dialog():
password = None
try:
- seed = wallet.pw_decode( wallet.seed, password)
+ seed = wallet.decode_seed(password)
except:
modal_dialog('error','incorrect password')
return
@@ -956,8 +956,10 @@ class ElectrumGui:
except:
modal_dialog('error: could not decode this seed')
return
- wallet.seed = str(seed)
- return True
+
+ gap = 5 # default
+
+ return str(seed), gap
def network_dialog(self):
diff --git a/lib/gui_lite.py b/lib/gui_lite.py
@@ -31,6 +31,7 @@ import util
import csv
import datetime
+from version import ELECTRUM_VERSION as electrum_version
from wallet import format_satoshis
import gui_qt
import shutil
@@ -85,16 +86,62 @@ def load_theme_paths():
return theme_paths
+def csv_transaction(wallet):
+ try:
+ fileName = QFileDialog.getSaveFileName(QWidget(), 'Select file to export your wallet transactions to', os.path.expanduser('~/electrum-history.csv'), "*.csv")
+ if fileName:
+ with open(fileName, "w+") as csvfile:
+ transaction = csv.writer(csvfile)
+ transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
+ for item in wallet.get_tx_history():
+ tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
+ if confirmations:
+ if timestamp is not None:
+ try:
+ time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
+ except [RuntimeError, TypeError, NameError] as reason:
+ time_string = "unknown"
+ pass
+ else:
+ time_string = "unknown"
+ else:
+ time_string = "pending"
+
+ if value is not None:
+ value_string = format_satoshis(value, True, wallet.num_zeros)
+ else:
+ value_string = '--'
+
+ if fee is not None:
+ fee_string = format_satoshis(fee, True, wallet.num_zeros)
+ else:
+ fee_string = '0'
+
+ if tx_hash:
+ label, is_default_label = wallet.get_label(tx_hash)
+ else:
+ label = ""
+
+ balance_string = format_satoshis(balance, False, wallet.num_zeros)
+ transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
+ QMessageBox.information(None,"CSV Export created", "Your CSV export has been successfully created.")
+ except (IOError, os.error), reason:
+ QMessageBox.critical(None,"Unable to create csv", "Electrum was unable to produce a transaction export.\n" + str(reason))
+
+
class ElectrumGui(QObject):
- def __init__(self, wallet, config):
+ def __init__(self, wallet, config, expert=None):
super(QObject, self).__init__()
self.wallet = wallet
self.config = config
self.check_qt_version()
- self.app = QApplication(sys.argv)
-
+ self.expert = expert
+ if self.expert != None:
+ self.app = self.expert.app
+ else:
+ self.app = QApplication(sys.argv)
def check_qt_version(self):
qtVersion = qVersion()
@@ -121,17 +168,19 @@ class ElectrumGui(QObject):
if url:
self.set_url(url)
-
- timer = Timer()
- timer.start()
- self.expert = gui_qt.ElectrumWindow(self.wallet, self.config)
- self.expert.app = self.app
- self.expert.connect_slots(timer)
- self.expert.update_wallet()
- self.app.exec_()
+
+ if self.expert == None:
+ timer = Timer()
+ timer.start()
+ self.expert = gui_qt.ElectrumWindow(self.wallet, self.config)
+ self.expert.app = self.app
+ self.expert.connect_slots(timer)
+ self.expert.update_wallet()
+ self.app.exec_()
def expand(self):
"""Hide the lite mode window and show pro-mode."""
+ self.config.set_key('gui', 'classic', True)
self.mini.hide()
self.expert.show()
@@ -196,7 +245,7 @@ class MiniWindow(QDialog):
self.actuator = actuator
self.config = config
self.btc_balance = None
- self.quote_currencies = ["EUR", "USD", "GBP"]
+ self.quote_currencies = ["BRL", "CNY", "EUR", "GBP", "RUB", "USD"]
self.actuator.set_configured_currency(self.set_quote_currency)
self.exchanger = exchange_rate.Exchanger(self)
# Needed because price discovery is done in a different thread
@@ -209,12 +258,12 @@ class MiniWindow(QDialog):
# Bitcoin address code
self.address_input = QLineEdit()
- self.address_input.setPlaceholderText(_("Enter a Bitcoin address..."))
+ self.address_input.setPlaceholderText(_("Enter a Bitcoin address or contact"))
self.address_input.setObjectName("address_input")
self.address_input.setFocusPolicy(Qt.ClickFocus)
- self.address_input.textEdited.connect(self.address_field_changed)
+ self.address_input.textChanged.connect(self.address_field_changed)
resize_line_edit_width(self.address_input,
"1BtaFUr3qVvAmwrsuDuu5zk6e4s2rxd2Gy")
@@ -249,27 +298,28 @@ class MiniWindow(QDialog):
self.send_button.clicked.connect(self.send)
# Creating the receive button
- self.receive_button = QPushButton(_("&Receive"))
- self.receive_button.setObjectName("receive_button")
- self.receive_button.setDefault(True)
+ self.switch_button = QPushButton( QIcon(":icons/switchgui.png"),'' )
+ self.switch_button.setMaximumWidth(25)
+ self.switch_button.setFlat(True)
+ self.switch_button.clicked.connect(expand_callback)
main_layout = QGridLayout(self)
- main_layout.addWidget(self.balance_label, 0, 0)
- main_layout.addWidget(self.receive_button, 0, 1)
+ main_layout.addWidget(self.balance_label, 0, 0, 1, 3)
+ main_layout.addWidget(self.switch_button, 0, 3)
- main_layout.addWidget(self.address_input, 1, 0)
+ main_layout.addWidget(self.address_input, 1, 0, 1, 4)
+ main_layout.addWidget(self.amount_input, 2, 0, 1, 2)
+ main_layout.addWidget(self.send_button, 2, 2, 1, 2)
- main_layout.addWidget(self.amount_input, 2, 0)
- main_layout.addWidget(self.send_button, 2, 1)
+ self.send_button.setMaximumWidth(125)
self.history_list = history_widget.HistoryWidget()
self.history_list.setObjectName("history")
self.history_list.hide()
self.history_list.setAlternatingRowColors(True)
- main_layout.addWidget(self.history_list, 3, 0, 1, 2)
-
+ main_layout.addWidget(self.history_list, 3, 0, 1, 4)
self.receiving = receiving_widget.ReceivingWidget(self)
self.receiving.setObjectName("receiving")
@@ -288,6 +338,7 @@ class MiniWindow(QDialog):
self.receiving.itemDoubleClicked.connect(self.receiving.edit_label)
self.receiving.itemChanged.connect(self.receiving.update_label)
+
# Label
extra_layout.addWidget( QLabel(_('Selecting an address will copy it to the clipboard.\nDouble clicking the label will allow you to edit it.') ),0,0)
@@ -296,18 +347,15 @@ class MiniWindow(QDialog):
extra_layout.setColumnMinimumWidth(0,200)
self.receiving_box.setLayout(extra_layout)
- main_layout.addWidget(self.receiving_box,0,3,-1,3)
+ main_layout.addWidget(self.receiving_box,0,4,-1,3)
self.receiving_box.hide()
- self.receive_button.clicked.connect(self.toggle_receiving_layout)
-
# Creating the menu bar
menubar = QMenuBar()
- electrum_menu = menubar.addMenu(_("&Bitcoin"))
+ electrum_menu = menubar.addMenu(_("&Electrum"))
- electrum_menu.addSeparator()
+ quit_option = electrum_menu.addAction(_("&Close"))
- quit_option = electrum_menu.addAction(_("&Quit"))
quit_option.triggered.connect(self.close)
view_menu = menubar.addMenu(_("&View"))
@@ -317,7 +365,7 @@ class MiniWindow(QDialog):
backup_wallet.triggered.connect(self.backup_wallet)
export_csv = extra_menu.addAction( _("&Export transactions to CSV") )
- export_csv.triggered.connect(self.actuator.csv_transaction)
+ export_csv.triggered.connect(lambda: csv_transaction(self.actuator.wallet))
master_key = extra_menu.addAction( _("Copy master public key to clipboard") )
master_key.triggered.connect(self.actuator.copy_master_public_key)
@@ -343,6 +391,18 @@ class MiniWindow(QDialog):
theme_action.toggled.connect(delegate)
theme_group.addAction(theme_action)
view_menu.addSeparator()
+
+ show_receiving = view_menu.addAction(_("Show Receiving addresses"))
+ show_receiving.setCheckable(True)
+ show_receiving.toggled.connect(self.toggle_receiving_layout)
+
+ show_receiving_toggle = self.config.get("gui_show_receiving",False)
+ show_receiving.setChecked(show_receiving_toggle)
+ self.show_receiving = show_receiving
+
+ self.toggle_receiving_layout(show_receiving_toggle)
+
+
show_history = view_menu.addAction(_("Show History"))
show_history.setCheckable(True)
show_history.toggled.connect(self.show_history)
@@ -377,19 +437,6 @@ class MiniWindow(QDialog):
self.setObjectName("main_window")
self.show()
- def toggle_receiving_layout(self):
- if self.receiving_box.isVisible():
- self.receiving_box.hide()
- self.receive_button.setProperty("isActive", False)
-
- qApp.style().unpolish(self.receive_button)
- qApp.style().polish(self.receive_button)
- else:
- self.receiving_box.show()
- self.receive_button.setProperty("isActive", 'true')
-
- qApp.style().unpolish(self.receive_button)
- qApp.style().polish(self.receive_button)
def toggle_theme(self, theme_name):
old_path = QDir.currentPath()
@@ -403,6 +450,7 @@ class MiniWindow(QDialog):
g = self.geometry()
self.config.set_key("winpos-lite", [g.left(),g.top(),g.width(),g.height()],True)
self.config.set_key("gui_show_history", self.history_list.isVisible(),True)
+ self.config.set_key("gui_show_receiving", self.receiving_box.isVisible(),True)
super(MiniWindow, self).closeEvent(event)
qApp.quit()
@@ -447,7 +495,7 @@ class MiniWindow(QDialog):
quote_text = "(%s)" % quote_text
btc_balance = "%.2f" % (btc_balance / bitcoin(1))
self.balance_label.set_balance_text(btc_balance, quote_text)
- self.setWindowTitle("Electrum - %s BTC" % btc_balance)
+ self.setWindowTitle("Electrum %s - %s BTC" % (electrum_version, btc_balance))
def amount_input_changed(self, amount_text):
"""Update the number of bitcoins displayed."""
@@ -499,6 +547,13 @@ class MiniWindow(QDialog):
self.send_button.setDisabled(True)
def address_field_changed(self, address):
+ # label or alias, with address in brackets
+ match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
+ address)
+ if match2:
+ address = match2.group(2)
+ self.address_input.setText(address)
+
if self.actuator.is_valid(address):
self.check_button_status()
self.address_input.setProperty("isValid", True)
@@ -540,15 +595,21 @@ class MiniWindow(QDialog):
self.actuator.acceptbit(self.quote_currencies[0])
def the_website(self):
- webbrowser.open("http://electrum-desktop.com")
+ webbrowser.open("http://electrum.org")
def show_about(self):
QMessageBox.about(self, "Electrum",
- _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjuction with high-performance servers that handle the most complicated parts of the Bitcoin system.\n\nSend donations to 1JwTMv4GWaPdf931N6LNPJeZBfZgZJ3zX1"))
+ _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjunction with high-performance servers that handle the most complicated parts of the Bitcoin system."))
def show_report_bug(self):
QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
- _("Please report any bugs as issues on github: https://github.com/spesmilo/electrum/issues"))
+ _("Please report any bugs as issues on github: <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>"))
+
+ def toggle_receiving_layout(self, toggle_state):
+ if toggle_state:
+ self.receiving_box.show()
+ else:
+ self.receiving_box.hide()
def show_history(self, toggle_state):
if toggle_state:
@@ -684,7 +745,7 @@ class ReceivePopup(QDialog):
class MiniActuator:
"""Initialize the definitions relating to themes and
- sending/recieving bitcoins."""
+ sending/receiving bitcoins."""
def __init__(self, wallet):
@@ -761,47 +822,6 @@ class MiniActuator:
w.exec_()
w.destroy()
- def csv_transaction(self):
- try:
- fileName = QFileDialog.getSaveFileName(QWidget(), 'Select file to export your wallet transactions to', os.path.expanduser('~/'), "*.csv")
- if fileName:
- with open(fileName, "w+") as csvfile:
- transaction = csv.writer(csvfile)
- transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
- for item in self.wallet.get_tx_history():
- tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
- if confirmations:
- if timestamp is not None:
- try:
- time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
- except [RuntimeError, TypeError, NameError] as reason:
- time_string = "unknown"
- pass
- else:
- time_string = "unknown"
- else:
- time_string = "pending"
-
- if value is not None:
- value_string = format_satoshis(value, True, self.wallet.num_zeros)
- else:
- value_string = '--'
-
- if fee is not None:
- fee_string = format_satoshis(fee, True, self.wallet.num_zeros)
- else:
- fee_string = '0'
-
- if tx_hash:
- label, is_default_label = self.wallet.get_label(tx_hash)
- else:
- label = ""
-
- balance_string = format_satoshis(balance, False, self.wallet.num_zeros)
- transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
- QMessageBox.information(None,"CSV Export created", "Your CSV export has been succesfully created.")
- except (IOError, os.error), reason:
- QMessageBox.critical(None,"Unable to create csv", "Electrum was unable to produce a transaction export.\n" + str(reason))
def send(self, address, amount, parent_window):
"""Send bitcoins to the target address."""
@@ -879,12 +899,13 @@ class MiniActuator:
def is_valid(self, address):
"""Check if bitcoin address is valid."""
+
return self.wallet.is_valid(address)
def copy_master_public_key(self):
master_pubkey = self.wallet.master_public_key
qApp.clipboard().setText(master_pubkey)
- QMessageBox.information(None,"Copy succesful", "Your public master key has been copied to your clipboard.")
+ QMessageBox.information(None,"Copy successful", "Your master public key has been copied to your clipboard.")
def acceptbit(self, currency):
diff --git a/lib/gui_qt.py b/lib/gui_qt.py
@@ -19,6 +19,7 @@
import sys, time, datetime, re
from i18n import _
from util import print_error
+import os.path, json, util
try:
import PyQt4
@@ -38,10 +39,14 @@ except:
from wallet import format_satoshis
import bmp, mnemonic, pyqrnative, qrscanner
+import exchange_rate
from decimal import Decimal
import platform
+import httplib
+import socket
+import webbrowser
if platform.system() == 'Windows':
MONOSPACE_FONT = 'Lucida Console'
@@ -52,6 +57,81 @@ else:
ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
+from version import ELECTRUM_VERSION
+import re
+
+class UpdateLabel(QtGui.QLabel):
+ def __init__(self, config, parent=None):
+ QtGui.QLabel.__init__(self, parent)
+
+ try:
+ con = httplib.HTTPConnection('electrum.org', 80, timeout=5)
+ con.request("GET", "/version")
+ res = con.getresponse()
+ except socket.error as msg:
+ print_error("Could not retrieve version information")
+ return
+
+ if res.status == 200:
+ self.latest_version = res.read()
+ self.latest_version = self.latest_version.replace("\n","")
+ if(re.match('^\d+(\.\d+)*$', self.latest_version)):
+ self.config = config
+ self.current_version = ELECTRUM_VERSION
+ if(self.compare_versions(self.latest_version, self.current_version) == 1):
+ latest_seen = self.config.get("last_seen_version",ELECTRUM_VERSION)
+ if(self.compare_versions(self.latest_version, latest_seen) == 1):
+ self.setText(_("New version available") + ": " + self.latest_version)
+
+
+ def compare_versions(self, version1, version2):
+ def normalize(v):
+ return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
+ return cmp(normalize(version1), normalize(version2))
+
+ def ignore_this_version(self):
+ self.setText("")
+ self.config.set_key("last_seen_version", self.latest_version, True)
+ QMessageBox.information(self, _("Preference saved"), _("Notifications about this update will not be shown again."))
+ self.dialog.done(0)
+
+ def ignore_all_version(self):
+ self.setText("")
+ self.config.set_key("last_seen_version", "9.9.9", True)
+ QMessageBox.information(self, _("Preference saved"), _("No more notifications about version updates will be shown."))
+ self.dialog.done(0)
+
+ def open_website(self):
+ webbrowser.open("http://electrum.org/download.html")
+ self.dialog.done(0)
+
+ def mouseReleaseEvent(self, event):
+ dialog = QDialog(self)
+ dialog.setWindowTitle(_('Electrum update'))
+ dialog.setModal(1)
+
+ main_layout = QGridLayout()
+ main_layout.addWidget(QLabel("A new version of Electrum is available: " + self.latest_version), 0,0,1,3)
+
+ ignore_version = QPushButton(_("Ignore this version"))
+ ignore_version.clicked.connect(self.ignore_this_version)
+
+ ignore_all_versions = QPushButton(_("Ignore all versions"))
+ ignore_all_versions.clicked.connect(self.ignore_all_version)
+
+ open_website = QPushButton(_("Goto download page"))
+ open_website.clicked.connect(self.open_website)
+
+ main_layout.addWidget(ignore_version, 1, 0)
+ main_layout.addWidget(ignore_all_versions, 1, 1)
+ main_layout.addWidget(open_website, 1, 2)
+
+ dialog.setLayout(main_layout)
+
+ self.dialog = dialog
+
+ if not dialog.exec_(): return
+
def numbify(entry, is_int = False):
text = unicode(entry.text()).strip()
pos = entry.cursorPosition()
@@ -137,11 +217,12 @@ class StatusBarButton(QPushButton):
class QRCodeWidget(QWidget):
- def __init__(self, data = None):
+ def __init__(self, data = None, size=4):
QWidget.__init__(self)
self.setMinimumSize(210, 210)
self.addr = None
self.qr = None
+ self.size = size
if data:
self.set_addr(data)
self.update_qr()
@@ -154,7 +235,7 @@ class QRCodeWidget(QWidget):
def update_qr(self):
if self.addr and not self.qr:
- self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
+ self.qr = pyqrnative.QRCode(self.size, pyqrnative.QRErrorCorrectLevel.L)
self.qr.addData(self.addr)
self.qr.make()
self.update()
@@ -166,7 +247,6 @@ class QRCodeWidget(QWidget):
black = QColor(0, 0, 0, 255)
white = QColor(255, 255, 255, 255)
- boxsize = 6
if not self.qr:
qp = QtGui.QPainter()
@@ -176,11 +256,16 @@ class QRCodeWidget(QWidget):
qp.drawRect(0, 0, 198, 198)
qp.end()
return
-
- size = self.qr.getModuleCount()*boxsize
+
k = self.qr.getModuleCount()
qp = QtGui.QPainter()
qp.begin(self)
+ r = qp.viewport()
+ boxsize = min(r.width(), r.height())*0.8/k
+ size = k*boxsize
+ left = (r.width() - size)/2
+ top = (r.height() - size)/2
+
for r in range(k):
for c in range(k):
if self.qr.isDark(r, c):
@@ -189,15 +274,16 @@ class QRCodeWidget(QWidget):
else:
qp.setBrush(white)
qp.setPen(white)
- qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
+ qp.drawRect(left+c*boxsize, top+r*boxsize, boxsize, boxsize)
qp.end()
class QR_Window(QWidget):
- def __init__(self):
+ def __init__(self, exchanger):
QWidget.__init__(self)
+ self.exchanger = exchanger
self.setWindowTitle('Electrum - Invoice')
self.setMinimumSize(800, 250)
self.address = ''
@@ -208,13 +294,11 @@ class QR_Window(QWidget):
main_box = QHBoxLayout()
self.qrw = QRCodeWidget()
- main_box.addWidget(self.qrw)
+ main_box.addWidget(self.qrw, 1)
vbox = QVBoxLayout()
main_box.addLayout(vbox)
- main_box.addStretch(1)
-
self.address_label = QLabel("")
self.address_label.setFont(QFont(MONOSPACE_FONT))
vbox.addWidget(self.address_label)
@@ -229,13 +313,23 @@ class QR_Window(QWidget):
self.setLayout(main_box)
- def set_content(self, addr, label, amount):
+ def set_content(self, addr, label, amount, currency):
self.address = addr
address_text = "<span style='font-size: 18pt'>%s</span>" % addr if addr else ""
self.address_label.setText(address_text)
- self.amount = amount
- amount_text = "<span style='font-size: 21pt'>%s</span> <span style='font-size: 16pt'>BTC</span> " % format_satoshis(amount) if amount else ""
+ if currency == 'BTC': currency = None
+ amount_text = ''
+ if amount:
+ if currency:
+ self.amount = Decimal(amount) / self.exchanger.exchange(1, currency) if currency else amount
+ else:
+ self.amount = Decimal(amount)
+ self.amount = self.amount.quantize(Decimal('1.0000'))
+
+ if currency:
+ amount_text += "<span style='font-size: 18pt'>%s %s</span><br/>" % (amount, currency)
+ amount_text += "<span style='font-size: 21pt'>%s</span> <span style='font-size: 16pt'>BTC</span> " % str(self.amount)
self.amount_label.setText(amount_text)
self.label = label
@@ -244,7 +338,7 @@ class QR_Window(QWidget):
msg = 'bitcoin:'+self.address
if self.amount is not None:
- msg += '?amount=%s'%(str( Decimal(self.amount) /100000000))
+ msg += '?amount=%s'%(str( self.amount))
if self.label is not None:
msg += '&label=%s'%(self.label)
elif self.label is not None:
@@ -288,10 +382,14 @@ def ok_cancel_buttons(dialog):
return hbox
+default_column_widths = { "history":[40,140,350,140,140], "contacts":[350,330,100],
+ "receive":[[50,310,200,130,130,10],[50,310,200,130,130,10],[50,310,200,130,130,10]] }
+
class ElectrumWindow(QMainWindow):
def __init__(self, wallet, config):
QMainWindow.__init__(self)
+ self.lite = None
self.wallet = wallet
self.config = config
self.wallet.interface.register_callback('updated', self.update_callback)
@@ -307,6 +405,7 @@ class ElectrumWindow(QMainWindow):
self.completions = QStringListModel()
self.tabs = tabs = QTabWidget(self)
+ self.column_widths = self.config.get("column-widths", default_column_widths )
tabs.addTab(self.create_history_tab(), _('History') )
tabs.addTab(self.create_send_tab(), _('Send') )
tabs.addTab(self.create_receive_tab(), _('Receive') )
@@ -316,7 +415,6 @@ class ElectrumWindow(QMainWindow):
tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setCentralWidget(tabs)
self.create_status_bar()
- self.toggle_QR_window(self.receive_tab_mode == 2)
g = self.config.get("winpos-qt",[100, 100, 840, 400])
self.setGeometry(g[0], g[1], g[2], g[3])
@@ -332,6 +430,10 @@ class ElectrumWindow(QMainWindow):
self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
#self.connect(self, SIGNAL('editamount'), self.edit_amount)
self.history_list.setFocus(True)
+
+ self.exchanger = exchange_rate.Exchanger(self)
+ self.toggle_QR_window(self.receive_tab_mode == 2)
+ self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
# dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
if platform.system() == 'Windows':
@@ -346,9 +448,8 @@ class ElectrumWindow(QMainWindow):
self.qr_window = None
def connect_slots(self, sender):
- if self.wallet.seed:
- self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
- self.previous_payto_e=''
+ self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
+ self.previous_payto_e=''
def timer_actions(self):
if self.qr_window:
@@ -382,14 +483,13 @@ class ElectrumWindow(QMainWindow):
c, u = self.wallet.get_balance()
text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
+ text += self.create_quote_text(Decimal(c+u)/100000000)
icon = QIcon(":icons/status_connected.png")
else:
text = _( "Not connected" )
icon = QIcon(":icons/status_disconnected.png")
- if self.funds_error:
- text = _( "Not enough funds" )
-
+ self.status_text = text
self.statusBar().showMessage(text)
self.status_button.setIcon( icon )
@@ -400,15 +500,20 @@ class ElectrumWindow(QMainWindow):
self.update_contacts_tab()
self.update_completions()
-
+ def create_quote_text(self, btc_balance):
+ quote_currency = self.config.get("currency", "None")
+ quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
+ if quote_balance is None:
+ quote_text = ""
+ else:
+ quote_text = " (%.2f %s)" % (quote_balance, quote_currency)
+ return quote_text
+
def create_history_tab(self):
self.history_list = l = MyTreeWidget(self)
l.setColumnCount(5)
- l.setColumnWidth(0, 40)
- l.setColumnWidth(1, 140)
- l.setColumnWidth(2, 350)
- l.setColumnWidth(3, 140)
- l.setColumnWidth(4, 140)
+ for i,width in enumerate(self.column_widths['history']):
+ l.setColumnWidth(i, width)
l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
@@ -422,7 +527,7 @@ class ElectrumWindow(QMainWindow):
self.history_list.selectedIndexes()
item = self.history_list.currentItem()
if not item: return
- tx_hash = str(item.toolTip(0))
+ tx_hash = str(item.data(0, Qt.UserRole).toString())
if not tx_hash: return
menu = QMenu()
menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
@@ -432,15 +537,32 @@ class ElectrumWindow(QMainWindow):
def tx_details(self, tx_hash):
- tx_details = self.wallet.get_tx_details(tx_hash)
- QMessageBox.information(self, 'Details', tx_details, 'OK')
+ dialog = QDialog(None)
+ dialog.setModal(1)
+ dialog.setWindowTitle(_("Transaction Details"))
+ main_text = QTextEdit()
+ main_text.setText(self.wallet.get_tx_details(tx_hash))
+ main_text.setReadOnly(True)
+ main_text.setMinimumSize(550,275)
+
+ ok_button = QPushButton(_("OK"))
+ ok_button.setDefault(True)
+ ok_button.clicked.connect(dialog.accept)
+
+ hbox = QHBoxLayout()
+ hbox.addStretch(1)
+ hbox.addWidget(ok_button)
+
+ vbox = QVBoxLayout()
+ vbox.addWidget(main_text)
+ vbox.addLayout(hbox)
+ dialog.setLayout(vbox)
+ dialog.exec_()
def tx_label_clicked(self, item, column):
if column==2 and item.isSelected():
- tx_hash = str(item.toolTip(0))
self.is_edit=True
- #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
self.history_list.editItem( item, column )
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
@@ -450,7 +572,7 @@ class ElectrumWindow(QMainWindow):
if self.is_edit:
return
self.is_edit=True
- tx_hash = str(item.toolTip(0))
+ tx_hash = str(item.data(0, Qt.UserRole).toString())
tx = self.wallet.transactions.get(tx_hash)
s = self.wallet.labels.get(tx_hash)
text = unicode( item.text(2) )
@@ -522,34 +644,38 @@ class ElectrumWindow(QMainWindow):
self.recv_changed(item)
if column == 3:
- address = unicode( item.text(column_addr) )
- text = unicode( item.text(3) )
+ address = str( item.text(column_addr) )
+ text = str( item.text(3) )
try:
index = self.wallet.addresses.index(address)
except:
return
- try:
- amount = int( Decimal(text) * 100000000 )
- item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
- except:
- amount = self.wallet.requested_amounts.get(address)
- if amount:
- item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
+ text = text.strip().upper()
+ m = re.match('^(\d+(|\.\d*))\s*(|BTC|EUR|USD|GBP|CNY|JPY|RUB|BRL)$', text)
+ if m:
+ amount = m.group(1)
+ currency = m.group(3)
+ if not currency:
+ currency = 'BTC'
else:
- item.setText(3,"")
- return
+ currency = currency.upper()
+ self.wallet.requested_amounts[address] = (amount, currency)
- self.wallet.requested_amounts[address] = amount
+ label = self.wallet.labels.get(address)
+ if label is None:
+ label = self.merchant_name + ' - %04d'%(index+1)
+ self.wallet.labels[address] = label
- label = self.wallet.labels.get(address)
- if label is None:
- label = self.merchant_name + ' - %04d'%(index+1)
- self.wallet.labels[address] = label
+ if self.qr_window:
+ self.qr_window.set_content( address, label, amount, currency )
+ else:
+ item.setText(3,'')
+ if address in self.wallet.requested_amounts:
+ self.wallet.requested_amounts.pop(address)
+
self.update_receive_item(self.receive_list.currentItem())
- if self.qr_window:
- self.qr_window.set_content( address, label, amount )
def recv_changed(self, a):
@@ -557,8 +683,11 @@ class ElectrumWindow(QMainWindow):
if a is not None and self.qr_window and self.qr_window.isVisible():
address = str(a.text(1))
label = self.wallet.labels.get(address)
- amount = self.wallet.requested_amounts.get(address)
- self.qr_window.set_content( address, label, amount )
+ try:
+ amount, currency = self.wallet.requested_amounts.get(address, (None, None))
+ except:
+ amount, currency = None, None
+ self.qr_window.set_content( address, label, amount, currency )
def update_history_tab(self):
@@ -600,8 +729,11 @@ class ElectrumWindow(QMainWindow):
item.setFont(2, QFont(MONOSPACE_FONT))
item.setFont(3, QFont(MONOSPACE_FONT))
item.setFont(4, QFont(MONOSPACE_FONT))
+ if value < 0:
+ item.setForeground(3, QBrush(QColor("#BC1E1E")))
if tx_hash:
- item.setToolTip(0, tx_hash)
+ item.setData(0, Qt.UserRole, tx_hash)
+ item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
if is_default_label:
item.setForeground(2, QBrush(QColor('grey')))
@@ -700,10 +832,14 @@ class ElectrumWindow(QMainWindow):
if inputs:
palette = QPalette()
palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
+ text = self.status_text
else:
palette = QPalette()
palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
self.funds_error = True
+ text = _( "Not enough funds" )
+
+ self.statusBar().showMessage(text)
self.amount_e.setPalette(palette)
self.fee_e.setPalette(palette)
@@ -878,23 +1014,30 @@ class ElectrumWindow(QMainWindow):
self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.recv_changed(a))
self.receive_list = l
self.receive_buttons_hbox = hbox
- view_combo = QComboBox()
- view_combo.addItems([_('Simple View'), _('Detailed View'), _('Point of Sale')])
- view_combo.setCurrentIndex(self.receive_tab_mode)
- hbox.addWidget(view_combo)
- view_combo.currentIndexChanged.connect(self.receive_tab_set_mode)
hbox.addStretch(1)
return w
def receive_tab_set_mode(self, i):
+ self.save_column_widths()
self.receive_tab_mode = i
self.config.set_key('qt_receive_tab_mode', self.receive_tab_mode, True)
self.wallet.save()
self.update_receive_tab()
self.toggle_QR_window(self.receive_tab_mode == 2)
+ def save_column_widths(self):
+ widths = []
+ for i in range(self.receive_list.columnCount()):
+ widths.append(self.receive_list.columnWidth(i))
+ self.column_widths["receive"][self.receive_tab_mode] = widths
+ self.column_widths["history"] = []
+ for i in range(self.history_list.columnCount()):
+ self.column_widths["history"].append(self.history_list.columnWidth(i))
+ self.column_widths["contacts"] = []
+ for i in range(self.contacts_list.columnCount()):
+ self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
def create_contacts_tab(self):
l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
@@ -909,6 +1052,14 @@ class ElectrumWindow(QMainWindow):
return w
+ def delete_imported_key(self, addr):
+ if self.question("Do you want to remove %s from your wallet?"%addr):
+ self.wallet.imported_keys.pop(addr)
+ self.update_receive_tab()
+ self.update_history_tab()
+ self.wallet.save()
+
+
def create_receive_menu(self, position):
# fixme: this function apparently has a side effect.
# if it is not called the menu pops up several times
@@ -924,11 +1075,15 @@ class ElectrumWindow(QMainWindow):
menu.addAction(_("View QR"), lambda: ElectrumWindow.show_qrcode("Address","bitcoin:"+addr) )
menu.addAction(_("Edit label"), lambda: self.edit_label(True))
menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
-
- t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
- menu.addAction(t, lambda: self.toggle_freeze(addr))
- t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
- menu.addAction(t, lambda: self.toggle_priority(addr))
+ if addr in self.wallet.imported_keys:
+ menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
+
+ if self.receive_tab_mode == 1:
+ t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
+ menu.addAction(t, lambda: self.toggle_freeze(addr))
+ t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
+ menu.addAction(t, lambda: self.toggle_priority(addr))
+
menu.exec_(self.receive_list.viewport().mapToGlobal(position))
@@ -989,18 +1144,23 @@ class ElectrumWindow(QMainWindow):
label = self.wallet.labels.get(address,'')
item.setData(2,0,label)
- amount = self.wallet.requested_amounts.get(address,None)
- amount_str = format_satoshis( amount, False, self.wallet.num_zeros ) if amount is not None else ""
+ try:
+ amount, currency = self.wallet.requested_amounts.get(address, (None, None))
+ except:
+ amount, currency = None, None
+
+ amount_str = amount + (' ' + currency if currency else '') if amount is not None else ''
item.setData(3,0,amount_str)
-
+
c, u = self.wallet.get_addr_balance(address)
balance = format_satoshis( c + u, False, self.wallet.num_zeros )
item.setData(4,0,balance)
- if address in self.wallet.frozen_addresses:
- item.setBackgroundColor(1, QColor('lightblue'))
- elif address in self.wallet.prioritized_addresses:
- item.setBackgroundColor(1, QColor('lightgreen'))
+ if self.receive_tab_mode == 1:
+ if address in self.wallet.frozen_addresses:
+ item.setBackgroundColor(1, QColor('lightblue'))
+ elif address in self.wallet.prioritized_addresses:
+ item.setBackgroundColor(1, QColor('lightgreen'))
def update_receive_tab(self):
@@ -1011,12 +1171,8 @@ class ElectrumWindow(QMainWindow):
l.setColumnHidden(3, not self.receive_tab_mode == 2)
l.setColumnHidden(4, self.receive_tab_mode == 0)
l.setColumnHidden(5, not self.receive_tab_mode == 1)
- l.setColumnWidth(0, 50)
- l.setColumnWidth(1, 310)
- l.setColumnWidth(2, 200)
- l.setColumnWidth(3, 130)
- l.setColumnWidth(4, 130)
- l.setColumnWidth(5, 10)
+ for i,width in enumerate(self.column_widths['receive'][self.receive_tab_mode]):
+ l.setColumnWidth(i, width)
gap = 0
is_red = False
@@ -1072,9 +1228,8 @@ class ElectrumWindow(QMainWindow):
l = self.contacts_list
l.clear()
- l.setColumnWidth(0, 350)
- l.setColumnWidth(1, 330)
- l.setColumnWidth(2, 100)
+ for i,width in enumerate(self.column_widths['contacts']):
+ l.setColumnWidth(i, width)
alias_targets = []
for alias, v in self.wallet.aliases.items():
@@ -1103,17 +1258,37 @@ class ElectrumWindow(QMainWindow):
textbox.setReadOnly(True)
return textbox
+
def create_status_bar(self):
+ self.status_text = ""
sb = QStatusBar()
sb.setFixedHeight(35)
+ qtVersion = qVersion()
+
+ update_notification = UpdateLabel(self.config)
+ sb.addPermanentWidget(update_notification)
+
+ if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
+ sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
if self.wallet.seed:
- sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
- sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
+ sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), _("Password"), lambda: self.change_password_dialog(self.wallet, self) ) )
+ sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
if self.wallet.seed:
- sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
- self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
+ sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), lambda: self.show_seed_dialog(self.wallet, self) ) )
+ self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.network_dialog(self.wallet, self) )
sb.addPermanentWidget( self.status_button )
+
self.setStatusBar(sb)
+
+ def go_lite(self):
+ import gui_lite
+ self.config.set_key('gui', 'lite', True)
+ self.hide()
+ if self.lite:
+ self.lite.mini.show()
+ else:
+ self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
+ self.lite.main(None)
def new_contact_dialog(self):
text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
@@ -1128,11 +1303,42 @@ class ElectrumWindow(QMainWindow):
else:
QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
+ def show_master_public_key(self):
+ dialog = QDialog(None)
+ dialog.setModal(1)
+ dialog.setWindowTitle("Master Public Key")
+
+ main_text = QTextEdit()
+ main_text.setText(self.wallet.master_public_key)
+ main_text.setReadOnly(True)
+ main_text.setMaximumHeight(170)
+ qrw = QRCodeWidget(self.wallet.master_public_key, 6)
+
+ ok_button = QPushButton(_("OK"))
+ ok_button.setDefault(True)
+ ok_button.clicked.connect(dialog.accept)
+
+ main_layout = QGridLayout()
+ main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
+
+ main_layout.addWidget(main_text, 1, 0)
+ main_layout.addWidget(qrw, 1, 1 )
+
+ vbox = QVBoxLayout()
+ vbox.addLayout(main_layout)
+ hbox = QHBoxLayout()
+ hbox.addStretch(1)
+ hbox.addWidget(ok_button)
+ vbox.addLayout(hbox)
+
+ dialog.setLayout(vbox)
+ dialog.exec_()
+
+
@staticmethod
def show_seed_dialog(wallet, parent=None):
if not wallet.seed:
- QMessageBox.information(parent, _('Message'),
- _('No seed'), _('OK'))
+ QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
return
if wallet.use_encryption:
@@ -1143,54 +1349,60 @@ class ElectrumWindow(QMainWindow):
password = None
try:
- seed = wallet.pw_decode(wallet.seed, password)
+ seed = wallet.decode_seed(password)
except:
- QMessageBox.warning(parent, _('Error'),
- _('Incorrect Password'), _('OK'))
+ QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
return
dialog = QDialog(None)
dialog.setModal(1)
- dialog.setWindowTitle("Electrum")
+ dialog.setWindowTitle(_("Electrum") + ' - ' + _('Seed'))
brainwallet = ' '.join(mnemonic.mn_encode(seed))
- msg = _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
- + _("Please write down or memorize these 12 words (order is important).") + " " \
- + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
- + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
+ label1 = QLabel(_("Your wallet generation seed is")+ ":")
- main_text = QLabel(msg)
- main_text.setWordWrap(True)
+ seed_text = QTextEdit(brainwallet)
+ seed_text.setReadOnly(True)
+ seed_text.setMaximumHeight(130)
+
+ msg2 = _("Please write down or memorize these 12 words (order is important).") + " " \
+ + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
+ + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
+ + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
+ label2 = QLabel(msg2)
+ label2.setWordWrap(True)
logo = QLabel()
logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
+ logo.setMaximumWidth(60)
- if parent:
- app = parent.app
- else:
- app = QApplication
-
- copy_function = lambda: app.clipboard().setText(brainwallet)
- copy_button = QPushButton(_("Copy to Clipboard"))
- copy_button.clicked.connect(copy_function)
-
- show_qr_function = lambda: ElectrumWindow.show_qrcode(_("Seed"), seed)
- qr_button = QPushButton(_("View as QR Code"))
- qr_button.clicked.connect(show_qr_function)
+ qrw = QRCodeWidget(seed, 4)
ok_button = QPushButton(_("OK"))
ok_button.setDefault(True)
ok_button.clicked.connect(dialog.accept)
- main_layout = QGridLayout()
- main_layout.addWidget(logo, 0, 0)
- main_layout.addWidget(main_text, 0, 1, 1, -1)
- main_layout.addWidget(copy_button, 1, 1)
- main_layout.addWidget(qr_button, 1, 2)
- main_layout.addWidget(ok_button, 1, 3)
- dialog.setLayout(main_layout)
+ grid = QGridLayout()
+ #main_layout.addWidget(logo, 0, 0)
+
+ grid.addWidget(logo, 0, 0)
+ grid.addWidget(label1, 0, 1)
+
+ grid.addWidget(seed_text, 1, 0, 1, 2)
+
+ grid.addWidget(qrw, 0, 2, 2, 1)
+
+ vbox = QVBoxLayout()
+ vbox.addLayout(grid)
+ vbox.addWidget(label2)
+
+ hbox = QHBoxLayout()
+ hbox.addStretch(1)
+ hbox.addWidget(ok_button)
+ vbox.addLayout(hbox)
+ dialog.setLayout(vbox)
dialog.exec_()
@staticmethod
@@ -1202,8 +1414,8 @@ class ElectrumWindow(QMainWindow):
d.setMinimumSize(270, 300)
vbox = QVBoxLayout()
qrw = QRCodeWidget(data)
- vbox.addWidget(qrw)
- vbox.addWidget(QLabel(data))
+ vbox.addWidget(qrw, 1)
+ vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
hbox = QHBoxLayout()
hbox.addStretch(1)
@@ -1229,24 +1441,27 @@ class ElectrumWindow(QMainWindow):
d = QDialog(self)
d.setModal(1)
d.setWindowTitle('Sign Message')
- d.setMinimumSize(270, 350)
+ d.setMinimumSize(410, 290)
tab_widget = QTabWidget()
tab = QWidget()
layout = QGridLayout(tab)
sign_address = QLineEdit()
+
sign_address.setText(address)
layout.addWidget(QLabel(_('Address')), 1, 0)
layout.addWidget(sign_address, 1, 1)
sign_message = QTextEdit()
layout.addWidget(QLabel(_('Message')), 2, 0)
- layout.addWidget(sign_message, 2, 1, 2, 1)
+ layout.addWidget(sign_message, 2, 1)
+ layout.setRowStretch(2,3)
- sign_signature = QLineEdit()
+ sign_signature = QTextEdit()
layout.addWidget(QLabel(_('Signature')), 3, 0)
layout.addWidget(sign_signature, 3, 1)
+ layout.setRowStretch(3,1)
def do_sign():
if self.wallet.use_encryption:
@@ -1257,7 +1472,7 @@ class ElectrumWindow(QMainWindow):
password = None
try:
- signature = self.wallet.sign_message(sign_address.text(), str(sign_message.toPlainText()), password)
+ signature = self.wallet.sign_message(str(sign_address.text()), str(sign_message.toPlainText()), password)
sign_signature.setText(signature)
except BaseException, e:
self.show_message(str(e))
@@ -1283,15 +1498,17 @@ class ElectrumWindow(QMainWindow):
verify_message = QTextEdit()
layout.addWidget(QLabel(_('Message')), 2, 0)
- layout.addWidget(verify_message, 2, 1, 2, 1)
+ layout.addWidget(verify_message, 2, 1)
+ layout.setRowStretch(2,3)
- verify_signature = QLineEdit()
+ verify_signature = QTextEdit()
layout.addWidget(QLabel(_('Signature')), 3, 0)
layout.addWidget(verify_signature, 3, 1)
+ layout.setRowStretch(3,1)
def do_verify():
try:
- self.wallet.verify_message(verify_address.text(), verify_signature.text(), str(verify_message.toPlainText()))
+ self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
self.show_message("Signature verified")
except BaseException, e:
self.show_message(str(e))
@@ -1315,15 +1532,15 @@ class ElectrumWindow(QMainWindow):
def toggle_QR_window(self, show):
if show and not self.qr_window:
- self.qr_window = 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)
- amount = self.wallet.requested_amounts.get(address)
- self.qr_window.set_content( address, label, amount )
+ amount, currency = self.wallet.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)
@@ -1422,7 +1639,7 @@ class ElectrumWindow(QMainWindow):
new_password2 = unicode(conf_pw.text())
try:
- seed = wallet.pw_decode( wallet.seed, password)
+ seed = wallet.decode_seed(password)
except:
QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
return
@@ -1465,10 +1682,10 @@ class ElectrumWindow(QMainWindow):
gap = int(unicode(gap_e.text()))
except:
QMessageBox.warning(None, _('Error'), 'error', 'OK')
- sys.exit(0)
+ return
try:
- seed = unicode(seed_e.text())
+ seed = str(seed_e.text())
seed.decode('hex')
except:
print_error("Warning: Not hex, trying decode")
@@ -1476,93 +1693,214 @@ class ElectrumWindow(QMainWindow):
seed = mnemonic.mn_decode( seed.split(' ') )
except:
QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
- sys.exit(0)
+ return
+
if not seed:
QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
- sys.exit(0)
+ return
+
+ return seed, gap
+
+
+ def do_import_labels(self):
+ labelsFile = QFileDialog.getOpenFileName(QWidget(), "Open text file", util.user_dir(), self.tr("Text Files (labels.dat)"))
+ if not labelsFile: return
+ try:
+ f = open(labelsFile, 'r')
+ data = f.read()
+ f.close()
+ 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))
+ except (IOError, os.error), reason:
+ QMessageBox.critical(None, "Unable to import labels", "Electrum was unable to import your labels.\n" + str(reason))
- wallet.seed = str(seed)
- #print repr(wallet.seed)
- wallet.gap_limit = gap
- return True
+ def do_export_labels(self):
+ labels = self.wallet.labels
+ try:
+ labelsFile = util.user_dir() + '/labels.dat'
+ f = open(labelsFile, 'w+')
+ json.dump(labels, f)
+ f.close()
+ QMessageBox.information(None, "Labels exported", "Your labels where exported to '%s'" % str(labelsFile))
+ except (IOError, os.error), reason:
+ QMessageBox.critical(None, "Unable to export labels", "Electrum was unable to export your labels.\n" + str(reason))
+
+ def do_export_history(self):
+ from gui_lite import csv_transaction
+ csv_transaction(self.wallet)
+
+ def do_import_privkey(self):
+ 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' \
+ + _('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()
+ if self.wallet.use_encryption:
+ password = self.password_dialog()
+ if not password:
+ return
+ else:
+ password = None
+ try:
+ addr = self.wallet.import_key(sec, password)
+ if not addr:
+ QMessageBox.critical(None, "Unable to import key", "error")
+ 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))
def settings_dialog(self):
d = QDialog(self)
+ d.setWindowTitle(_('Electrum Settings'))
d.setModal(1)
vbox = QVBoxLayout()
- msg = _('Here are the settings of your wallet.') + '\n'\
- + _('For more explanations, click on the help buttons next to each field.')
- label = QLabel(msg)
- label.setFixedWidth(250)
- label.setWordWrap(True)
- label.setAlignment(Qt.AlignJustify)
- vbox.addWidget(label)
+ tabs = QTabWidget(self)
+ vbox.addWidget(tabs)
- grid = QGridLayout()
- grid.setSpacing(8)
- vbox.addLayout(grid)
+ tab1 = QWidget()
+ grid_ui = QGridLayout(tab1)
+ grid_ui.setColumnStretch(0,1)
+ tabs.addTab(tab1, _('Display') )
+
+ nz_label = QLabel(_('Display zeros'))
+ grid_ui.addWidget(nz_label, 3, 0)
+ nz_e = QLineEdit()
+ nz_e.setText("%d"% self.wallet.num_zeros)
+ grid_ui.addWidget(nz_e, 3, 1)
+ msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
+ grid_ui.addWidget(HelpButton(msg), 3, 2)
+ nz_e.textChanged.connect(lambda: numbify(nz_e,True))
+ if not self.config.is_modifiable('num_zeros'):
+ for w in [nz_e, nz_label]: w.setEnabled(False)
+
+ lang_label=QLabel(_('Language') + ':')
+ grid_ui.addWidget(lang_label , 8, 0)
+ lang_combo = QComboBox()
+ from i18n import languages
+ lang_combo.addItems(languages.values())
+ try:
+ index = languages.keys().index(self.config.get("language",''))
+ except:
+ index = 0
+ lang_combo.setCurrentIndex(index)
+ grid_ui.addWidget(lang_combo, 8, 1)
+ grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 8, 2)
+ if not self.config.is_modifiable('language'):
+ for w in [lang_combo, lang_label]: w.setEnabled(False)
+
+ currencies = self.exchanger.get_currencies()
+ currencies.insert(0, "None")
+
+ cur_label=QLabel(_('Currency') + ':')
+ grid_ui.addWidget(cur_label , 9, 0)
+ cur_combo = QComboBox()
+ cur_combo.addItems(currencies)
+ try:
+ index = currencies.index(self.config.get('currency', "None"))
+ except:
+ index = 0
+ cur_combo.setCurrentIndex(index)
+ grid_ui.addWidget(cur_combo, 9, 1)
+ grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 9, 2)
+
+ view_label=QLabel(_('Receive Tab') + ':')
+ grid_ui.addWidget(view_label , 10, 0)
+ view_combo = QComboBox()
+ view_combo.addItems([_('Simple'), _('Advanced'), _('Point of Sale')])
+ view_combo.setCurrentIndex(self.receive_tab_mode)
+ grid_ui.addWidget(view_combo, 10, 1)
+ hh = _('This selects the interaction mode of the "Receive" tab.')+' ' + '\n\n' \
+ + _('Simple') + ': ' + _('Show only addresses and labels.') + '\n\n' \
+ + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' \
+ + _('Point of Sale') + ': ' + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') + '\n\n'
+
+ grid_ui.addWidget(HelpButton(hh), 10, 2)
+ # wallet tab
+ tab2 = QWidget()
+ grid_wallet = QGridLayout(tab2)
+ grid_wallet.setColumnStretch(0,1)
+ tabs.addTab(tab2, _('Wallet') )
+
fee_label = QLabel(_('Transaction fee'))
- grid.addWidget(fee_label, 2, 0)
+ grid_wallet.addWidget(fee_label, 0, 0)
fee_e = QLineEdit()
fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
- grid.addWidget(fee_e, 2, 1)
+ grid_wallet.addWidget(fee_e, 0, 1)
msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
+ _('Recommended value') + ': 0.001'
- grid.addWidget(HelpButton(msg), 2, 2)
+ grid_wallet.addWidget(HelpButton(msg), 0, 2)
fee_e.textChanged.connect(lambda: numbify(fee_e,False))
if not self.config.is_modifiable('fee'):
for w in [fee_e, fee_label]: w.setEnabled(False)
- nz_label = QLabel(_('Display zeros'))
- grid.addWidget(nz_label, 3, 0)
- nz_e = QLineEdit()
- nz_e.setText("%d"% self.wallet.num_zeros)
- grid.addWidget(nz_e, 3, 1)
- msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
- grid.addWidget(HelpButton(msg), 3, 2)
- nz_e.textChanged.connect(lambda: numbify(nz_e,True))
- if not self.config.is_modifiable('num_zeros'):
- for w in [nz_e, nz_label]: w.setEnabled(False)
-
- usechange_cb = QCheckBox(_('Use change addresses'))
- grid.addWidget(usechange_cb, 5, 0)
- usechange_cb.setChecked(self.wallet.use_change)
- grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
- if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
+ usechange_label = QLabel(_('Use change addresses'))
+ grid_wallet.addWidget(usechange_label, 1, 0)
+ usechange_combo = QComboBox()
+ usechange_combo.addItems([_('Yes'), _('No')])
+ usechange_combo.setCurrentIndex(not self.wallet.use_change)
+ grid_wallet.addWidget(usechange_combo, 1, 1)
+ grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 2)
+ if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
gap_label = QLabel(_('Gap limit'))
- grid.addWidget(gap_label, 6, 0)
+ grid_wallet.addWidget(gap_label, 2, 0)
gap_e = QLineEdit()
gap_e.setText("%d"% self.wallet.gap_limit)
- grid.addWidget(gap_e, 6, 1)
+ grid_wallet.addWidget(gap_e, 2, 1)
msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
+ _('You may increase it if you need more receiving addresses.') + '\n\n' \
+ _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
- + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
+ + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
+ _('Warning') + ': ' \
+ _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
+ _('Do not modify it if you do not understand what you are doing, or if you expect to recover your wallet without knowing it!') + '\n\n'
- grid.addWidget(HelpButton(msg), 6, 2)
+ grid_wallet.addWidget(HelpButton(msg), 2, 2)
gap_e.textChanged.connect(lambda: numbify(nz_e,True))
if not self.config.is_modifiable('gap_limit'):
for w in [gap_e, gap_label]: w.setEnabled(False)
-
- gui_label=QLabel(_('Default GUI') + ':')
- grid.addWidget(gui_label , 7, 0)
- gui_combo = QComboBox()
- gui_combo.addItems(['Lite', 'Classic', 'Gtk', 'Text'])
- index = gui_combo.findText(self.config.get("gui","classic").capitalize())
- if index==-1: index = 1
- gui_combo.setCurrentIndex(index)
- grid.addWidget(gui_combo, 7, 1)
- grid.addWidget(HelpButton(_('Select which GUI mode to use at start up. ')), 7, 2)
- if not self.config.is_modifiable('gui'):
- for w in [gui_combo, gui_label]: w.setEnabled(False)
+ grid_wallet.setRowStretch(3,1)
+
+
+ # wallet tab
+ tab3 = QWidget()
+ grid_io = QGridLayout(tab3)
+ grid_io.setColumnStretch(0,1)
+ tabs.addTab(tab3, _('Import/Export') )
+
+ grid_io.addWidget(QLabel(_('Labels')), 1, 0)
+ grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
+ grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
+ grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
+
+ grid_io.addWidget(QLabel(_('History')), 2, 0)
+ grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
+ grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
+
+ grid_io.addWidget(QLabel(_('Private key')), 3, 0)
+ grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
+ grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
+
+ grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
+ grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
+ grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
+ + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
+ + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
+
+ grid_io.setRowStretch(4,1)
vbox.addLayout(ok_cancel_buttons(d))
d.setLayout(vbox)
@@ -1594,8 +1932,9 @@ class ElectrumWindow(QMainWindow):
self.update_history_tab()
self.update_receive_tab()
- if self.wallet.use_change != usechange_cb.isChecked():
- self.wallet.use_change = usechange_cb.isChecked()
+ usechange_result = usechange_combo.currentIndex() == 0
+ if self.wallet.use_change != usechange_result:
+ self.wallet.use_change = usechange_result
self.config.set_key('use_change', self.wallet.use_change, True)
try:
@@ -1611,9 +1950,23 @@ class ElectrumWindow(QMainWindow):
self.config.set_key('gap_limit', self.wallet.gap_limit, True)
else:
QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
-
- self.config.set_key("gui", str(gui_combo.currentText()).lower(), True)
+ need_restart = False
+
+ lang_request = languages.keys()[lang_combo.currentIndex()]
+ if lang_request != self.config.get('language'):
+ self.config.set_key("language", lang_request, True)
+ need_restart = True
+
+ cur_request = str(currencies[cur_combo.currentIndex()])
+ if cur_request != self.config.get('currency', "None"):
+ self.config.set_key('currency', cur_request, True)
+ self.update_wallet()
+
+ if need_restart:
+ QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
+
+ self.receive_tab_set_mode(view_combo.currentIndex())
@staticmethod
@@ -1621,7 +1974,7 @@ class ElectrumWindow(QMainWindow):
interface = wallet.interface
if parent:
if interface.is_connected:
- status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
+ status = _("Connected to")+" %s\n%d "%(interface.host, wallet.verifier.height)+_("blocks")
else:
status = _("Not connected")
server = interface.server
@@ -1690,7 +2043,7 @@ class ElectrumWindow(QMainWindow):
servers_list_widget.setMaximumHeight(150)
servers_list_widget.setColumnWidth(0, 240)
for _host in servers_list.keys():
- _type = 'pruning' if servers_list[_host].get('pruning') else 'full'
+ _type = 'P' if servers_list[_host].get('pruning') else 'F'
servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
def change_server(host, protocol=None):
@@ -1731,6 +2084,12 @@ class ElectrumWindow(QMainWindow):
if not wallet.config.is_modifiable('server'):
for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
+ # auto cycle
+ autocycle_cb = QCheckBox(_('Try random servers if disconnected'))
+ autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
+ grid.addWidget(autocycle_cb, 3, 1, 3, 2)
+ if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
+
# proxy setting
proxy_mode = QComboBox()
proxy_host = QLineEdit()
@@ -1778,12 +2137,14 @@ class ElectrumWindow(QMainWindow):
wallet.config.set_key("proxy", proxy, True)
wallet.config.set_key("server", server, True)
interface.set_server(server, proxy)
-
+ wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
return True
def closeEvent(self, event):
g = self.geometry()
self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
+ self.save_column_widths()
+ self.config.set_key("column-widths", self.column_widths, True)
event.accept()
diff --git a/lib/history_widget.py b/lib/history_widget.py
@@ -20,5 +20,7 @@ class HistoryWidget(QTreeWidget):
if date is None:
date = "Unknown"
item = QTreeWidgetItem([amount, address, date])
+ if float(amount) < 0:
+ item.setForeground(0, QBrush(QColor("#BC1E1E")))
self.insertTopLevelItem(0, item)
diff --git a/lib/i18n.py b/lib/i18n.py
@@ -16,10 +16,38 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import gettext
+import gettext, os
-LOCALE_DIR = '/usr/share/locale'
-#LOCALE_DIR = './locale'
+if os.path.exists('./locale'):
+ LOCALE_DIR = './locale'
+else:
+ LOCALE_DIR = '/usr/share/locale'
language = gettext.translation('electrum', LOCALE_DIR, fallback = True)
-_ = language.ugettext
+
+def _(x):
+ global language
+ return language.ugettext(x)
+
+def set_language(x):
+ global language
+ if x: language = gettext.translation('electrum', LOCALE_DIR, fallback = True, languages=[x])
+
+
+languages = {
+ '':_('Default'),
+ 'br':_('Brasilian'),
+ 'cs':_('Czech'),
+ 'de':_('German'),
+ 'eo':_('Esperanto'),
+ 'en':_('English'),
+ 'es':_('Spanish'),
+ 'fr':_('French'),
+ 'it':_('Italian'),
+ 'lv':_('Latvian'),
+ 'nl':_('Dutch'),
+ 'ru':_('Russian'),
+ 'sl':_('Slovenian'),
+ 'vi':_('Vietnamese'),
+ 'zh':_('Chinese')
+ }
diff --git a/lib/interface.py b/lib/interface.py
@@ -21,7 +21,7 @@ import random, socket, ast, re, ssl
import threading, traceback, sys, time, json, Queue
from version import ELECTRUM_VERSION, PROTOCOL_VERSION
-from util import print_error
+from util import print_error, print_msg
DEFAULT_TIMEOUT = 5
@@ -39,6 +39,11 @@ DEFAULT_SERVERS = [
'ecdsa.org:50001:t'
]
+# add only port 80 servers here
+DEFAULT_HTTP_SERVERS = [
+ 'electrum.no-ip.org:80:h'
+]
+
proxy_modes = ['socks4', 'socks5', 'http']
@@ -174,7 +179,14 @@ class Interface(threading.Thread):
self.init_server(host, port, proxy, use_ssl)
self.session_id = None
self.connection_msg = ('https' if self.use_ssl else 'http') + '://%s:%d'%( self.host, self.port )
- self.is_connected = True
+ try:
+ self.poll()
+ except:
+ return
+
+ if self.session_id:
+ print_error('http session:',self.session_id)
+ self.is_connected = True
def run_http(self):
self.is_connected = True
@@ -232,7 +244,7 @@ class Interface(threading.Thread):
headers['cookie'] = 'SESSION=%s'%self.session_id
req = urllib2.Request(self.connection_msg, data_json, headers)
- response_stream = urllib2.urlopen(req)
+ response_stream = urllib2.urlopen(req, timeout=DEFAULT_TIMEOUT)
for index, cookie in enumerate(cj):
if cookie.name=='SESSION':
@@ -376,6 +388,7 @@ class Interface(threading.Thread):
self.servers = {} # actual list from IRC
self.rtime = 0
self.bytes_received = 0
+ self.is_connected = False
@@ -383,18 +396,28 @@ class Interface(threading.Thread):
if self.config.get('server'):
self.init_with_server(self.config)
else:
- print "Using random server..."
- servers = DEFAULT_SERVERS[:]
- while servers:
- server = random.choice( servers )
- servers.remove(server)
+ if self.config.get('auto_cycle') is None:
+ self.config.set_key('auto_cycle', True, False)
+
+ if not self.is_connected and self.config.get('auto_cycle'):
+ print_msg("Using random server...")
+ servers_tcp = DEFAULT_SERVERS[:]
+ servers_http = DEFAULT_HTTP_SERVERS[:]
+ while servers_tcp or servers_http:
+ if servers_tcp:
+ server = random.choice( servers_tcp )
+ servers_tcp.remove(server)
+ else:
+ # try HTTP if we can't get a TCP connection
+ server = random.choice( servers_http )
+ servers_http.remove(server)
+ print server
self.config.set_key('server', server, False)
self.init_with_server(self.config)
if self.is_connected: break
- if not servers:
+ if not self.is_connected:
print 'no server available'
- self.is_connected = False
self.connect_event.set() # to finish start
self.server = 'ecdsa.org:50001:t'
self.proxy = None
diff --git a/lib/simple_config.py b/lib/simple_config.py
@@ -94,7 +94,7 @@ a SimpleConfig instance then reads the wallet file.
try:
out = ast.literal_eval(out)
except:
- print "type error, using default value"
+ print "type error for '%s': using default value"%key
out = default
return out
diff --git a/lib/verifier.py b/lib/verifier.py
@@ -298,7 +298,8 @@ class WalletVerifier(threading.Thread):
return
try:
- import urllib
+ import urllib, socket
+ socket.setdefaulttimeout(30)
print_error("downloading ", self.headers_url )
urllib.urlretrieve(self.headers_url, filename)
except:
diff --git a/lib/version.py b/lib/version.py
@@ -1,4 +1,4 @@
-ELECTRUM_VERSION = "1.5.7" # version of the client package
-PROTOCOL_VERSION = '0.6' # protocol version requested
-SEED_VERSION = 4 # bump this everytime the seed generation is modified
-TRANSLATION_ID = 33853 # version of the wiki page
+ELECTRUM_VERSION = "1.6.1" # version of the client package
+PROTOCOL_VERSION = '0.6' # protocol version requested
+SEED_VERSION = 4 # bump this everytime the seed generation is modified
+TRANSLATION_ID = 34952 # version of the wiki page
diff --git a/lib/wallet.py b/lib/wallet.py
@@ -64,13 +64,13 @@ class Wallet:
self.addresses = config.get('addresses', []) # receiving addresses visible for user
self.change_addresses = config.get('change_addresses', []) # addresses used as change
self.seed = config.get('seed', '') # encrypted
- self.labels = config.get('labels',{}) # labels for addresses and transactions
+ self.labels = config.get('labels',{'1NmduGNyC5XejoysbuioodCN3jR3yf64xM':'Electrum donation address'})
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', []) # outgoing addresses, for payments
+ self.addressbook = config.get('contacts', ['1NmduGNyC5XejoysbuioodCN3jR3yf64xM'])
self.imported_keys = config.get('imported_keys',{})
self.history = config.get('addr_history',{}) # address -> list(txid, height)
self.transactions = config.get('transactions',{}) # txid -> deserialised
@@ -112,23 +112,33 @@ class Wallet:
self.interface.poke('synchronizer')
while not self.is_up_to_date(): time.sleep(0.1)
- def import_key(self, keypair, password):
- address, key = keypair.split(':')
- if not self.is_valid(address):
- raise BaseException('Invalid Bitcoin address')
+ def import_key(self, sec, password):
+ # try password
+ try:
+ seed = self.decode_seed(password)
+ except:
+ raise BaseException("Invalid password")
+
+ # rebuild public key from private key, compressed or uncompressed
+ pkey = regenerate_key(sec)
+ if not pkey:
+ return False
+
+ # figure out if private key is compressed
+ compressed = is_compressed(sec)
+
+ # rebuild private and public key from regenerated secret
+ private_key = GetPrivKey(pkey, compressed)
+ public_key = GetPubKey(pkey, compressed)
+ address = public_key_to_bc_address(public_key)
+
if address in self.all_addresses():
raise BaseException('Address already in wallet')
- b = ASecretToSecret( key )
- if not b:
- raise BaseException('Unsupported key format')
- secexp = int( b.encode('hex'), 16)
- private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 )
- # sanity check
- public_key = private_key.get_verifying_key()
- if not address == public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ):
- raise BaseException('Address does not match private key')
- self.imported_keys[address] = self.pw_encode( key, password )
-
+
+ # store the originally requested keypair into the imported keys table
+ self.imported_keys[address] = self.pw_encode(sec, password )
+ return address
+
def new_seed(self, password):
seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
@@ -172,19 +182,22 @@ class Wallet:
return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key.decode('hex') ) )
def get_private_key_base58(self, address, password):
- pk = self.get_private_key(address, password)
- if pk is None: return None
- return SecretToASecret( pk )
+ secexp, compressed = self.get_private_key(address, password)
+ if secexp is None: return None
+ pk = number_to_string( secexp, generator_secp256k1.order() )
+ return SecretToASecret( pk, compressed )
def get_private_key(self, address, password):
""" Privatekey(type,n) = Master_private_key + H(n|S|type) """
order = generator_secp256k1.order()
if address in self.imported_keys.keys():
- b = self.pw_decode( self.imported_keys[address], password )
- if not b: return None
- b = ASecretToSecret( b )
- secexp = int( b.encode('hex'), 16)
+ sec = self.pw_decode( self.imported_keys[address], password )
+ if not sec: return None, None
+ pkey = regenerate_key(sec)
+ compressed = is_compressed(sec)
+ secexp = pkey.secret
+
else:
if address in self.addresses:
n = self.addresses.index(address)
@@ -194,27 +207,33 @@ class Wallet:
for_change = True
else:
raise BaseException("unknown address")
- try:
- seed = self.pw_decode( self.seed, password)
- except:
- raise BaseException("Invalid password")
+
+ seed = self.pw_decode( self.seed, password)
if not seed: return None
secexp = self.stretch_key(seed)
secexp = ( secexp + self.get_sequence(n,for_change) ) % order
+ compressed = False
+ pkey = EC_KEY(secexp)
+
+ public_key = GetPubKey(pkey, compressed)
+ addr = public_key_to_bc_address(public_key)
+ if addr != address:
+ print_error('Invalid password with correct decoding')
+ raise BaseException('Invalid password')
- pk = number_to_string(secexp,order)
- return pk
+ return secexp, compressed
def msg_magic(self, message):
return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message
def sign_message(self, address, message, password):
- private_key = ecdsa.SigningKey.from_string( self.get_private_key(address, password), curve = SECP256k1 )
+ secexp, compressed = self.get_private_key(address, password)
+ private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
signature = private_key.sign_digest( Hash( self.msg_magic( message ) ), sigencode = ecdsa.util.sigencode_string )
assert public_key.verify_digest( signature, Hash( self.msg_magic( message ) ), sigdecode = ecdsa.util.sigdecode_string)
for i in range(4):
- sig = base64.b64encode( chr(27+i) + signature )
+ sig = base64.b64encode( chr(27 + i + (4 if compressed else 0)) + signature )
try:
self.verify_message( address, sig, message)
return sig
@@ -485,15 +504,16 @@ class Wallet:
h = self.history.get(address,[])
if h == ['*']: return 0,0
c = u = 0
- received_coins = []
-
+ received_coins = [] # list of coins received at address
+
for tx_hash, tx_height in h:
d = self.transactions.get(tx_hash)
if not d: continue
for item in d.get('outputs'):
addr = item.get('address')
- key = tx_hash + ':%d'%item['index']
- received_coins.append(key)
+ if addr == address:
+ key = tx_hash + ':%d'%item['index']
+ received_coins.append(key)
for tx_hash, tx_height in h:
d = self.transactions.get(tx_hash)
@@ -597,9 +617,13 @@ class Wallet:
s_inputs = []
for i in range(len(inputs)):
addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i]
- private_key = ecdsa.SigningKey.from_string( self.get_private_key(addr, password), curve = SECP256k1 )
+ secexp, compressed = self.get_private_key(addr, password)
+ private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
- pubkey = public_key.to_string()
+
+ pkey = EC_KEY(secexp)
+ pubkey = GetPubKey(pkey, compressed)
+
tx = filter( raw_tx( inputs, outputs, for_sig = i ) )
sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
@@ -616,16 +640,28 @@ class Wallet:
def pw_decode(self, s, password):
if password is not None:
secret = Hash(password)
- d = DecodeAES(secret, s)
- if s == self.seed:
- try:
- d.decode('hex')
- except:
- raise ValueError("Invalid password")
+ try:
+ d = DecodeAES(secret, s)
+ except:
+ raise BaseException('Invalid password')
return d
else:
return s
+ def decode_seed(self, password):
+ seed = self.pw_decode(self.seed, password)
+
+ # check decoded seed with master public key
+ 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().encode('hex')
+ if master_public_key != self.master_public_key:
+ print_error('invalid password (mpk)')
+ raise BaseException('Invalid password')
+
+ return seed
+
def get_history(self, address):
with self.lock:
@@ -699,7 +735,7 @@ class Wallet:
balance = c + u - balance
for tx in history:
tx_hash = tx['tx_hash']
- conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else None
+ conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
is_mine, value, fee = self.get_tx_value(tx_hash)
if value is not None:
balance += value
diff --git a/lib/wallet_bitkey.py b/lib/wallet_bitkey.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2011 thomasv@gitorious
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from wallet import Wallet
+#import bitkeylib.bitkey_pb2 as proto
+
+from version import ELECTRUM_VERSION
+SEED_VERSION = 4 # Version of bitkey algorithm
+
+class WalletBitkey(Wallet):
+ pass
diff --git a/lib/wallet_factory.py b/lib/wallet_factory.py
@@ -0,0 +1,11 @@
+class WalletFactory(object):
+ def __new__(cls, config):
+ if config.get('bitkey', False):
+ # if user requested support for Bitkey device,
+ # import Bitkey driver
+ from wallet_bitkey import WalletBitkey
+ return WalletBitkey(config)
+
+ # Load standard wallet
+ from wallet import Wallet
+ return Wallet(config)
diff --git a/setup-release.py b/setup-release.py
@@ -60,7 +60,7 @@ if sys.platform == 'darwin':
qt_menu_location = "/opt/local/lib/Resources/qt_menu.nib"
else:
# No dice? Then let's try the brew version
- qt_menu_location = os.popen("mdfind -name qt_menu.nib | grep Cellar | head").read()
+ qt_menu_location = os.popen("find /usr/local/Cellar -name qt_menu.nib | head").read()
qt_menu_location = re.sub('\n','', qt_menu_location)
if(len(qt_menu_location) == 0):
diff --git a/setup.py b/setup.py
@@ -30,6 +30,10 @@ data_files += [
"data/cleanlook/name.cfg",
"data/cleanlook/style.css"
]),
+ (os.path.join(util.appdata_dir(), "sahara"), [
+ "data/sahara/name.cfg",
+ "data/sahara/style.css"
+ ]),
(os.path.join(util.appdata_dir(), "dark"), [
"data/dark/background.png",
"data/dark/name.cfg",
@@ -46,6 +50,8 @@ setup(name = "Electrum",
data_files = data_files,
py_modules = ['electrum.version',
'electrum.wallet',
+ 'electrum.wallet_bitkey',
+ 'electrum.wallet_factory',
'electrum.interface',
'electrum.gui',
'electrum.gui_qt',