electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 6c55410820038538626fc5adbc1afa7836131f1a
parent 6abda697117d578130df92c8883f338be94a16f9
Author: thomasv <thomasv@gitorious>
Date:   Wed,  2 May 2012 11:11:41 +0200

Merge branch 'master' of gitorious.org:electrum/electrum

Diffstat:
Rclient/LICENCE -> LICENCE | 0
Rclient/MANIFEST.in -> MANIFEST.in | 0
Rclient/README -> README | 0
Rclient/RELEASE-NOTES -> RELEASE-NOTES | 0
Rclient/blocks -> blocks | 0
Abmp.py | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dclient/bmp.py | 206-------------------------------------------------------------------------------
Dclient/electrum | 408-------------------------------------------------------------------------------
Dclient/electrum4a.py | 981-------------------------------------------------------------------------------
Dclient/gui.py | 1265-------------------------------------------------------------------------------
Dclient/gui_qt.py | 1059-------------------------------------------------------------------------------
Dclient/interface.py | 487-------------------------------------------------------------------------------
Dclient/version.py | 2--
Dclient/wallet.py | 953-------------------------------------------------------------------------------
Rclient/docs/android.html -> docs/android.html | 0
Aelectrum | 408+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum4a.py | 980+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rclient/electrum_text_320.png -> electrum_text_320.png | 0
Agui.py | 1259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui_qt.py | 1114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rclient/icons.qrc -> icons.qrc | 0
Rclient/icons/confirmed.png -> icons/confirmed.png | 0
Rclient/icons/lock.png -> icons/lock.png | 0
Rclient/icons/lock.svg -> icons/lock.svg | 0
Rclient/icons/network.png -> icons/network.png | 0
Rclient/icons/preferences.png -> icons/preferences.png | 0
Rclient/icons/seed.png -> icons/seed.png | 0
Rclient/icons/status_connected.png -> icons/status_connected.png | 0
Rclient/icons/status_disconnected.png -> icons/status_disconnected.png | 0
Rclient/icons/status_disconnected.svg -> icons/status_disconnected.svg | 0
Rclient/icons/status_waiting.png -> icons/status_waiting.png | 0
Rclient/icons/status_waiting.svg -> icons/status_waiting.svg | 0
Rclient/icons/unconfirmed.png -> icons/unconfirmed.png | 0
Rclient/icons/unconfirmed.svg -> icons/unconfirmed.svg | 0
Ainterface.py | 425+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rclient/mnemonic.py -> mnemonic.py | 0
Rclient/msqr.py -> msqr.py | 0
Rclient/peers -> peers | 0
Rclient/pyqrnative.py -> pyqrnative.py | 0
Rclient/remote.php -> remote.php | 0
Rclient/remote_wallet.py -> remote_wallet.py | 0
Rclient/ripemd.py -> ripemd.py | 0
Rclient/setup.py -> setup.py | 0
Rclient/upgrade.py -> upgrade.py | 0
Aversion.py | 2++
Awallet.py | 950+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awatch_address | 31+++++++++++++++++++++++++++++++
47 files changed, 5391 insertions(+), 5361 deletions(-)

diff --git a/client/LICENCE b/LICENCE diff --git a/client/MANIFEST.in b/MANIFEST.in diff --git a/client/README b/README diff --git a/client/RELEASE-NOTES b/RELEASE-NOTES diff --git a/client/blocks b/blocks diff --git a/bmp.py b/bmp.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +""" +bmp.py - module for constructing simple BMP graphics files + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +__version__ = "0.3" +__about = "bmp module, version %s, written by Paul McGuire, October, 2003, updated by Margus Laak, September, 2009" % __version__ + +from math import ceil, hypot + + +def shortToString(i): + hi = (i & 0xff00) >> 8 + lo = i & 0x00ff + return chr(lo) + chr(hi) + +def longToString(i): + hi = (long(i) & 0x7fff0000) >> 16 + lo = long(i) & 0x0000ffff + return shortToString(lo) + shortToString(hi) + +def long24ToString(i): + return chr(i & 0xff) + chr(i >> 8 & 0xff) + chr(i >> 16 & 0xff) + +def stringToLong(input_string, offset): + return ord(input_string[offset+3]) << 24 | ord(input_string[offset+2]) << 16 | ord(input_string[offset+1]) << 8 | ord(input_string[offset]) + +def stringToLong24(input_string, offset): + return ord(input_string[offset+2]) << 16 | ord(input_string[offset+1]) << 8 | ord(input_string[offset]) + +class Color(object): + """class for specifying colors while drawing BitMap elements""" + __slots__ = [ 'red', 'grn', 'blu' ] + __shade = 32 + + def __init__( self, r=0, g=0, b=0 ): + self.red = r + self.grn = g + self.blu = b + + def __setattr__(self, name, value): + if hasattr(self, name): + raise AttributeError, "Color is immutable" + else: + object.__setattr__(self, name, value) + + def __str__( self ): + return "R:%d G:%d B:%d" % (self.red, self.grn, self.blu ) + + def __hash__( self ): + return ( ( long(self.blu) ) + + ( long(self.grn) << 8 ) + + ( long(self.red) << 16 ) ) + + def __eq__( self, other ): + return (self is other) or (self.toLong == other.toLong) + + def lighten( self ): + return Color( + min( self.red + Color.__shade, 255), + min( self.grn + Color.__shade, 255), + min( self.blu + Color.__shade, 255) + ) + + def darken( self ): + return Color( + max( self.red - Color.__shade, 0), + max( self.grn - Color.__shade, 0), + max( self.blu - Color.__shade, 0) + ) + + def toLong( self ): + return self.__hash__() + + def fromLong( l ): + b = l & 0xff + l = l >> 8 + g = l & 0xff + l = l >> 8 + r = l & 0xff + return Color( r, g, b ) + fromLong = staticmethod(fromLong) + +# define class constants for common colors +Color.BLACK = Color( 0, 0, 0 ) +Color.RED = Color( 255, 0, 0 ) +Color.GREEN = Color( 0, 255, 0 ) +Color.BLUE = Color( 0, 0, 255 ) +Color.CYAN = Color( 0, 255, 255 ) +Color.MAGENTA = Color( 255, 0, 255 ) +Color.YELLOW = Color( 255, 255, 0 ) +Color.WHITE = Color( 255, 255, 255 ) +Color.DKRED = Color( 128, 0, 0 ) +Color.DKGREEN = Color( 0, 128, 0 ) +Color.DKBLUE = Color( 0, 0, 128 ) +Color.TEAL = Color( 0, 128, 128 ) +Color.PURPLE = Color( 128, 0, 128 ) +Color.BROWN = Color( 128, 128, 0 ) +Color.GRAY = Color( 128, 128, 128 ) + + +class BitMap(object): + """class for drawing and saving simple Windows bitmap files""" + + LINE_SOLID = 0 + LINE_DASHED = 1 + LINE_DOTTED = 2 + LINE_DOT_DASH=3 + _DASH_LEN = 12.0 + _DOT_LEN = 6.0 + _DOT_DASH_LEN = _DOT_LEN + _DASH_LEN + + def __init__( self, width, height, + bkgd = Color.WHITE, frgd = Color.BLACK ): + self.wd = int( ceil(width) ) + self.ht = int( ceil(height) ) + self.bgcolor = 0 + self.fgcolor = 1 + self.palette = [] + self.palette.append( bkgd.toLong() ) + self.palette.append( frgd.toLong() ) + self.currentPen = self.fgcolor + + tmparray = [ self.bgcolor ] * self.wd + self.bitarray = [ tmparray[:] for i in range( self.ht ) ] + self.currentPen = 1 + + + def plotPoint( self, x, y ): + if ( 0 <= x < self.wd and 0 <= y < self.ht ): + x = int(x) + y = int(y) + self.bitarray[y][x] = self.currentPen + + + def _saveBitMapNoCompression( self ): + line_padding = (4 - (self.wd % 4)) % 4 + + # write bitmap header + _bitmap = "BM" + _bitmap += longToString( 54 + self.ht*(self.wd*3 + line_padding) ) # DWORD size in bytes of the file + _bitmap += longToString( 0 ) # DWORD 0 + _bitmap += longToString( 54 ) + _bitmap += longToString( 40 ) # DWORD header size = 40 + _bitmap += longToString( self.wd ) # DWORD image width + _bitmap += longToString( self.ht ) # DWORD image height + _bitmap += shortToString( 1 ) # WORD planes = 1 + _bitmap += shortToString( 24 ) # WORD bits per pixel = 8 + _bitmap += longToString( 0 ) # DWORD compression = 0 + _bitmap += longToString( self.ht * (self.wd * 3 + line_padding) ) # DWORD sizeimage = size in bytes of the bitmap = width * height + _bitmap += longToString( 0 ) # DWORD horiz pixels per meter (?) + _bitmap += longToString( 0 ) # DWORD ver pixels per meter (?) + _bitmap += longToString( 0 ) # DWORD number of colors used = 256 + _bitmap += longToString( 0 ) # DWORD number of "import colors = len( self.palette ) + + # write pixels + self.bitarray.reverse() + for row in self.bitarray: + for pixel in row: + c = self.palette[pixel] + _bitmap += long24ToString(c) + for i in range(line_padding): + _bitmap += chr( 0 ) + + return _bitmap + + + + def saveFile( self, filename): + _b = self._saveBitMapNoCompression( ) + + f = file(filename, 'wb') + f.write(_b) + f.close() + + +def save_qrcode(qr, filename): + bitmap = BitMap( 35*8, 35*8 ) + #print len(bitmap.bitarray) + bitmap.bitarray = [] + k = 33 + for r in range(35): + tmparray = [ 0 ] * 35*8 + + if 0 < r < 34: + for c in range(k): + if qr.isDark(r-1, c): + tmparray[ (1+c)*8:(2+c)*8] = [1]*8 + + for i in range(8): + bitmap.bitarray.append( tmparray[:] ) + + bitmap.saveFile( filename ) + + + +if __name__ == "__main__": + + bmp = BitMap( 10, 10 ) + bmp.plotPoint( 5, 5 ) + bmp.plotPoint( 0, 0 ) + bmp.saveFile( "test.bmp" ) + diff --git a/client/bmp.py b/client/bmp.py @@ -1,206 +0,0 @@ -# -*- coding: utf-8 -*- -""" -bmp.py - module for constructing simple BMP graphics files - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" -__version__ = "0.3" -__about = "bmp module, version %s, written by Paul McGuire, October, 2003, updated by Margus Laak, September, 2009" % __version__ - -from math import ceil, hypot - - -def shortToString(i): - hi = (i & 0xff00) >> 8 - lo = i & 0x00ff - return chr(lo) + chr(hi) - -def longToString(i): - hi = (long(i) & 0x7fff0000) >> 16 - lo = long(i) & 0x0000ffff - return shortToString(lo) + shortToString(hi) - -def long24ToString(i): - return chr(i & 0xff) + chr(i >> 8 & 0xff) + chr(i >> 16 & 0xff) - -def stringToLong(input_string, offset): - return ord(input_string[offset+3]) << 24 | ord(input_string[offset+2]) << 16 | ord(input_string[offset+1]) << 8 | ord(input_string[offset]) - -def stringToLong24(input_string, offset): - return ord(input_string[offset+2]) << 16 | ord(input_string[offset+1]) << 8 | ord(input_string[offset]) - -class Color(object): - """class for specifying colors while drawing BitMap elements""" - __slots__ = [ 'red', 'grn', 'blu' ] - __shade = 32 - - def __init__( self, r=0, g=0, b=0 ): - self.red = r - self.grn = g - self.blu = b - - def __setattr__(self, name, value): - if hasattr(self, name): - raise AttributeError, "Color is immutable" - else: - object.__setattr__(self, name, value) - - def __str__( self ): - return "R:%d G:%d B:%d" % (self.red, self.grn, self.blu ) - - def __hash__( self ): - return ( ( long(self.blu) ) + - ( long(self.grn) << 8 ) + - ( long(self.red) << 16 ) ) - - def __eq__( self, other ): - return (self is other) or (self.toLong == other.toLong) - - def lighten( self ): - return Color( - min( self.red + Color.__shade, 255), - min( self.grn + Color.__shade, 255), - min( self.blu + Color.__shade, 255) - ) - - def darken( self ): - return Color( - max( self.red - Color.__shade, 0), - max( self.grn - Color.__shade, 0), - max( self.blu - Color.__shade, 0) - ) - - def toLong( self ): - return self.__hash__() - - def fromLong( l ): - b = l & 0xff - l = l >> 8 - g = l & 0xff - l = l >> 8 - r = l & 0xff - return Color( r, g, b ) - fromLong = staticmethod(fromLong) - -# define class constants for common colors -Color.BLACK = Color( 0, 0, 0 ) -Color.RED = Color( 255, 0, 0 ) -Color.GREEN = Color( 0, 255, 0 ) -Color.BLUE = Color( 0, 0, 255 ) -Color.CYAN = Color( 0, 255, 255 ) -Color.MAGENTA = Color( 255, 0, 255 ) -Color.YELLOW = Color( 255, 255, 0 ) -Color.WHITE = Color( 255, 255, 255 ) -Color.DKRED = Color( 128, 0, 0 ) -Color.DKGREEN = Color( 0, 128, 0 ) -Color.DKBLUE = Color( 0, 0, 128 ) -Color.TEAL = Color( 0, 128, 128 ) -Color.PURPLE = Color( 128, 0, 128 ) -Color.BROWN = Color( 128, 128, 0 ) -Color.GRAY = Color( 128, 128, 128 ) - - -class BitMap(object): - """class for drawing and saving simple Windows bitmap files""" - - LINE_SOLID = 0 - LINE_DASHED = 1 - LINE_DOTTED = 2 - LINE_DOT_DASH=3 - _DASH_LEN = 12.0 - _DOT_LEN = 6.0 - _DOT_DASH_LEN = _DOT_LEN + _DASH_LEN - - def __init__( self, width, height, - bkgd = Color.WHITE, frgd = Color.BLACK ): - self.wd = int( ceil(width) ) - self.ht = int( ceil(height) ) - self.bgcolor = 0 - self.fgcolor = 1 - self.palette = [] - self.palette.append( bkgd.toLong() ) - self.palette.append( frgd.toLong() ) - self.currentPen = self.fgcolor - - tmparray = [ self.bgcolor ] * self.wd - self.bitarray = [ tmparray[:] for i in range( self.ht ) ] - self.currentPen = 1 - - - def plotPoint( self, x, y ): - if ( 0 <= x < self.wd and 0 <= y < self.ht ): - x = int(x) - y = int(y) - self.bitarray[y][x] = self.currentPen - - - def _saveBitMapNoCompression( self ): - line_padding = (4 - (self.wd % 4)) % 4 - - # write bitmap header - _bitmap = "BM" - _bitmap += longToString( 54 + self.ht*(self.wd*3 + line_padding) ) # DWORD size in bytes of the file - _bitmap += longToString( 0 ) # DWORD 0 - _bitmap += longToString( 54 ) - _bitmap += longToString( 40 ) # DWORD header size = 40 - _bitmap += longToString( self.wd ) # DWORD image width - _bitmap += longToString( self.ht ) # DWORD image height - _bitmap += shortToString( 1 ) # WORD planes = 1 - _bitmap += shortToString( 24 ) # WORD bits per pixel = 8 - _bitmap += longToString( 0 ) # DWORD compression = 0 - _bitmap += longToString( self.ht * (self.wd * 3 + line_padding) ) # DWORD sizeimage = size in bytes of the bitmap = width * height - _bitmap += longToString( 0 ) # DWORD horiz pixels per meter (?) - _bitmap += longToString( 0 ) # DWORD ver pixels per meter (?) - _bitmap += longToString( 0 ) # DWORD number of colors used = 256 - _bitmap += longToString( 0 ) # DWORD number of "import colors = len( self.palette ) - - # write pixels - self.bitarray.reverse() - for row in self.bitarray: - for pixel in row: - c = self.palette[pixel] - _bitmap += long24ToString(c) - for i in range(line_padding): - _bitmap += chr( 0 ) - - return _bitmap - - - - def saveFile( self, filename): - _b = self._saveBitMapNoCompression( ) - - f = file(filename, 'wb') - f.write(_b) - f.close() - - - - - - -if __name__ == "__main__": - - bmp = BitMap( 10, 10 ) - bmp.plotPoint( 5, 5 ) - bmp.plotPoint( 0, 0 ) - bmp.saveFile( "test.bmp" ) - diff --git a/client/electrum b/client/electrum @@ -1,408 +0,0 @@ -#!/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 re, sys, getpass - -from optparse import OptionParser -from wallet import Wallet, SecretToASecret -from interface import WalletSynchronizer -from decimal import Decimal -from wallet import format_satoshis - -known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'restore', 'payto', 'sendtx', 'password', 'addresses', 'history', 'label', 'mktx','seed','import','signmessage','verifymessage','eval'] -offline_commands = ['password', 'mktx', 'history', 'label', 'contacts', 'help', 'validateaddress', 'signmessage', 'verifymessage', 'eval', 'create', 'addresses', 'import', 'seed'] -protected_commands = ['payto', 'password', 'mktx', 'seed', 'import','signmessage' ] - -if __name__ == '__main__': - - usage = "usage: %prog [options] command args\nCommands: "+ (', '.join(known_commands)) - parser = OptionParser(usage=usage) - parser.add_option("-g", "--gui", dest="gui", default="qt", help="gui") - parser.add_option("-w", "--wallet", dest="wallet_path", help="wallet path (default: electrum.dat)") - parser.add_option("-a", "--all", action="store_true", dest="show_all", default=False, help="show all addresses") - parser.add_option("-b", "--balance", action="store_true", dest="show_balance", default=False, help="show the balance at listed addresses") - parser.add_option("-k", "--keys",action="store_true", dest="show_keys",default=False, help="show the private keys of listed addresses") - parser.add_option("-f", "--fee", dest="tx_fee", default="0.005", help="set tx fee") - parser.add_option("-s", "--fromaddr", dest="from_addr", default=None, help="set source address for payto/mktx. if it isn't in the wallet, it will ask for the private key unless supplied in the format public_key:private_key. It's not saved in the wallet.") - parser.add_option("-c", "--changeaddr", dest="change_addr", default=None, help="set the change address for payto/mktx. default is a spare address, or the source address if it's not in the wallet") - parser.add_option("-r", "--remote", dest="remote_url", default=None, help="URL of a remote wallet") - options, args = parser.parse_args() - - wallet = Wallet() - wallet.set_path(options.wallet_path) - wallet.read() - wallet.remote_url = options.remote_url - - if len(args)==0: - url = None - cmd = 'gui' - elif len(args)==1 and re.match('^bitcoin:', args[0]): - url = args[0] - cmd = 'gui' - else: - cmd = args[0] - firstarg = args[1] if len(args) > 1 else '' - - if cmd == 'gui': - if options.gui=='gtk': - import gui - elif options.gui=='qt': - import gui_qt as gui - else: - print "unknown gui", options.gui - exit(1) - - gui = gui.ElectrumGui(wallet) - WalletSynchronizer(wallet,True).start() - - try: - found = wallet.file_exists - if not found: - found = gui.restore_or_create() - except SystemExit, e: - exit(e) - except BaseException, e: - import traceback - traceback.print_exc(file=sys.stdout) - #gui.show_message(e.message) - exit(1) - - if not found: exit(1) - - gui.main(url) - wallet.save() - sys.exit(0) - - if cmd not in known_commands: - cmd = 'help' - - if not wallet.file_exists and cmd not in ['help','create','restore']: - print "Wallet file not found." - print "Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option" - sys.exit(0) - - if cmd in ['create', 'restore']: - import mnemonic - if wallet.file_exists: - print "remove the existing wallet first!" - sys.exit(0) - password = getpass.getpass("Password (hit return if you do not wish to encrypt your wallet):") - if password: - password2 = getpass.getpass("Confirm password:") - if password != password2: - print "error" - sys.exit(1) - else: - password = None - - w_host, w_port, w_protocol = wallet.server.split(':') - host = raw_input("server (default:%s):"%w_host) - port = raw_input("port (default:%s):"%w_port) - protocol = raw_input("protocol [t=tcp;h=http;n=native] (default:%s):"%w_protocol) - fee = raw_input("fee (default:%s):"%( str(Decimal(wallet.fee)/100000000)) ) - gap = raw_input("gap limit (default 5):") - if host: w_host = host - if port: w_port = port - if protocol: w_protocol = protocol - wallet.server = w_host + ':' + w_port + ':' +w_protocol - if fee: wallet.fee = float(fee) - if gap: wallet.gap_limit = int(gap) - - if cmd == 'restore': - seed = raw_input("seed:") - try: - seed.decode('hex') - except: - print "not hex, trying decode" - seed = mnemonic.mn_decode( seed.split(' ') ) - if not seed: - print "no seed" - sys.exit(1) - - wallet.seed = str(seed) - WalletSynchronizer(wallet).start() - print "recovering wallet..." - wallet.init_mpk( wallet.seed ) - wallet.up_to_date_event.clear() - wallet.up_to_date = False - wallet.update() - if wallet.is_found(): - wallet.fill_addressbook() - wallet.save() - print "recovery successful" - else: - print "found no history for this wallet" - else: - wallet.new_seed(None) - wallet.init_mpk( wallet.seed ) - wallet.synchronize() # there is no wallet thread - wallet.save() - print "Your wallet generation seed is: " + wallet.seed - print "Please keep it in a safe place; if you lose it, you will not be able to restore your wallet." - print "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:" - print "\""+' '.join(mnemonic.mn_encode(wallet.seed))+"\"" - - # check syntax - if cmd in ['payto', 'mktx']: - try: - to_address = args[1] - amount = int( 100000000 * Decimal(args[2]) ) - change_addr = None - label = ' '.join(args[3:]) - if options.tx_fee: - options.tx_fee = int( 100000000 * Decimal(options.tx_fee) ) - except: - firstarg = cmd - cmd = 'help' - - # open session - if cmd not in offline_commands: - WalletSynchronizer(wallet).start() - wallet.update() - wallet.save() - - # check if --from_addr not in wallet (for mktx/payto) - is_temporary = False - from_addr = None - if options.from_addr: - from_addr = options.from_addr - if from_addr not in wallet.all_addresses(): - is_temporary = True - - # commands needing password - if cmd in protected_commands or ( cmd=='addresses' and options.show_keys): - password = getpass.getpass('Password:') if wallet.use_encryption and not is_temporary else None - # check password - try: - wallet.pw_decode( wallet.seed, password) - except: - print "invalid password" - exit(1) - - if cmd == 'import': - keypair = args[1] - if wallet.import_key(keypair,password): - print "keypair imported" - else: - print "error" - wallet.save() - - if cmd=='help': - cmd2 = firstarg - if cmd2 not in known_commands: - print "known commands:", ', '.join(known_commands) - print "'electrum help <command>' shows the help on a specific command" - print "'electrum --help' shows the list of options" - elif cmd2 == 'balance': - print "Display the balance of your wallet or a specific address. The address does not have to be a owned address (you know the private key)." - print "syntax: balance [<address>]" - elif cmd2 == 'contacts': - print "show your list of contacts" - elif cmd2 == 'payto': - print "payto <recipient> <amount> [label]" - print "create and broadcast a transaction." - print "<recipient> can be a bitcoin address or a label" - print "options: --fee, --fromaddr, --changeaddr" - elif cmd2== 'sendtx': - print "sendtx <tx>" - print "broadcast a transaction to the network. <tx> must be in hexadecimal" - elif cmd2 == 'password': - print "change your password" - elif cmd2 == 'addresses': - print "show your list of addresses. options: -a, -k, -b" - elif cmd2 == 'history': - print "show the transaction history" - elif cmd2 == 'label': - print "assign a label to an item" - elif cmd2 == 'gtk': - print "start the GUI" - elif cmd2 == 'mktx': - print "create a signed transaction. password protected" - print "syntax: mktx <recipient> <amount> [label]" - print "options: --fee, --fromaddr, --changeaddr" - elif cmd2 == 'seed': - print "show generation seed of your wallet. password protected." - elif cmd2 == 'eval': - print "Run python eval() on an object\nSyntax: eval <expression>\nExample: eval \"wallet.aliases\"" - - elif cmd == 'seed': - import mnemonic - seed = wallet.pw_decode( wallet.seed, password) - print seed, '"'+' '.join(mnemonic.mn_encode(seed))+'"' - - elif cmd == 'validateaddress': - addr = args[1] - print wallet.is_valid(addr) - - elif cmd == 'balance': - try: - addrs = args[1:] - except: - pass - if addrs == []: - c, u = wallet.get_balance() - if u: - print Decimal( c ) / 100000000 , Decimal( u ) / 100000000 - else: - print Decimal( c ) / 100000000 - else: - for addr in addrs: - c, u = wallet.get_addr_balance(addr) - if u: - print "%s %s, %s" % (addr, str(Decimal(c)/100000000), str(Decimal(u)/100000000)) - else: - print "%s %s" % (addr, str(Decimal(c)/100000000)) - - elif cmd in [ 'contacts']: - for addr in wallet.addressbook: - print addr, " ", wallet.labels.get(addr) - - elif cmd == 'eval': - print eval(args[1]) - wallet.save() - - elif cmd in [ 'addresses']: - for addr in wallet.all_addresses(): - if options.show_all or not wallet.is_change(addr): - label = wallet.labels.get(addr) - _type = '' - if wallet.is_change(addr): _type = "[change]" - if addr in wallet.imported_keys.keys(): _type = "[imported]" - if label is None: label = '' - if options.show_balance: - h = wallet.history.get(addr,[]) - ni = no = 0 - for item in h: - if item['is_in']: ni += 1 - else: no += 1 - b = "%d %d %s"%(no, ni, str(Decimal(wallet.get_addr_balance(addr)[0])/100000000)) - else: b='' - if options.show_keys: - pk = wallet.get_private_key(addr, password) - addr = addr + ':' + SecretToASecret(pk) - print addr, b, _type, label - - if cmd == 'history': - lines = wallet.get_tx_history() - b = 0 - for line in lines: - import datetime - v = line['value'] - b += v - try: - time_str = str( datetime.datetime.fromtimestamp( line['timestamp'])) - except: - print line['timestamp'] - time_str = 'pending' - label = line.get('label') - if not label: label = line['tx_hash'] - else: label = label + ' '*(64 - len(label) ) - - print time_str , " " + label + " " + format_satoshis(v)+ " "+ format_satoshis(b) - print "# balance: ", format_satoshis(b) - - elif cmd == 'label': - try: - tx = args[1] - label = ' '.join(args[2:]) - except: - print "syntax: label <tx_hash> <text>" - sys.exit(1) - wallet.labels[tx] = label - wallet.save() - - elif cmd in ['payto', 'mktx']: - if from_addr and is_temporary: - if from_addr.find(":") == -1: - keypair = from_addr + ":" + getpass.getpass('Private key:') - else: - keypair = from_addr - from_addr = keypair.split(':')[0] - if not wallet.import_key(keypair,password): - print "invalid key pair" - exit(1) - wallet.history[from_addr] = interface.retrieve_history(from_addr) - wallet.update_tx_history() - change_addr = from_addr - - if options.change_addr: - change_addr = options.change_addr - - for k, v in wallet.labels.items(): - if v == to_address: - to_address = k - print "alias", to_address - break - if change_addr and v == change_addr: - change_addr = k - try: - tx = wallet.mktx( to_address, amount, label, password, - fee = options.tx_fee, change_addr = change_addr, from_addr = from_addr ) - except: - import traceback - traceback.print_exc(file=sys.stdout) - tx = None - - if tx and cmd=='payto': - r, h = wallet.sendtx( tx ) - print h - else: - print tx - - if is_temporary: - wallet.imported_keys.pop(from_addr) - del(wallet.history[from_addr]) - wallet.save() - - elif cmd == 'sendtx': - tx = args[1] - r, h = wallet.sendtx( tx ) - print h - - elif cmd == 'password': - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - print "sorry" - sys.exit(1) - new_password = getpass.getpass('New password:') - if new_password == getpass.getpass('Confirm new password:'): - wallet.use_encryption = (new_password != '') - wallet.seed = wallet.pw_encode( seed, new_password) - for k in wallet.imported_keys.keys(): - a = wallet.imported_keys[k] - b = wallet.pw_decode(a, password) - c = wallet.pw_encode(b, new_password) - wallet.imported_keys[k] = c - wallet.save() - else: - print "error: mismatch" - - elif cmd == 'signmessage': - address, message = args[1:3] - print wallet.sign_message(address, message, password) - - elif cmd == 'verifymessage': - address, signature, message = args[1:4] - try: - wallet.verify_message(address, signature, message) - print True - except: - print False - - diff --git a/client/electrum4a.py b/client/electrum4a.py @@ -1,981 +0,0 @@ -#!/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 android -from interface import WalletSynchronizer -from wallet import Wallet -from wallet import format_satoshis -from decimal import Decimal -import mnemonic - -import datetime - - - -def modal_dialog(title, msg = None): - droid.dialogCreateAlert(title,msg) - droid.dialogSetPositiveButtonText('OK') - droid.dialogShow() - droid.dialogGetResponse() - droid.dialogDismiss() - -def modal_input(title, msg, value = None, etype=None): - droid.dialogCreateInput(title, msg, value, etype) - droid.dialogSetPositiveButtonText('OK') - droid.dialogSetNegativeButtonText('Cancel') - droid.dialogShow() - response = droid.dialogGetResponse().result - droid.dialogDismiss() - if response.get('which') == 'positive': - return response.get('value') - -def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'): - droid.dialogCreateAlert(q, msg) - droid.dialogSetPositiveButtonText(pos_text) - droid.dialogSetNegativeButtonText(neg_text) - droid.dialogShow() - response = droid.dialogGetResponse().result - droid.dialogDismiss() - return response.get('which') == 'positive' - -def edit_label(addr): - v = modal_input('Edit label',None,wallet.labels.get(addr)) - if v is not None: - if v: - wallet.labels[addr] = v - else: - if addr in wallet.labels.keys(): - wallet.labels.pop(addr) - wallet.update_tx_history() - wallet.save() - droid.fullSetProperty("labelTextView", "text", v) - -def select_from_contacts(): - title = 'Contacts:' - droid.dialogCreateAlert(title) - l = [] - for i in range(len(wallet.addressbook)): - addr = wallet.addressbook[i] - label = wallet.labels.get(addr,addr) - l.append( label ) - droid.dialogSetItems(l) - droid.dialogSetPositiveButtonText('New contact') - droid.dialogShow() - response = droid.dialogGetResponse().result - droid.dialogDismiss() - - if response.get('which') == 'positive': - return 'newcontact' - - result = response.get('item') - print result - if result is not None: - addr = wallet.addressbook[result] - return addr - - -def select_from_addresses(): - droid.dialogCreateAlert("Addresses:") - l = [] - for i in range(len(wallet.addresses)): - addr = wallet.addresses[i] - label = wallet.labels.get(addr,addr) - l.append( label ) - droid.dialogSetItems(l) - droid.dialogShow() - response = droid.dialogGetResponse() - result = response.result.get('item') - droid.dialogDismiss() - if result is not None: - addr = wallet.addresses[result] - return addr - - -def protocol_name(p): - if p == 't': return 'TCP/stratum' - if p == 'h': return 'HTTP/Stratum' - if p == 'n': return 'TCP/native' - -def protocol_dialog(host, protocol, z): - droid.dialogCreateAlert('Protocol',host) - if z: - protocols = z.keys() - else: - protocols = ['t','h','n'] - l = [] - current = protocols.index(protocol) - for p in protocols: - l.append(protocol_name(p)) - droid.dialogSetSingleChoiceItems(l, current) - droid.dialogSetPositiveButtonText('OK') - droid.dialogSetNegativeButtonText('Cancel') - droid.dialogShow() - response = droid.dialogGetResponse().result - if not response: return - if response.get('which') == 'positive': - response = droid.dialogGetSelectedItems().result[0] - droid.dialogDismiss() - p = protocols[response] - port = z[p] - return host + ':' + port + ':' + p - - - - -def make_layout(s, scrollable = False): - content = """ - - <LinearLayout - android:id="@+id/zz" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="#ff222222"> - - <TextView - android:id="@+id/textElectrum" - android:text="Electrum" - android:textSize="7pt" - android:textColor="#ff4444ff" - android:gravity="left" - android:layout_height="wrap_content" - android:layout_width="match_parent" - /> - </LinearLayout> - - %s """%s - - if scrollable: - content = """ - <ScrollView - android:id="@+id/scrollview" - android:layout_width="match_parent" - android:layout_height="match_parent" > - - <LinearLayout - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="wrap_content" > - - %s - - </LinearLayout> - </ScrollView> - """%content - - - return """<?xml version="1.0" encoding="utf-8"?> - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/background" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="#ff000022"> - - %s - </LinearLayout>"""%content - - - - -def main_layout(): - return make_layout(""" - <TextView android:id="@+id/balanceTextView" - android:layout_width="match_parent" - android:text="" - android:textColor="#ffffffff" - android:textAppearance="?android:attr/textAppearanceLarge" - android:padding="7dip" - android:textSize="8pt" - android:gravity="center_vertical|center_horizontal|left"> - </TextView> - - <TextView android:id="@+id/historyTextView" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Recent transactions" - android:textAppearance="?android:attr/textAppearanceLarge" - android:gravity="center_vertical|center_horizontal|center"> - </TextView> - - %s """%get_history_layout(15),True) - - - -def qr_layout(addr): - return make_layout(""" - - <TextView android:id="@+id/addrTextView" - android:layout_width="match_parent" - android:layout_height="50" - android:text="%s" - android:textAppearance="?android:attr/textAppearanceLarge" - android:gravity="center_vertical|center_horizontal|center"> - </TextView> - - <ImageView - android:id="@+id/qrView" - android:gravity="center" - android:layout_width="match_parent" - android:layout_height="350" - android:antialias="false" - android:src="file:///sdcard/sl4a/qrcode.bmp" /> - - <TextView android:id="@+id/labelTextView" - android:layout_width="match_parent" - android:layout_height="50" - android:text="%s" - android:textAppearance="?android:attr/textAppearanceLarge" - android:gravity="center_vertical|center_horizontal|center"> - </TextView> - - """%(addr,wallet.labels.get(addr,'')), True) - -payto_layout = make_layout(""" - - <TextView android:id="@+id/recipientTextView" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Pay to:" - android:textAppearance="?android:attr/textAppearanceLarge" - android:gravity="left"> - </TextView> - - - <EditText android:id="@+id/recipient" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:tag="Tag Me" android:inputType="text"> - </EditText> - - <LinearLayout android:id="@+id/linearLayout1" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - <Button android:id="@+id/buttonQR" android:layout_width="wrap_content" - android:layout_height="wrap_content" android:text="From QR code"></Button> - <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content" - android:layout_height="wrap_content" android:text="From Contacts"></Button> - </LinearLayout> - - - <TextView android:id="@+id/labelTextView" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Description:" - android:textAppearance="?android:attr/textAppearanceLarge" - android:gravity="left"> - </TextView> - - <EditText android:id="@+id/label" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:tag="Tag Me" android:inputType="text"> - </EditText> - - <TextView android:id="@+id/amountLabelTextView" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Amount:" - android:textAppearance="?android:attr/textAppearanceLarge" - android:gravity="left"> - </TextView> - - <EditText android:id="@+id/amount" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:tag="Tag Me" android:inputType="numberDecimal"> - </EditText> - - <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content" android:id="@+id/linearLayout1"> - <Button android:id="@+id/buttonPay" android:layout_width="wrap_content" - android:layout_height="wrap_content" android:text="Send"></Button> - </LinearLayout>""",False) - - - -settings_layout = make_layout(""" <ListView - android:id="@+id/myListView" - android:layout_width="match_parent" - android:layout_height="wrap_content" />""") - - - -def get_history_values(n): - values = [] - h = wallet.get_tx_history() - - length = min(n, len(h)) - for i in range(length): - line = h[-i-1] - v = line['value'] - try: - dt = datetime.datetime.fromtimestamp( line['timestamp'] ) - if dt.date() == dt.today().date(): - time_str = str( dt.time() ) - else: - time_str = str( dt.date() ) - conf = 'v' - - except: - print line['timestamp'] - time_str = 'pending' - conf = 'o' - - tx_hash = line['tx_hash'] - label = wallet.labels.get(tx_hash) - is_default_label = (label == '') or (label is None) - if is_default_label: label = line['default_label'] - values.append((conf, ' ' + time_str, ' ' + format_satoshis(v,True), ' ' + label )) - - return values - - -def get_history_layout(n): - rows = "" - i = 0 - values = get_history_values(n) - for v in values: - a,b,c,d = v - color = "#ff00ff00" if a == 'v' else "#ffff0000" - rows += """ - <TableRow> - <TextView - android:id="@+id/hl_%d_col1" - android:layout_column="0" - android:text="%s" - android:textColor="%s" - android:padding="3" /> - <TextView - android:id="@+id/hl_%d_col2" - android:layout_column="1" - android:text="%s" - android:padding="3" /> - <TextView - android:id="@+id/hl_%d_col3" - android:layout_column="2" - android:text="%s" - android:padding="3" /> - <TextView - android:id="@+id/hl_%d_col4" - android:layout_column="3" - android:text="%s" - android:padding="4" /> - </TableRow>"""%(i,a,color,i,b,i,c,i,d) - i += 1 - - output = """ -<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:stretchColumns="0,1,2,3"> - %s -</TableLayout>"""% rows - return output - - -def set_history_layout(n): - values = get_history_values(n) - i = 0 - for v in values: - a,b,c,d = v - droid.fullSetProperty("hl_%d_col1"%i,"text", a) - - if a == 'v': - droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00") - else: - droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000") - - droid.fullSetProperty("hl_%d_col2"%i,"text", b) - droid.fullSetProperty("hl_%d_col3"%i,"text", c) - droid.fullSetProperty("hl_%d_col4"%i,"text", d) - i += 1 - - - - -status_text = '' -def update_layout(): - global status_text - if not wallet.interface.is_connected: - text = "Not connected..." - elif wallet.blocks == 0: - text = "Server not ready" - elif not wallet.up_to_date: - text = "Synchronizing..." - else: - c, u = wallet.get_balance() - text = "Balance:"+format_satoshis(c) - if u : text += ' [' + format_satoshis(u,True).strip() + ']' - - - # vibrate if status changed - if text != status_text: - if status_text and wallet.interface.is_connected and wallet.up_to_date: - droid.vibrate() - status_text = text - - droid.fullSetProperty("balanceTextView", "text", status_text) - - if wallet.up_to_date: - set_history_layout(15) - - - - -def pay_to(recipient, amount, fee, label): - - if wallet.use_encryption: - password = droid.dialogGetPassword('Password').result - if not password: return - else: - password = None - - droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...") - droid.dialogShow() - - try: - tx = wallet.mktx( recipient, amount, label, password, fee) - except BaseException, e: - modal_dialog('error', e.message) - droid.dialogDismiss() - return - - droid.dialogDismiss() - - r, h = wallet.sendtx( tx ) - if r: - modal_dialog('Payment sent', h) - return True - else: - modal_dialog('Error', h) - - - - - -def recover(): - - droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?") - droid.dialogSetPositiveButtonText('Create') - droid.dialogSetNeutralButtonText('Restore') - droid.dialogSetNegativeButtonText('Cancel') - droid.dialogShow() - response = droid.dialogGetResponse().result - droid.dialogDismiss() - if response.get('which') == 'negative': - exit(1) - - is_recovery = response.get('which') == 'neutral' - - if not is_recovery: - wallet.new_seed(None) - else: - if modal_question("Input method",None,'QR Code', 'mnemonic'): - code = droid.scanBarcode() - r = code.result - if r: - seed = r['extras']['SCAN_RESULT'] - else: - exit(1) - else: - m = modal_input('Mnemonic','please enter your code') - try: - seed = mnemonic.mn_decode(m.split(' ')) - except: - modal_dialog('error: could not decode this seed') - exit(1) - - wallet.seed = str(seed) - - modal_dialog('Your seed is:', wallet.seed) - modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(wallet.seed)) ) - - msg = "recovering wallet..." if is_recovery else "creating wallet..." - droid.dialogCreateSpinnerProgress("Electrum", msg) - droid.dialogShow() - - wallet.init_mpk( wallet.seed ) - WalletSynchronizer(wallet,True).start() - wallet.update() - - droid.dialogDismiss() - droid.vibrate() - - if is_recovery: - if wallet.is_found(): - wallet.update_tx_history() - wallet.fill_addressbook() - modal_dialog("recovery successful") - else: - if not modal_question("no transactions found for this seed","do you want to keep this wallet?"): - exit(1) - - change_password_dialog() - wallet.save() - - - -def make_new_contact(): - code = droid.scanBarcode() - r = code.result - if r: - address = r['extras']['SCAN_RESULT'] - if address: - if wallet.is_valid(address): - if modal_question('Add to contacts?', address): - wallet.addressbook.append(address) - wallet.save() - else: - modal_dialog('Invalid address', address) - - -do_refresh = False - -def update_callback(): - global do_refresh - print "gui callback", wallet.interface.is_connected, wallet.up_to_date - do_refresh = True - droid.eventPost("refresh",'z') - -def main_loop(): - global do_refresh - - update_layout() - out = None - quitting = False - while out is None: - - event = droid.eventWait(1000).result - if event is None: - if do_refresh: - update_layout() - do_refresh = False - continue - - print "got event in main loop", repr(event) - if event == 'OK': continue - if event is None: continue - #if event["name"]=="refresh": - - - # request 2 taps before we exit - if event["name"]=="key": - if event["data"]["key"] == '4': - if quitting: - out = 'quit' - else: - quitting = True - else: quitting = False - - if event["name"]=="click": - id=event["data"]["id"] - - elif event["name"]=="settings": - out = 'settings' - - elif event["name"] in menu_commands: - out = event["name"] - - if out == 'contacts': - global contact_addr - contact_addr = select_from_contacts() - if contact_addr == 'newcontact': - make_new_contact() - contact_addr = None - if not contact_addr: - out = None - - elif out == "receive": - global receive_addr - receive_addr = select_from_addresses() - if not receive_addr: - out = None - - - return out - - -def payto_loop(): - global recipient - if recipient: - droid.fullSetProperty("recipient","text",recipient) - recipient = None - - out = None - while out is None: - event = droid.eventWait().result - print "got event in payto loop", event - - if event["name"] == "click": - id = event["data"]["id"] - - if id=="buttonPay": - - droid.fullQuery() - recipient = droid.fullQueryDetail("recipient").result.get('text') - label = droid.fullQueryDetail("label").result.get('text') - amount = droid.fullQueryDetail('amount').result.get('text') - - if not wallet.is_valid(recipient): - modal_dialog('Error','Invalid Bitcoin address') - continue - - try: - amount = int( 100000000 * Decimal(amount) ) - except: - modal_dialog('Error','Invalid amount') - continue - - result = pay_to(recipient, amount, wallet.fee, label) - if result: - out = 'main' - - elif id=="buttonContacts": - addr = select_from_contacts() - droid.fullSetProperty("recipient","text",addr) - - elif id=="buttonQR": - code = droid.scanBarcode() - r = code.result - if r: - addr = r['extras']['SCAN_RESULT'] - if addr: - droid.fullSetProperty("recipient","text",addr) - - elif event["name"] in menu_commands: - out = event["name"] - - elif event["name"]=="key": - if event["data"]["key"] == '4': - out = 'main' - - #elif event["name"]=="screen": - # if event["data"]=="destroy": - # out = 'main' - - return out - - -receive_addr = '' -contact_addr = '' -recipient = '' - -def receive_loop(): - out = None - while out is None: - event = droid.eventWait().result - print "got event", event - if event["name"]=="key": - if event["data"]["key"] == '4': - out = 'main' - - elif event["name"]=="clipboard": - droid.setClipboard(receive_addr) - modal_dialog('Address copied to clipboard',receive_addr) - - elif event["name"]=="edit": - edit_label(receive_addr) - - return out - -def contacts_loop(): - global recipient - out = None - while out is None: - event = droid.eventWait().result - print "got event", event - if event["name"]=="key": - if event["data"]["key"] == '4': - out = 'main' - - elif event["name"]=="clipboard": - droid.setClipboard(contact_addr) - modal_dialog('Address copied to clipboard',contact_addr) - - elif event["name"]=="edit": - edit_label(contact_addr) - - elif event["name"]=="paytocontact": - recipient = contact_addr - out = 'send' - - elif event["name"]=="deletecontact": - if modal_question('delete contact', contact_addr): - out = 'main' - - return out - - -def server_dialog(plist): - droid.dialogCreateAlert("Public servers") - droid.dialogSetItems( plist.keys() ) - droid.dialogSetPositiveButtonText('Private server') - droid.dialogShow() - response = droid.dialogGetResponse().result - droid.dialogDismiss() - - if response.get('which') == 'positive': - return modal_input('Private server', None) - - i = response.get('item') - if i is not None: - response = plist.keys()[i] - return response - - -def seed_dialog(): - if wallet.use_encryption: - password = droid.dialogGetPassword('Seed').result - if not password: return - else: - password = None - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - modal_dialog('error','incorrect password') - return - - modal_dialog('Your seed is',seed) - modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(seed)) ) - -def change_password_dialog(): - if wallet.use_encryption: - password = droid.dialogGetPassword('Your wallet is encrypted').result - if password is None: return - else: - password = None - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - modal_dialog('error','incorrect password') - return - - new_password = droid.dialogGetPassword('Choose a password').result - if new_password == None: - return - - if new_password != '': - password2 = droid.dialogGetPassword('Confirm new password').result - if new_password != password2: - modal_dialog('error','passwords do not match') - return - - wallet.update_password(seed, new_password) - if new_password: - modal_dialog('Password updated','your wallet is encrypted') - else: - modal_dialog('No password','your wallet is not encrypted') - return True - - -def settings_loop(): - - - def set_listview(): - server, port, p = wallet.server.split(':') - fee = str( Decimal( wallet.fee)/100000000 ) - is_encrypted = 'yes' if wallet.use_encryption else 'no' - protocol = protocol_name(p) - droid.fullShow(settings_layout) - droid.fullSetList("myListView",['Server: ' + server, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed']) - - set_listview() - - out = None - while out is None: - event = droid.eventWait().result - print "got event", event - if event == 'OK': continue - if not event: continue - - plist = {} - for item in wallet.interface.servers: - host, pp = item - z = {} - for item2 in pp: - protocol, port = item2 - z[protocol] = port - plist[host] = z - - if event["name"] == "itemclick": - pos = event["data"]["position"] - host, port, protocol = wallet.server.split(':') - - if pos == "0": #server - host = server_dialog(plist) - if host: - p = plist[host] - port = p['t'] - srv = host + ':' + port + ':t' - try: - wallet.set_server(srv) - except: - modal_dialog('error','invalid server') - set_listview() - - elif pos == "1": #protocol - if host in plist: - srv = protocol_dialog(host, protocol, plist[host]) - if srv: - try: - wallet.set_server(srv) - except: - modal_dialog('error','invalid server') - set_listview() - - elif pos == "2": #port - a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number") - if a_port: - if a_port != port: - srv = host + ':' + a_port + ':'+ protocol - try: - wallet.set_server(srv) - except: - modal_dialog('error','invalid port number') - set_listview() - - elif pos == "3": #fee - fee = modal_input('Transaction fee', 'The fee will be this amount multiplied by the number of inputs in your transaction. ', str( Decimal( wallet.fee)/100000000 ), "numberDecimal") - if fee: - try: - fee = int( 100000000 * Decimal(fee) ) - except: - modal_dialog('error','invalid fee value') - if wallet.fee != fee: - wallet.fee = fee - wallet.save() - set_listview() - - elif pos == "4": - if change_password_dialog(): - set_listview() - - elif pos == "5": - seed_dialog() - - - elif event["name"] in menu_commands: - out = event["name"] - - elif event["name"] == 'cancel': - out = 'main' - - elif event["name"] == "key": - if event["data"]["key"] == '4': - out = 'main' - - return out - - - - -menu_commands = ["send", "receive", "settings", "contacts", "main"] -droid = android.Android() -wallet = Wallet(update_callback) - -wallet.set_path("/sdcard/electrum.dat") -wallet.read() -if not wallet.file_exists: - recover() -else: - WalletSynchronizer(wallet,True).start() - - -s = 'main' - -def add_menu(s): - droid.clearOptionsMenu() - if s == 'main': - droid.addOptionsMenuItem("Send","send",None,"") - droid.addOptionsMenuItem("Receive","receive",None,"") - droid.addOptionsMenuItem("Contacts","contacts",None,"") - droid.addOptionsMenuItem("Settings","settings",None,"") - elif s == 'receive': - droid.addOptionsMenuItem("Copy","clipboard",None,"") - droid.addOptionsMenuItem("Label","edit",None,"") - elif s == 'contacts': - droid.addOptionsMenuItem("Copy","clipboard",None,"") - droid.addOptionsMenuItem("Label","edit",None,"") - droid.addOptionsMenuItem("Pay to","paytocontact",None,"") - #droid.addOptionsMenuItem("Delete","deletecontact",None,"") - -def make_bitmap(addr): - # fixme: this is highly inefficient - droid.dialogCreateSpinnerProgress("please wait") - droid.dialogShow() - import pyqrnative, bmp - qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H) - qr.addData(addr) - qr.make() - k = qr.getModuleCount() - bitmap = bmp.BitMap( 35*8, 35*8 ) - print len(bitmap.bitarray) - bitmap.bitarray = [] - assert k == 33 - - for r in range(35): - tmparray = [ 0 ] * 35*8 - - if 0 < r < 34: - for c in range(k): - if qr.isDark(r-1, c): - tmparray[ (1+c)*8:(2+c)*8] = [1]*8 - - for i in range(8): - bitmap.bitarray.append( tmparray[:] ) - - bitmap.saveFile( "/sdcard/sl4a/qrcode.bmp" ) - droid.dialogDismiss() - - - -while True: - add_menu(s) - if s == 'main': - droid.fullShow(main_layout()) - s = main_loop() - #droid.fullDismiss() - - elif s == 'send': - droid.fullShow(payto_layout) - s = payto_loop() - #droid.fullDismiss() - - elif s == 'receive': - make_bitmap(receive_addr) - droid.fullShow(qr_layout(receive_addr)) - s = receive_loop() - - elif s == 'contacts': - make_bitmap(contact_addr) - droid.fullShow(qr_layout(contact_addr)) - s = contacts_loop() - - elif s == 'settings': - #droid.fullShow(settings_layout) - s = settings_loop() - #droid.fullDismiss() - else: - break - -droid.makeToast("Bye!") diff --git a/client/gui.py b/client/gui.py @@ -1,1265 +0,0 @@ -#!/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 datetime -import thread, time, ast, sys, re -import socket, traceback -import pygtk -pygtk.require('2.0') -import gtk, gobject -import pyqrnative -from decimal import Decimal - -gtk.gdk.threads_init() -APP_NAME = "Electrum" -import platform -MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' - -from wallet import format_satoshis -from interface import DEFAULT_SERVERS - -def numbify(entry, is_int = False): - text = entry.get_text().strip() - chars = '0123456789' - if not is_int: chars +='.' - s = ''.join([i for i in text if i in chars]) - if not is_int: - if '.' in s: - p = s.find('.') - s = s.replace('.','') - s = s[:p] + '.' + s[p:p+8] - try: - amount = int( Decimal(s) * 100000000 ) - except: - amount = None - else: - try: - amount = int( s ) - except: - amount = None - entry.set_text(s) - return amount - - - - -def show_seed_dialog(wallet, password, parent): - import mnemonic - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - show_message("Incorrect password") - return - dialog = gtk.MessageDialog( - parent = parent, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_OK, - message_format = "Your wallet generation seed is:\n\n" + seed \ - + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \ - + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" ) - dialog.set_title("Seed") - dialog.show() - dialog.run() - dialog.destroy() - -def restore_create_dialog(wallet): - - # ask if the user wants to create a new wallet, or recover from a seed. - # if he wants to recover, and nothing is found, do not create wallet - dialog = gtk.Dialog("electrum", parent=None, - flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, - buttons= ("create", 0, "restore",1, "cancel",2) ) - - label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" ) - label.show() - dialog.vbox.pack_start(label) - dialog.show() - r = dialog.run() - dialog.destroy() - - if r==2: return False - - is_recovery = (r==1) - - # ask for the server. - if not run_network_dialog( wallet, parent=None ): return False - - if not is_recovery: - - wallet.new_seed(None) - # generate first key - wallet.init_mpk( wallet.seed ) - wallet.up_to_date_event.clear() - wallet.update() - - # run a dialog indicating the seed, ask the user to remember it - show_seed_dialog(wallet, None, None) - - #ask for password - change_password_dialog(wallet, None, None) - else: - # ask for seed and gap. - run_recovery_dialog( wallet ) - - dialog = gtk.MessageDialog( - parent = None, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_CANCEL, - message_format = "Please wait..." ) - dialog.show() - - def recover_thread( wallet, dialog ): - wallet.init_mpk( wallet.seed ) # not encrypted at this point - wallet.up_to_date_event.clear() - wallet.update() - - if wallet.is_found(): - # history and addressbook - wallet.update_tx_history() - wallet.fill_addressbook() - print "recovery successful" - - gobject.idle_add( dialog.destroy ) - - thread.start_new_thread( recover_thread, ( wallet, dialog ) ) - r = dialog.run() - dialog.destroy() - if r==gtk.RESPONSE_CANCEL: return False - if not wallet.is_found: - show_message("No transactions found for this seed") - - wallet.save() - return True - - -def run_recovery_dialog(wallet): - message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." - dialog = gtk.MessageDialog( - parent = None, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_OK_CANCEL, - message_format = message) - - vbox = dialog.vbox - dialog.set_default_response(gtk.RESPONSE_OK) - - # ask seed, server and gap in the same dialog - seed_box = gtk.HBox() - seed_label = gtk.Label('Seed or mnemonic:') - seed_label.set_size_request(150,-1) - seed_box.pack_start(seed_label, False, False, 10) - seed_label.show() - seed_entry = gtk.Entry() - seed_entry.show() - seed_entry.set_size_request(450,-1) - seed_box.pack_start(seed_entry, False, False, 10) - add_help_button(seed_box, '.') - seed_box.show() - vbox.pack_start(seed_box, False, False, 5) - - gap = gtk.HBox() - gap_label = gtk.Label('Gap limit:') - gap_label.set_size_request(150,10) - gap_label.show() - gap.pack_start(gap_label,False, False, 10) - gap_entry = gtk.Entry() - gap_entry.set_text("%d"%wallet.gap_limit) - gap_entry.connect('changed', numbify, True) - gap_entry.show() - gap.pack_start(gap_entry,False,False, 10) - add_help_button(gap, 'The maximum gap that is allowed between unused addresses in your wallet. During wallet recovery, this parameter is used to decide when to stop the recovery process. If you increase this value, you will need to remember it in order to be able to recover your wallet from seed.') - gap.show() - vbox.pack_start(gap, False,False, 5) - - dialog.show() - r = dialog.run() - gap = gap_entry.get_text() - seed = seed_entry.get_text() - dialog.destroy() - - if r==gtk.RESPONSE_CANCEL: - sys.exit(1) - try: - gap = int(gap) - except: - show_message("error") - sys.exit(1) - - try: - seed.decode('hex') - except: - import mnemonic - print "not hex, trying decode" - seed = mnemonic.mn_decode( seed.split(' ') ) - if not seed: - show_message("no seed") - sys.exit(1) - - wallet.seed = seed - wallet.gap_limit = gap - wallet.save() - - - -def run_settings_dialog(wallet, parent): - - message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field." - - dialog = gtk.MessageDialog( - parent = parent, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_OK_CANCEL, - message_format = message) - - image = gtk.Image() - image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG) - image.show() - dialog.set_image(image) - dialog.set_title("Settings") - - vbox = dialog.vbox - dialog.set_default_response(gtk.RESPONSE_OK) - - fee = gtk.HBox() - fee_entry = gtk.Entry() - fee_label = gtk.Label('Transaction fee:') - fee_label.set_size_request(150,10) - fee_label.show() - fee.pack_start(fee_label,False, False, 10) - fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) ) - fee_entry.connect('changed', numbify, False) - fee_entry.show() - fee.pack_start(fee_entry,False,False, 10) - add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005') - fee.show() - vbox.pack_start(fee, False,False, 5) - - dialog.show() - r = dialog.run() - fee = fee_entry.get_text() - - dialog.destroy() - if r==gtk.RESPONSE_CANCEL: - return - - try: - fee = int( 100000000 * Decimal(fee) ) - except: - show_message("error") - return - - wallet.fee = fee - wallet.save() - - - - -def run_network_dialog( wallet, parent ): - image = gtk.Image() - image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG) - interface = wallet.interface - if parent: - if interface.is_connected: - status = "Connected to %s:%d\n%d blocks\nresponse time: %f"%(interface.host, interface.port, wallet.blocks, interface.rtime) - else: - status = "Not connected" - server = wallet.server - else: - import random - status = "Please choose a server." - server = random.choice( DEFAULT_SERVERS ) - - plist = {} - for item in wallet.interface.servers: - host, pp = item - z = {} - for item2 in pp: - protocol, port = item2 - z[protocol] = port - plist[host] = z - - dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status) - dialog.set_title("Server") - dialog.set_image(image) - image.show() - - vbox = dialog.vbox - host_box = gtk.HBox() - host_label = gtk.Label('Connect to:') - host_label.set_size_request(100,-1) - host_label.show() - host_box.pack_start(host_label, False, False, 10) - host_entry = gtk.Entry() - host_entry.set_size_request(200,-1) - host_entry.set_text(server) - host_entry.show() - host_box.pack_start(host_entry, False, False, 10) - add_help_button(host_box, 'The name and port number of your Electrum server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, port 50000 will be tried. Some servers allow you to connect through http (port 80) or https (port 443)') - host_box.show() - - - p_box = gtk.HBox(False, 10) - p_box.show() - - p_label = gtk.Label('Protocol:') - p_label.set_size_request(100,-1) - p_label.show() - p_box.pack_start(p_label, False, False, 10) - - radio1 = gtk.RadioButton(None, "tcp") - p_box.pack_start(radio1, True, True, 0) - radio1.show() - radio2 = gtk.RadioButton(radio1, "http") - p_box.pack_start(radio2, True, True, 0) - radio2.show() - radio3 = gtk.RadioButton(radio1, "native") - p_box.pack_start(radio3, True, True, 0) - radio3.show() - - def current_line(): - return unicode(host_entry.get_text()).split(':') - - def set_button(protocol): - if protocol == 't': - radio1.set_active(1) - elif protocol == 'h': - radio2.set_active(1) - elif protocol == 'n': - radio3.set_active(1) - - def set_protocol(protocol): - host = current_line()[0] - pp = plist[host] - if protocol not in pp.keys(): - protocol = pp.keys()[0] - set_button(protocol) - port = pp[protocol] - host_entry.set_text( host + ':' + port + ':' + protocol) - - radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1") - radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1") - radio3.connect("toggled", lambda x,y:set_protocol('n'), "radio button 1") - - server_list = gtk.ListStore(str) - for host in plist.keys(): - server_list.append([host]) - - treeview = gtk.TreeView(model=server_list) - treeview.show() - - tvcolumn = gtk.TreeViewColumn('Active servers') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 0) - - scroll = gtk.ScrolledWindow() - scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scroll.add(treeview) - scroll.show() - - vbox.pack_start(host_box, False,False, 5) - vbox.pack_start(p_box, True, True, 0) - vbox.pack_start(scroll) - - def my_treeview_cb(treeview): - path, view_column = treeview.get_cursor() - host = server_list.get_value( server_list.get_iter(path), 0) - - pp = plist[host] - if 't' in pp.keys(): - protocol = 't' - else: - protocol = pp.keys()[0] - port = pp[protocol] - host_entry.set_text( host + ':' + port + ':' + protocol) - set_button(protocol) - - treeview.connect('cursor-changed', my_treeview_cb) - - dialog.show() - r = dialog.run() - server = host_entry.get_text() - dialog.destroy() - - if r==gtk.RESPONSE_CANCEL: - return False - - try: - wallet.set_server(server) - except: - show_message("error:" + server) - return False - - if parent: - wallet.save() - return True - - - -def show_message(message, parent=None): - dialog = gtk.MessageDialog( - parent = parent, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_CLOSE, - message_format = message ) - dialog.show() - dialog.run() - dialog.destroy() - -def password_line(label): - password = gtk.HBox() - password_label = gtk.Label(label) - password_label.set_size_request(120,10) - password_label.show() - password.pack_start(password_label,False, False, 10) - password_entry = gtk.Entry() - password_entry.set_size_request(300,-1) - password_entry.set_visibility(False) - password_entry.show() - password.pack_start(password_entry,False,False, 10) - password.show() - return password, password_entry - -def password_dialog(parent): - dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.") - dialog.get_image().set_visible(False) - current_pw, current_pw_entry = password_line('Password:') - current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK) - dialog.vbox.pack_start(current_pw, False, True, 0) - dialog.show() - result = dialog.run() - pw = current_pw_entry.get_text() - dialog.destroy() - if result != gtk.RESPONSE_CANCEL: return pw - -def change_password_dialog(wallet, parent, icon): - if parent: - msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' - else: - msg = "Please choose a password to encrypt your wallet keys" - - dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) - dialog.set_title("Change password") - image = gtk.Image() - image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG) - image.show() - dialog.set_image(image) - - if wallet.use_encryption: - current_pw, current_pw_entry = password_line('Current password:') - dialog.vbox.pack_start(current_pw, False, True, 0) - - password, password_entry = password_line('New password:') - dialog.vbox.pack_start(password, False, True, 5) - password2, password2_entry = password_line('Confirm password:') - dialog.vbox.pack_start(password2, False, True, 5) - - dialog.show() - result = dialog.run() - password = current_pw_entry.get_text() if wallet.use_encryption else None - new_password = password_entry.get_text() - new_password2 = password2_entry.get_text() - dialog.destroy() - if result == gtk.RESPONSE_CANCEL: - return - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - show_message("Incorrect password") - return - - if new_password != new_password2: - show_message("passwords do not match") - return - - wallet.update_password(seed, new_password) - - if icon: - if wallet.use_encryption: - icon.set_tooltip_text('wallet is encrypted') - else: - icon.set_tooltip_text('wallet is unencrypted') - - -def add_help_button(hbox, message): - button = gtk.Button('?') - button.connect("clicked", lambda x: show_message(message)) - button.show() - hbox.pack_start(button,False, False) - - -class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) ) - -gobject.type_register(MyWindow) -gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W') -gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q') - - -class ElectrumWindow: - - def show_message(self, msg): - show_message(msg, self.window) - - def __init__(self, wallet): - self.wallet = wallet - self.funds_error = False # True if not enough funds - - self.window = MyWindow(gtk.WINDOW_TOPLEVEL) - self.window.set_title(APP_NAME + " " + self.wallet.electrum_version) - self.window.connect("destroy", gtk.main_quit) - self.window.set_border_width(0) - self.window.connect('mykeypress', gtk.main_quit) - self.window.set_default_size(720, 350) - - vbox = gtk.VBox() - - self.notebook = gtk.Notebook() - self.create_history_tab() - self.create_send_tab() - self.create_recv_tab() - self.create_book_tab() - self.create_about_tab() - self.notebook.show() - vbox.pack_start(self.notebook, True, True, 2) - - self.status_bar = gtk.Statusbar() - vbox.pack_start(self.status_bar, False, False, 0) - - self.status_image = gtk.Image() - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - self.status_image.set_alignment(True, 0.5 ) - self.status_image.show() - - self.network_button = gtk.Button() - self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) ) - self.network_button.add(self.status_image) - self.network_button.set_relief(gtk.RELIEF_NONE) - self.network_button.show() - self.status_bar.pack_end(self.network_button, False, False) - - def seedb(w, wallet): - if wallet.use_encryption: - password = password_dialog(self.window) - if not password: return - else: password = None - show_seed_dialog(wallet, password, self.window) - button = gtk.Button('S') - button.connect("clicked", seedb, wallet ) - button.set_relief(gtk.RELIEF_NONE) - button.show() - self.status_bar.pack_end(button,False, False) - - settings_icon = gtk.Image() - settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) - settings_icon.set_alignment(0.5, 0.5) - settings_icon.set_size_request(16,16 ) - settings_icon.show() - - prefs_button = gtk.Button() - prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) ) - prefs_button.add(settings_icon) - prefs_button.set_tooltip_text("Settings") - prefs_button.set_relief(gtk.RELIEF_NONE) - prefs_button.show() - self.status_bar.pack_end(prefs_button,False,False) - - pw_icon = gtk.Image() - pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) - pw_icon.set_alignment(0.5, 0.5) - pw_icon.set_size_request(16,16 ) - pw_icon.show() - - password_button = gtk.Button() - password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon)) - password_button.add(pw_icon) - password_button.set_relief(gtk.RELIEF_NONE) - password_button.show() - self.status_bar.pack_end(password_button,False,False) - - self.window.add(vbox) - self.window.show_all() - #self.fee_box.hide() - - self.context_id = self.status_bar.get_context_id("statusbar") - self.update_status_bar() - - def update_status_bar_thread(): - while True: - gobject.idle_add( self.update_status_bar ) - time.sleep(0.5) - - - def check_recipient_thread(): - old_r = '' - while True: - time.sleep(0.5) - if self.payto_entry.is_focus(): - continue - r = self.payto_entry.get_text() - if r != old_r: - old_r = r - r = r.strip() - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): - try: - to_address = self.wallet.get_alias(r, interactive=False) - except: - continue - if to_address: - s = r + ' <' + to_address + '>' - gobject.idle_add( lambda: self.payto_entry.set_text(s) ) - - - thread.start_new_thread(update_status_bar_thread, ()) - thread.start_new_thread(check_recipient_thread, ()) - self.notebook.set_current_page(0) - - - def add_tab(self, page, name): - tab_label = gtk.Label(name) - tab_label.show() - self.notebook.append_page(page, tab_label) - - - def create_send_tab(self): - - page = vbox = gtk.VBox() - page.show() - - payto = gtk.HBox() - payto_label = gtk.Label('Pay to:') - payto_label.set_size_request(100,-1) - payto.pack_start(payto_label, False) - payto_entry = gtk.Entry() - payto_entry.set_size_request(450, 26) - payto.pack_start(payto_entry, False) - vbox.pack_start(payto, False, False, 5) - - message = gtk.HBox() - message_label = gtk.Label('Description:') - message_label.set_size_request(100,-1) - message.pack_start(message_label, False) - message_entry = gtk.Entry() - message_entry.set_size_request(450, 26) - message.pack_start(message_entry, False) - vbox.pack_start(message, False, False, 5) - - amount_box = gtk.HBox() - amount_label = gtk.Label('Amount:') - amount_label.set_size_request(100,-1) - amount_box.pack_start(amount_label, False) - amount_entry = gtk.Entry() - amount_entry.set_size_request(120, -1) - amount_box.pack_start(amount_entry, False) - vbox.pack_start(amount_box, False, False, 5) - - self.fee_box = fee_box = gtk.HBox() - fee_label = gtk.Label('Fee:') - fee_label.set_size_request(100,-1) - fee_box.pack_start(fee_label, False) - fee_entry = gtk.Entry() - fee_entry.set_size_request(60, 26) - fee_box.pack_start(fee_entry, False) - vbox.pack_start(fee_box, False, False, 5) - - end_box = gtk.HBox() - empty_label = gtk.Label('') - empty_label.set_size_request(100,-1) - end_box.pack_start(empty_label, False) - send_button = gtk.Button("Send") - send_button.show() - end_box.pack_start(send_button, False, False, 0) - clear_button = gtk.Button("Clear") - clear_button.show() - end_box.pack_start(clear_button, False, False, 15) - send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry)) - clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry)) - - vbox.pack_start(end_box, False, False, 5) - - # display this line only if there is a signature - payto_sig = gtk.HBox() - payto_sig_id = gtk.Label('') - payto_sig.pack_start(payto_sig_id, False) - vbox.pack_start(payto_sig, True, True, 5) - - - self.user_fee = False - - def entry_changed( entry, is_fee ): - self.funds_error = False - amount = numbify(amount_entry) - fee = numbify(fee_entry) - if not is_fee: fee = None - if amount is None: - return - inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) - if not is_fee: - fee_entry.set_text( str( Decimal( fee ) / 100000000 ) ) - self.fee_box.show() - if inputs: - amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) - fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) - send_button.set_sensitive(True) - else: - send_button.set_sensitive(False) - amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) - fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) - self.funds_error = True - - amount_entry.connect('changed', entry_changed, False) - fee_entry.connect('changed', entry_changed, True) - - self.payto_entry = payto_entry - self.payto_fee_entry = fee_entry - self.payto_sig_id = payto_sig_id - self.payto_sig = payto_sig - self.amount_entry = amount_entry - self.message_entry = message_entry - self.add_tab(page, 'Send') - - def set_frozen(self,entry,frozen): - if frozen: - entry.set_editable(False) - entry.set_has_frame(False) - entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee")) - else: - entry.set_editable(True) - entry.set_has_frame(True) - entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff")) - - def set_url(self, url): - payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) - self.notebook.set_current_page(1) - self.payto_entry.set_text(payto) - self.message_entry.set_text(message) - self.amount_entry.set_text(amount) - if identity: - self.set_frozen(self.payto_entry,True) - self.set_frozen(self.amount_entry,True) - self.set_frozen(self.message_entry,True) - self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity ) - else: - self.payto_sig.set_visible(False) - - def create_about_tab(self): - import pango - page = gtk.VBox() - page.show() - tv = gtk.TextView() - tv.set_editable(False) - tv.set_cursor_visible(False) - tv.modify_font(pango.FontDescription(MONOSPACE_FONT)) - page.pack_start(tv) - self.info = tv.get_buffer() - self.add_tab(page, 'Wall') - - def do_clear(self, w, data): - self.payto_sig.set_visible(False) - self.payto_fee_entry.set_text('') - for entry in [self.payto_entry,self.amount_entry,self.message_entry]: - self.set_frozen(entry,False) - entry.set_text('') - - def question(self,msg): - dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) - dialog.show() - result = dialog.run() - dialog.destroy() - return result == gtk.RESPONSE_OK - - def do_send(self, w, data): - payto_entry, label_entry, amount_entry, fee_entry = data - label = label_entry.get_text() - r = payto_entry.get_text() - r = r.strip() - - m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) - m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) - - if m1: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - if not to_address: - return - else: - self.update_sending_tab() - - elif m2: - to_address = m2.group(5) - else: - to_address = r - - if not self.wallet.is_valid(to_address): - self.show_message( "invalid bitcoin address:\n"+to_address) - return - - try: - amount = int( Decimal(amount_entry.get_text()) * 100000000 ) - except: - self.show_message( "invalid amount") - return - try: - fee = int( Decimal(fee_entry.get_text()) * 100000000 ) - except: - self.show_message( "invalid fee") - return - - if self.wallet.use_encryption: - password = password_dialog(self.window) - if not password: - return - else: - password = None - - try: - tx = self.wallet.mktx( to_address, amount, label, password, fee ) - except BaseException, e: - self.show_message(e.message) - return - - status, msg = self.wallet.sendtx( tx ) - if status: - self.show_message( "payment sent.\n" + msg ) - payto_entry.set_text("") - label_entry.set_text("") - amount_entry.set_text("") - fee_entry.set_text("") - #self.fee_box.hide() - self.update_sending_tab() - else: - self.show_message( msg ) - - - def treeview_button_press(self, treeview, event): - if event.type == gtk.gdk._2BUTTON_PRESS: - c = treeview.get_cursor()[0] - if treeview == self.history_treeview: - tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) - self.show_message(tx_details) - elif treeview == self.contacts_treeview: - m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) - - - def treeview_key_press(self, treeview, event): - c = treeview.get_cursor()[0] - if event.keyval == gtk.keysyms.Up: - if c and c[0] == 0: - treeview.parent.grab_focus() - treeview.set_cursor((0,)) - elif event.keyval == gtk.keysyms.Return: - if treeview == self.history_treeview: - tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) - self.show_message(tx_details) - elif treeview == self.contacts_treeview: - m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self" - msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) - - return False - - def create_history_tab(self): - - self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str) - treeview = gtk.TreeView(model=self.history_list) - self.history_treeview = treeview - treeview.set_tooltip_column(7) - treeview.show() - treeview.connect('key-press-event', self.treeview_key_press) - treeview.connect('button-press-event', self.treeview_button_press) - - tvcolumn = gtk.TreeViewColumn('') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererPixbuf() - tvcolumn.pack_start(cell, False) - tvcolumn.set_attributes(cell, stock_id=1) - - tvcolumn = gtk.TreeViewColumn('Date') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 2) - - tvcolumn = gtk.TreeViewColumn('Description') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_property('foreground', 'grey') - cell.set_property('family', MONOSPACE_FONT) - cell.set_property('editable', True) - def edited_cb(cell, path, new_text, h_list): - tx = h_list.get_value( h_list.get_iter(path), 0) - self.wallet.labels[tx] = new_text - self.wallet.save() - self.update_history_tab() - cell.connect('edited', edited_cb, self.history_list) - def editing_started(cell, entry, path, h_list): - tx = h_list.get_value( h_list.get_iter(path), 0) - if not self.wallet.labels.get(tx): entry.set_text('') - cell.connect('editing-started', editing_started, self.history_list) - tvcolumn.set_expand(True) - tvcolumn.pack_start(cell, True) - tvcolumn.set_attributes(cell, text=3, foreground_set = 4) - - tvcolumn = gtk.TreeViewColumn('Amount') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_alignment(1, 0.5) - cell.set_property('family', MONOSPACE_FONT) - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 5) - - tvcolumn = gtk.TreeViewColumn('Balance') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_alignment(1, 0.5) - cell.set_property('family', MONOSPACE_FONT) - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 6) - - tvcolumn = gtk.TreeViewColumn('Tooltip') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 7) - tvcolumn.set_visible(False) - - scroll = gtk.ScrolledWindow() - scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - scroll.add(treeview) - - self.add_tab(scroll, 'History') - self.update_history_tab() - - - def create_recv_tab(self): - self.recv_list = gtk.ListStore(str, str, str) - self.add_tab( self.make_address_list(True), 'Receive') - self.update_receiving_tab() - - def create_book_tab(self): - self.addressbook_list = gtk.ListStore(str, str, str) - self.add_tab( self.make_address_list(False), 'Contacts') - self.update_sending_tab() - - def make_address_list(self, is_recv): - liststore = self.recv_list if is_recv else self.addressbook_list - treeview = gtk.TreeView(model= liststore) - treeview.connect('key-press-event', self.treeview_key_press) - treeview.connect('button-press-event', self.treeview_button_press) - treeview.show() - if not is_recv: - self.contacts_treeview = treeview - - tvcolumn = gtk.TreeViewColumn('Address') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_property('family', MONOSPACE_FONT) - tvcolumn.pack_start(cell, True) - tvcolumn.add_attribute(cell, 'text', 0) - - tvcolumn = gtk.TreeViewColumn('Label') - tvcolumn.set_expand(True) - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_property('editable', True) - def edited_cb2(cell, path, new_text, liststore): - address = liststore.get_value( liststore.get_iter(path), 0) - self.wallet.labels[address] = new_text - self.wallet.save() - self.wallet.update_tx_labels() - self.update_receiving_tab() - self.update_sending_tab() - self.update_history_tab() - cell.connect('edited', edited_cb2, liststore) - tvcolumn.pack_start(cell, True) - tvcolumn.add_attribute(cell, 'text', 1) - - tvcolumn = gtk.TreeViewColumn('Tx') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, True) - tvcolumn.add_attribute(cell, 'text', 2) - - scroll = gtk.ScrolledWindow() - scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scroll.add(treeview) - - hbox = gtk.HBox() - if not is_recv: - button = gtk.Button("New") - button.connect("clicked", self.newaddress_dialog) - button.show() - hbox.pack_start(button,False) - - def showqrcode(w, treeview, liststore): - path, col = treeview.get_cursor() - if not path: return - address = liststore.get_value(liststore.get_iter(path), 0) - qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H) - qr.addData(address) - qr.make() - boxsize = 7 - size = qr.getModuleCount()*boxsize - def area_expose_cb(area, event): - style = area.get_style() - k = qr.getModuleCount() - for r in range(k): - for c in range(k): - gc = style.black_gc if qr.isDark(r, c) else style.white_gc - area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize) - area = gtk.DrawingArea() - area.set_size_request(size, size) - area.connect("expose-event", area_expose_cb) - area.show() - dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1)) - dialog.vbox.add(area) - dialog.run() - dialog.destroy() - - button = gtk.Button("QR") - button.connect("clicked", showqrcode, treeview, liststore) - button.show() - hbox.pack_start(button,False) - - button = gtk.Button("Copy to clipboard") - def copy2clipboard(w, treeview, liststore): - import platform - path, col = treeview.get_cursor() - if path: - address = liststore.get_value( liststore.get_iter(path), 0) - if platform.system() == 'Windows': - from Tkinter import Tk - r = Tk() - r.withdraw() - r.clipboard_clear() - r.clipboard_append( address ) - r.destroy() - else: - c = gtk.clipboard_get() - c.set_text( address ) - button.connect("clicked", copy2clipboard, treeview, liststore) - button.show() - hbox.pack_start(button,False) - - if not is_recv: - button = gtk.Button("Pay to") - def payto(w, treeview, liststore): - path, col = treeview.get_cursor() - if path: - address = liststore.get_value( liststore.get_iter(path), 0) - self.payto_entry.set_text( address ) - self.notebook.set_current_page(1) - self.amount_entry.grab_focus() - - button.connect("clicked", payto, treeview, liststore) - button.show() - hbox.pack_start(button,False) - - vbox = gtk.VBox() - vbox.pack_start(scroll,True) - vbox.pack_start(hbox, False) - return vbox - - def update_status_bar(self): - interface = self.wallet.interface - if self.funds_error: - text = "Not enough funds" - elif interface.is_connected: - self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime)) - if self.wallet.blocks == -1: - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - text = "Connecting..." - elif self.wallet.blocks == 0: - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - text = "Server not ready" - elif not self.wallet.up_to_date: - self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU) - text = "Synchronizing..." - else: - self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) - self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime)) - c, u = self.wallet.get_balance() - text = "Balance: %s "%( format_satoshis(c) ) - if u: text += "[%s unconfirmed]"%( format_satoshis(u,True).strip() ) - else: - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.host, self.wallet.blocks)) - text = "Not connected" - - self.status_bar.pop(self.context_id) - self.status_bar.push(self.context_id, text) - - if self.wallet.was_updated and self.wallet.up_to_date: - self.update_history_tab() - self.update_receiving_tab() - # addressbook too... - self.info.set_text( self.wallet.banner ) - self.wallet.was_updated = False - - - def update_receiving_tab(self): - self.recv_list.clear() - for address in self.wallet.all_addresses(): - if self.wallet.is_change(address):continue - label = self.wallet.labels.get(address) - n = 0 - h = self.wallet.history.get(address,[]) - for item in h: - if not item['is_input'] : n=n+1 - tx = "None" if n==0 else "%d"%n - self.recv_list.append((address, label, tx )) - - def update_sending_tab(self): - # detect addresses that are not mine in history, add them here... - self.addressbook_list.clear() - for alias, v in self.wallet.aliases.items(): - s, target = v - label = self.wallet.labels.get(alias) - self.addressbook_list.append((alias, label, '-')) - - for address in self.wallet.addressbook: - label = self.wallet.labels.get(address) - n = 0 - for item in self.wallet.tx_history.values(): - if address in item['outputs'] : n=n+1 - tx = "None" if n==0 else "%d"%n - self.addressbook_list.append((address, label, tx)) - - def update_history_tab(self): - cursor = self.history_treeview.get_cursor()[0] - self.history_list.clear() - balance = 0 - for tx in self.wallet.get_tx_history(): - tx_hash = tx['tx_hash'] - if tx['height']: - conf = self.wallet.blocks - tx['height'] + 1 - time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] - conf_icon = gtk.STOCK_APPLY - else: - conf = 0 - time_str = 'pending' - conf_icon = gtk.STOCK_EXECUTE - v = tx['value'] - balance += v - label = self.wallet.labels.get(tx_hash) - is_default_label = (label == '') or (label is None) - if is_default_label: label = tx['default_label'] - tooltip = tx_hash + "\n%d confirmations"%conf - - # tx = self.wallet.tx_history.get(tx_hash) - details = "Transaction Details:\n\n" \ - + "Transaction ID:\n" + tx_hash + "\n\n" \ - + "Status: %d confirmations\n\n"%conf \ - + "Date: %s\n\n"%time_str \ - + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ - + "Outputs:\n-"+ '\n-'.join(tx['outputs']) - r = self.wallet.receipts.get(tx_hash) - if r: - details += "\n_______________________________________" \ - + '\n\nSigned URI: ' + r[2] \ - + "\n\nSigned by: " + r[0] \ - + '\n\nSignature: ' + r[1] - - - self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, - format_satoshis(v,True), format_satoshis(balance), tooltip, details] ) - if cursor: self.history_treeview.set_cursor( cursor ) - - - - def newaddress_dialog(self, w): - - title = "New Contact" - dialog = gtk.Dialog(title, parent=self.window, - flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, - buttons= ("cancel", 0, "ok",1) ) - dialog.show() - - label = gtk.HBox() - label_label = gtk.Label('Label:') - label_label.set_size_request(120,10) - label_label.show() - label.pack_start(label_label) - label_entry = gtk.Entry() - label_entry.show() - label.pack_start(label_entry) - label.show() - dialog.vbox.pack_start(label, False, True, 5) - - address = gtk.HBox() - address_label = gtk.Label('Address:') - address_label.set_size_request(120,10) - address_label.show() - address.pack_start(address_label) - address_entry = gtk.Entry() - address_entry.show() - address.pack_start(address_entry) - address.show() - dialog.vbox.pack_start(address, False, True, 5) - - result = dialog.run() - address = address_entry.get_text() - label = label_entry.get_text() - dialog.destroy() - - if result == 1: - if self.wallet.is_valid(address): - self.wallet.addressbook.append(address) - if label: self.wallet.labels[address] = label - self.wallet.save() - self.update_sending_tab() - else: - errorDialog = gtk.MessageDialog( - parent=self.window, - flags=gtk.DIALOG_MODAL, - buttons= gtk.BUTTONS_CLOSE, - message_format = "Invalid address") - errorDialog.show() - errorDialog.run() - errorDialog.destroy() - - - -class ElectrumGui(): - - def __init__(self, wallet): - self.wallet = wallet - - def main(self, url=None): - ew = ElectrumWindow(self.wallet) - if url: ew.set_url(url) - gtk.main() - - def restore_or_create(self): - return restore_create_dialog(self.wallet) diff --git a/client/gui_qt.py b/client/gui_qt.py @@ -1,1059 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2012 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 sys, time, datetime, re - -# todo: see PySide - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -import PyQt4.QtCore as QtCore -import PyQt4.QtGui as QtGui -from interface import DEFAULT_SERVERS - -try: - import icons_rc -except: - print "Could not import icons_rp.py" - print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'" - sys.exit(1) - -from wallet import format_satoshis -from decimal import Decimal - -import platform -MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' - - -def numbify(entry, is_int = False): - text = unicode(entry.text()).strip() - chars = '0123456789' - if not is_int: chars +='.' - s = ''.join([i for i in text if i in chars]) - if not is_int: - if '.' in s: - p = s.find('.') - s = s.replace('.','') - s = s[:p] + '.' + s[p:p+8] - try: - amount = int( Decimal(s) * 100000000 ) - except: - amount = None - else: - try: - amount = int( s ) - except: - amount = None - entry.setText(s) - return amount - - -class Timer(QtCore.QThread): - def run(self): - while True: - self.emit(QtCore.SIGNAL('timersignal')) - time.sleep(0.5) - -class EnterButton(QPushButton): - def __init__(self, text, func): - QPushButton.__init__(self, text) - self.func = func - self.clicked.connect(func) - - def keyPressEvent(self, e): - if e.key() == QtCore.Qt.Key_Return: - apply(self.func,()) - -class StatusBarButton(QPushButton): - def __init__(self, icon, tooltip, func): - QPushButton.__init__(self, icon, '') - self.setToolTip(tooltip) - self.setFlat(True) - self.setMaximumWidth(25) - self.clicked.connect(func) - self.func = func - - def keyPressEvent(self, e): - if e.key() == QtCore.Qt.Key_Return: - apply(self.func,()) - - -class QRCodeWidget(QWidget): - - def __init__(self, addr): - import pyqrnative - super(QRCodeWidget, self).__init__() - self.addr = addr - self.setGeometry(300, 300, 350, 350) - self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H) - self.qr.addData(addr) - self.qr.make() - - def paintEvent(self, e): - qp = QtGui.QPainter() - qp.begin(self) - boxsize = 7 - size = self.qr.getModuleCount()*boxsize - k = self.qr.getModuleCount() - black = QColor(0, 0, 0, 255) - white = QColor(255, 255, 255, 255) - for r in range(k): - for c in range(k): - if self.qr.isDark(r, c): - qp.setBrush(black) - qp.setPen(black) - else: - qp.setBrush(white) - qp.setPen(white) - qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize) - qp.end() - - - -def ok_cancel_buttons(dialog): - hbox = QHBoxLayout() - hbox.addStretch(1) - b = QPushButton("OK") - hbox.addWidget(b) - b.clicked.connect(dialog.accept) - b = QPushButton("Cancel") - hbox.addWidget(b) - b.clicked.connect(dialog.reject) - return hbox - - -class ElectrumWindow(QMainWindow): - - def __init__(self, wallet): - QMainWindow.__init__(self) - self.wallet = wallet - self.wallet.gui_callback = self.update_callback - - self.funds_error = False - - self.tabs = tabs = QTabWidget(self) - tabs.addTab(self.create_history_tab(), 'History') - tabs.addTab(self.create_send_tab(), 'Send') - tabs.addTab(self.create_receive_tab(), 'Receive') - tabs.addTab(self.create_contacts_tab(),'Contacts') - tabs.addTab(self.create_wall_tab(), 'Wall') - tabs.setMinimumSize(600, 400) - tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.setCentralWidget(tabs) - self.create_status_bar() - self.setGeometry(100,100,840,400) - self.setWindowTitle( 'Electrum ' + self.wallet.electrum_version ) - self.show() - - QShortcut(QKeySequence("Ctrl+W"), self, self.close) - QShortcut(QKeySequence("Ctrl+Q"), self, self.close) - QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() )) - QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() )) - - self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet) - - - def connect_slots(self, sender): - self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient) - self.previous_payto_e='' - - def check_recipient(self): - if self.payto_e.hasFocus(): - return - r = unicode( self.payto_e.text() ) - if r != self.previous_payto_e: - self.previous_payto_e = r - r = r.strip() - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): - try: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - except: - return - if to_address: - s = r + ' <' + to_address + '>' - self.payto_e.setText(s) - - - def update_callback(self): - self.emit(QtCore.SIGNAL('updatesignal')) - - def update_wallet(self): - if self.wallet.interface.is_connected: - if self.wallet.blocks == -1: - text = "Connecting..." - icon = QIcon(":icons/status_disconnected.png") - elif self.wallet.blocks == 0: - text = "Server not ready" - icon = QIcon(":icons/status_disconnected.png") - elif not self.wallet.up_to_date: - text = "Synchronizing..." - icon = QIcon(":icons/status_waiting.png") - else: - c, u = self.wallet.get_balance() - text = "Balance: %s "%( format_satoshis(c) ) - if u: text += "[%s unconfirmed]"%( format_satoshis(u,True).strip() ) - 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.statusBar().showMessage(text) - self.status_button.setIcon( icon ) - - if self.wallet.up_to_date: - self.textbox.setText( self.wallet.banner ) - self.update_history_tab() - self.update_receive_tab() - self.update_contacts_tab() - - - def create_history_tab(self): - self.history_list = w = QTreeWidget(self) - #print w.getContentsMargins() - w.setColumnCount(5) - w.setColumnWidth(0, 40) - w.setColumnWidth(1, 140) - w.setColumnWidth(2, 350) - w.setColumnWidth(3, 140) - w.setColumnWidth(4, 140) - w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] ) - self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details) - self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked) - self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed) - return w - - def tx_details(self, item, column): - tx_hash = str(item.toolTip(0)) - tx = self.wallet.tx_history.get(tx_hash) - - if tx['height']: - conf = self.wallet.blocks - tx['height'] + 1 - time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] - else: - conf = 0 - time_str = 'pending' - - tx_details = "Transaction Details:\n\n" \ - + "Transaction ID:\n" + tx_hash + "\n\n" \ - + "Status: %d confirmations\n\n"%conf \ - + "Date: %s\n\n"%time_str \ - + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ - + "Outputs:\n-"+ '\n-'.join(tx['outputs']) - - r = self.wallet.receipts.get(tx_hash) - if r: - tx_details += "\n_______________________________________" \ - + '\n\nSigned URI: ' + r[2] \ - + "\n\nSigned by: " + r[0] \ - + '\n\nSignature: ' + r[1] - - QMessageBox.information(self, 'Details', tx_details, 'OK') - - - 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) - self.is_edit=False - - def tx_label_changed(self, item, column): - if self.is_edit: - return - self.is_edit=True - tx_hash = str(item.toolTip(0)) - tx = self.wallet.tx_history.get(tx_hash) - s = self.wallet.labels.get(tx_hash) - text = unicode( item.text(2) ) - if text: - self.wallet.labels[tx_hash] = text - item.setForeground(2, QBrush(QColor('black'))) - else: - if s: self.wallet.labels.pop(tx_hash) - text = tx['default_label'] - item.setText(2, text) - item.setForeground(2, QBrush(QColor('gray'))) - self.is_edit=False - - def address_label_clicked(self, item, column, l): - if column==1 and item.isSelected(): - addr = unicode( item.text(0) ) - if addr in map(lambda x:x[1], self.wallet.aliases.values()): - return - item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - l.editItem( item, column ) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - - def address_label_changed(self, item, column, l): - addr = unicode( item.text(0) ) - text = unicode( item.text(1) ) - if text: - self.wallet.labels[addr] = text - else: - s = self.wallet.labels.get(addr) - if s: self.wallet.labels.pop(addr) - self.update_history_tab() - - def update_history_tab(self): - self.history_list.clear() - balance = 0 - for tx in self.wallet.get_tx_history(): - tx_hash = tx['tx_hash'] - if tx['height']: - conf = self.wallet.blocks - tx['height'] + 1 - time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] - icon = QIcon(":icons/confirmed.png") - else: - conf = 0 - time_str = 'pending' - icon = QIcon(":icons/unconfirmed.png") - v = tx['value'] - balance += v - label = self.wallet.labels.get(tx_hash) - is_default_label = (label == '') or (label is None) - if is_default_label: label = tx['default_label'] - - item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True), format_satoshis(balance)] ) - item.setFont(2, QFont(MONOSPACE_FONT)) - item.setFont(3, QFont(MONOSPACE_FONT)) - item.setFont(4, QFont(MONOSPACE_FONT)) - item.setToolTip(0, tx_hash) - if is_default_label: - item.setForeground(2, QBrush(QColor('grey'))) - - item.setIcon(0, icon) - self.history_list.insertTopLevelItem(0,item) - - - def create_send_tab(self): - w = QWidget() - - grid = QGridLayout() - grid.setSpacing(8) - grid.setColumnMinimumWidth(3,300) - grid.setColumnStretch(4,1) - - self.payto_e = QLineEdit() - grid.addWidget(QLabel('Pay to'), 1, 0) - grid.addWidget(self.payto_e, 1, 1, 1, 3) - - self.message_e = QLineEdit() - grid.addWidget(QLabel('Description'), 2, 0) - grid.addWidget(self.message_e, 2, 1, 1, 3) - - self.amount_e = QLineEdit() - grid.addWidget(QLabel('Amount'), 3, 0) - grid.addWidget(self.amount_e, 3, 1, 1, 2) - - self.fee_e = QLineEdit() - grid.addWidget(QLabel('Fee'), 4, 0) - grid.addWidget(self.fee_e, 4, 1, 1, 2) - - b = EnterButton("Send", self.do_send) - grid.addWidget(b, 5, 1) - - b = EnterButton("Clear",self.do_clear) - grid.addWidget(b, 5, 2) - - self.payto_sig = QLabel('') - grid.addWidget(self.payto_sig, 6, 0, 1, 4) - - w.setLayout(grid) - w.show() - - w2 = QWidget() - vbox = QVBoxLayout() - vbox.addWidget(w) - vbox.addStretch(1) - w2.setLayout(vbox) - - def entry_changed( is_fee ): - self.funds_error = False - amount = numbify(self.amount_e) - fee = numbify(self.fee_e) - if not is_fee: fee = None - if amount is None: - return - inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) - if not is_fee: - self.fee_e.setText( str( Decimal( fee ) / 100000000 ) ) - if inputs: - palette = QPalette() - palette.setColor(self.amount_e.foregroundRole(), QColor('black')) - else: - palette = QPalette() - palette.setColor(self.amount_e.foregroundRole(), QColor('red')) - self.funds_error = True - self.amount_e.setPalette(palette) - self.fee_e.setPalette(palette) - - self.amount_e.textChanged.connect(lambda: entry_changed(False) ) - self.fee_e.textChanged.connect(lambda: entry_changed(True) ) - - return w2 - - def do_send(self): - - label = unicode( self.message_e.text() ) - r = unicode( self.payto_e.text() ) - r = r.strip() - - m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) - m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) - - if m1: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - if not to_address: - return - elif m2: - to_address = m2.group(5) - else: - to_address = r - - if not self.wallet.is_valid(to_address): - QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK') - return - - try: - amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 ) - except: - QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK') - return - try: - fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 ) - except: - QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK') - return - - if self.wallet.use_encryption: - password = self.password_dialog() - if not password: - return - else: - password = None - - try: - tx = self.wallet.mktx( to_address, amount, label, password, fee ) - except BaseException, e: - self.show_message(e.message) - return - - status, msg = self.wallet.sendtx( tx ) - if status: - QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK') - self.do_clear() - self.update_contacts_tab() - else: - QMessageBox.warning(self, 'Error', msg, 'OK') - - - def set_url(self, url): - payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) - self.tabs.setCurrentIndex(1) - self.payto_e.setText(payto) - self.message_e.setText(message) - self.amount_e.setText(amount) - if identity: - self.set_frozen(self.payto_e,True) - self.set_frozen(self.amount_e,True) - self.set_frozen(self.message_e,True) - self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity ) - else: - self.payto_sig.setVisible(False) - - def do_clear(self): - self.payto_sig.setVisible(False) - for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]: - e.setText('') - self.set_frozen(e,False) - - def set_frozen(self,entry,frozen): - if frozen: - entry.setReadOnly(True) - entry.setFrame(False) - palette = QPalette() - palette.setColor(entry.backgroundRole(), QColor('lightgray')) - entry.setPalette(palette) - else: - entry.setReadOnly(False) - entry.setFrame(True) - palette = QPalette() - palette.setColor(entry.backgroundRole(), QColor('white')) - entry.setPalette(palette) - - - def make_address_list(self, is_recv): - - l = QTreeWidget(self) - l.setColumnCount(3) - l.setColumnWidth(0, 350) - l.setColumnWidth(1, 330) - l.setColumnWidth(2, 20) - l.setHeaderLabels( ['Address', 'Label','Tx']) - - vbox = QVBoxLayout() - vbox.setMargin(0) - vbox.setSpacing(0) - vbox.addWidget(l) - - hbox = QHBoxLayout() - hbox.setMargin(0) - hbox.setSpacing(0) - - def get_addr(l): - i = l.currentItem() - if not i: return - addr = unicode( i.text(0) ) - return addr - - qrButton = EnterButton("QR",lambda: ElectrumWindow.showqrcode(get_addr(l))) - - def copy2clipboard(addr): - self.app.clipboard().setText(addr) - copyButton = EnterButton("Copy to Clipboard", lambda: copy2clipboard(get_addr(l))) - hbox.addWidget(qrButton) - hbox.addWidget(copyButton) - if not is_recv: - addButton = EnterButton("New", self.newaddress_dialog) - hbox.addWidget(addButton) - def payto(addr): - if not addr:return - self.tabs.setCurrentIndex(1) - self.payto_e.setText(addr) - self.amount_e.setFocus() - paytoButton = EnterButton('Pay to', lambda: payto(get_addr(l))) - hbox.addWidget(paytoButton) - hbox.addStretch(1) - buttons = QWidget() - buttons.setLayout(hbox) - vbox.addWidget(buttons) - - w = QWidget() - w.setLayout(vbox) - return w, l - - def create_receive_tab(self): - w, l = self.make_address_list(True) - self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) - self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) - self.receive_list = l - return w - - def create_contacts_tab(self): - w, l = self.make_address_list(False) - self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) - self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) - self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details) - self.contacts_list = l - return w - - def update_receive_tab(self): - self.receive_list.clear() - for address in self.wallet.all_addresses(): - if self.wallet.is_change(address):continue - label = self.wallet.labels.get(address,'') - n = 0 - h = self.wallet.history.get(address,[]) - for item in h: - if not item['is_input'] : n=n+1 - tx = "None" if n==0 else "%d"%n - item = QTreeWidgetItem( [ address, label, tx] ) - item.setFont(0, QFont(MONOSPACE_FONT)) - self.receive_list.addTopLevelItem(item) - - def show_contact_details(self, item, column): - m = unicode(item.text(0)) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] - QMessageBox.information(self, 'Alias', msg, 'OK') - - def update_contacts_tab(self): - self.contacts_list.clear() - for alias, v in self.wallet.aliases.items(): - s, target = v - item = QTreeWidgetItem( [ target, alias, '-'] ) - self.contacts_list.addTopLevelItem(item) - - for address in self.wallet.addressbook: - label = self.wallet.labels.get(address,'') - n = 0 - for item in self.wallet.tx_history.values(): - if address in item['outputs'] : n=n+1 - tx = "None" if n==0 else "%d"%n - item = QTreeWidgetItem( [ address, label, tx] ) - item.setFont(0, QFont(MONOSPACE_FONT)) - self.contacts_list.addTopLevelItem(item) - - - def create_wall_tab(self): - self.textbox = textbox = QTextEdit(self) - textbox.setFont(QFont(MONOSPACE_FONT)) - textbox.setReadOnly(True) - return textbox - - def create_status_bar(self): - sb = QStatusBar() - sb.setFixedHeight(35) - 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/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 newaddress_dialog(self): - text, ok = QInputDialog.getText(self, 'New Contact', 'Address:') - address = unicode(text) - if ok: - if self.wallet.is_valid(address): - self.wallet.addressbook.append(address) - self.wallet.save() - self.update_contacts_tab() - else: - QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK') - - @staticmethod - def show_seed_dialog(wallet, parent=None): - import mnemonic - if wallet.use_encryption: - password = parent.password_dialog() - if not password: return - else: - password = None - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - QMessageBox.warning(parent, 'Error', 'Invalid Password', 'OK') - return - - msg = "Your wallet generation seed is:\n\n" + seed \ - + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \ - + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" \ - + ' '.join(mnemonic.mn_encode(seed)) + "\"" - - QMessageBox.information(parent, 'Seed', msg, 'OK') - ElectrumWindow.showqrcode(seed) - - @staticmethod - def showqrcode(address): - if not address: return - d = QDialog(None) - d.setModal(1) - d.setWindowTitle(address) - d.setMinimumSize(270, 300) - vbox = QVBoxLayout() - vbox.addWidget(QRCodeWidget(address)) - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - d.exec_() - - def question(self, msg): - return QMessageBox.question(self, 'Message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes - - def show_message(self, msg): - QMessageBox.information(self, 'Message', msg, 'OK') - - def password_dialog(self ): - d = QDialog(self) - d.setModal(1) - - pw = QLineEdit() - pw.setEchoMode(2) - - vbox = QVBoxLayout() - msg = 'Please enter your password' - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - grid.addWidget(QLabel('Password'), 1, 0) - grid.addWidget(pw, 1, 1) - vbox.addLayout(grid) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - return unicode(pw.text()) - - @staticmethod - def change_password_dialog( wallet, parent=None ): - d = QDialog(parent) - d.setModal(1) - - pw = QLineEdit() - pw.setEchoMode(2) - new_pw = QLineEdit() - new_pw.setEchoMode(2) - conf_pw = QLineEdit() - conf_pw.setEchoMode(2) - - vbox = QVBoxLayout() - if parent: - msg = 'Your wallet is encrypted. Use this dialog to change your password.\nTo disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' - else: - msg = "Please choose a password to encrypt your wallet keys.\nLeave these fields empty if you want to disable encryption." - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - - if wallet.use_encryption: - grid.addWidget(QLabel('Password'), 1, 0) - grid.addWidget(pw, 1, 1) - - grid.addWidget(QLabel('New Password'), 2, 0) - grid.addWidget(new_pw, 2, 1) - - grid.addWidget(QLabel('Confirm Password'), 3, 0) - grid.addWidget(conf_pw, 3, 1) - vbox.addLayout(grid) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - - password = unicode(pw.text()) if wallet.use_encryption else None - new_password = unicode(new_pw.text()) - new_password2 = unicode(conf_pw.text()) - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - QMessageBox.warning(parent, 'Error', 'Incorrect Password', 'OK') - return - - if new_password != new_password2: - QMessageBox.warning(parent, 'Error', 'Passwords do not match', 'OK') - return - - wallet.update_password(seed, new_password) - - @staticmethod - def seed_dialog(wallet, parent=None): - d = QDialog(parent) - d.setModal(1) - - vbox = QVBoxLayout() - msg = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - - seed_e = QLineEdit() - grid.addWidget(QLabel('Seed or mnemonic'), 1, 0) - grid.addWidget(seed_e, 1, 1) - - gap_e = QLineEdit() - gap_e.setText("5") - grid.addWidget(QLabel('Gap limit'), 2, 0) - grid.addWidget(gap_e, 2, 1) - gap_e.textChanged.connect(lambda: numbify(gap_e,True)) - vbox.addLayout(grid) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - - try: - gap = int(unicode(gap_e.text())) - except: - QMessageBox.warning(None, 'Error', 'error', 'OK') - sys.exit(0) - - try: - seed = unicode(seed_e.text()) - seed.decode('hex') - except: - import mnemonic - print "not hex, trying decode" - try: - seed = mnemonic.mn_decode( seed.split(' ') ) - except: - QMessageBox.warning(None, 'Error', 'I cannot decode this', 'OK') - sys.exit(0) - if not seed: - QMessageBox.warning(None, 'Error', 'no seed', 'OK') - sys.exit(0) - - wallet.seed = str(seed) - #print repr(wallet.seed) - wallet.gap_limit = gap - return True - - - def settings_dialog(self): - d = QDialog(self) - d.setModal(1) - - vbox = QVBoxLayout() - - msg = 'Here are the settings of your wallet.' - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - - fee_e = QLineEdit() - fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) ) - grid.addWidget(QLabel('Fee per tx. input'), 2, 0) - grid.addWidget(fee_e, 2, 1) - vbox.addLayout(grid) - fee_e.textChanged.connect(lambda: numbify(fee_e,False)) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - - fee = unicode(fee_e.text()) - try: - fee = int( 100000000 * Decimal(fee) ) - except: - QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK') - return - - self.wallet.fee = fee - self.wallet.save() - - @staticmethod - def network_dialog(wallet, parent=None): - interface = wallet.interface - if parent: - if interface.is_connected: - status = "Connected to %s:%d\n%d blocks\nresponse time: %f"%(interface.host, interface.port, wallet.blocks, interface.rtime) - else: - status = "Not connected" - server = wallet.server - else: - import random - status = "Please choose a server." - server = random.choice( DEFAULT_SERVERS ) - - plist = {} - for item in wallet.interface.servers: - host, pp = item - z = {} - for item2 in pp: - protocol, port = item2 - z[protocol] = port - plist[host] = z - - d = QDialog(parent) - d.setModal(1) - d.setWindowTitle('Server') - d.setMinimumSize(375, 20) - - vbox = QVBoxLayout() - vbox.setSpacing(20) - - hbox = QHBoxLayout() - l = QLabel() - l.setPixmap(QPixmap(":icons/network.png")) - hbox.addWidget(l) - hbox.addWidget(QLabel(status)) - - vbox.addLayout(hbox) - - hbox = QHBoxLayout() - host_line = QLineEdit() - host_line.setText(server) - hbox.addWidget(QLabel('Connect to:')) - hbox.addWidget(host_line) - vbox.addLayout(hbox) - - hbox = QHBoxLayout() - - buttonGroup = QGroupBox("protocol") - radio1 = QRadioButton("tcp", buttonGroup) - radio2 = QRadioButton("http", buttonGroup) - radio3 = QRadioButton("native", buttonGroup) - - def current_line(): - return unicode(host_line.text()).split(':') - - def set_button(protocol): - if protocol == 't': - radio1.setChecked(1) - elif protocol == 'h': - radio2.setChecked(1) - elif protocol == 'n': - radio3.setChecked(1) - - def set_protocol(protocol): - host = current_line()[0] - pp = plist[host] - if protocol not in pp.keys(): - protocol = pp.keys()[0] - set_button(protocol) - port = pp[protocol] - host_line.setText( host + ':' + port + ':' + protocol) - - radio1.clicked.connect(lambda x: set_protocol('t') ) - radio2.clicked.connect(lambda x: set_protocol('h') ) - radio3.clicked.connect(lambda x: set_protocol('n') ) - - set_button(current_line()[2]) - - hbox.addWidget(QLabel('Protocol:')) - hbox.addWidget(radio1) - hbox.addWidget(radio2) - hbox.addWidget(radio3) - - vbox.addLayout(hbox) - - if wallet.interface.servers: - servers_list = QTreeWidget(parent) - servers_list.setHeaderLabels( [ 'Active servers'] ) - servers_list.setMaximumHeight(150) - for host in plist.keys(): - servers_list.addTopLevelItem(QTreeWidgetItem( [ host ] )) - - def do_set_line(x): - host = unicode(x.text(0)) - pp = plist[host] - if 't' in pp.keys(): - protocol = 't' - else: - protocol = pp.keys()[0] - port = pp[protocol] - host_line.setText( host + ':' + port + ':' + protocol) - set_button(protocol) - - servers_list.connect(servers_list, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line) - vbox.addWidget(servers_list) - else: - hbox = QHBoxLayout() - hbox.addWidget(QLabel('No nodes available')) - b = EnterButton("Find nodes", lambda: wallet.interface.get_servers(wallet) ) - hbox.addWidget(b) - vbox.addLayout(hbox) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - server = unicode( host_line.text() ) - - try: - wallet.set_server(server) - except: - QMessageBox.information(None, 'Error', 'error', 'OK') - if parent == None: - sys.exit(1) - else: - return - - return True - - - -class ElectrumGui(): - - def __init__(self, wallet): - self.wallet = wallet - self.app = QApplication(sys.argv) - - def waiting_dialog(self): - - s = Timer() - s.start() - w = QDialog() - w.resize(200, 70) - w.setWindowTitle('Electrum') - l = QLabel('') - vbox = QVBoxLayout() - vbox.addWidget(l) - w.setLayout(vbox) - w.show() - def f(): - if self.wallet.up_to_date: w.close() - else: - l.setText("Please wait...\nGenerating addresses: %d"%len(self.wallet.all_addresses())) - pass - w.connect(s, QtCore.SIGNAL('timersignal'), f) - self.wallet.interface.poke() - w.exec_() - w.destroy() - - - def restore_or_create(self): - - msg = "Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" - r = QMessageBox.question(None, 'Message', msg, 'create', 'restore', 'cancel', 0, 2) - if r==2: return False - - is_recovery = (r==1) - wallet = self.wallet - # ask for the server. - if not ElectrumWindow.network_dialog( wallet, parent=None ): return False - - if not is_recovery: - wallet.new_seed(None) - wallet.init_mpk( wallet.seed ) - wallet.up_to_date_event.clear() - wallet.up_to_date = False - self.waiting_dialog() - # run a dialog indicating the seed, ask the user to remember it - ElectrumWindow.show_seed_dialog(wallet) - #ask for password - ElectrumWindow.change_password_dialog(wallet) - else: - # ask for seed and gap. - if not ElectrumWindow.seed_dialog( wallet ): return False - wallet.init_mpk( wallet.seed ) - wallet.up_to_date_event.clear() - wallet.up_to_date = False - self.waiting_dialog() - if wallet.is_found(): - # history and addressbook - wallet.update_tx_history() - wallet.fill_addressbook() - print "recovery successful" - wallet.save() - else: - QMessageBox.information(None, 'Message', "No transactions found for this seed", 'OK') - - wallet.save() - return True - - def main(self,url): - s = Timer() - s.start() - w = ElectrumWindow(self.wallet) - if url: w.set_url(url) - w.app = self.app - w.connect_slots(s) - w.update_wallet() - - self.app.exec_() diff --git a/client/interface.py b/client/interface.py @@ -1,487 +0,0 @@ -#!/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 random, socket, ast, re -import threading, traceback, sys, time, json, Queue - -DEFAULT_TIMEOUT = 5 -DEFAULT_SERVERS = [ 'ecdsa.org:50001:t', 'electrum.novit.ro:50001:t', 'electrum.bitcoins.sk:50001:t'] # list of default servers - - -def old_to_new(s): - s = s.replace("'blk_hash'", "'block_hash'") - s = s.replace("'pos'", "'index'") - s = s.replace("'nTime'", "'timestamp'") - s = s.replace("'is_in'", "'is_input'") - s = s.replace("'raw_scriptPubKey'","'raw_output_script'") - return s - - -class Interface(threading.Thread): - def __init__(self, host, port): - threading.Thread.__init__(self) - self.daemon = True - self.host = host - self.port = port - - self.servers = [] # actual list from IRC - self.rtime = 0 - - self.is_connected = True - self.poll_interval = 1 - - #json - self.message_id = 0 - self.responses = Queue.Queue() - - def poke(self): - # push a fake response so that the getting thread exits its loop - self.responses.put(None) - - def queue_json_response(self, c): - #print repr(c) - msg_id = c.get('id') - result = c.get('result') - error = c.get('error') - params = c.get('params',[]) - method = c.get('method',None) - if not method: - return - - if error: - print "received error:", c, method, params - else: - self.responses.put({'method':method, 'params':params, 'result':result}) - - - def subscribe(self, addresses): - messages = [] - for addr in addresses: - messages.append(('blockchain.address.subscribe', [addr])) - self.send(messages) - - - def get_servers(self, wallet): - # loop over default servers - # requesting servers could be an independent process - addresses = wallet.all_addresses() - version = wallet.electrum_version - - for server in DEFAULT_SERVERS: - print "connecting to", server - try: - self.host = server - self.start_session(addresses, version) - wallet.host = self.host - break - except socket.timeout: - continue - except socket.error: - continue - except: - traceback.print_exc(file=sys.stdout) - - - def start_session(self, addresses, version): - #print "Starting new session: %s:%d"%(self.host,self.port) - self.send([('server.version', [version]), ('server.banner',[]), ('blockchain.numblocks.subscribe',[]), ('server.peers.subscribe',[])]) - self.subscribe(addresses) - - -class PollingInterface(Interface): - """ non-persistent connection. synchronous calls""" - - def __init__(self, host, port): - Interface.__init__(self, host, port) - self.session_id = None - - def get_history(self, address): - self.send([('blockchain.address.get_history', [address] )]) - - def poll(self): - pass - #if is_new or wallet.remote_url: - # self.was_updated = True - # is_new = wallet.synchronize() - # wallet.update_tx_history() - # wallet.save() - # return is_new - #else: - # return False - - def run(self): - self.is_connected = True - while self.is_connected: - try: - if self.session_id: - self.poll() - time.sleep(self.poll_interval) - except socket.gaierror: - break - except socket.error: - break - except: - traceback.print_exc(file=sys.stdout) - break - - self.is_connected = False - self.poke() - - - - - - -class NativeInterface(PollingInterface): - - def start_session(self, addresses, version): - self.send([('session.new', [ version, addresses ])] ) - self.send([('server.peers.subscribe',[])]) - - def poll(self): - self.send([('session.poll', [])]) - - def send(self, messages): - import time - cmds = {'session.new':'new_session', - 'server.peers.subscribe':'peers', - 'session.poll':'poll', - 'blockchain.transaction.broadcast':'tx', - 'blockchain.address.get_history':'h', - 'blockchain.address.subscribe':'address.subscribe' - } - - for m in messages: - method, params = m - cmd = cmds[method] - - if cmd == 'poll': - params = self.session_id - - if cmd == 'address.subscribe': - params = [ self.session_id ] + params - - if cmd in ['h', 'tx']: - str_params = params[0] - elif type(params) != type(''): - str_params = repr( params ) - else: - str_params = params - t1 = time.time() - request = repr ( (cmd, str_params) ) + "#" - s = socket.socket( socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(DEFAULT_TIMEOUT) - s.connect(( self.host, self.port) ) - s.send( request ) - out = '' - while 1: - msg = s.recv(1024) - if msg: out += msg - else: break - s.close() - self.rtime = time.time() - t1 - self.is_connected = True - - if cmd == 'h': - out = old_to_new(out) - - if cmd in ['peers','h','poll']: - out = ast.literal_eval( out ) - - if out == '': - out = None - - if cmd == 'new_session': - self.session_id, msg = ast.literal_eval( out ) - self.responses.put({'method':'server.banner', 'params':[], 'result':msg}) - else: - self.responses.put({'method':method, 'params':params, 'result':out}) - - - - -class HttpStratumInterface(PollingInterface): - - def poll(self): - self.send([]) - - def send(self, messages): - import urllib2, json, time, cookielib - - cj = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) - urllib2.install_opener(opener) - - t1 = time.time() - - data = [] - for m in messages: - method, params = m - if type(params) != type([]): params = [params] - data.append( { 'method':method, 'id':self.message_id, 'params':params } ) - self.message_id += 1 - - if data: - data_json = json.dumps(data) - else: - # poll with GET - data_json = None - - host = 'http://%s:%d'%( self.host, self.port ) - headers = {'content-type': 'application/json'} - if self.session_id: - headers['cookie'] = 'SESSION=%s'%self.session_id - - req = urllib2.Request(host, data_json, headers) - response_stream = urllib2.urlopen(req) - - for index, cookie in enumerate(cj): - if cookie.name=='SESSION': - self.session_id = cookie.value - - response = response_stream.read() - if response: - response = json.loads( response ) - if type(response) is not type([]): - self.queue_json_response(response) - else: - for item in response: - self.queue_json_response(item) - - if response: - self.poll_interval = 1 - else: - if self.poll_interval < 15: - self.poll_interval += 1 - #print self.poll_interval, response - - self.rtime = time.time() - t1 - self.is_connected = True - - - - -class TcpStratumInterface(Interface): - """json-rpc over persistent TCP connection, asynchronous""" - - def __init__(self, host, port): - Interface.__init__(self, host, port) - self.s = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) - self.s.settimeout(5) - self.s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - try: - self.s.connect(( self.host, self.port)) - self.is_connected = True - except: - self.is_connected = False - print "not connected" - - def run(self): - try: - out = '' - while self.is_connected: - try: msg = self.s.recv(1024) - except socket.timeout: - continue - out += msg - if msg == '': - self.is_connected = False - print "disconnected." - - while True: - s = out.find('\n') - if s==-1: break - c = out[0:s] - out = out[s+1:] - c = json.loads(c) - self.queue_json_response(c) - - except: - traceback.print_exc(file=sys.stdout) - - self.is_connected = False - print "poking" - self.poke() - - def send(self, messages): - out = '' - for m in messages: - method, params = m - request = json.dumps( { 'id':self.message_id, 'method':method, 'params':params } ) - self.message_id += 1 - out += request + '\n' - self.s.send( out ) - - def get_history(self, addr): - self.send([('blockchain.address.get_history', [addr])]) - - - - - -class WalletSynchronizer(threading.Thread): - - def __init__(self, wallet, loop=False): - threading.Thread.__init__(self) - self.daemon = True - self.wallet = wallet - self.loop = loop - self.start_interface() - - - def handle_response(self, r): - if r is None: - return - - method = r['method'] - params = r['params'] - result = r['result'] - - if method == 'server.banner': - self.wallet.banner = result - self.wallet.was_updated = True - - elif method == 'session.poll': - # native poll - blocks, changed_addresses = result - if blocks == -1: raise BaseException("session not found") - self.wallet.blocks = int(blocks) - if changed_addresses: - self.wallet.was_updated = True - for addr, status in changed_addresses.items(): - self.wallet.receive_status_callback(addr, status) - - elif method == 'server.peers.subscribe': - servers = [] - for item in result: - s = [] - host = item[1] - ports = [] - if len(item)>2: - for v in item[2]: - if re.match("[thn]\d+",v): - ports.append((v[0],v[1:])) - #if not s: - # s.append(host+":50000:n") - #else: - # s.append(host+":50000:n") - if ports: - servers.append( (host, ports) ) - self.interface.servers = servers - - elif method == 'blockchain.address.subscribe': - addr = params[-1] - self.wallet.receive_status_callback(addr, result) - - elif method == 'blockchain.address.get_history': - addr = params[0] - self.wallet.receive_history_callback(addr, result) - self.wallet.was_updated = True - - elif method == 'blockchain.transaction.broadcast': - self.wallet.tx_result = result - self.wallet.tx_event.set() - - elif method == 'blockchain.numblocks.subscribe': - self.wallet.blocks = result - - elif method == 'server.version': - pass - - else: - print "unknown message:", method, params, result - - - def start_interface(self): - try: - host, port, protocol = self.wallet.server.split(':') - port = int(port) - except: - self.wallet.pick_random_server() - host, port, protocol = self.wallet.server.split(':') - port = int(port) - - #print protocol, host, port - if protocol == 'n': - InterfaceClass = NativeInterface - elif protocol == 't': - InterfaceClass = TcpStratumInterface - elif protocol == 'h': - InterfaceClass = HttpStratumInterface - else: - print "unknown protocol" - InterfaceClass = NativeInterface - - self.interface = InterfaceClass(host, port) - self.wallet.interface = self.interface - - with self.wallet.lock: - self.wallet.addresses_waiting_for_status = [] - self.wallet.addresses_waiting_for_history = [] - addresses = self.wallet.all_addresses() - version = self.wallet.electrum_version - for addr in addresses: - self.wallet.addresses_waiting_for_status.append(addr) - - try: - self.interface.start() - self.interface.start_session(addresses,version) - except: - self.interface.is_connected = False - - - def run(self): - import socket, time - while True: - while self.interface.is_connected: - new_addresses = self.wallet.synchronize() - if new_addresses: - self.interface.subscribe(new_addresses) - for addr in new_addresses: - with self.wallet.lock: - self.wallet.addresses_waiting_for_status.append(addr) - - if self.wallet.is_up_to_date(): - if not self.wallet.up_to_date: - self.wallet.up_to_date = True - self.wallet.was_updated = True - self.wallet.up_to_date_event.set() - else: - if self.wallet.up_to_date: - self.wallet.up_to_date = False - self.wallet.was_updated = True - - if self.wallet.was_updated: - self.wallet.gui_callback() - self.wallet.was_updated = False - - response = self.interface.responses.get() - self.handle_response(response) - - print "disconnected, gui callback" - self.wallet.gui_callback() - if self.loop: - time.sleep(5) - self.start_interface() - continue - else: - break - - - diff --git a/client/version.py b/client/version.py @@ -1,2 +0,0 @@ -ELECTRUM_VERSION = "0.43e" -SEED_VERSION = 4 # bump this everytime the seed generation is modified diff --git a/client/wallet.py b/client/wallet.py @@ -1,953 +0,0 @@ -#!/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 sys, base64, os, re, hashlib, copy, operator, ast, threading, random - -try: - import ecdsa - from ecdsa.util import string_to_number, number_to_string -except: - print "python-ecdsa does not seem to be installed. Try 'sudo easy_install ecdsa'" - sys.exit(1) - -try: - import aes -except: - print "AES does not seem to be installed. Try 'sudo easy_install slowaes'" - sys.exit(1) - - -############ functions from pywallet ##################### - -addrtype = 0 - -def hash_160(public_key): - try: - md = hashlib.new('ripemd160') - md.update(hashlib.sha256(public_key).digest()) - return md.digest() - except: - import ripemd - md = ripemd.new(hashlib.sha256(public_key).digest()) - return md.digest() - - -def public_key_to_bc_address(public_key): - h160 = hash_160(public_key) - return hash_160_to_bc_address(h160) - -def hash_160_to_bc_address(h160): - vh160 = chr(addrtype) + h160 - h = Hash(vh160) - addr = vh160 + h[0:4] - return b58encode(addr) - -def bc_address_to_hash_160(addr): - bytes = b58decode(addr, 25) - return bytes[1:21] - -__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -__b58base = len(__b58chars) - -def b58encode(v): - """ encode v, which is a string of bytes, to base58. - """ - - long_value = 0L - for (i, c) in enumerate(v[::-1]): - long_value += (256**i) * ord(c) - - result = '' - while long_value >= __b58base: - div, mod = divmod(long_value, __b58base) - result = __b58chars[mod] + result - long_value = div - result = __b58chars[long_value] + result - - # Bitcoin does a little leading-zero-compression: - # leading 0-bytes in the input become leading-1s - nPad = 0 - for c in v: - if c == '\0': nPad += 1 - else: break - - return (__b58chars[0]*nPad) + result - -def b58decode(v, length): - """ decode v into a string of len bytes - """ - long_value = 0L - for (i, c) in enumerate(v[::-1]): - long_value += __b58chars.find(c) * (__b58base**i) - - result = '' - while long_value >= 256: - div, mod = divmod(long_value, 256) - result = chr(mod) + result - long_value = div - result = chr(long_value) + result - - nPad = 0 - for c in v: - if c == __b58chars[0]: nPad += 1 - else: break - - result = chr(0)*nPad + result - if length is not None and len(result) != length: - return None - - return result - - -def Hash(data): - return hashlib.sha256(hashlib.sha256(data).digest()).digest() - -def EncodeBase58Check(vchIn): - hash = Hash(vchIn) - return b58encode(vchIn + hash[0:4]) - -def DecodeBase58Check(psz): - vchRet = b58decode(psz, None) - key = vchRet[0:-4] - csum = vchRet[-4:] - hash = Hash(key) - cs32 = hash[0:4] - if cs32 != csum: - return None - else: - return key - -def PrivKeyToSecret(privkey): - return privkey[9:9+32] - -def SecretToASecret(secret): - vchIn = chr(addrtype+128) + secret - return EncodeBase58Check(vchIn) - -def ASecretToSecret(key): - vch = DecodeBase58Check(key) - if vch and vch[0] == chr(addrtype+128): - return vch[1:] - else: - return False - -########### end pywallet functions ####################### - -# URL decode -_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) -urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) - - -def int_to_hex(i, length=1): - s = hex(i)[2:].rstrip('L') - s = "0"*(2*length - len(s)) + s - return s.decode('hex')[::-1].encode('hex') - - -# AES -EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s)) -DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e)) - - - -# secp256k1, http://www.oid-info.com/get/1.3.132.0.10 -_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL -_r = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L -_b = 0x0000000000000000000000000000000000000000000000000000000000000007L -_a = 0x0000000000000000000000000000000000000000000000000000000000000000L -_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L -_Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L -curve_secp256k1 = ecdsa.ellipticcurve.CurveFp( _p, _a, _b ) -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 ) - - -def filter(s): - out = re.sub('( [^\n]*|)\n','',s) - out = out.replace(' ','') - out = out.replace('\n','') - return out - -def raw_tx( inputs, outputs, for_sig = None ): - s = int_to_hex(1,4) + ' version\n' - s += int_to_hex( len(inputs) ) + ' number of inputs\n' - for i in range(len(inputs)): - _, _, p_hash, p_index, p_script, pubkey, sig = inputs[i] - s += p_hash.decode('hex')[::-1].encode('hex') + ' prev hash\n' - s += int_to_hex(p_index,4) + ' prev index\n' - if for_sig is 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: - script = p_script + ' scriptsig \n' - else: - script='' - s += int_to_hex( len(filter(script))/2 ) + ' script length \n' - s += script - s += "ffffffff" + ' sequence\n' - s += int_to_hex( len(outputs) ) + ' number of outputs\n' - for output in outputs: - addr, amount = output - s += int_to_hex( amount, 8) + ' amount: %d\n'%amount - script = '76a9' # op_dup, op_hash_160 - script += '14' # push 0x14 bytes - script += bc_address_to_hash_160(addr).encode('hex') - script += '88ac' # op_equalverify, op_checksig - s += int_to_hex( len(filter(script))/2 ) + ' script length \n' - s += script + ' script \n' - s += int_to_hex(0,4) # lock time - if for_sig is not None: s += int_to_hex(1, 4) # hash type - return s - - - - -def format_satoshis(x, is_diff=False): - from decimal import Decimal - s = str( Decimal(x) /100000000 ) - if is_diff and x>0: - s = "+" + s - if not '.' in s: s += '.' - p = s.find('.') - s += " "*( 9 - ( len(s) - p )) - s = " "*( 5 - ( p )) + s - return s - - -from version import ELECTRUM_VERSION, SEED_VERSION -from interface import DEFAULT_SERVERS - - - - -class Wallet: - def __init__(self, gui_callback = lambda: None): - - self.electrum_version = ELECTRUM_VERSION - self.seed_version = SEED_VERSION - self.gui_callback = gui_callback - - self.gap_limit = 5 # configuration - self.fee = 100000 - self.master_public_key = '' - - # saved fields - self.use_encryption = False - self.addresses = [] # receiving addresses visible for user - self.change_addresses = [] # addresses used as change - self.seed = '' # encrypted - self.history = {} - self.labels = {} # labels for addresses and transactions - self.aliases = {} # aliases for addresses - self.authorities = {} # trusted addresses - - self.receipts = {} # signed URIs - self.receipt = None # next receipt - self.addressbook = [] # outgoing addresses, for payments - - # not saved - self.tx_history = {} - - self.imported_keys = {} - self.remote_url = None - - self.was_updated = True - self.blocks = -1 - self.banner = '' - self.up_to_date_event = threading.Event() - self.up_to_date_event.clear() - self.up_to_date = False - self.lock = threading.Lock() - self.tx_event = threading.Event() - - # - self.addresses_waiting_for_status = [] - self.addresses_waiting_for_history = [] - self.pick_random_server() - - - - def pick_random_server(self): - self.server = random.choice( DEFAULT_SERVERS ) # random choice when the wallet is created - - def is_up_to_date(self): - return self.interface.responses.empty() and not ( self.addresses_waiting_for_status or self.addresses_waiting_for_history ) - - - def set_server(self, server): - # raise an error if the format isnt correct - a,b,c = server.split(':') - b = int(b) - assert c in ['t','h','n'] - # set the server - if server != self.server: - self.server = server - self.save() - self.interface.is_connected = False # this exits the polling loop - - def set_path(self, wallet_path): - - if wallet_path is not None: - self.path = wallet_path - else: - # backward compatibility: look for wallet file in the default data directory - if "HOME" in os.environ: - wallet_dir = os.path.join( os.environ["HOME"], '.electrum') - elif "LOCALAPPDATA" in os.environ: - wallet_dir = os.path.join( os.environ["LOCALAPPDATA"], 'Electrum' ) - elif "APPDATA" in os.environ: - wallet_dir = os.path.join( os.environ["APPDATA"], 'Electrum' ) - else: - raise BaseException("No home directory found in environment variables.") - - if not os.path.exists( wallet_dir ): os.mkdir( wallet_dir ) - self.path = os.path.join( wallet_dir, 'electrum.dat' ) - - def import_key(self, keypair, password): - address, key = keypair.split(':') - if not self.is_valid(address): return False - if address in self.all_addresses(): return False - b = ASecretToSecret( key ) - if not b: return False - 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() ): return False - self.imported_keys[address] = self.pw_encode( key, password ) - return True - - def new_seed(self, password): - seed = "%032x"%ecdsa.util.randrange( pow(2,128) ) - #self.init_mpk(seed) - # encrypt - self.seed = self.pw_encode( seed, password ) - - - def init_mpk(self,seed): - # public key - curve = SECP256k1 - secexp = self.stretch_key(seed) - master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) - self.master_public_key = master_private_key.get_verifying_key().to_string() - - def all_addresses(self): - return self.addresses + self.change_addresses + self.imported_keys.keys() - - def is_mine(self, address): - return address in self.all_addresses() - - def is_change(self, address): - return address in self.change_addresses - - def is_valid(self,addr): - ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z') - if not ADDRESS_RE.match(addr): return False - try: - h = bc_address_to_hash_160(addr) - except: - return False - return addr == hash_160_to_bc_address(h) - - def stretch_key(self,seed): - oldseed = seed - for i in range(100000): - seed = hashlib.sha256(seed + oldseed).digest() - return string_to_number( seed ) - - def get_sequence(self,n,for_change): - return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key ) ) - - 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 ) - b = ASecretToSecret( b ) - secexp = int( b.encode('hex'), 16) - else: - if address in self.addresses: - n = self.addresses.index(address) - for_change = False - elif address in self.change_addresses: - n = self.change_addresses.index(address) - for_change = True - else: - raise BaseException("unknown address") - try: - seed = self.pw_decode( self.seed, password) - except: - raise BaseException("Invalid password") - secexp = self.stretch_key(seed) - secexp = ( secexp + self.get_sequence(n,for_change) ) % order - - pk = number_to_string(secexp,order) - return pk - - 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 ) - 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 ) - try: - self.verify_message( address, sig, message) - return sig - except: - continue - else: - raise BaseException("error: cannot sign message") - - - def verify_message(self, address, signature, message): - """ See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """ - from ecdsa import numbertheory, ellipticcurve, util - import msqr - curve = curve_secp256k1 - G = generator_secp256k1 - order = G.order() - # extract r,s from signature - sig = base64.b64decode(signature) - if len(sig) != 65: raise BaseException("Wrong encoding") - r,s = util.sigdecode_string(sig[1:], order) - recid = ord(sig[0]) - 27 - # 1.1 - x = r + (recid/2) * order - # 1.3 - alpha = ( x * x * x + curve.a() * x + curve.b() ) % curve.p() - beta = msqr.modular_sqrt(alpha, curve.p()) - y = beta if (beta - recid) % 2 == 0 else curve.p() - beta - # 1.4 the constructor checks that nR is at infinity - R = ellipticcurve.Point(curve, x, y, order) - # 1.5 compute e from message: - h = Hash( self.msg_magic( message ) ) - e = string_to_number(h) - minus_e = -e % order - # 1.6 compute Q = r^-1 (sR - eG) - inv_r = numbertheory.inverse_mod(r,order) - Q = inv_r * ( s * R + minus_e * G ) - public_key = ecdsa.VerifyingKey.from_public_point( Q, curve = SECP256k1 ) - # check that Q is the public key - public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string) - # check that we get the original signing address - addr = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ) - # print addr - if address != addr: - print "bad signature" - raise BaseException("Bad signature") - - - def create_new_address(self, for_change): - """ Publickey(type,n) = Master_public_key + H(n|S|type)*point """ - curve = SECP256k1 - n = len(self.change_addresses) if for_change else len(self.addresses) - z = self.get_sequence(n,for_change) - master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key, curve = SECP256k1 ) - pubkey_point = master_public_key.pubkey.point + z*curve.generator - public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 ) - address = public_key_to_bc_address( '04'.decode('hex') + public_key2.to_string() ) - if for_change: - self.change_addresses.append(address) - else: - self.addresses.append(address) - - self.history[address] = [] - print address - return address - - - - def synchronize(self): - if not self.master_public_key: - return [] - - new_addresses = [] - while True: - if self.change_addresses == []: - new_addresses.append( self.create_new_address(True) ) - continue - a = self.change_addresses[-1] - if self.history.get(a): - new_addresses.append( self.create_new_address(True) ) - else: - break - - n = self.gap_limit - while True: - if len(self.addresses) < n: - new_addresses.append( self.create_new_address(False) ) - continue - if map( lambda a: self.history.get(a), self.addresses[-n:] ) == n*[[]]: - break - else: - new_addresses.append( self.create_new_address(False) ) - - if self.remote_url: - num = self.get_remote_number() - while len(self.addresses)<num: - new_addresses.append( self.create_new_address(False) ) - - return new_addresses - - - def get_remote_number(self): - import jsonrpclib - server = jsonrpclib.Server(self.remote_url) - out = server.getnum() - return out - - def get_remote_mpk(self): - import jsonrpclib - server = jsonrpclib.Server(self.remote_url) - out = server.getkey() - return out - - def is_found(self): - return (len(self.change_addresses) > 1 ) or ( len(self.addresses) > self.gap_limit ) - - def fill_addressbook(self): - for tx in self.tx_history.values(): - if tx['value']<0: - for i in tx['outputs']: - if not self.is_mine(i) and i not in self.addressbook: - self.addressbook.append(i) - # redo labels - self.update_tx_labels() - - - def save(self): - s = { - 'seed_version':self.seed_version, - 'use_encryption':self.use_encryption, - 'master_public_key': self.master_public_key.encode('hex'), - 'fee':self.fee, - 'server':self.server, - 'seed':self.seed, - 'addresses':self.addresses, - 'change_addresses':self.change_addresses, - 'history':self.history, - 'labels':self.labels, - 'contacts':self.addressbook, - 'imported_keys':self.imported_keys, - 'aliases':self.aliases, - 'authorities':self.authorities, - 'receipts':self.receipts, - } - f = open(self.path,"w") - f.write( repr(s) ) - f.close() - - def read(self): - import interface - - upgrade_msg = """This wallet seed is deprecated. Please run upgrade.py for a diagnostic.""" - self.file_exists = False - try: - f = open(self.path,"r") - data = f.read() - f.close() - except: - return - data = interface.old_to_new(data) - try: - d = ast.literal_eval( data ) - self.seed_version = d.get('seed_version') - self.master_public_key = d.get('master_public_key').decode('hex') - self.use_encryption = d.get('use_encryption') - self.fee = int( d.get('fee') ) - self.seed = d.get('seed') - self.server = d.get('server') - #blocks = d.get('blocks') - self.addresses = d.get('addresses') - self.change_addresses = d.get('change_addresses') - self.history = d.get('history') - self.labels = d.get('labels') - self.addressbook = d.get('contacts') - self.imported_keys = d.get('imported_keys',{}) - self.aliases = d.get('aliases',{}) - self.authorities = d.get('authorities',{}) - self.receipts = d.get('receipts',{}) - except: - raise BaseException("cannot read wallet file") - - self.update_tx_history() - - if self.seed_version != SEED_VERSION: - raise BaseException(upgrade_msg) - - if self.remote_url: assert self.master_public_key.encode('hex') == self.get_remote_mpk() - - self.file_exists = True - - - - - def get_addr_balance(self, addr): - if self.is_mine(addr): - h = self.history.get(addr) - else: - h = self.interface.retrieve_history(addr) - if not h: return 0,0 - c = u = 0 - for item in h: - v = item['value'] - if item['height']: - c += v - else: - u += v - return c, u - - def get_balance(self): - conf = unconf = 0 - for addr in self.all_addresses(): - c, u = self.get_addr_balance(addr) - conf += c - unconf += u - return conf, unconf - - - def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ): - """ todo: minimize tx size """ - total = 0 - fee = self.fee if fixed_fee is None else fixed_fee - - coins = [] - domain = [from_addr] if from_addr else self.all_addresses() - for addr in domain: - h = self.history.get(addr) - if h is None: continue - for item in h: - if item.get('raw_output_script'): - coins.append( (addr,item)) - - coins = sorted( coins, key = lambda x: x[1]['timestamp'] ) - inputs = [] - for c in coins: - addr, item = c - v = item.get('value') - total += v - inputs.append((addr, v, item['tx_hash'], item['index'], item['raw_output_script'], None, None) ) - fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee - if total >= amount + fee: break - else: - #print "not enough funds: %d %d"%(total, fee) - inputs = [] - return inputs, total, fee - - def choose_tx_outputs( self, to_addr, amount, fee, total, change_addr=None ): - outputs = [ (to_addr, amount) ] - change_amount = total - ( amount + fee ) - if change_amount != 0: - # normally, the update thread should ensure that the last change address is unused - if not change_addr: - change_addr = self.change_addresses[-1] - outputs.append( ( change_addr, change_amount) ) - return outputs - - def sign_inputs( self, inputs, outputs, password ): - 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 ) - public_key = private_key.get_verifying_key() - pubkey = public_key.to_string() - 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) - s_inputs.append( (addr, v, p_hash, p_pos, p_scriptPubKey, pubkey, sig) ) - return s_inputs - - def pw_encode(self, s, password): - if password: - secret = Hash(password) - return EncodeAES(secret, s) - else: - return s - - 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 BaseException("Invalid password") - return d - else: - return s - - def get_status(self, address): - h = self.history.get(address) - if not h: - status = None - else: - lastpoint = h[-1] - status = lastpoint['block_hash'] - if status == 'mempool': - status = status + ':%d'% len(h) - return status - - def receive_status_callback(self, addr, status): - with self.lock: - if self.get_status(addr) != status: - #print "updating status for", addr, status - self.addresses_waiting_for_history.append(addr) - self.interface.get_history(addr) - if addr in self.addresses_waiting_for_status: - self.addresses_waiting_for_status.remove(addr) - - def receive_history_callback(self, addr, data): - #print "updating history for", addr - with self.lock: - self.history[addr] = data - self.update_tx_history() - self.save() - if addr in self.addresses_waiting_for_history: self.addresses_waiting_for_history.remove(addr) - - def get_tx_history(self): - lines = self.tx_history.values() - lines = sorted(lines, key=operator.itemgetter("timestamp")) - return lines - - def update_tx_history(self): - self.tx_history= {} - for addr in self.all_addresses(): - h = self.history.get(addr) - if h is None: continue - for tx in h: - tx_hash = tx['tx_hash'] - line = self.tx_history.get(tx_hash) - if not line: - self.tx_history[tx_hash] = copy.copy(tx) - line = self.tx_history.get(tx_hash) - else: - line['value'] += tx['value'] - if line['height'] == 0: - line['timestamp'] = 1e12 - self.update_tx_labels() - - def update_tx_labels(self): - for tx in self.tx_history.values(): - default_label = '' - if tx['value']<0: - for o_addr in tx['outputs']: - if not self.is_change(o_addr): - dest_label = self.labels.get(o_addr) - if dest_label: - default_label = 'to: ' + dest_label - else: - default_label = 'to: ' + o_addr - else: - for o_addr in tx['outputs']: - if self.is_mine(o_addr) and not self.is_change(o_addr): - dest_label = self.labels.get(o_addr) - if dest_label: - default_label = 'at: ' + dest_label - else: - default_label = 'at: ' + o_addr - tx['default_label'] = default_label - - def mktx(self, to_address, amount, label, password, fee=None, change_addr=None, from_addr= None): - if not self.is_valid(to_address): - raise BaseException("Invalid address") - inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr ) - if not inputs: - raise BaseException("Not enough funds") - outputs = self.choose_tx_outputs( to_address, amount, fee, total, change_addr ) - s_inputs = self.sign_inputs( inputs, outputs, password ) - - tx = filter( raw_tx( s_inputs, outputs ) ) - if to_address not in self.addressbook: - self.addressbook.append(to_address) - if label: - tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex') - self.labels[tx_hash] = label - - return tx - - def sendtx(self, tx): - tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex') - self.tx_event.clear() - self.interface.send([('blockchain.transaction.broadcast', [tx])]) - self.tx_event.wait() - out = self.tx_result - if out != tx_hash: - return False, "error: " + out - if self.receipt: - self.receipts[tx_hash] = self.receipt - self.receipt = None - return True, out - - - def read_alias(self, alias): - # this might not be the right place for this function. - import urllib - - m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) - m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) - if m1: - url = 'http://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) - elif m2: - url = 'http://' + alias + '/bitcoin.id' - else: - return '' - try: - lines = urllib.urlopen(url).readlines() - except: - return '' - - # line 0 - line = lines[0].strip().split(':') - if len(line) == 1: - auth_name = None - target = signing_addr = line[0] - else: - target, auth_name, signing_addr, signature = line - msg = "alias:%s:%s:%s"%(alias,target,auth_name) - print msg, signature - self.verify_message(signing_addr, signature, msg) - - # other lines are signed updates - for line in lines[1:]: - line = line.strip() - if not line: continue - line = line.split(':') - previous = target - print repr(line) - target, signature = line - self.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) - - if not self.is_valid(target): - raise BaseException("Invalid bitcoin address") - - return target, signing_addr, auth_name - - def update_password(self, seed, new_password): - if new_password == '': new_password = None - self.use_encryption = (new_password != None) - self.seed = self.pw_encode( seed, new_password) - for k in self.imported_keys.keys(): - a = self.imported_keys[k] - b = self.pw_decode(a, password) - c = self.pw_encode(b, new_password) - self.imported_keys[k] = c - self.save() - - def get_alias(self, alias, interactive = False, show_message=None, question = None): - try: - target, signing_address, auth_name = self.read_alias(alias) - except BaseException, e: - # raise exception if verify fails (verify the chain) - if interactive: - show_message("Alias error: " + e.message) - return - - print target, signing_address, auth_name - - if auth_name is None: - a = self.aliases.get(alias) - if not a: - msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) - if interactive and question( msg ): - self.aliases[alias] = (signing_address, target) - else: - target = None - else: - if signing_address != a[0]: - msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias - if interactive and question( msg ): - self.aliases[alias] = (signing_address, target) - else: - target = None - else: - if signing_address not in self.authorities.keys(): - msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) - if interactive and question( msg ): - self.authorities[signing_address] = auth_name - else: - target = None - - if target: - self.aliases[alias] = (signing_address, target) - - return target - - - def parse_url(self, url, show_message, question): - o = url[8:].split('?') - address = o[0] - if len(o)>1: - params = o[1].split('&') - else: - params = [] - - amount = label = message = signature = identity = '' - for p in params: - k,v = p.split('=') - uv = urldecode(v) - if k == 'amount': amount = uv - elif k == 'message': message = uv - elif k == 'label': label = uv - elif k == 'signature': - identity, signature = uv.split(':') - url = url.replace('&%s=%s'%(k,v),'') - else: - print k,v - - if signature: - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): - signing_address = self.get_alias(identity, True, show_message, question) - elif self.is_valid(identity): - signing_address = identity - else: - signing_address = None - if not signing_address: - return - try: - self.verify_message(signing_address, signature, url ) - self.receipt = (signing_address, signature, url) - except: - show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') - address = amount = label = identity = message = '' - - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): - payto_address = self.get_alias(address, True, show_message, question) - if payto_address: - address = address + ' <' + payto_address + '>' - - return address, amount, label, message, signature, identity, url - - - def update(self): - self.interface.poke() - self.up_to_date_event.wait() - - - diff --git a/client/docs/android.html b/docs/android.html diff --git a/electrum b/electrum @@ -0,0 +1,408 @@ +#!/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 re, sys, getpass + +from optparse import OptionParser +from wallet import Wallet, SecretToASecret +from interface import WalletSynchronizer +from decimal import Decimal +from wallet import format_satoshis + +known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'restore', 'payto', 'sendtx', 'password', 'addresses', 'history', 'label', 'mktx','seed','import','signmessage','verifymessage','eval'] +offline_commands = ['password', 'mktx', 'history', 'label', 'contacts', 'help', 'validateaddress', 'signmessage', 'verifymessage', 'eval', 'create', 'addresses', 'import', 'seed'] +protected_commands = ['payto', 'password', 'mktx', 'seed', 'import','signmessage' ] + +if __name__ == '__main__': + + usage = "usage: %prog [options] command args\nCommands: "+ (', '.join(known_commands)) + parser = OptionParser(usage=usage) + parser.add_option("-g", "--gui", dest="gui", default="qt", help="gui") + parser.add_option("-w", "--wallet", dest="wallet_path", help="wallet path (default: electrum.dat)") + parser.add_option("-a", "--all", action="store_true", dest="show_all", default=False, help="show all addresses") + parser.add_option("-b", "--balance", action="store_true", dest="show_balance", default=False, help="show the balance at listed addresses") + parser.add_option("-k", "--keys",action="store_true", dest="show_keys",default=False, help="show the private keys of listed addresses") + parser.add_option("-f", "--fee", dest="tx_fee", default="0.005", help="set tx fee") + parser.add_option("-s", "--fromaddr", dest="from_addr", default=None, help="set source address for payto/mktx. if it isn't in the wallet, it will ask for the private key unless supplied in the format public_key:private_key. It's not saved in the wallet.") + parser.add_option("-c", "--changeaddr", dest="change_addr", default=None, help="set the change address for payto/mktx. default is a spare address, or the source address if it's not in the wallet") + parser.add_option("-r", "--remote", dest="remote_url", default=None, help="URL of a remote wallet") + options, args = parser.parse_args() + + wallet = Wallet() + wallet.set_path(options.wallet_path) + wallet.read() + wallet.remote_url = options.remote_url + + if len(args)==0: + url = None + cmd = 'gui' + elif len(args)==1 and re.match('^bitcoin:', args[0]): + url = args[0] + cmd = 'gui' + else: + cmd = args[0] + firstarg = args[1] if len(args) > 1 else '' + + if cmd == 'gui': + if options.gui=='gtk': + import gui + elif options.gui=='qt': + import gui_qt as gui + else: + print "unknown gui", options.gui + exit(1) + + gui = gui.ElectrumGui(wallet) + WalletSynchronizer(wallet,True).start() + + try: + found = wallet.file_exists + if not found: + found = gui.restore_or_create() + except SystemExit, e: + exit(e) + except BaseException, e: + import traceback + traceback.print_exc(file=sys.stdout) + #gui.show_message(e.message) + exit(1) + + if not found: exit(1) + + gui.main(url) + wallet.save() + sys.exit(0) + + if cmd not in known_commands: + cmd = 'help' + + if not wallet.file_exists and cmd not in ['help','create','restore']: + print "Wallet file not found." + print "Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option" + sys.exit(0) + + if cmd in ['create', 'restore']: + import mnemonic + if wallet.file_exists: + print "remove the existing wallet first!" + sys.exit(0) + password = getpass.getpass("Password (hit return if you do not wish to encrypt your wallet):") + if password: + password2 = getpass.getpass("Confirm password:") + if password != password2: + print "error" + sys.exit(1) + else: + password = None + + w_host, w_port, w_protocol = wallet.server.split(':') + host = raw_input("server (default:%s):"%w_host) + port = raw_input("port (default:%s):"%w_port) + protocol = raw_input("protocol [t=tcp;h=http;n=native] (default:%s):"%w_protocol) + fee = raw_input("fee (default:%s):"%( str(Decimal(wallet.fee)/100000000)) ) + gap = raw_input("gap limit (default 5):") + if host: w_host = host + if port: w_port = port + if protocol: w_protocol = protocol + wallet.server = w_host + ':' + w_port + ':' +w_protocol + if fee: wallet.fee = float(fee) + if gap: wallet.gap_limit = int(gap) + + if cmd == 'restore': + seed = raw_input("seed:") + try: + seed.decode('hex') + except: + print "not hex, trying decode" + seed = mnemonic.mn_decode( seed.split(' ') ) + if not seed: + print "no seed" + sys.exit(1) + + wallet.seed = str(seed) + WalletSynchronizer(wallet).start() + print "recovering wallet..." + wallet.init_mpk( wallet.seed ) + wallet.up_to_date_event.clear() + wallet.up_to_date = False + wallet.update() + if wallet.is_found(): + wallet.fill_addressbook() + wallet.save() + print "recovery successful" + else: + print "found no history for this wallet" + else: + wallet.new_seed(None) + wallet.init_mpk( wallet.seed ) + wallet.synchronize() # there is no wallet thread + wallet.save() + print "Your wallet generation seed is: " + wallet.seed + print "Please keep it in a safe place; if you lose it, you will not be able to restore your wallet." + print "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:" + print "\""+' '.join(mnemonic.mn_encode(wallet.seed))+"\"" + + # check syntax + if cmd in ['payto', 'mktx']: + try: + to_address = args[1] + amount = int( 100000000 * Decimal(args[2]) ) + change_addr = None + label = ' '.join(args[3:]) + if options.tx_fee: + options.tx_fee = int( 100000000 * Decimal(options.tx_fee) ) + except: + firstarg = cmd + cmd = 'help' + + # open session + if cmd not in offline_commands: + WalletSynchronizer(wallet).start() + wallet.update() + wallet.save() + + # check if --from_addr not in wallet (for mktx/payto) + is_temporary = False + from_addr = None + if options.from_addr: + from_addr = options.from_addr + if from_addr not in wallet.all_addresses(): + is_temporary = True + + # commands needing password + if cmd in protected_commands or ( cmd=='addresses' and options.show_keys): + password = getpass.getpass('Password:') if wallet.use_encryption and not is_temporary else None + # check password + try: + wallet.pw_decode( wallet.seed, password) + except: + print "invalid password" + exit(1) + + if cmd == 'import': + keypair = args[1] + if wallet.import_key(keypair,password): + print "keypair imported" + else: + print "error" + wallet.save() + + if cmd=='help': + cmd2 = firstarg + if cmd2 not in known_commands: + print "known commands:", ', '.join(known_commands) + print "'electrum help <command>' shows the help on a specific command" + print "'electrum --help' shows the list of options" + elif cmd2 == 'balance': + print "Display the balance of your wallet or a specific address. The address does not have to be a owned address (you know the private key)." + print "syntax: balance [<address>]" + elif cmd2 == 'contacts': + print "show your list of contacts" + elif cmd2 == 'payto': + print "payto <recipient> <amount> [label]" + print "create and broadcast a transaction." + print "<recipient> can be a bitcoin address or a label" + print "options: --fee, --fromaddr, --changeaddr" + elif cmd2== 'sendtx': + print "sendtx <tx>" + print "broadcast a transaction to the network. <tx> must be in hexadecimal" + elif cmd2 == 'password': + print "change your password" + elif cmd2 == 'addresses': + print "show your list of addresses. options: -a, -k, -b" + elif cmd2 == 'history': + print "show the transaction history" + elif cmd2 == 'label': + print "assign a label to an item" + elif cmd2 == 'gtk': + print "start the GUI" + elif cmd2 == 'mktx': + print "create a signed transaction. password protected" + print "syntax: mktx <recipient> <amount> [label]" + print "options: --fee, --fromaddr, --changeaddr" + elif cmd2 == 'seed': + print "show generation seed of your wallet. password protected." + elif cmd2 == 'eval': + print "Run python eval() on an object\nSyntax: eval <expression>\nExample: eval \"wallet.aliases\"" + + elif cmd == 'seed': + import mnemonic + seed = wallet.pw_decode( wallet.seed, password) + print seed, '"'+' '.join(mnemonic.mn_encode(seed))+'"' + + elif cmd == 'validateaddress': + addr = args[1] + print wallet.is_valid(addr) + + elif cmd == 'balance': + try: + addrs = args[1:] + except: + pass + if addrs == []: + c, u = wallet.get_balance() + if u: + print Decimal( c ) / 100000000 , Decimal( u ) / 100000000 + else: + print Decimal( c ) / 100000000 + else: + for addr in addrs: + c, u = wallet.get_addr_balance(addr) + if u: + print "%s %s, %s" % (addr, str(Decimal(c)/100000000), str(Decimal(u)/100000000)) + else: + print "%s %s" % (addr, str(Decimal(c)/100000000)) + + elif cmd in [ 'contacts']: + for addr in wallet.addressbook: + print addr, " ", wallet.labels.get(addr) + + elif cmd == 'eval': + print eval(args[1]) + wallet.save() + + elif cmd in [ 'addresses']: + for addr in wallet.all_addresses(): + if options.show_all or not wallet.is_change(addr): + label = wallet.labels.get(addr) + _type = '' + if wallet.is_change(addr): _type = "[change]" + if addr in wallet.imported_keys.keys(): _type = "[imported]" + if label is None: label = '' + if options.show_balance: + h = wallet.history.get(addr,[]) + ni = no = 0 + for item in h: + if item['is_input']: ni += 1 + else: no += 1 + b = "%d %d %s"%(no, ni, str(Decimal(wallet.get_addr_balance(addr)[0])/100000000)) + else: b='' + if options.show_keys: + pk = wallet.get_private_key(addr, password) + addr = addr + ':' + SecretToASecret(pk) + print addr, b, _type, label + + if cmd == 'history': + lines = wallet.get_tx_history() + b = 0 + for line in lines: + import datetime + v = line['value'] + b += v + try: + time_str = str( datetime.datetime.fromtimestamp( line['timestamp'])) + except: + print line['timestamp'] + time_str = 'pending' + label = line.get('label') + if not label: label = line['tx_hash'] + else: label = label + ' '*(64 - len(label) ) + + print time_str , " " + label + " " + format_satoshis(v)+ " "+ format_satoshis(b) + print "# balance: ", format_satoshis(b) + + elif cmd == 'label': + try: + tx = args[1] + label = ' '.join(args[2:]) + except: + print "syntax: label <tx_hash> <text>" + sys.exit(1) + wallet.labels[tx] = label + wallet.save() + + elif cmd in ['payto', 'mktx']: + if from_addr and is_temporary: + if from_addr.find(":") == -1: + keypair = from_addr + ":" + getpass.getpass('Private key:') + else: + keypair = from_addr + from_addr = keypair.split(':')[0] + if not wallet.import_key(keypair,password): + print "invalid key pair" + exit(1) + wallet.history[from_addr] = interface.retrieve_history(from_addr) + wallet.update_tx_history() + change_addr = from_addr + + if options.change_addr: + change_addr = options.change_addr + + for k, v in wallet.labels.items(): + if v == to_address: + to_address = k + print "alias", to_address + break + if change_addr and v == change_addr: + change_addr = k + try: + tx = wallet.mktx( to_address, amount, label, password, + fee = options.tx_fee, change_addr = change_addr, from_addr = from_addr ) + except: + import traceback + traceback.print_exc(file=sys.stdout) + tx = None + + if tx and cmd=='payto': + r, h = wallet.sendtx( tx ) + print h + else: + print tx + + if is_temporary: + wallet.imported_keys.pop(from_addr) + del(wallet.history[from_addr]) + wallet.save() + + elif cmd == 'sendtx': + tx = args[1] + r, h = wallet.sendtx( tx ) + print h + + elif cmd == 'password': + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + print "sorry" + sys.exit(1) + new_password = getpass.getpass('New password:') + if new_password == getpass.getpass('Confirm new password:'): + wallet.use_encryption = (new_password != '') + wallet.seed = wallet.pw_encode( seed, new_password) + for k in wallet.imported_keys.keys(): + a = wallet.imported_keys[k] + b = wallet.pw_decode(a, password) + c = wallet.pw_encode(b, new_password) + wallet.imported_keys[k] = c + wallet.save() + else: + print "error: mismatch" + + elif cmd == 'signmessage': + address, message = args[1:3] + print wallet.sign_message(address, message, password) + + elif cmd == 'verifymessage': + address, signature, message = args[1:4] + try: + wallet.verify_message(address, signature, message) + print True + except: + print False + + diff --git a/electrum4a.py b/electrum4a.py @@ -0,0 +1,980 @@ +#!/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 android +from interface import WalletSynchronizer +from wallet import Wallet +from wallet import format_satoshis +from decimal import Decimal +import mnemonic + +import datetime, re + + + +def modal_dialog(title, msg = None): + droid.dialogCreateAlert(title,msg) + droid.dialogSetPositiveButtonText('OK') + droid.dialogShow() + droid.dialogGetResponse() + droid.dialogDismiss() + +def modal_input(title, msg, value = None, etype=None): + droid.dialogCreateInput(title, msg, value, etype) + droid.dialogSetPositiveButtonText('OK') + droid.dialogSetNegativeButtonText('Cancel') + droid.dialogShow() + response = droid.dialogGetResponse().result + droid.dialogDismiss() + if response.get('which') == 'positive': + return response.get('value') + +def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'): + droid.dialogCreateAlert(q, msg) + droid.dialogSetPositiveButtonText(pos_text) + droid.dialogSetNegativeButtonText(neg_text) + droid.dialogShow() + response = droid.dialogGetResponse().result + droid.dialogDismiss() + return response.get('which') == 'positive' + +def edit_label(addr): + v = modal_input('Edit label',None,wallet.labels.get(addr)) + if v is not None: + if v: + wallet.labels[addr] = v + else: + if addr in wallet.labels.keys(): + wallet.labels.pop(addr) + wallet.update_tx_history() + wallet.save() + droid.fullSetProperty("labelTextView", "text", v) + +def select_from_contacts(): + title = 'Contacts:' + droid.dialogCreateAlert(title) + l = [] + for i in range(len(wallet.addressbook)): + addr = wallet.addressbook[i] + label = wallet.labels.get(addr,addr) + l.append( label ) + droid.dialogSetItems(l) + droid.dialogSetPositiveButtonText('New contact') + droid.dialogShow() + response = droid.dialogGetResponse().result + droid.dialogDismiss() + + if response.get('which') == 'positive': + return 'newcontact' + + result = response.get('item') + print result + if result is not None: + addr = wallet.addressbook[result] + return addr + + +def select_from_addresses(): + droid.dialogCreateAlert("Addresses:") + l = [] + for i in range(len(wallet.addresses)): + addr = wallet.addresses[i] + label = wallet.labels.get(addr,addr) + l.append( label ) + droid.dialogSetItems(l) + droid.dialogShow() + response = droid.dialogGetResponse() + result = response.result.get('item') + droid.dialogDismiss() + if result is not None: + addr = wallet.addresses[result] + return addr + + +def protocol_name(p): + if p == 't': return 'TCP/stratum' + if p == 'h': return 'HTTP/Stratum' + if p == 'n': return 'TCP/native' + +def protocol_dialog(host, protocol, z): + droid.dialogCreateAlert('Protocol',host) + if z: + protocols = z.keys() + else: + protocols = ['t','h','n'] + l = [] + current = protocols.index(protocol) + for p in protocols: + l.append(protocol_name(p)) + droid.dialogSetSingleChoiceItems(l, current) + droid.dialogSetPositiveButtonText('OK') + droid.dialogSetNegativeButtonText('Cancel') + droid.dialogShow() + response = droid.dialogGetResponse().result + if not response: return + if response.get('which') == 'positive': + response = droid.dialogGetSelectedItems().result[0] + droid.dialogDismiss() + p = protocols[response] + port = z[p] + return host + ':' + port + ':' + p + + + + +def make_layout(s, scrollable = False): + content = """ + + <LinearLayout + android:id="@+id/zz" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="#ff222222"> + + <TextView + android:id="@+id/textElectrum" + android:text="Electrum" + android:textSize="7pt" + android:textColor="#ff4444ff" + android:gravity="left" + android:layout_height="wrap_content" + android:layout_width="match_parent" + /> + </LinearLayout> + + %s """%s + + if scrollable: + content = """ + <ScrollView + android:id="@+id/scrollview" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" > + + %s + + </LinearLayout> + </ScrollView> + """%content + + + return """<?xml version="1.0" encoding="utf-8"?> + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/background" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#ff000022"> + + %s + </LinearLayout>"""%content + + + + +def main_layout(): + return make_layout(""" + <TextView android:id="@+id/balanceTextView" + android:layout_width="match_parent" + android:text="" + android:textColor="#ffffffff" + android:textAppearance="?android:attr/textAppearanceLarge" + android:padding="7dip" + android:textSize="8pt" + android:gravity="center_vertical|center_horizontal|left"> + </TextView> + + <TextView android:id="@+id/historyTextView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Recent transactions" + android:textAppearance="?android:attr/textAppearanceLarge" + android:gravity="center_vertical|center_horizontal|center"> + </TextView> + + %s """%get_history_layout(15),True) + + + +def qr_layout(addr): + return make_layout(""" + + <TextView android:id="@+id/addrTextView" + android:layout_width="match_parent" + android:layout_height="50" + android:text="%s" + android:textAppearance="?android:attr/textAppearanceLarge" + android:gravity="center_vertical|center_horizontal|center"> + </TextView> + + <ImageView + android:id="@+id/qrView" + android:gravity="center" + android:layout_width="match_parent" + android:layout_height="350" + android:antialias="false" + android:src="file:///sdcard/sl4a/qrcode.bmp" /> + + <TextView android:id="@+id/labelTextView" + android:layout_width="match_parent" + android:layout_height="50" + android:text="%s" + android:textAppearance="?android:attr/textAppearanceLarge" + android:gravity="center_vertical|center_horizontal|center"> + </TextView> + + """%(addr,wallet.labels.get(addr,'')), True) + +payto_layout = make_layout(""" + + <TextView android:id="@+id/recipientTextView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Pay to:" + android:textAppearance="?android:attr/textAppearanceLarge" + android:gravity="left"> + </TextView> + + + <EditText android:id="@+id/recipient" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:tag="Tag Me" android:inputType="text"> + </EditText> + + <LinearLayout android:id="@+id/linearLayout1" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <Button android:id="@+id/buttonQR" android:layout_width="wrap_content" + android:layout_height="wrap_content" android:text="From QR code"></Button> + <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content" + android:layout_height="wrap_content" android:text="From Contacts"></Button> + </LinearLayout> + + + <TextView android:id="@+id/labelTextView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Description:" + android:textAppearance="?android:attr/textAppearanceLarge" + android:gravity="left"> + </TextView> + + <EditText android:id="@+id/label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:tag="Tag Me" android:inputType="text"> + </EditText> + + <TextView android:id="@+id/amountLabelTextView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Amount:" + android:textAppearance="?android:attr/textAppearanceLarge" + android:gravity="left"> + </TextView> + + <EditText android:id="@+id/amount" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:tag="Tag Me" android:inputType="numberDecimal"> + </EditText> + + <LinearLayout android:layout_width="match_parent" + android:layout_height="wrap_content" android:id="@+id/linearLayout1"> + <Button android:id="@+id/buttonPay" android:layout_width="wrap_content" + android:layout_height="wrap_content" android:text="Send"></Button> + </LinearLayout>""",False) + + + +settings_layout = make_layout(""" <ListView + android:id="@+id/myListView" + android:layout_width="match_parent" + android:layout_height="wrap_content" />""") + + + +def get_history_values(n): + values = [] + h = wallet.get_tx_history() + + length = min(n, len(h)) + for i in range(length): + line = h[-i-1] + v = line['value'] + try: + dt = datetime.datetime.fromtimestamp( line['timestamp'] ) + if dt.date() == dt.today().date(): + time_str = str( dt.time() ) + else: + time_str = str( dt.date() ) + conf = 'v' + + except: + print line['timestamp'] + time_str = 'pending' + conf = 'o' + + tx_hash = line['tx_hash'] + label = wallet.labels.get(tx_hash) + is_default_label = (label == '') or (label is None) + if is_default_label: label = line['default_label'] + values.append((conf, ' ' + time_str, ' ' + format_satoshis(v,True), ' ' + label )) + + return values + + +def get_history_layout(n): + rows = "" + i = 0 + values = get_history_values(n) + for v in values: + a,b,c,d = v + color = "#ff00ff00" if a == 'v' else "#ffff0000" + rows += """ + <TableRow> + <TextView + android:id="@+id/hl_%d_col1" + android:layout_column="0" + android:text="%s" + android:textColor="%s" + android:padding="3" /> + <TextView + android:id="@+id/hl_%d_col2" + android:layout_column="1" + android:text="%s" + android:padding="3" /> + <TextView + android:id="@+id/hl_%d_col3" + android:layout_column="2" + android:text="%s" + android:padding="3" /> + <TextView + android:id="@+id/hl_%d_col4" + android:layout_column="3" + android:text="%s" + android:padding="4" /> + </TableRow>"""%(i,a,color,i,b,i,c,i,d) + i += 1 + + output = """ +<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:stretchColumns="0,1,2,3"> + %s +</TableLayout>"""% rows + return output + + +def set_history_layout(n): + values = get_history_values(n) + i = 0 + for v in values: + a,b,c,d = v + droid.fullSetProperty("hl_%d_col1"%i,"text", a) + + if a == 'v': + droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00") + else: + droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000") + + droid.fullSetProperty("hl_%d_col2"%i,"text", b) + droid.fullSetProperty("hl_%d_col3"%i,"text", c) + droid.fullSetProperty("hl_%d_col4"%i,"text", d) + i += 1 + + + + +status_text = '' +def update_layout(): + global status_text + if not wallet.interface.is_connected: + text = "Not connected..." + elif wallet.blocks == 0: + text = "Server not ready" + elif not wallet.up_to_date: + text = "Synchronizing..." + else: + c, u = wallet.get_balance() + text = "Balance:"+format_satoshis(c) + if u : text += ' [' + format_satoshis(u,True).strip() + ']' + + + # vibrate if status changed + if text != status_text: + if status_text and wallet.interface.is_connected and wallet.up_to_date: + droid.vibrate() + status_text = text + + droid.fullSetProperty("balanceTextView", "text", status_text) + + if wallet.up_to_date: + set_history_layout(15) + + + + +def pay_to(recipient, amount, fee, label): + + if wallet.use_encryption: + password = droid.dialogGetPassword('Password').result + if not password: return + else: + password = None + + droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...") + droid.dialogShow() + + try: + tx = wallet.mktx( recipient, amount, label, password, fee) + except BaseException, e: + modal_dialog('error', e.message) + droid.dialogDismiss() + return + + droid.dialogDismiss() + + r, h = wallet.sendtx( tx ) + if r: + modal_dialog('Payment sent', h) + return True + else: + modal_dialog('Error', h) + + + + + +def recover(): + + droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?") + droid.dialogSetPositiveButtonText('Create') + droid.dialogSetNeutralButtonText('Restore') + droid.dialogSetNegativeButtonText('Cancel') + droid.dialogShow() + response = droid.dialogGetResponse().result + droid.dialogDismiss() + if response.get('which') == 'negative': + exit(1) + + is_recovery = response.get('which') == 'neutral' + + if not is_recovery: + wallet.new_seed(None) + else: + if modal_question("Input method",None,'QR Code', 'mnemonic'): + code = droid.scanBarcode() + r = code.result + if r: + seed = r['extras']['SCAN_RESULT'] + else: + exit(1) + else: + m = modal_input('Mnemonic','please enter your code') + try: + seed = mnemonic.mn_decode(m.split(' ')) + except: + modal_dialog('error: could not decode this seed') + exit(1) + + wallet.seed = str(seed) + + modal_dialog('Your seed is:', wallet.seed) + modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(wallet.seed)) ) + + msg = "recovering wallet..." if is_recovery else "creating wallet..." + droid.dialogCreateSpinnerProgress("Electrum", msg) + droid.dialogShow() + + wallet.init_mpk( wallet.seed ) + WalletSynchronizer(wallet,True).start() + wallet.update() + + droid.dialogDismiss() + droid.vibrate() + + if is_recovery: + if wallet.is_found(): + wallet.update_tx_history() + wallet.fill_addressbook() + modal_dialog("recovery successful") + else: + if not modal_question("no transactions found for this seed","do you want to keep this wallet?"): + exit(1) + + change_password_dialog() + wallet.save() + + + +def make_new_contact(): + code = droid.scanBarcode() + r = code.result + if r: + address = r['extras']['SCAN_RESULT'] + if address: + if wallet.is_valid(address): + if modal_question('Add to contacts?', address): + wallet.addressbook.append(address) + wallet.save() + else: + modal_dialog('Invalid address', address) + + +do_refresh = False + +def update_callback(): + global do_refresh + print "gui callback", wallet.interface.is_connected, wallet.up_to_date + do_refresh = True + droid.eventPost("refresh",'z') + +def main_loop(): + global do_refresh + + update_layout() + out = None + quitting = False + while out is None: + + event = droid.eventWait(1000).result + if event is None: + if do_refresh: + update_layout() + do_refresh = False + continue + + print "got event in main loop", repr(event) + if event == 'OK': continue + if event is None: continue + #if event["name"]=="refresh": + + + # request 2 taps before we exit + if event["name"]=="key": + if event["data"]["key"] == '4': + if quitting: + out = 'quit' + else: + quitting = True + else: quitting = False + + if event["name"]=="click": + id=event["data"]["id"] + + elif event["name"]=="settings": + out = 'settings' + + elif event["name"] in menu_commands: + out = event["name"] + + if out == 'contacts': + global contact_addr + contact_addr = select_from_contacts() + if contact_addr == 'newcontact': + make_new_contact() + contact_addr = None + if not contact_addr: + out = None + + elif out == "receive": + global receive_addr + receive_addr = select_from_addresses() + if receive_addr: + amount = modal_input('Amount', 'Amount you want receive. ', '', "numberDecimal") + if amount: + receive_addr = 'bitcoin:%s?amount=%s'%(receive_addr, amount) + + if not receive_addr: + out = None + + + return out + + +def payto_loop(): + global recipient + if recipient: + droid.fullSetProperty("recipient","text",recipient) + recipient = None + + out = None + while out is None: + event = droid.eventWait().result + print "got event in payto loop", event + + if event["name"] == "click": + id = event["data"]["id"] + + if id=="buttonPay": + + droid.fullQuery() + recipient = droid.fullQueryDetail("recipient").result.get('text') + label = droid.fullQueryDetail("label").result.get('text') + amount = droid.fullQueryDetail('amount').result.get('text') + + if not wallet.is_valid(recipient): + modal_dialog('Error','Invalid Bitcoin address') + continue + + try: + amount = int( 100000000 * Decimal(amount) ) + except: + modal_dialog('Error','Invalid amount') + continue + + result = pay_to(recipient, amount, wallet.fee, label) + if result: + out = 'main' + + elif id=="buttonContacts": + addr = select_from_contacts() + droid.fullSetProperty("recipient","text",addr) + + elif id=="buttonQR": + code = droid.scanBarcode() + r = code.result + if r: + data = r['extras']['SCAN_RESULT'] + if data: + if re.match('^bitcoin:', data): + payto, amount, label, _, _, _, _ = wallet.parse_url(data, None, None) + droid.fullSetProperty("recipient", "text",payto) + droid.fullSetProperty("amount", "text", amount) + droid.fullSetProperty("label", "text", label) + else: + droid.fullSetProperty("recipient", "text", data) + + + elif event["name"] in menu_commands: + out = event["name"] + + elif event["name"]=="key": + if event["data"]["key"] == '4': + out = 'main' + + #elif event["name"]=="screen": + # if event["data"]=="destroy": + # out = 'main' + + return out + + +receive_addr = '' +contact_addr = '' +recipient = '' + +def receive_loop(): + out = None + while out is None: + event = droid.eventWait().result + print "got event", event + if event["name"]=="key": + if event["data"]["key"] == '4': + out = 'main' + + elif event["name"]=="clipboard": + droid.setClipboard(receive_addr) + modal_dialog('Address copied to clipboard',receive_addr) + + elif event["name"]=="edit": + edit_label(receive_addr) + + return out + +def contacts_loop(): + global recipient + out = None + while out is None: + event = droid.eventWait().result + print "got event", event + if event["name"]=="key": + if event["data"]["key"] == '4': + out = 'main' + + elif event["name"]=="clipboard": + droid.setClipboard(contact_addr) + modal_dialog('Address copied to clipboard',contact_addr) + + elif event["name"]=="edit": + edit_label(contact_addr) + + elif event["name"]=="paytocontact": + recipient = contact_addr + out = 'send' + + elif event["name"]=="deletecontact": + if modal_question('delete contact', contact_addr): + out = 'main' + + return out + + +def server_dialog(plist): + droid.dialogCreateAlert("Public servers") + droid.dialogSetItems( plist.keys() ) + droid.dialogSetPositiveButtonText('Private server') + droid.dialogShow() + response = droid.dialogGetResponse().result + droid.dialogDismiss() + + if response.get('which') == 'positive': + return modal_input('Private server', None) + + i = response.get('item') + if i is not None: + response = plist.keys()[i] + return response + + +def seed_dialog(): + if wallet.use_encryption: + password = droid.dialogGetPassword('Seed').result + if not password: return + else: + password = None + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + modal_dialog('error','incorrect password') + return + + modal_dialog('Your seed is',seed) + modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(seed)) ) + +def change_password_dialog(): + if wallet.use_encryption: + password = droid.dialogGetPassword('Your wallet is encrypted').result + if password is None: return + else: + password = None + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + modal_dialog('error','incorrect password') + return + + new_password = droid.dialogGetPassword('Choose a password').result + if new_password == None: + return + + if new_password != '': + password2 = droid.dialogGetPassword('Confirm new password').result + if new_password != password2: + modal_dialog('error','passwords do not match') + return + + wallet.update_password(seed, new_password) + if new_password: + modal_dialog('Password updated','your wallet is encrypted') + else: + modal_dialog('No password','your wallet is not encrypted') + return True + + +def settings_loop(): + + + def set_listview(): + server, port, p = wallet.server.split(':') + fee = str( Decimal( wallet.fee)/100000000 ) + is_encrypted = 'yes' if wallet.use_encryption else 'no' + protocol = protocol_name(p) + droid.fullShow(settings_layout) + droid.fullSetList("myListView",['Server: ' + server, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed']) + + set_listview() + + out = None + while out is None: + event = droid.eventWait().result + print "got event", event + if event == 'OK': continue + if not event: continue + + plist = {} + for item in wallet.interface.servers: + host, pp = item + z = {} + for item2 in pp: + protocol, port = item2 + z[protocol] = port + plist[host] = z + + if event["name"] == "itemclick": + pos = event["data"]["position"] + host, port, protocol = wallet.server.split(':') + + if pos == "0": #server + host = server_dialog(plist) + if host: + p = plist[host] + port = p['t'] + srv = host + ':' + port + ':t' + try: + wallet.set_server(srv) + except: + modal_dialog('error','invalid server') + set_listview() + + elif pos == "1": #protocol + if host in plist: + srv = protocol_dialog(host, protocol, plist[host]) + if srv: + try: + wallet.set_server(srv) + except: + modal_dialog('error','invalid server') + set_listview() + + elif pos == "2": #port + a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number") + if a_port: + if a_port != port: + srv = host + ':' + a_port + ':'+ protocol + try: + wallet.set_server(srv) + except: + modal_dialog('error','invalid port number') + set_listview() + + elif pos == "3": #fee + fee = modal_input('Transaction fee', 'The fee will be this amount multiplied by the number of inputs in your transaction. ', str( Decimal( wallet.fee)/100000000 ), "numberDecimal") + if fee: + try: + fee = int( 100000000 * Decimal(fee) ) + except: + modal_dialog('error','invalid fee value') + if wallet.fee != fee: + wallet.fee = fee + wallet.save() + set_listview() + + elif pos == "4": + if change_password_dialog(): + set_listview() + + elif pos == "5": + seed_dialog() + + + elif event["name"] in menu_commands: + out = event["name"] + + elif event["name"] == 'cancel': + out = 'main' + + elif event["name"] == "key": + if event["data"]["key"] == '4': + out = 'main' + + return out + + + + +menu_commands = ["send", "receive", "settings", "contacts", "main"] +droid = android.Android() +wallet = Wallet(update_callback) + +wallet.set_path("/sdcard/electrum.dat") +wallet.read() +if not wallet.file_exists: + recover() +else: + WalletSynchronizer(wallet,True).start() + + +s = 'main' + +def add_menu(s): + droid.clearOptionsMenu() + if s == 'main': + droid.addOptionsMenuItem("Send","send",None,"") + droid.addOptionsMenuItem("Receive","receive",None,"") + droid.addOptionsMenuItem("Contacts","contacts",None,"") + droid.addOptionsMenuItem("Settings","settings",None,"") + elif s == 'receive': + droid.addOptionsMenuItem("Copy","clipboard",None,"") + droid.addOptionsMenuItem("Label","edit",None,"") + elif s == 'contacts': + droid.addOptionsMenuItem("Copy","clipboard",None,"") + droid.addOptionsMenuItem("Label","edit",None,"") + droid.addOptionsMenuItem("Pay to","paytocontact",None,"") + #droid.addOptionsMenuItem("Delete","deletecontact",None,"") + +def make_bitmap(addr): + # fixme: this is highly inefficient + droid.dialogCreateSpinnerProgress("please wait") + droid.dialogShow() + try: + import pyqrnative, bmp + qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L) + qr.addData(addr) + qr.make() + k = qr.getModuleCount() + assert k == 33 + bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp") + finally: + droid.dialogDismiss() + + + +while True: + add_menu(s) + if s == 'main': + droid.fullShow(main_layout()) + s = main_loop() + #droid.fullDismiss() + + elif s == 'send': + droid.fullShow(payto_layout) + s = payto_loop() + #droid.fullDismiss() + + elif s == 'receive': + make_bitmap(receive_addr) + droid.fullShow(qr_layout(receive_addr)) + s = receive_loop() + + elif s == 'contacts': + make_bitmap(contact_addr) + droid.fullShow(qr_layout(contact_addr)) + s = contacts_loop() + + elif s == 'settings': + #droid.fullShow(settings_layout) + s = settings_loop() + #droid.fullDismiss() + else: + break + +droid.makeToast("Bye!") diff --git a/client/electrum_text_320.png b/electrum_text_320.png Binary files differ. diff --git a/gui.py b/gui.py @@ -0,0 +1,1259 @@ +#!/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 datetime +import thread, time, ast, sys, re +import socket, traceback +import pygtk +pygtk.require('2.0') +import gtk, gobject +import pyqrnative +from decimal import Decimal + +gtk.gdk.threads_init() +APP_NAME = "Electrum" +import platform +MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' + +from wallet import format_satoshis +from interface import DEFAULT_SERVERS + +def numbify(entry, is_int = False): + text = entry.get_text().strip() + chars = '0123456789' + if not is_int: chars +='.' + s = ''.join([i for i in text if i in chars]) + if not is_int: + if '.' in s: + p = s.find('.') + s = s.replace('.','') + s = s[:p] + '.' + s[p:p+8] + try: + amount = int( Decimal(s) * 100000000 ) + except: + amount = None + else: + try: + amount = int( s ) + except: + amount = None + entry.set_text(s) + return amount + + + + +def show_seed_dialog(wallet, password, parent): + import mnemonic + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + show_message("Incorrect password") + return + dialog = gtk.MessageDialog( + parent = parent, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_OK, + message_format = "Your wallet generation seed is:\n\n" + seed \ + + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \ + + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" ) + dialog.set_title("Seed") + dialog.show() + dialog.run() + dialog.destroy() + +def restore_create_dialog(wallet): + + # ask if the user wants to create a new wallet, or recover from a seed. + # if he wants to recover, and nothing is found, do not create wallet + dialog = gtk.Dialog("electrum", parent=None, + flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, + buttons= ("create", 0, "restore",1, "cancel",2) ) + + label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" ) + label.show() + dialog.vbox.pack_start(label) + dialog.show() + r = dialog.run() + dialog.destroy() + + if r==2: return False + + is_recovery = (r==1) + + # ask for the server. + if not run_network_dialog( wallet, parent=None ): return False + + if not is_recovery: + + wallet.new_seed(None) + # generate first key + wallet.init_mpk( wallet.seed ) + wallet.up_to_date_event.clear() + wallet.update() + + # run a dialog indicating the seed, ask the user to remember it + show_seed_dialog(wallet, None, None) + + #ask for password + change_password_dialog(wallet, None, None) + else: + # ask for seed and gap. + run_recovery_dialog( wallet ) + + dialog = gtk.MessageDialog( + parent = None, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_CANCEL, + message_format = "Please wait..." ) + dialog.show() + + def recover_thread( wallet, dialog ): + wallet.init_mpk( wallet.seed ) # not encrypted at this point + wallet.up_to_date_event.clear() + wallet.update() + + if wallet.is_found(): + # history and addressbook + wallet.update_tx_history() + wallet.fill_addressbook() + print "recovery successful" + + gobject.idle_add( dialog.destroy ) + + thread.start_new_thread( recover_thread, ( wallet, dialog ) ) + r = dialog.run() + dialog.destroy() + if r==gtk.RESPONSE_CANCEL: return False + if not wallet.is_found: + show_message("No transactions found for this seed") + + wallet.save() + return True + + +def run_recovery_dialog(wallet): + message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." + dialog = gtk.MessageDialog( + parent = None, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_OK_CANCEL, + message_format = message) + + vbox = dialog.vbox + dialog.set_default_response(gtk.RESPONSE_OK) + + # ask seed, server and gap in the same dialog + seed_box = gtk.HBox() + seed_label = gtk.Label('Seed or mnemonic:') + seed_label.set_size_request(150,-1) + seed_box.pack_start(seed_label, False, False, 10) + seed_label.show() + seed_entry = gtk.Entry() + seed_entry.show() + seed_entry.set_size_request(450,-1) + seed_box.pack_start(seed_entry, False, False, 10) + add_help_button(seed_box, '.') + seed_box.show() + vbox.pack_start(seed_box, False, False, 5) + + gap = gtk.HBox() + gap_label = gtk.Label('Gap limit:') + gap_label.set_size_request(150,10) + gap_label.show() + gap.pack_start(gap_label,False, False, 10) + gap_entry = gtk.Entry() + gap_entry.set_text("%d"%wallet.gap_limit) + gap_entry.connect('changed', numbify, True) + gap_entry.show() + gap.pack_start(gap_entry,False,False, 10) + add_help_button(gap, 'The maximum gap that is allowed between unused addresses in your wallet. During wallet recovery, this parameter is used to decide when to stop the recovery process. If you increase this value, you will need to remember it in order to be able to recover your wallet from seed.') + gap.show() + vbox.pack_start(gap, False,False, 5) + + dialog.show() + r = dialog.run() + gap = gap_entry.get_text() + seed = seed_entry.get_text() + dialog.destroy() + + if r==gtk.RESPONSE_CANCEL: + sys.exit(1) + try: + gap = int(gap) + except: + show_message("error") + sys.exit(1) + + try: + seed.decode('hex') + except: + import mnemonic + print "not hex, trying decode" + seed = mnemonic.mn_decode( seed.split(' ') ) + if not seed: + show_message("no seed") + sys.exit(1) + + wallet.seed = seed + wallet.gap_limit = gap + wallet.save() + + + +def run_settings_dialog(wallet, parent): + + message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field." + + dialog = gtk.MessageDialog( + parent = parent, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_OK_CANCEL, + message_format = message) + + image = gtk.Image() + image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG) + image.show() + dialog.set_image(image) + dialog.set_title("Settings") + + vbox = dialog.vbox + dialog.set_default_response(gtk.RESPONSE_OK) + + fee = gtk.HBox() + fee_entry = gtk.Entry() + fee_label = gtk.Label('Transaction fee:') + fee_label.set_size_request(150,10) + fee_label.show() + fee.pack_start(fee_label,False, False, 10) + fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) ) + fee_entry.connect('changed', numbify, False) + fee_entry.show() + fee.pack_start(fee_entry,False,False, 10) + add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005') + fee.show() + vbox.pack_start(fee, False,False, 5) + + dialog.show() + r = dialog.run() + fee = fee_entry.get_text() + + dialog.destroy() + if r==gtk.RESPONSE_CANCEL: + return + + try: + fee = int( 100000000 * Decimal(fee) ) + except: + show_message("error") + return + + wallet.fee = fee + wallet.save() + + + + +def run_network_dialog( wallet, parent ): + image = gtk.Image() + image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG) + interface = wallet.interface + if parent: + if interface.is_connected: + status = "Connected to %s:%d\n%d blocks\nresponse time: %f"%(interface.host, interface.port, wallet.blocks, interface.rtime) + else: + status = "Not connected" + server = wallet.server + else: + import random + status = "Please choose a server." + server = random.choice( DEFAULT_SERVERS ) + + plist = {} + for item in wallet.interface.servers: + host, pp = item + z = {} + for item2 in pp: + protocol, port = item2 + z[protocol] = port + plist[host] = z + + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status) + dialog.set_title("Server") + dialog.set_image(image) + image.show() + + vbox = dialog.vbox + host_box = gtk.HBox() + host_label = gtk.Label('Connect to:') + host_label.set_size_request(100,-1) + host_label.show() + host_box.pack_start(host_label, False, False, 10) + host_entry = gtk.Entry() + host_entry.set_size_request(200,-1) + host_entry.set_text(server) + host_entry.show() + host_box.pack_start(host_entry, False, False, 10) + add_help_button(host_box, 'The name and port number of your Electrum server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, port 50000 will be tried. Some servers allow you to connect through http (port 80) or https (port 443)') + host_box.show() + + + p_box = gtk.HBox(False, 10) + p_box.show() + + p_label = gtk.Label('Protocol:') + p_label.set_size_request(100,-1) + p_label.show() + p_box.pack_start(p_label, False, False, 10) + + radio1 = gtk.RadioButton(None, "tcp") + p_box.pack_start(radio1, True, True, 0) + radio1.show() + radio2 = gtk.RadioButton(radio1, "http") + p_box.pack_start(radio2, True, True, 0) + radio2.show() + + def current_line(): + return unicode(host_entry.get_text()).split(':') + + def set_button(protocol): + if protocol == 't': + radio1.set_active(1) + elif protocol == 'h': + radio2.set_active(1) + + def set_protocol(protocol): + host = current_line()[0] + pp = plist[host] + if protocol not in pp.keys(): + protocol = pp.keys()[0] + set_button(protocol) + port = pp[protocol] + host_entry.set_text( host + ':' + port + ':' + protocol) + + radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1") + radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1") + + server_list = gtk.ListStore(str) + for host in plist.keys(): + server_list.append([host]) + + treeview = gtk.TreeView(model=server_list) + treeview.show() + + tvcolumn = gtk.TreeViewColumn('Active servers') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 0) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.add(treeview) + scroll.show() + + vbox.pack_start(host_box, False,False, 5) + vbox.pack_start(p_box, True, True, 0) + vbox.pack_start(scroll) + + def my_treeview_cb(treeview): + path, view_column = treeview.get_cursor() + host = server_list.get_value( server_list.get_iter(path), 0) + + pp = plist[host] + if 't' in pp.keys(): + protocol = 't' + else: + protocol = pp.keys()[0] + port = pp[protocol] + host_entry.set_text( host + ':' + port + ':' + protocol) + set_button(protocol) + + treeview.connect('cursor-changed', my_treeview_cb) + + dialog.show() + r = dialog.run() + server = host_entry.get_text() + dialog.destroy() + + if r==gtk.RESPONSE_CANCEL: + return False + + try: + wallet.set_server(server) + except: + show_message("error:" + server) + return False + + if parent: + wallet.save() + return True + + + +def show_message(message, parent=None): + dialog = gtk.MessageDialog( + parent = parent, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_CLOSE, + message_format = message ) + dialog.show() + dialog.run() + dialog.destroy() + +def password_line(label): + password = gtk.HBox() + password_label = gtk.Label(label) + password_label.set_size_request(120,10) + password_label.show() + password.pack_start(password_label,False, False, 10) + password_entry = gtk.Entry() + password_entry.set_size_request(300,-1) + password_entry.set_visibility(False) + password_entry.show() + password.pack_start(password_entry,False,False, 10) + password.show() + return password, password_entry + +def password_dialog(parent): + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.") + dialog.get_image().set_visible(False) + current_pw, current_pw_entry = password_line('Password:') + current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK) + dialog.vbox.pack_start(current_pw, False, True, 0) + dialog.show() + result = dialog.run() + pw = current_pw_entry.get_text() + dialog.destroy() + if result != gtk.RESPONSE_CANCEL: return pw + +def change_password_dialog(wallet, parent, icon): + if parent: + msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' + else: + msg = "Please choose a password to encrypt your wallet keys" + + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) + dialog.set_title("Change password") + image = gtk.Image() + image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG) + image.show() + dialog.set_image(image) + + if wallet.use_encryption: + current_pw, current_pw_entry = password_line('Current password:') + dialog.vbox.pack_start(current_pw, False, True, 0) + + password, password_entry = password_line('New password:') + dialog.vbox.pack_start(password, False, True, 5) + password2, password2_entry = password_line('Confirm password:') + dialog.vbox.pack_start(password2, False, True, 5) + + dialog.show() + result = dialog.run() + password = current_pw_entry.get_text() if wallet.use_encryption else None + new_password = password_entry.get_text() + new_password2 = password2_entry.get_text() + dialog.destroy() + if result == gtk.RESPONSE_CANCEL: + return + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + show_message("Incorrect password") + return + + if new_password != new_password2: + show_message("passwords do not match") + return + + wallet.update_password(seed, new_password) + + if icon: + if wallet.use_encryption: + icon.set_tooltip_text('wallet is encrypted') + else: + icon.set_tooltip_text('wallet is unencrypted') + + +def add_help_button(hbox, message): + button = gtk.Button('?') + button.connect("clicked", lambda x: show_message(message)) + button.show() + hbox.pack_start(button,False, False) + + +class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) ) + +gobject.type_register(MyWindow) +gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W') +gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q') + + +class ElectrumWindow: + + def show_message(self, msg): + show_message(msg, self.window) + + def __init__(self, wallet): + self.wallet = wallet + self.funds_error = False # True if not enough funds + + self.window = MyWindow(gtk.WINDOW_TOPLEVEL) + self.window.set_title(APP_NAME + " " + self.wallet.electrum_version) + self.window.connect("destroy", gtk.main_quit) + self.window.set_border_width(0) + self.window.connect('mykeypress', gtk.main_quit) + self.window.set_default_size(720, 350) + + vbox = gtk.VBox() + + self.notebook = gtk.Notebook() + self.create_history_tab() + self.create_send_tab() + self.create_recv_tab() + self.create_book_tab() + self.create_about_tab() + self.notebook.show() + vbox.pack_start(self.notebook, True, True, 2) + + self.status_bar = gtk.Statusbar() + vbox.pack_start(self.status_bar, False, False, 0) + + self.status_image = gtk.Image() + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + self.status_image.set_alignment(True, 0.5 ) + self.status_image.show() + + self.network_button = gtk.Button() + self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) ) + self.network_button.add(self.status_image) + self.network_button.set_relief(gtk.RELIEF_NONE) + self.network_button.show() + self.status_bar.pack_end(self.network_button, False, False) + + def seedb(w, wallet): + if wallet.use_encryption: + password = password_dialog(self.window) + if not password: return + else: password = None + show_seed_dialog(wallet, password, self.window) + button = gtk.Button('S') + button.connect("clicked", seedb, wallet ) + button.set_relief(gtk.RELIEF_NONE) + button.show() + self.status_bar.pack_end(button,False, False) + + settings_icon = gtk.Image() + settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) + settings_icon.set_alignment(0.5, 0.5) + settings_icon.set_size_request(16,16 ) + settings_icon.show() + + prefs_button = gtk.Button() + prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) ) + prefs_button.add(settings_icon) + prefs_button.set_tooltip_text("Settings") + prefs_button.set_relief(gtk.RELIEF_NONE) + prefs_button.show() + self.status_bar.pack_end(prefs_button,False,False) + + pw_icon = gtk.Image() + pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) + pw_icon.set_alignment(0.5, 0.5) + pw_icon.set_size_request(16,16 ) + pw_icon.show() + + password_button = gtk.Button() + password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon)) + password_button.add(pw_icon) + password_button.set_relief(gtk.RELIEF_NONE) + password_button.show() + self.status_bar.pack_end(password_button,False,False) + + self.window.add(vbox) + self.window.show_all() + #self.fee_box.hide() + + self.context_id = self.status_bar.get_context_id("statusbar") + self.update_status_bar() + + def update_status_bar_thread(): + while True: + gobject.idle_add( self.update_status_bar ) + time.sleep(0.5) + + + def check_recipient_thread(): + old_r = '' + while True: + time.sleep(0.5) + if self.payto_entry.is_focus(): + continue + r = self.payto_entry.get_text() + if r != old_r: + old_r = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = self.wallet.get_alias(r, interactive=False) + except: + continue + if to_address: + s = r + ' <' + to_address + '>' + gobject.idle_add( lambda: self.payto_entry.set_text(s) ) + + + thread.start_new_thread(update_status_bar_thread, ()) + thread.start_new_thread(check_recipient_thread, ()) + self.notebook.set_current_page(0) + + + def add_tab(self, page, name): + tab_label = gtk.Label(name) + tab_label.show() + self.notebook.append_page(page, tab_label) + + + def create_send_tab(self): + + page = vbox = gtk.VBox() + page.show() + + payto = gtk.HBox() + payto_label = gtk.Label('Pay to:') + payto_label.set_size_request(100,-1) + payto.pack_start(payto_label, False) + payto_entry = gtk.Entry() + payto_entry.set_size_request(450, 26) + payto.pack_start(payto_entry, False) + vbox.pack_start(payto, False, False, 5) + + message = gtk.HBox() + message_label = gtk.Label('Description:') + message_label.set_size_request(100,-1) + message.pack_start(message_label, False) + message_entry = gtk.Entry() + message_entry.set_size_request(450, 26) + message.pack_start(message_entry, False) + vbox.pack_start(message, False, False, 5) + + amount_box = gtk.HBox() + amount_label = gtk.Label('Amount:') + amount_label.set_size_request(100,-1) + amount_box.pack_start(amount_label, False) + amount_entry = gtk.Entry() + amount_entry.set_size_request(120, -1) + amount_box.pack_start(amount_entry, False) + vbox.pack_start(amount_box, False, False, 5) + + self.fee_box = fee_box = gtk.HBox() + fee_label = gtk.Label('Fee:') + fee_label.set_size_request(100,-1) + fee_box.pack_start(fee_label, False) + fee_entry = gtk.Entry() + fee_entry.set_size_request(60, 26) + fee_box.pack_start(fee_entry, False) + vbox.pack_start(fee_box, False, False, 5) + + end_box = gtk.HBox() + empty_label = gtk.Label('') + empty_label.set_size_request(100,-1) + end_box.pack_start(empty_label, False) + send_button = gtk.Button("Send") + send_button.show() + end_box.pack_start(send_button, False, False, 0) + clear_button = gtk.Button("Clear") + clear_button.show() + end_box.pack_start(clear_button, False, False, 15) + send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry)) + clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry)) + + vbox.pack_start(end_box, False, False, 5) + + # display this line only if there is a signature + payto_sig = gtk.HBox() + payto_sig_id = gtk.Label('') + payto_sig.pack_start(payto_sig_id, False) + vbox.pack_start(payto_sig, True, True, 5) + + + self.user_fee = False + + def entry_changed( entry, is_fee ): + self.funds_error = False + amount = numbify(amount_entry) + fee = numbify(fee_entry) + if not is_fee: fee = None + if amount is None: + return + inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) + if not is_fee: + fee_entry.set_text( str( Decimal( fee ) / 100000000 ) ) + self.fee_box.show() + if inputs: + amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) + fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) + send_button.set_sensitive(True) + else: + send_button.set_sensitive(False) + amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) + fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) + self.funds_error = True + + amount_entry.connect('changed', entry_changed, False) + fee_entry.connect('changed', entry_changed, True) + + self.payto_entry = payto_entry + self.payto_fee_entry = fee_entry + self.payto_sig_id = payto_sig_id + self.payto_sig = payto_sig + self.amount_entry = amount_entry + self.message_entry = message_entry + self.add_tab(page, 'Send') + + def set_frozen(self,entry,frozen): + if frozen: + entry.set_editable(False) + entry.set_has_frame(False) + entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee")) + else: + entry.set_editable(True) + entry.set_has_frame(True) + entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff")) + + def set_url(self, url): + payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) + self.notebook.set_current_page(1) + self.payto_entry.set_text(payto) + self.message_entry.set_text(message) + self.amount_entry.set_text(amount) + if identity: + self.set_frozen(self.payto_entry,True) + self.set_frozen(self.amount_entry,True) + self.set_frozen(self.message_entry,True) + self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity ) + else: + self.payto_sig.set_visible(False) + + def create_about_tab(self): + import pango + page = gtk.VBox() + page.show() + tv = gtk.TextView() + tv.set_editable(False) + tv.set_cursor_visible(False) + tv.modify_font(pango.FontDescription(MONOSPACE_FONT)) + page.pack_start(tv) + self.info = tv.get_buffer() + self.add_tab(page, 'Wall') + + def do_clear(self, w, data): + self.payto_sig.set_visible(False) + self.payto_fee_entry.set_text('') + for entry in [self.payto_entry,self.amount_entry,self.message_entry]: + self.set_frozen(entry,False) + entry.set_text('') + + def question(self,msg): + dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) + dialog.show() + result = dialog.run() + dialog.destroy() + return result == gtk.RESPONSE_OK + + def do_send(self, w, data): + payto_entry, label_entry, amount_entry, fee_entry = data + label = label_entry.get_text() + r = payto_entry.get_text() + r = r.strip() + + m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) + m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + + if m1: + to_address = self.wallet.get_alias(r, True, self.show_message, self.question) + if not to_address: + return + else: + self.update_sending_tab() + + elif m2: + to_address = m2.group(5) + else: + to_address = r + + if not self.wallet.is_valid(to_address): + self.show_message( "invalid bitcoin address:\n"+to_address) + return + + try: + amount = int( Decimal(amount_entry.get_text()) * 100000000 ) + except: + self.show_message( "invalid amount") + return + try: + fee = int( Decimal(fee_entry.get_text()) * 100000000 ) + except: + self.show_message( "invalid fee") + return + + if self.wallet.use_encryption: + password = password_dialog(self.window) + if not password: + return + else: + password = None + + try: + tx = self.wallet.mktx( to_address, amount, label, password, fee ) + except BaseException, e: + self.show_message(e.message) + return + + status, msg = self.wallet.sendtx( tx ) + if status: + self.show_message( "payment sent.\n" + msg ) + payto_entry.set_text("") + label_entry.set_text("") + amount_entry.set_text("") + fee_entry.set_text("") + #self.fee_box.hide() + self.update_sending_tab() + else: + self.show_message( msg ) + + + def treeview_button_press(self, treeview, event): + if event.type == gtk.gdk._2BUTTON_PRESS: + c = treeview.get_cursor()[0] + if treeview == self.history_treeview: + tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) + self.show_message(tx_details) + elif treeview == self.contacts_treeview: + m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self-signed" + msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] + self.show_message(msg) + + + def treeview_key_press(self, treeview, event): + c = treeview.get_cursor()[0] + if event.keyval == gtk.keysyms.Up: + if c and c[0] == 0: + treeview.parent.grab_focus() + treeview.set_cursor((0,)) + elif event.keyval == gtk.keysyms.Return: + if treeview == self.history_treeview: + tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) + self.show_message(tx_details) + elif treeview == self.contacts_treeview: + m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self" + msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] + self.show_message(msg) + + return False + + def create_history_tab(self): + + self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str) + treeview = gtk.TreeView(model=self.history_list) + self.history_treeview = treeview + treeview.set_tooltip_column(7) + treeview.show() + treeview.connect('key-press-event', self.treeview_key_press) + treeview.connect('button-press-event', self.treeview_button_press) + + tvcolumn = gtk.TreeViewColumn('') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererPixbuf() + tvcolumn.pack_start(cell, False) + tvcolumn.set_attributes(cell, stock_id=1) + + tvcolumn = gtk.TreeViewColumn('Date') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 2) + + tvcolumn = gtk.TreeViewColumn('Description') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_property('foreground', 'grey') + cell.set_property('family', MONOSPACE_FONT) + cell.set_property('editable', True) + def edited_cb(cell, path, new_text, h_list): + tx = h_list.get_value( h_list.get_iter(path), 0) + self.wallet.labels[tx] = new_text + self.wallet.save() + self.update_history_tab() + cell.connect('edited', edited_cb, self.history_list) + def editing_started(cell, entry, path, h_list): + tx = h_list.get_value( h_list.get_iter(path), 0) + if not self.wallet.labels.get(tx): entry.set_text('') + cell.connect('editing-started', editing_started, self.history_list) + tvcolumn.set_expand(True) + tvcolumn.pack_start(cell, True) + tvcolumn.set_attributes(cell, text=3, foreground_set = 4) + + tvcolumn = gtk.TreeViewColumn('Amount') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_alignment(1, 0.5) + cell.set_property('family', MONOSPACE_FONT) + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 5) + + tvcolumn = gtk.TreeViewColumn('Balance') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_alignment(1, 0.5) + cell.set_property('family', MONOSPACE_FONT) + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 6) + + tvcolumn = gtk.TreeViewColumn('Tooltip') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 7) + tvcolumn.set_visible(False) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scroll.add(treeview) + + self.add_tab(scroll, 'History') + self.update_history_tab() + + + def create_recv_tab(self): + self.recv_list = gtk.ListStore(str, str, str) + self.add_tab( self.make_address_list(True), 'Receive') + self.update_receiving_tab() + + def create_book_tab(self): + self.addressbook_list = gtk.ListStore(str, str, str) + self.add_tab( self.make_address_list(False), 'Contacts') + self.update_sending_tab() + + def make_address_list(self, is_recv): + liststore = self.recv_list if is_recv else self.addressbook_list + treeview = gtk.TreeView(model= liststore) + treeview.connect('key-press-event', self.treeview_key_press) + treeview.connect('button-press-event', self.treeview_button_press) + treeview.show() + if not is_recv: + self.contacts_treeview = treeview + + tvcolumn = gtk.TreeViewColumn('Address') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_property('family', MONOSPACE_FONT) + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 0) + + tvcolumn = gtk.TreeViewColumn('Label') + tvcolumn.set_expand(True) + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_property('editable', True) + def edited_cb2(cell, path, new_text, liststore): + address = liststore.get_value( liststore.get_iter(path), 0) + self.wallet.labels[address] = new_text + self.wallet.save() + self.wallet.update_tx_labels() + self.update_receiving_tab() + self.update_sending_tab() + self.update_history_tab() + cell.connect('edited', edited_cb2, liststore) + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 1) + + tvcolumn = gtk.TreeViewColumn('Tx') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 2) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.add(treeview) + + hbox = gtk.HBox() + if not is_recv: + button = gtk.Button("New") + button.connect("clicked", self.newaddress_dialog) + button.show() + hbox.pack_start(button,False) + + def showqrcode(w, treeview, liststore): + path, col = treeview.get_cursor() + if not path: return + address = liststore.get_value(liststore.get_iter(path), 0) + qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H) + qr.addData(address) + qr.make() + boxsize = 7 + size = qr.getModuleCount()*boxsize + def area_expose_cb(area, event): + style = area.get_style() + k = qr.getModuleCount() + for r in range(k): + for c in range(k): + gc = style.black_gc if qr.isDark(r, c) else style.white_gc + area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize) + area = gtk.DrawingArea() + area.set_size_request(size, size) + area.connect("expose-event", area_expose_cb) + area.show() + dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1)) + dialog.vbox.add(area) + dialog.run() + dialog.destroy() + + button = gtk.Button("QR") + button.connect("clicked", showqrcode, treeview, liststore) + button.show() + hbox.pack_start(button,False) + + button = gtk.Button("Copy to clipboard") + def copy2clipboard(w, treeview, liststore): + import platform + path, col = treeview.get_cursor() + if path: + address = liststore.get_value( liststore.get_iter(path), 0) + if platform.system() == 'Windows': + from Tkinter import Tk + r = Tk() + r.withdraw() + r.clipboard_clear() + r.clipboard_append( address ) + r.destroy() + else: + c = gtk.clipboard_get() + c.set_text( address ) + button.connect("clicked", copy2clipboard, treeview, liststore) + button.show() + hbox.pack_start(button,False) + + if not is_recv: + button = gtk.Button("Pay to") + def payto(w, treeview, liststore): + path, col = treeview.get_cursor() + if path: + address = liststore.get_value( liststore.get_iter(path), 0) + self.payto_entry.set_text( address ) + self.notebook.set_current_page(1) + self.amount_entry.grab_focus() + + button.connect("clicked", payto, treeview, liststore) + button.show() + hbox.pack_start(button,False) + + vbox = gtk.VBox() + vbox.pack_start(scroll,True) + vbox.pack_start(hbox, False) + return vbox + + def update_status_bar(self): + interface = self.wallet.interface + if self.funds_error: + text = "Not enough funds" + elif interface.is_connected: + self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime)) + if self.wallet.blocks == -1: + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + text = "Connecting..." + elif self.wallet.blocks == 0: + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + text = "Server not ready" + elif not self.wallet.up_to_date: + self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU) + text = "Synchronizing..." + else: + self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) + self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime)) + c, u = self.wallet.get_balance() + text = "Balance: %s "%( format_satoshis(c) ) + if u: text += "[%s unconfirmed]"%( format_satoshis(u,True).strip() ) + else: + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.host, self.wallet.blocks)) + text = "Not connected" + + self.status_bar.pop(self.context_id) + self.status_bar.push(self.context_id, text) + + if self.wallet.was_updated and self.wallet.up_to_date: + self.update_history_tab() + self.update_receiving_tab() + # addressbook too... + self.info.set_text( self.wallet.banner ) + self.wallet.was_updated = False + + + def update_receiving_tab(self): + self.recv_list.clear() + for address in self.wallet.all_addresses(): + if self.wallet.is_change(address):continue + label = self.wallet.labels.get(address) + n = 0 + h = self.wallet.history.get(address,[]) + for item in h: + if not item['is_input'] : n=n+1 + tx = "None" if n==0 else "%d"%n + self.recv_list.append((address, label, tx )) + + def update_sending_tab(self): + # detect addresses that are not mine in history, add them here... + self.addressbook_list.clear() + for alias, v in self.wallet.aliases.items(): + s, target = v + label = self.wallet.labels.get(alias) + self.addressbook_list.append((alias, label, '-')) + + for address in self.wallet.addressbook: + label = self.wallet.labels.get(address) + n = 0 + for item in self.wallet.tx_history.values(): + if address in item['outputs'] : n=n+1 + tx = "None" if n==0 else "%d"%n + self.addressbook_list.append((address, label, tx)) + + def update_history_tab(self): + cursor = self.history_treeview.get_cursor()[0] + self.history_list.clear() + balance = 0 + for tx in self.wallet.get_tx_history(): + tx_hash = tx['tx_hash'] + if tx['height']: + conf = self.wallet.blocks - tx['height'] + 1 + time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] + conf_icon = gtk.STOCK_APPLY + else: + conf = 0 + time_str = 'pending' + conf_icon = gtk.STOCK_EXECUTE + v = tx['value'] + balance += v + label = self.wallet.labels.get(tx_hash) + is_default_label = (label == '') or (label is None) + if is_default_label: label = tx['default_label'] + tooltip = tx_hash + "\n%d confirmations"%conf + + # tx = self.wallet.tx_history.get(tx_hash) + details = "Transaction Details:\n\n" \ + + "Transaction ID:\n" + tx_hash + "\n\n" \ + + "Status: %d confirmations\n\n"%conf \ + + "Date: %s\n\n"%time_str \ + + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ + + "Outputs:\n-"+ '\n-'.join(tx['outputs']) + r = self.wallet.receipts.get(tx_hash) + if r: + details += "\n_______________________________________" \ + + '\n\nSigned URI: ' + r[2] \ + + "\n\nSigned by: " + r[0] \ + + '\n\nSignature: ' + r[1] + + + self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, + format_satoshis(v,True), format_satoshis(balance), tooltip, details] ) + if cursor: self.history_treeview.set_cursor( cursor ) + + + + def newaddress_dialog(self, w): + + title = "New Contact" + dialog = gtk.Dialog(title, parent=self.window, + flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, + buttons= ("cancel", 0, "ok",1) ) + dialog.show() + + label = gtk.HBox() + label_label = gtk.Label('Label:') + label_label.set_size_request(120,10) + label_label.show() + label.pack_start(label_label) + label_entry = gtk.Entry() + label_entry.show() + label.pack_start(label_entry) + label.show() + dialog.vbox.pack_start(label, False, True, 5) + + address = gtk.HBox() + address_label = gtk.Label('Address:') + address_label.set_size_request(120,10) + address_label.show() + address.pack_start(address_label) + address_entry = gtk.Entry() + address_entry.show() + address.pack_start(address_entry) + address.show() + dialog.vbox.pack_start(address, False, True, 5) + + result = dialog.run() + address = address_entry.get_text() + label = label_entry.get_text() + dialog.destroy() + + if result == 1: + if self.wallet.is_valid(address): + self.wallet.addressbook.append(address) + if label: self.wallet.labels[address] = label + self.wallet.save() + self.update_sending_tab() + else: + errorDialog = gtk.MessageDialog( + parent=self.window, + flags=gtk.DIALOG_MODAL, + buttons= gtk.BUTTONS_CLOSE, + message_format = "Invalid address") + errorDialog.show() + errorDialog.run() + errorDialog.destroy() + + + +class ElectrumGui(): + + def __init__(self, wallet): + self.wallet = wallet + + def main(self, url=None): + ew = ElectrumWindow(self.wallet) + if url: ew.set_url(url) + gtk.main() + + def restore_or_create(self): + return restore_create_dialog(self.wallet) diff --git a/gui_qt.py b/gui_qt.py @@ -0,0 +1,1114 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2012 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 sys, time, datetime, re + +# todo: see PySide + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui +from interface import DEFAULT_SERVERS + +try: + import icons_rc +except: + print "Could not import icons_rp.py" + print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'" + sys.exit(1) + +from wallet import format_satoshis +from decimal import Decimal + +import platform +MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' + + +def numbify(entry, is_int = False): + text = unicode(entry.text()).strip() + chars = '0123456789' + if not is_int: chars +='.' + s = ''.join([i for i in text if i in chars]) + if not is_int: + if '.' in s: + p = s.find('.') + s = s.replace('.','') + s = s[:p] + '.' + s[p:p+8] + try: + amount = int( Decimal(s) * 100000000 ) + except: + amount = None + else: + try: + amount = int( s ) + except: + amount = None + entry.setText(s) + return amount + + +class Timer(QtCore.QThread): + def run(self): + while True: + self.emit(QtCore.SIGNAL('timersignal')) + time.sleep(0.5) + +class EnterButton(QPushButton): + def __init__(self, text, func): + QPushButton.__init__(self, text) + self.func = func + self.clicked.connect(func) + + def keyPressEvent(self, e): + if e.key() == QtCore.Qt.Key_Return: + apply(self.func,()) + +class StatusBarButton(QPushButton): + def __init__(self, icon, tooltip, func): + QPushButton.__init__(self, icon, '') + self.setToolTip(tooltip) + self.setFlat(True) + self.setMaximumWidth(25) + self.clicked.connect(func) + self.func = func + + def keyPressEvent(self, e): + if e.key() == QtCore.Qt.Key_Return: + apply(self.func,()) + + +class QRCodeWidget(QWidget): + + def __init__(self, addr): + super(QRCodeWidget, self).__init__() + self.setGeometry(300, 300, 350, 350) + self.set_addr(addr) + + def set_addr(self, addr): + import pyqrnative + self.addr = addr + self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L) + self.qr.addData(addr) + self.qr.make() + + def paintEvent(self, e): + qp = QtGui.QPainter() + qp.begin(self) + boxsize = 7 + size = self.qr.getModuleCount()*boxsize + k = self.qr.getModuleCount() + black = QColor(0, 0, 0, 255) + white = QColor(255, 255, 255, 255) + for r in range(k): + for c in range(k): + if self.qr.isDark(r, c): + qp.setBrush(black) + qp.setPen(black) + else: + qp.setBrush(white) + qp.setPen(white) + qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize) + qp.end() + + + +def ok_cancel_buttons(dialog): + hbox = QHBoxLayout() + hbox.addStretch(1) + b = QPushButton("OK") + hbox.addWidget(b) + b.clicked.connect(dialog.accept) + b = QPushButton("Cancel") + hbox.addWidget(b) + b.clicked.connect(dialog.reject) + return hbox + + +class ElectrumWindow(QMainWindow): + + def __init__(self, wallet): + QMainWindow.__init__(self) + self.wallet = wallet + self.wallet.gui_callback = self.update_callback + + self.funds_error = False + + self.tabs = tabs = QTabWidget(self) + tabs.addTab(self.create_history_tab(), 'History') + tabs.addTab(self.create_send_tab(), 'Send') + tabs.addTab(self.create_receive_tab(), 'Receive') + tabs.addTab(self.create_contacts_tab(),'Contacts') + tabs.addTab(self.create_wall_tab(), 'Wall') + tabs.setMinimumSize(600, 400) + tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.setCentralWidget(tabs) + self.create_status_bar() + self.setGeometry(100,100,840,400) + self.setWindowTitle( 'Electrum ' + self.wallet.electrum_version ) + self.show() + + QShortcut(QKeySequence("Ctrl+W"), self, self.close) + QShortcut(QKeySequence("Ctrl+Q"), self, self.close) + QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() )) + QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() )) + + self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet) + + + def connect_slots(self, sender): + self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient) + self.previous_payto_e='' + + def check_recipient(self): + if self.payto_e.hasFocus(): + return + r = unicode( self.payto_e.text() ) + if r != self.previous_payto_e: + self.previous_payto_e = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = self.wallet.get_alias(r, True, self.show_message, self.question) + except: + return + if to_address: + s = r + ' <' + to_address + '>' + self.payto_e.setText(s) + + + def update_callback(self): + self.emit(QtCore.SIGNAL('updatesignal')) + + def update_wallet(self): + if self.wallet.interface.is_connected: + if self.wallet.blocks == -1: + text = "Connecting..." + icon = QIcon(":icons/status_disconnected.png") + elif self.wallet.blocks == 0: + text = "Server not ready" + icon = QIcon(":icons/status_disconnected.png") + elif not self.wallet.up_to_date: + text = "Synchronizing..." + icon = QIcon(":icons/status_waiting.png") + else: + c, u = self.wallet.get_balance() + text = "Balance: %s "%( format_satoshis(c) ) + if u: text += "[%s unconfirmed]"%( format_satoshis(u,True).strip() ) + 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.statusBar().showMessage(text) + self.status_button.setIcon( icon ) + + if self.wallet.up_to_date: + self.textbox.setText( self.wallet.banner ) + self.update_history_tab() + self.update_receive_tab() + self.update_contacts_tab() + + + def create_history_tab(self): + self.history_list = w = QTreeWidget(self) + #print w.getContentsMargins() + w.setColumnCount(5) + w.setColumnWidth(0, 40) + w.setColumnWidth(1, 140) + w.setColumnWidth(2, 350) + w.setColumnWidth(3, 140) + w.setColumnWidth(4, 140) + w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] ) + self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details) + self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked) + self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed) + return w + + def tx_details(self, item, column): + tx_hash = str(item.toolTip(0)) + tx = self.wallet.tx_history.get(tx_hash) + + if tx['height']: + conf = self.wallet.blocks - tx['height'] + 1 + time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] + else: + conf = 0 + time_str = 'pending' + + tx_details = "Transaction Details:\n\n" \ + + "Transaction ID:\n" + tx_hash + "\n\n" \ + + "Status: %d confirmations\n\n"%conf \ + + "Date: %s\n\n"%time_str \ + + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ + + "Outputs:\n-"+ '\n-'.join(tx['outputs']) + + r = self.wallet.receipts.get(tx_hash) + if r: + tx_details += "\n_______________________________________" \ + + '\n\nSigned URI: ' + r[2] \ + + "\n\nSigned by: " + r[0] \ + + '\n\nSignature: ' + r[1] + + QMessageBox.information(self, 'Details', tx_details, 'OK') + + + 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) + self.is_edit=False + + def tx_label_changed(self, item, column): + if self.is_edit: + return + self.is_edit=True + tx_hash = str(item.toolTip(0)) + tx = self.wallet.tx_history.get(tx_hash) + s = self.wallet.labels.get(tx_hash) + text = unicode( item.text(2) ) + if text: + self.wallet.labels[tx_hash] = text + item.setForeground(2, QBrush(QColor('black'))) + else: + if s: self.wallet.labels.pop(tx_hash) + text = tx['default_label'] + item.setText(2, text) + item.setForeground(2, QBrush(QColor('gray'))) + self.is_edit=False + + def address_label_clicked(self, item, column, l): + if column==1 and item.isSelected(): + addr = unicode( item.text(0) ) + if addr in map(lambda x:x[1], self.wallet.aliases.values()): + return + item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + l.editItem( item, column ) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + + def address_label_changed(self, item, column, l): + addr = unicode( item.text(0) ) + text = unicode( item.text(1) ) + if text: + self.wallet.labels[addr] = text + else: + s = self.wallet.labels.get(addr) + if s: self.wallet.labels.pop(addr) + self.update_history_tab() + + def update_history_tab(self): + self.history_list.clear() + balance = 0 + for tx in self.wallet.get_tx_history(): + tx_hash = tx['tx_hash'] + if tx['height']: + conf = self.wallet.blocks - tx['height'] + 1 + time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] + icon = QIcon(":icons/confirmed.png") + else: + conf = 0 + time_str = 'pending' + icon = QIcon(":icons/unconfirmed.png") + v = tx['value'] + balance += v + label = self.wallet.labels.get(tx_hash) + is_default_label = (label == '') or (label is None) + if is_default_label: label = tx['default_label'] + + item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True), format_satoshis(balance)] ) + item.setFont(2, QFont(MONOSPACE_FONT)) + item.setFont(3, QFont(MONOSPACE_FONT)) + item.setFont(4, QFont(MONOSPACE_FONT)) + item.setToolTip(0, tx_hash) + if is_default_label: + item.setForeground(2, QBrush(QColor('grey'))) + + item.setIcon(0, icon) + self.history_list.insertTopLevelItem(0,item) + + + def create_send_tab(self): + w = QWidget() + + grid = QGridLayout() + grid.setSpacing(8) + grid.setColumnMinimumWidth(3,300) + grid.setColumnStretch(4,1) + + self.payto_e = QLineEdit() + grid.addWidget(QLabel('Pay to'), 1, 0) + grid.addWidget(self.payto_e, 1, 1, 1, 3) + + self.message_e = QLineEdit() + grid.addWidget(QLabel('Description'), 2, 0) + grid.addWidget(self.message_e, 2, 1, 1, 3) + + self.amount_e = QLineEdit() + grid.addWidget(QLabel('Amount'), 3, 0) + grid.addWidget(self.amount_e, 3, 1, 1, 2) + + self.fee_e = QLineEdit() + grid.addWidget(QLabel('Fee'), 4, 0) + grid.addWidget(self.fee_e, 4, 1, 1, 2) + + b = EnterButton("Send", self.do_send) + grid.addWidget(b, 5, 1) + + b = EnterButton("Clear",self.do_clear) + grid.addWidget(b, 5, 2) + + self.payto_sig = QLabel('') + grid.addWidget(self.payto_sig, 6, 0, 1, 4) + + w.setLayout(grid) + w.show() + + w2 = QWidget() + vbox = QVBoxLayout() + vbox.addWidget(w) + vbox.addStretch(1) + w2.setLayout(vbox) + + def entry_changed( is_fee ): + self.funds_error = False + amount = numbify(self.amount_e) + fee = numbify(self.fee_e) + if not is_fee: fee = None + if amount is None: + return + inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) + if not is_fee: + self.fee_e.setText( str( Decimal( fee ) / 100000000 ) ) + if inputs: + palette = QPalette() + palette.setColor(self.amount_e.foregroundRole(), QColor('black')) + else: + palette = QPalette() + palette.setColor(self.amount_e.foregroundRole(), QColor('red')) + self.funds_error = True + self.amount_e.setPalette(palette) + self.fee_e.setPalette(palette) + + self.amount_e.textChanged.connect(lambda: entry_changed(False) ) + self.fee_e.textChanged.connect(lambda: entry_changed(True) ) + + return w2 + + def do_send(self): + + label = unicode( self.message_e.text() ) + r = unicode( self.payto_e.text() ) + r = r.strip() + + m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) + m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + + if m1: + to_address = self.wallet.get_alias(r, True, self.show_message, self.question) + if not to_address: + return + elif m2: + to_address = m2.group(5) + else: + to_address = r + + if not self.wallet.is_valid(to_address): + QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK') + return + + try: + amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 ) + except: + QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK') + return + try: + fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 ) + except: + QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK') + return + + if self.wallet.use_encryption: + password = self.password_dialog() + if not password: + return + else: + password = None + + try: + tx = self.wallet.mktx( to_address, amount, label, password, fee ) + except BaseException, e: + self.show_message(e.message) + return + + status, msg = self.wallet.sendtx( tx ) + if status: + QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK') + self.do_clear() + self.update_contacts_tab() + else: + QMessageBox.warning(self, 'Error', msg, 'OK') + + + def set_url(self, url): + payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) + self.tabs.setCurrentIndex(1) + self.payto_e.setText(payto) + self.message_e.setText(message) + self.amount_e.setText(amount) + if identity: + self.set_frozen(self.payto_e,True) + self.set_frozen(self.amount_e,True) + self.set_frozen(self.message_e,True) + self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity ) + else: + self.payto_sig.setVisible(False) + + def do_clear(self): + self.payto_sig.setVisible(False) + for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]: + e.setText('') + self.set_frozen(e,False) + + def set_frozen(self,entry,frozen): + if frozen: + entry.setReadOnly(True) + entry.setFrame(False) + palette = QPalette() + palette.setColor(entry.backgroundRole(), QColor('lightgray')) + entry.setPalette(palette) + else: + entry.setReadOnly(False) + entry.setFrame(True) + palette = QPalette() + palette.setColor(entry.backgroundRole(), QColor('white')) + entry.setPalette(palette) + + + def make_address_list(self, is_recv): + + l = QTreeWidget(self) + l.setColumnCount(3) + l.setColumnWidth(0, 350) + l.setColumnWidth(1, 330) + l.setColumnWidth(2, 20) + l.setHeaderLabels( ['Address', 'Label','Tx']) + + vbox = QVBoxLayout() + vbox.setMargin(0) + vbox.setSpacing(0) + vbox.addWidget(l) + + hbox = QHBoxLayout() + hbox.setMargin(0) + hbox.setSpacing(0) + + def get_addr(l): + i = l.currentItem() + if not i: return + addr = unicode( i.text(0) ) + return addr + + qrButton = EnterButton("QR",lambda: self.show_address_qrcode(get_addr(l))) + + def copy2clipboard(addr): + self.app.clipboard().setText(addr) + copyButton = EnterButton("Copy to Clipboard", lambda: copy2clipboard(get_addr(l))) + hbox.addWidget(qrButton) + hbox.addWidget(copyButton) + if not is_recv: + addButton = EnterButton("New", self.newaddress_dialog) + hbox.addWidget(addButton) + def payto(addr): + if not addr:return + self.tabs.setCurrentIndex(1) + self.payto_e.setText(addr) + self.amount_e.setFocus() + paytoButton = EnterButton('Pay to', lambda: payto(get_addr(l))) + hbox.addWidget(paytoButton) + hbox.addStretch(1) + buttons = QWidget() + buttons.setLayout(hbox) + vbox.addWidget(buttons) + + w = QWidget() + w.setLayout(vbox) + return w, l + + def create_receive_tab(self): + w, l = self.make_address_list(True) + self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) + self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) + self.receive_list = l + return w + + def create_contacts_tab(self): + w, l = self.make_address_list(False) + self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) + self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) + self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details) + self.contacts_list = l + return w + + def update_receive_tab(self): + self.receive_list.clear() + for address in self.wallet.all_addresses(): + if self.wallet.is_change(address):continue + label = self.wallet.labels.get(address,'') + n = 0 + h = self.wallet.history.get(address,[]) + for item in h: + if not item['is_input'] : n=n+1 + tx = "None" if n==0 else "%d"%n + item = QTreeWidgetItem( [ address, label, tx] ) + item.setFont(0, QFont(MONOSPACE_FONT)) + self.receive_list.addTopLevelItem(item) + + def show_contact_details(self, item, column): + m = unicode(item.text(0)) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self-signed" + msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] + QMessageBox.information(self, 'Alias', msg, 'OK') + + def update_contacts_tab(self): + self.contacts_list.clear() + for alias, v in self.wallet.aliases.items(): + s, target = v + item = QTreeWidgetItem( [ target, alias, '-'] ) + self.contacts_list.addTopLevelItem(item) + + for address in self.wallet.addressbook: + label = self.wallet.labels.get(address,'') + n = 0 + for item in self.wallet.tx_history.values(): + if address in item['outputs'] : n=n+1 + tx = "None" if n==0 else "%d"%n + item = QTreeWidgetItem( [ address, label, tx] ) + item.setFont(0, QFont(MONOSPACE_FONT)) + self.contacts_list.addTopLevelItem(item) + + + def create_wall_tab(self): + self.textbox = textbox = QTextEdit(self) + textbox.setFont(QFont(MONOSPACE_FONT)) + textbox.setReadOnly(True) + return textbox + + def create_status_bar(self): + sb = QStatusBar() + sb.setFixedHeight(35) + 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/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 newaddress_dialog(self): + text, ok = QInputDialog.getText(self, 'New Contact', 'Address:') + address = unicode(text) + if ok: + if self.wallet.is_valid(address): + self.wallet.addressbook.append(address) + self.wallet.save() + self.update_contacts_tab() + else: + QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK') + + @staticmethod + def show_seed_dialog(wallet, parent=None): + import mnemonic + if wallet.use_encryption: + password = parent.password_dialog() + if not password: return + else: + password = None + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + QMessageBox.warning(parent, 'Error', 'Invalid Password', 'OK') + return + + msg = "Your wallet generation seed is:\n\n" + seed \ + + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \ + + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" \ + + ' '.join(mnemonic.mn_encode(seed)) + "\"" + + QMessageBox.information(parent, 'Seed', msg, 'OK') + if parent: ElectrumWindow.show_seed_qrcode(seed) + + @staticmethod + def show_seed_qrcode(seed): + if not seed: return + d = QDialog(None) + d.setModal(1) + d.setWindowTitle(seed) + d.setMinimumSize(270, 300) + vbox = QVBoxLayout() + vbox.addWidget(QRCodeWidget(seed)) + hbox = QHBoxLayout() + hbox.addStretch(1) + b = QPushButton("OK") + hbox.addWidget(b) + b.clicked.connect(d.accept) + + vbox.addLayout(hbox) + d.setLayout(vbox) + d.exec_() + + def show_address_qrcode(self,address): + if not address: return + d = QDialog(None) + d.setModal(1) + d.setWindowTitle(address) + d.setMinimumSize(270, 350) + vbox = QVBoxLayout() + qrw = QRCodeWidget(address) + vbox.addWidget(qrw) + + hbox = QHBoxLayout() + amount_e = QLineEdit() + hbox.addWidget(QLabel('Amount')) + hbox.addWidget(amount_e) + vbox.addLayout(hbox) + + #hbox = QHBoxLayout() + #label_e = QLineEdit() + #hbox.addWidget(QLabel('Label')) + #hbox.addWidget(label_e) + #vbox.addLayout(hbox) + + def amount_changed(): + amount = numbify(amount_e) + #label = str( label_e.getText() ) + if amount is not None: + qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000))) + else: + qrw.set_addr( address ) + qrw.repaint() + + def do_save(): + import bmp + bmp.save_qrcode(qrw.qr, "qrcode.bmp") + self.show_message("QR code saved to file 'qrcode.bmp'") + + amount_e.textChanged.connect( amount_changed ) + + hbox = QHBoxLayout() + hbox.addStretch(1) + b = QPushButton("Save") + b.clicked.connect(do_save) + hbox.addWidget(b) + b = QPushButton("Close") + hbox.addWidget(b) + b.clicked.connect(d.accept) + + vbox.addLayout(hbox) + d.setLayout(vbox) + d.exec_() + + def question(self, msg): + return QMessageBox.question(self, 'Message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes + + def show_message(self, msg): + QMessageBox.information(self, 'Message', msg, 'OK') + + def password_dialog(self ): + d = QDialog(self) + d.setModal(1) + + pw = QLineEdit() + pw.setEchoMode(2) + + vbox = QVBoxLayout() + msg = 'Please enter your password' + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + grid.addWidget(QLabel('Password'), 1, 0) + grid.addWidget(pw, 1, 1) + vbox.addLayout(grid) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + return unicode(pw.text()) + + @staticmethod + def change_password_dialog( wallet, parent=None ): + d = QDialog(parent) + d.setModal(1) + + pw = QLineEdit() + pw.setEchoMode(2) + new_pw = QLineEdit() + new_pw.setEchoMode(2) + conf_pw = QLineEdit() + conf_pw.setEchoMode(2) + + vbox = QVBoxLayout() + if parent: + msg = 'Your wallet is encrypted. Use this dialog to change your password.\nTo disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' + else: + msg = "Please choose a password to encrypt your wallet keys.\nLeave these fields empty if you want to disable encryption." + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + + if wallet.use_encryption: + grid.addWidget(QLabel('Password'), 1, 0) + grid.addWidget(pw, 1, 1) + + grid.addWidget(QLabel('New Password'), 2, 0) + grid.addWidget(new_pw, 2, 1) + + grid.addWidget(QLabel('Confirm Password'), 3, 0) + grid.addWidget(conf_pw, 3, 1) + vbox.addLayout(grid) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + + password = unicode(pw.text()) if wallet.use_encryption else None + new_password = unicode(new_pw.text()) + new_password2 = unicode(conf_pw.text()) + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + QMessageBox.warning(parent, 'Error', 'Incorrect Password', 'OK') + return + + if new_password != new_password2: + QMessageBox.warning(parent, 'Error', 'Passwords do not match', 'OK') + return + + wallet.update_password(seed, new_password) + + @staticmethod + def seed_dialog(wallet, parent=None): + d = QDialog(parent) + d.setModal(1) + + vbox = QVBoxLayout() + msg = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + + seed_e = QLineEdit() + grid.addWidget(QLabel('Seed or mnemonic'), 1, 0) + grid.addWidget(seed_e, 1, 1) + + gap_e = QLineEdit() + gap_e.setText("5") + grid.addWidget(QLabel('Gap limit'), 2, 0) + grid.addWidget(gap_e, 2, 1) + gap_e.textChanged.connect(lambda: numbify(gap_e,True)) + vbox.addLayout(grid) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + + try: + gap = int(unicode(gap_e.text())) + except: + QMessageBox.warning(None, 'Error', 'error', 'OK') + sys.exit(0) + + try: + seed = unicode(seed_e.text()) + seed.decode('hex') + except: + import mnemonic + print "not hex, trying decode" + try: + seed = mnemonic.mn_decode( seed.split(' ') ) + except: + QMessageBox.warning(None, 'Error', 'I cannot decode this', 'OK') + sys.exit(0) + if not seed: + QMessageBox.warning(None, 'Error', 'no seed', 'OK') + sys.exit(0) + + wallet.seed = str(seed) + #print repr(wallet.seed) + wallet.gap_limit = gap + return True + + + def settings_dialog(self): + d = QDialog(self) + d.setModal(1) + + vbox = QVBoxLayout() + + msg = 'Here are the settings of your wallet.' + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + + fee_e = QLineEdit() + fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) ) + grid.addWidget(QLabel('Fee per tx. input'), 2, 0) + grid.addWidget(fee_e, 2, 1) + vbox.addLayout(grid) + fee_e.textChanged.connect(lambda: numbify(fee_e,False)) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + + fee = unicode(fee_e.text()) + try: + fee = int( 100000000 * Decimal(fee) ) + except: + QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK') + return + + self.wallet.fee = fee + self.wallet.save() + + @staticmethod + def network_dialog(wallet, parent=None): + interface = wallet.interface + if parent: + if interface.is_connected: + status = "Connected to %s:%d\n%d blocks\nresponse time: %f"%(interface.host, interface.port, wallet.blocks, interface.rtime) + else: + status = "Not connected" + server = wallet.server + else: + import random + status = "Please choose a server." + server = random.choice( DEFAULT_SERVERS ) + + plist = {} + for item in wallet.interface.servers: + host, pp = item + z = {} + for item2 in pp: + protocol, port = item2 + z[protocol] = port + plist[host] = z + + d = QDialog(parent) + d.setModal(1) + d.setWindowTitle('Server') + d.setMinimumSize(375, 20) + + vbox = QVBoxLayout() + vbox.setSpacing(20) + + hbox = QHBoxLayout() + l = QLabel() + l.setPixmap(QPixmap(":icons/network.png")) + hbox.addWidget(l) + hbox.addWidget(QLabel(status)) + + vbox.addLayout(hbox) + + hbox = QHBoxLayout() + host_line = QLineEdit() + host_line.setText(server) + hbox.addWidget(QLabel('Connect to:')) + hbox.addWidget(host_line) + vbox.addLayout(hbox) + + hbox = QHBoxLayout() + + buttonGroup = QGroupBox("protocol") + radio1 = QRadioButton("tcp", buttonGroup) + radio2 = QRadioButton("http", buttonGroup) + + def current_line(): + return unicode(host_line.text()).split(':') + + def set_button(protocol): + if protocol == 't': + radio1.setChecked(1) + elif protocol == 'h': + radio2.setChecked(1) + + def set_protocol(protocol): + host = current_line()[0] + pp = plist[host] + if protocol not in pp.keys(): + protocol = pp.keys()[0] + set_button(protocol) + port = pp[protocol] + host_line.setText( host + ':' + port + ':' + protocol) + + radio1.clicked.connect(lambda x: set_protocol('t') ) + radio2.clicked.connect(lambda x: set_protocol('h') ) + + set_button(current_line()[2]) + + hbox.addWidget(QLabel('Protocol:')) + hbox.addWidget(radio1) + hbox.addWidget(radio2) + + vbox.addLayout(hbox) + + if wallet.interface.servers: + servers_list = QTreeWidget(parent) + servers_list.setHeaderLabels( [ 'Active servers'] ) + servers_list.setMaximumHeight(150) + for host in plist.keys(): + servers_list.addTopLevelItem(QTreeWidgetItem( [ host ] )) + + def do_set_line(x): + host = unicode(x.text(0)) + pp = plist[host] + if 't' in pp.keys(): + protocol = 't' + else: + protocol = pp.keys()[0] + port = pp[protocol] + host_line.setText( host + ':' + port + ':' + protocol) + set_button(protocol) + + servers_list.connect(servers_list, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line) + vbox.addWidget(servers_list) + else: + hbox = QHBoxLayout() + hbox.addWidget(QLabel('No nodes available')) + b = EnterButton("Find nodes", lambda: wallet.interface.get_servers(wallet) ) + hbox.addWidget(b) + vbox.addLayout(hbox) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + server = unicode( host_line.text() ) + + try: + wallet.set_server(server) + except: + QMessageBox.information(None, 'Error', 'error', 'OK') + if parent == None: + sys.exit(1) + else: + return + + return True + + + +class ElectrumGui(): + + def __init__(self, wallet): + self.wallet = wallet + self.app = QApplication(sys.argv) + + def waiting_dialog(self): + + s = Timer() + s.start() + w = QDialog() + w.resize(200, 70) + w.setWindowTitle('Electrum') + l = QLabel('') + vbox = QVBoxLayout() + vbox.addWidget(l) + w.setLayout(vbox) + w.show() + def f(): + if self.wallet.up_to_date: w.close() + else: + l.setText("Please wait...\nGenerating addresses: %d"%len(self.wallet.all_addresses())) + pass + w.connect(s, QtCore.SIGNAL('timersignal'), f) + self.wallet.interface.poke() + w.exec_() + w.destroy() + + + def restore_or_create(self): + + msg = "Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" + r = QMessageBox.question(None, 'Message', msg, 'create', 'restore', 'cancel', 0, 2) + if r==2: return False + + is_recovery = (r==1) + wallet = self.wallet + # ask for the server. + if not ElectrumWindow.network_dialog( wallet, parent=None ): return False + + if not is_recovery: + wallet.new_seed(None) + wallet.init_mpk( wallet.seed ) + wallet.up_to_date_event.clear() + wallet.up_to_date = False + self.waiting_dialog() + # run a dialog indicating the seed, ask the user to remember it + ElectrumWindow.show_seed_dialog(wallet) + #ask for password + ElectrumWindow.change_password_dialog(wallet) + else: + # ask for seed and gap. + if not ElectrumWindow.seed_dialog( wallet ): return False + wallet.init_mpk( wallet.seed ) + wallet.up_to_date_event.clear() + wallet.up_to_date = False + self.waiting_dialog() + if wallet.is_found(): + # history and addressbook + wallet.update_tx_history() + wallet.fill_addressbook() + print "recovery successful" + wallet.save() + else: + QMessageBox.information(None, 'Message', "No transactions found for this seed", 'OK') + + wallet.save() + return True + + def main(self,url): + s = Timer() + s.start() + w = ElectrumWindow(self.wallet) + if url: w.set_url(url) + w.app = self.app + w.connect_slots(s) + w.update_wallet() + + self.app.exec_() diff --git a/client/icons.qrc b/icons.qrc diff --git a/client/icons/confirmed.png b/icons/confirmed.png Binary files differ. diff --git a/client/icons/lock.png b/icons/lock.png Binary files differ. diff --git a/client/icons/lock.svg b/icons/lock.svg diff --git a/client/icons/network.png b/icons/network.png Binary files differ. diff --git a/client/icons/preferences.png b/icons/preferences.png Binary files differ. diff --git a/client/icons/seed.png b/icons/seed.png Binary files differ. diff --git a/client/icons/status_connected.png b/icons/status_connected.png Binary files differ. diff --git a/client/icons/status_disconnected.png b/icons/status_disconnected.png Binary files differ. diff --git a/client/icons/status_disconnected.svg b/icons/status_disconnected.svg diff --git a/client/icons/status_waiting.png b/icons/status_waiting.png Binary files differ. diff --git a/client/icons/status_waiting.svg b/icons/status_waiting.svg diff --git a/client/icons/unconfirmed.png b/icons/unconfirmed.png Binary files differ. diff --git a/client/icons/unconfirmed.svg b/icons/unconfirmed.svg diff --git a/interface.py b/interface.py @@ -0,0 +1,425 @@ +#!/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 random, socket, ast, re +import threading, traceback, sys, time, json, Queue + +DEFAULT_TIMEOUT = 5 +DEFAULT_SERVERS = [ 'ecdsa.org:50001:t', 'electrum.novit.ro:50001:t', 'electrum.bitcoins.sk:50001:t'] # list of default servers + + +def old_to_new(s): + s = s.replace("'blk_hash'", "'block_hash'") + s = s.replace("'pos'", "'index'") + s = s.replace("'nTime'", "'timestamp'") + s = s.replace("'is_in'", "'is_input'") + s = s.replace("'raw_scriptPubKey'","'raw_output_script'") + return s + + +class Interface(threading.Thread): + def __init__(self, host, port): + threading.Thread.__init__(self) + self.daemon = True + self.host = host + self.port = port + + self.servers = [] # actual list from IRC + self.rtime = 0 + + self.is_connected = True + self.poll_interval = 1 + + #json + self.message_id = 0 + self.responses = Queue.Queue() + self.methods = {} + + def poke(self): + # push a fake response so that the getting thread exits its loop + self.responses.put(None) + + def queue_json_response(self, c): + + #print "<--",c + msg_id = c.get('id') + error = c.get('error') + + if error: + print "received error:", c + return + + if msg_id is not None: + method, params = self.methods.pop(msg_id) + result = c.get('result') + else: + # notification + method = c.get('method') + params = c.get('params') + + if method == 'blockchain.numblocks.subscribe': + result = params[0] + params = [] + + elif method == 'blockchain.address.subscribe': + addr = params[0] + result = params[1] + params = [addr] + + self.responses.put({'method':method, 'params':params, 'result':result}) + + + + def subscribe(self, addresses): + messages = [] + for addr in addresses: + messages.append(('blockchain.address.subscribe', [addr])) + self.send(messages) + + + def get_servers(self, wallet): + # loop over default servers + # requesting servers could be an independent process + addresses = wallet.all_addresses() + version = wallet.electrum_version + + for server in DEFAULT_SERVERS: + print "connecting to", server + try: + self.host = server + self.start_session(addresses, version) + wallet.host = self.host + break + except socket.timeout: + continue + except socket.error: + continue + except: + traceback.print_exc(file=sys.stdout) + + + def start_session(self, addresses, version): + #print "Starting new session: %s:%d"%(self.host,self.port) + self.send([('server.version', [version]), ('server.banner',[]), ('blockchain.numblocks.subscribe',[]), ('server.peers.subscribe',[])]) + self.subscribe(addresses) + + +class PollingInterface(Interface): + """ non-persistent connection. synchronous calls""" + + def __init__(self, host, port): + Interface.__init__(self, host, port) + self.session_id = None + + def get_history(self, address): + self.send([('blockchain.address.get_history', [address] )]) + + def poll(self): + pass + #if is_new or wallet.remote_url: + # self.was_updated = True + # is_new = wallet.synchronize() + # wallet.update_tx_history() + # wallet.save() + # return is_new + #else: + # return False + + def run(self): + self.is_connected = True + while self.is_connected: + try: + if self.session_id: + self.poll() + time.sleep(self.poll_interval) + except socket.gaierror: + break + except socket.error: + break + except: + traceback.print_exc(file=sys.stdout) + break + + self.is_connected = False + self.poke() + + + + + + + + + +class HttpStratumInterface(PollingInterface): + + def poll(self): + self.send([]) + + def send(self, messages): + import urllib2, json, time, cookielib + + cj = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) + urllib2.install_opener(opener) + + t1 = time.time() + + data = [] + for m in messages: + method, params = m + if type(params) != type([]): params = [params] + data.append( { 'method':method, 'id':self.message_id, 'params':params } ) + self.methods[self.message_id] = method, params + self.message_id += 1 + + if data: + data_json = json.dumps(data) + else: + # poll with GET + data_json = None + + host = 'http://%s:%d'%( self.host, self.port ) + headers = {'content-type': 'application/json'} + if self.session_id: + headers['cookie'] = 'SESSION=%s'%self.session_id + + req = urllib2.Request(host, data_json, headers) + response_stream = urllib2.urlopen(req) + + for index, cookie in enumerate(cj): + if cookie.name=='SESSION': + self.session_id = cookie.value + + response = response_stream.read() + if response: + response = json.loads( response ) + if type(response) is not type([]): + self.queue_json_response(response) + else: + for item in response: + self.queue_json_response(item) + + if response: + self.poll_interval = 1 + else: + if self.poll_interval < 15: + self.poll_interval += 1 + #print self.poll_interval, response + + self.rtime = time.time() - t1 + self.is_connected = True + + + + +class TcpStratumInterface(Interface): + """json-rpc over persistent TCP connection, asynchronous""" + + def __init__(self, host, port): + Interface.__init__(self, host, port) + self.s = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) + self.s.settimeout(5) + self.s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + try: + self.s.connect(( self.host, self.port)) + self.is_connected = True + except: + self.is_connected = False + print "not connected" + + def run(self): + try: + out = '' + while self.is_connected: + try: msg = self.s.recv(1024) + except socket.timeout: + continue + out += msg + if msg == '': + self.is_connected = False + print "disconnected." + + while True: + s = out.find('\n') + if s==-1: break + c = out[0:s] + out = out[s+1:] + c = json.loads(c) + self.queue_json_response(c) + + except: + traceback.print_exc(file=sys.stdout) + + self.is_connected = False + print "poking" + self.poke() + + def send(self, messages): + out = '' + for m in messages: + method, params = m + request = json.dumps( { 'id':self.message_id, 'method':method, 'params':params } ) + self.methods[self.message_id] = method, params + #print "-->",request + self.message_id += 1 + out += request + '\n' + self.s.send( out ) + + def get_history(self, addr): + self.send([('blockchain.address.get_history', [addr])]) + + + + + +class WalletSynchronizer(threading.Thread): + + def __init__(self, wallet, loop=False): + threading.Thread.__init__(self) + self.daemon = True + self.wallet = wallet + self.loop = loop + self.start_interface() + + + def handle_response(self, r): + if r is None: + return + + method = r['method'] + params = r['params'] + result = r['result'] + + if method == 'server.banner': + self.wallet.banner = result + self.wallet.was_updated = True + + elif method == 'server.peers.subscribe': + servers = [] + for item in result: + s = [] + host = item[1] + ports = [] + if len(item)>2: + for v in item[2]: + if re.match("[th]\d+",v): + ports.append((v[0],v[1:])) + if ports: + servers.append( (host, ports) ) + self.interface.servers = servers + + elif method == 'blockchain.address.subscribe': + addr = params[0] + self.wallet.receive_status_callback(addr, result) + + elif method == 'blockchain.address.get_history': + addr = params[0] + self.wallet.receive_history_callback(addr, result) + self.wallet.was_updated = True + + elif method == 'blockchain.transaction.broadcast': + self.wallet.tx_result = result + self.wallet.tx_event.set() + + elif method == 'blockchain.numblocks.subscribe': + self.wallet.blocks = result + + elif method == 'server.version': + pass + + else: + print "unknown message:", method, params, result + + + def start_interface(self): + try: + host, port, protocol = self.wallet.server.split(':') + port = int(port) + except: + self.wallet.pick_random_server() + host, port, protocol = self.wallet.server.split(':') + port = int(port) + + #print protocol, host, port + if protocol == 't': + InterfaceClass = TcpStratumInterface + elif protocol == 'h': + InterfaceClass = HttpStratumInterface + else: + print "unknown protocol" + InterfaceClass = TcpStratumInterface + + self.interface = InterfaceClass(host, port) + self.wallet.interface = self.interface + + with self.wallet.lock: + self.wallet.addresses_waiting_for_status = [] + self.wallet.addresses_waiting_for_history = [] + addresses = self.wallet.all_addresses() + version = self.wallet.electrum_version + for addr in addresses: + self.wallet.addresses_waiting_for_status.append(addr) + + try: + self.interface.start() + self.interface.start_session(addresses,version) + except: + self.interface.is_connected = False + + + def run(self): + import socket, time + while True: + while self.interface.is_connected: + new_addresses = self.wallet.synchronize() + if new_addresses: + self.interface.subscribe(new_addresses) + for addr in new_addresses: + with self.wallet.lock: + self.wallet.addresses_waiting_for_status.append(addr) + + if self.wallet.is_up_to_date(): + if not self.wallet.up_to_date: + self.wallet.up_to_date = True + self.wallet.was_updated = True + self.wallet.up_to_date_event.set() + else: + if self.wallet.up_to_date: + self.wallet.up_to_date = False + self.wallet.was_updated = True + + if self.wallet.was_updated: + self.wallet.gui_callback() + self.wallet.was_updated = False + + response = self.interface.responses.get() + self.handle_response(response) + + print "disconnected, gui callback" + self.wallet.gui_callback() + if self.loop: + time.sleep(5) + self.start_interface() + continue + else: + break + + + diff --git a/client/mnemonic.py b/mnemonic.py diff --git a/client/msqr.py b/msqr.py diff --git a/client/peers b/peers diff --git a/client/pyqrnative.py b/pyqrnative.py diff --git a/client/remote.php b/remote.php diff --git a/client/remote_wallet.py b/remote_wallet.py diff --git a/client/ripemd.py b/ripemd.py diff --git a/client/setup.py b/setup.py diff --git a/client/upgrade.py b/upgrade.py diff --git a/version.py b/version.py @@ -0,0 +1,2 @@ +ELECTRUM_VERSION = "0.45" +SEED_VERSION = 4 # bump this everytime the seed generation is modified diff --git a/wallet.py b/wallet.py @@ -0,0 +1,950 @@ +#!/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 sys, base64, os, re, hashlib, copy, operator, ast, threading, random + +try: + import ecdsa + from ecdsa.util import string_to_number, number_to_string +except: + print "python-ecdsa does not seem to be installed. Try 'sudo easy_install ecdsa'" + sys.exit(1) + +try: + import aes +except: + print "AES does not seem to be installed. Try 'sudo easy_install slowaes'" + sys.exit(1) + + +############ functions from pywallet ##################### + +addrtype = 0 + +def hash_160(public_key): + try: + md = hashlib.new('ripemd160') + md.update(hashlib.sha256(public_key).digest()) + return md.digest() + except: + import ripemd + md = ripemd.new(hashlib.sha256(public_key).digest()) + return md.digest() + + +def public_key_to_bc_address(public_key): + h160 = hash_160(public_key) + return hash_160_to_bc_address(h160) + +def hash_160_to_bc_address(h160): + vh160 = chr(addrtype) + h160 + h = Hash(vh160) + addr = vh160 + h[0:4] + return b58encode(addr) + +def bc_address_to_hash_160(addr): + bytes = b58decode(addr, 25) + return bytes[1:21] + +__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +__b58base = len(__b58chars) + +def b58encode(v): + """ encode v, which is a string of bytes, to base58. + """ + + long_value = 0L + for (i, c) in enumerate(v[::-1]): + long_value += (256**i) * ord(c) + + result = '' + while long_value >= __b58base: + div, mod = divmod(long_value, __b58base) + result = __b58chars[mod] + result + long_value = div + result = __b58chars[long_value] + result + + # Bitcoin does a little leading-zero-compression: + # leading 0-bytes in the input become leading-1s + nPad = 0 + for c in v: + if c == '\0': nPad += 1 + else: break + + return (__b58chars[0]*nPad) + result + +def b58decode(v, length): + """ decode v into a string of len bytes + """ + long_value = 0L + for (i, c) in enumerate(v[::-1]): + long_value += __b58chars.find(c) * (__b58base**i) + + result = '' + while long_value >= 256: + div, mod = divmod(long_value, 256) + result = chr(mod) + result + long_value = div + result = chr(long_value) + result + + nPad = 0 + for c in v: + if c == __b58chars[0]: nPad += 1 + else: break + + result = chr(0)*nPad + result + if length is not None and len(result) != length: + return None + + return result + + +def Hash(data): + return hashlib.sha256(hashlib.sha256(data).digest()).digest() + +def EncodeBase58Check(vchIn): + hash = Hash(vchIn) + return b58encode(vchIn + hash[0:4]) + +def DecodeBase58Check(psz): + vchRet = b58decode(psz, None) + key = vchRet[0:-4] + csum = vchRet[-4:] + hash = Hash(key) + cs32 = hash[0:4] + if cs32 != csum: + return None + else: + return key + +def PrivKeyToSecret(privkey): + return privkey[9:9+32] + +def SecretToASecret(secret): + vchIn = chr(addrtype+128) + secret + return EncodeBase58Check(vchIn) + +def ASecretToSecret(key): + vch = DecodeBase58Check(key) + if vch and vch[0] == chr(addrtype+128): + return vch[1:] + else: + return False + +########### end pywallet functions ####################### + +# URL decode +_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) +urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) + + +def int_to_hex(i, length=1): + s = hex(i)[2:].rstrip('L') + s = "0"*(2*length - len(s)) + s + return s.decode('hex')[::-1].encode('hex') + + +# AES +EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s)) +DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e)) + + + +# secp256k1, http://www.oid-info.com/get/1.3.132.0.10 +_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL +_r = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L +_b = 0x0000000000000000000000000000000000000000000000000000000000000007L +_a = 0x0000000000000000000000000000000000000000000000000000000000000000L +_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L +_Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L +curve_secp256k1 = ecdsa.ellipticcurve.CurveFp( _p, _a, _b ) +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 ) + + +def filter(s): + out = re.sub('( [^\n]*|)\n','',s) + out = out.replace(' ','') + out = out.replace('\n','') + return out + +def raw_tx( inputs, outputs, for_sig = None ): + s = int_to_hex(1,4) + ' version\n' + s += int_to_hex( len(inputs) ) + ' number of inputs\n' + for i in range(len(inputs)): + _, _, p_hash, p_index, p_script, pubkey, sig = inputs[i] + s += p_hash.decode('hex')[::-1].encode('hex') + ' prev hash\n' + s += int_to_hex(p_index,4) + ' prev index\n' + if for_sig is 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: + script = p_script + ' scriptsig \n' + else: + script='' + s += int_to_hex( len(filter(script))/2 ) + ' script length \n' + s += script + s += "ffffffff" + ' sequence\n' + s += int_to_hex( len(outputs) ) + ' number of outputs\n' + for output in outputs: + addr, amount = output + s += int_to_hex( amount, 8) + ' amount: %d\n'%amount + script = '76a9' # op_dup, op_hash_160 + script += '14' # push 0x14 bytes + script += bc_address_to_hash_160(addr).encode('hex') + script += '88ac' # op_equalverify, op_checksig + s += int_to_hex( len(filter(script))/2 ) + ' script length \n' + s += script + ' script \n' + s += int_to_hex(0,4) # lock time + if for_sig is not None: s += int_to_hex(1, 4) # hash type + return s + + + + +def format_satoshis(x, is_diff=False): + from decimal import Decimal + s = str( Decimal(x) /100000000 ) + if is_diff and x>0: + s = "+" + s + if not '.' in s: s += '.' + p = s.find('.') + s += " "*( 9 - ( len(s) - p )) + s = " "*( 5 - ( p )) + s + return s + + +from version import ELECTRUM_VERSION, SEED_VERSION +from interface import DEFAULT_SERVERS + + + + +class Wallet: + def __init__(self, gui_callback = lambda: None): + + self.electrum_version = ELECTRUM_VERSION + self.seed_version = SEED_VERSION + self.gui_callback = gui_callback + + self.gap_limit = 5 # configuration + self.fee = 100000 + self.master_public_key = '' + + # saved fields + self.use_encryption = False + self.addresses = [] # receiving addresses visible for user + self.change_addresses = [] # addresses used as change + self.seed = '' # encrypted + self.history = {} + self.labels = {} # labels for addresses and transactions + self.aliases = {} # aliases for addresses + self.authorities = {} # trusted addresses + + self.receipts = {} # signed URIs + self.receipt = None # next receipt + self.addressbook = [] # outgoing addresses, for payments + + # not saved + self.tx_history = {} + + self.imported_keys = {} + self.remote_url = None + + self.was_updated = True + self.blocks = -1 + self.banner = '' + self.up_to_date_event = threading.Event() + self.up_to_date_event.clear() + self.up_to_date = False + self.lock = threading.Lock() + self.tx_event = threading.Event() + + # + self.addresses_waiting_for_status = [] + self.addresses_waiting_for_history = [] + self.pick_random_server() + + + + def pick_random_server(self): + self.server = random.choice( DEFAULT_SERVERS ) # random choice when the wallet is created + + def is_up_to_date(self): + return self.interface.responses.empty() and not ( self.addresses_waiting_for_status or self.addresses_waiting_for_history ) + + + def set_server(self, server): + # raise an error if the format isnt correct + a,b,c = server.split(':') + b = int(b) + assert c in ['t','h','n'] + # set the server + if server != self.server: + self.server = server + self.save() + self.interface.is_connected = False # this exits the polling loop + + def set_path(self, wallet_path): + + if wallet_path is not None: + self.path = wallet_path + else: + # backward compatibility: look for wallet file in the default data directory + if "HOME" in os.environ: + wallet_dir = os.path.join( os.environ["HOME"], '.electrum') + elif "LOCALAPPDATA" in os.environ: + wallet_dir = os.path.join( os.environ["LOCALAPPDATA"], 'Electrum' ) + elif "APPDATA" in os.environ: + wallet_dir = os.path.join( os.environ["APPDATA"], 'Electrum' ) + else: + raise BaseException("No home directory found in environment variables.") + + if not os.path.exists( wallet_dir ): os.mkdir( wallet_dir ) + self.path = os.path.join( wallet_dir, 'electrum.dat' ) + + def import_key(self, keypair, password): + address, key = keypair.split(':') + if not self.is_valid(address): return False + if address in self.all_addresses(): return False + b = ASecretToSecret( key ) + if not b: return False + 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() ): return False + self.imported_keys[address] = self.pw_encode( key, password ) + return True + + def new_seed(self, password): + seed = "%032x"%ecdsa.util.randrange( pow(2,128) ) + #self.init_mpk(seed) + # encrypt + self.seed = self.pw_encode( seed, password ) + + + def init_mpk(self,seed): + # public key + curve = SECP256k1 + secexp = self.stretch_key(seed) + master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) + self.master_public_key = master_private_key.get_verifying_key().to_string() + + def all_addresses(self): + return self.addresses + self.change_addresses + self.imported_keys.keys() + + def is_mine(self, address): + return address in self.all_addresses() + + def is_change(self, address): + return address in self.change_addresses + + def is_valid(self,addr): + ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z') + if not ADDRESS_RE.match(addr): return False + try: + h = bc_address_to_hash_160(addr) + except: + return False + return addr == hash_160_to_bc_address(h) + + def stretch_key(self,seed): + oldseed = seed + for i in range(100000): + seed = hashlib.sha256(seed + oldseed).digest() + return string_to_number( seed ) + + def get_sequence(self,n,for_change): + return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key ) ) + + 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 ) + b = ASecretToSecret( b ) + secexp = int( b.encode('hex'), 16) + else: + if address in self.addresses: + n = self.addresses.index(address) + for_change = False + elif address in self.change_addresses: + n = self.change_addresses.index(address) + for_change = True + else: + raise BaseException("unknown address") + try: + seed = self.pw_decode( self.seed, password) + except: + raise BaseException("Invalid password") + secexp = self.stretch_key(seed) + secexp = ( secexp + self.get_sequence(n,for_change) ) % order + + pk = number_to_string(secexp,order) + return pk + + 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 ) + 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 ) + try: + self.verify_message( address, sig, message) + return sig + except: + continue + else: + raise BaseException("error: cannot sign message") + + + def verify_message(self, address, signature, message): + """ See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """ + from ecdsa import numbertheory, ellipticcurve, util + import msqr + curve = curve_secp256k1 + G = generator_secp256k1 + order = G.order() + # extract r,s from signature + sig = base64.b64decode(signature) + if len(sig) != 65: raise BaseException("Wrong encoding") + r,s = util.sigdecode_string(sig[1:], order) + recid = ord(sig[0]) - 27 + # 1.1 + x = r + (recid/2) * order + # 1.3 + alpha = ( x * x * x + curve.a() * x + curve.b() ) % curve.p() + beta = msqr.modular_sqrt(alpha, curve.p()) + y = beta if (beta - recid) % 2 == 0 else curve.p() - beta + # 1.4 the constructor checks that nR is at infinity + R = ellipticcurve.Point(curve, x, y, order) + # 1.5 compute e from message: + h = Hash( self.msg_magic( message ) ) + e = string_to_number(h) + minus_e = -e % order + # 1.6 compute Q = r^-1 (sR - eG) + inv_r = numbertheory.inverse_mod(r,order) + Q = inv_r * ( s * R + minus_e * G ) + public_key = ecdsa.VerifyingKey.from_public_point( Q, curve = SECP256k1 ) + # check that Q is the public key + public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string) + # check that we get the original signing address + addr = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ) + # print addr + if address != addr: + print "bad signature" + raise BaseException("Bad signature") + + + def create_new_address(self, for_change): + """ Publickey(type,n) = Master_public_key + H(n|S|type)*point """ + curve = SECP256k1 + n = len(self.change_addresses) if for_change else len(self.addresses) + z = self.get_sequence(n,for_change) + master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key, curve = SECP256k1 ) + pubkey_point = master_public_key.pubkey.point + z*curve.generator + public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 ) + address = public_key_to_bc_address( '04'.decode('hex') + public_key2.to_string() ) + if for_change: + self.change_addresses.append(address) + else: + self.addresses.append(address) + + self.history[address] = [] + print address + return address + + + + def synchronize(self): + if not self.master_public_key: + return [] + + new_addresses = [] + while True: + if self.change_addresses == []: + new_addresses.append( self.create_new_address(True) ) + continue + a = self.change_addresses[-1] + if self.history.get(a): + new_addresses.append( self.create_new_address(True) ) + else: + break + + n = self.gap_limit + while True: + if len(self.addresses) < n: + new_addresses.append( self.create_new_address(False) ) + continue + if map( lambda a: self.history.get(a), self.addresses[-n:] ) == n*[[]]: + break + else: + new_addresses.append( self.create_new_address(False) ) + + if self.remote_url: + num = self.get_remote_number() + while len(self.addresses)<num: + new_addresses.append( self.create_new_address(False) ) + + return new_addresses + + + def get_remote_number(self): + import jsonrpclib + server = jsonrpclib.Server(self.remote_url) + out = server.getnum() + return out + + def get_remote_mpk(self): + import jsonrpclib + server = jsonrpclib.Server(self.remote_url) + out = server.getkey() + return out + + def is_found(self): + return (len(self.change_addresses) > 1 ) or ( len(self.addresses) > self.gap_limit ) + + def fill_addressbook(self): + for tx in self.tx_history.values(): + if tx['value']<0: + for i in tx['outputs']: + if not self.is_mine(i) and i not in self.addressbook: + self.addressbook.append(i) + # redo labels + self.update_tx_labels() + + + def save(self): + s = { + 'seed_version':self.seed_version, + 'use_encryption':self.use_encryption, + 'master_public_key': self.master_public_key.encode('hex'), + 'fee':self.fee, + 'server':self.server, + 'seed':self.seed, + 'addresses':self.addresses, + 'change_addresses':self.change_addresses, + 'history':self.history, + 'labels':self.labels, + 'contacts':self.addressbook, + 'imported_keys':self.imported_keys, + 'aliases':self.aliases, + 'authorities':self.authorities, + 'receipts':self.receipts, + } + f = open(self.path,"w") + f.write( repr(s) ) + f.close() + + def read(self): + import interface + + upgrade_msg = """This wallet seed is deprecated. Please run upgrade.py for a diagnostic.""" + self.file_exists = False + try: + f = open(self.path,"r") + data = f.read() + f.close() + except: + return + data = interface.old_to_new(data) + try: + d = ast.literal_eval( data ) + self.seed_version = d.get('seed_version') + self.master_public_key = d.get('master_public_key').decode('hex') + self.use_encryption = d.get('use_encryption') + self.fee = int( d.get('fee') ) + self.seed = d.get('seed') + self.server = d.get('server') + #blocks = d.get('blocks') + self.addresses = d.get('addresses') + self.change_addresses = d.get('change_addresses') + self.history = d.get('history') + self.labels = d.get('labels') + self.addressbook = d.get('contacts') + self.imported_keys = d.get('imported_keys',{}) + self.aliases = d.get('aliases',{}) + self.authorities = d.get('authorities',{}) + self.receipts = d.get('receipts',{}) + except: + raise BaseException("cannot read wallet file") + + self.update_tx_history() + + if self.seed_version != SEED_VERSION: + raise BaseException(upgrade_msg) + + if self.remote_url: assert self.master_public_key.encode('hex') == self.get_remote_mpk() + + self.file_exists = True + + + + + def get_addr_balance(self, addr): + assert self.is_mine(addr) + h = self.history.get(addr,[]) + c = u = 0 + for item in h: + v = item['value'] + if item['height']: + c += v + else: + u += v + return c, u + + def get_balance(self): + conf = unconf = 0 + for addr in self.all_addresses(): + c, u = self.get_addr_balance(addr) + conf += c + unconf += u + return conf, unconf + + + def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ): + """ todo: minimize tx size """ + total = 0 + fee = self.fee if fixed_fee is None else fixed_fee + + coins = [] + domain = [from_addr] if from_addr else self.all_addresses() + for addr in domain: + h = self.history.get(addr) + if h is None: continue + for item in h: + if item.get('raw_output_script'): + coins.append( (addr,item)) + + coins = sorted( coins, key = lambda x: x[1]['timestamp'] ) + inputs = [] + for c in coins: + addr, item = c + v = item.get('value') + total += v + inputs.append((addr, v, item['tx_hash'], item['index'], item['raw_output_script'], None, None) ) + fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee + if total >= amount + fee: break + else: + #print "not enough funds: %d %d"%(total, fee) + inputs = [] + return inputs, total, fee + + def choose_tx_outputs( self, to_addr, amount, fee, total, change_addr=None ): + outputs = [ (to_addr, amount) ] + change_amount = total - ( amount + fee ) + if change_amount != 0: + # normally, the update thread should ensure that the last change address is unused + if not change_addr: + change_addr = self.change_addresses[-1] + outputs.append( ( change_addr, change_amount) ) + return outputs + + def sign_inputs( self, inputs, outputs, password ): + 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 ) + public_key = private_key.get_verifying_key() + pubkey = public_key.to_string() + 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) + s_inputs.append( (addr, v, p_hash, p_pos, p_scriptPubKey, pubkey, sig) ) + return s_inputs + + def pw_encode(self, s, password): + if password: + secret = Hash(password) + return EncodeAES(secret, s) + else: + return s + + 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 BaseException("Invalid password") + return d + else: + return s + + def get_status(self, address): + h = self.history.get(address) + if not h: + status = None + else: + lastpoint = h[-1] + status = lastpoint['block_hash'] + if status == 'mempool': + status = status + ':%d'% len(h) + return status + + def receive_status_callback(self, addr, status): + with self.lock: + if self.get_status(addr) != status: + #print "updating status for", addr, status + self.addresses_waiting_for_history.append(addr) + self.interface.get_history(addr) + if addr in self.addresses_waiting_for_status: + self.addresses_waiting_for_status.remove(addr) + + def receive_history_callback(self, addr, data): + #print "updating history for", addr + with self.lock: + self.history[addr] = data + self.update_tx_history() + self.save() + if addr in self.addresses_waiting_for_history: self.addresses_waiting_for_history.remove(addr) + + def get_tx_history(self): + lines = self.tx_history.values() + lines = sorted(lines, key=operator.itemgetter("timestamp")) + return lines + + def update_tx_history(self): + self.tx_history= {} + for addr in self.all_addresses(): + h = self.history.get(addr) + if h is None: continue + for tx in h: + tx_hash = tx['tx_hash'] + line = self.tx_history.get(tx_hash) + if not line: + self.tx_history[tx_hash] = copy.copy(tx) + line = self.tx_history.get(tx_hash) + else: + line['value'] += tx['value'] + if line['height'] == 0: + line['timestamp'] = 1e12 + self.update_tx_labels() + + def update_tx_labels(self): + for tx in self.tx_history.values(): + default_label = '' + if tx['value']<0: + for o_addr in tx['outputs']: + if not self.is_change(o_addr): + dest_label = self.labels.get(o_addr) + if dest_label: + default_label = 'to: ' + dest_label + else: + default_label = 'to: ' + o_addr + else: + for o_addr in tx['outputs']: + if self.is_mine(o_addr) and not self.is_change(o_addr): + dest_label = self.labels.get(o_addr) + if dest_label: + default_label = 'at: ' + dest_label + else: + default_label = 'at: ' + o_addr + tx['default_label'] = default_label + + def mktx(self, to_address, amount, label, password, fee=None, change_addr=None, from_addr= None): + if not self.is_valid(to_address): + raise BaseException("Invalid address") + inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr ) + if not inputs: + raise BaseException("Not enough funds") + outputs = self.choose_tx_outputs( to_address, amount, fee, total, change_addr ) + s_inputs = self.sign_inputs( inputs, outputs, password ) + + tx = filter( raw_tx( s_inputs, outputs ) ) + if to_address not in self.addressbook: + self.addressbook.append(to_address) + if label: + tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex') + self.labels[tx_hash] = label + + return tx + + def sendtx(self, tx): + tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex') + self.tx_event.clear() + self.interface.send([('blockchain.transaction.broadcast', [tx])]) + self.tx_event.wait() + out = self.tx_result + if out != tx_hash: + return False, "error: " + out + if self.receipt: + self.receipts[tx_hash] = self.receipt + self.receipt = None + return True, out + + + def read_alias(self, alias): + # this might not be the right place for this function. + import urllib + + m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) + m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) + if m1: + url = 'http://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) + elif m2: + url = 'http://' + alias + '/bitcoin.id' + else: + return '' + try: + lines = urllib.urlopen(url).readlines() + except: + return '' + + # line 0 + line = lines[0].strip().split(':') + if len(line) == 1: + auth_name = None + target = signing_addr = line[0] + else: + target, auth_name, signing_addr, signature = line + msg = "alias:%s:%s:%s"%(alias,target,auth_name) + print msg, signature + self.verify_message(signing_addr, signature, msg) + + # other lines are signed updates + for line in lines[1:]: + line = line.strip() + if not line: continue + line = line.split(':') + previous = target + print repr(line) + target, signature = line + self.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) + + if not self.is_valid(target): + raise BaseException("Invalid bitcoin address") + + return target, signing_addr, auth_name + + def update_password(self, seed, new_password): + if new_password == '': new_password = None + self.use_encryption = (new_password != None) + self.seed = self.pw_encode( seed, new_password) + for k in self.imported_keys.keys(): + a = self.imported_keys[k] + b = self.pw_decode(a, password) + c = self.pw_encode(b, new_password) + self.imported_keys[k] = c + self.save() + + def get_alias(self, alias, interactive = False, show_message=None, question = None): + try: + target, signing_address, auth_name = self.read_alias(alias) + except BaseException, e: + # raise exception if verify fails (verify the chain) + if interactive: + show_message("Alias error: " + e.message) + return + + print target, signing_address, auth_name + + if auth_name is None: + a = self.aliases.get(alias) + if not a: + msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) + if interactive and question( msg ): + self.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address != a[0]: + msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias + if interactive and question( msg ): + self.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address not in self.authorities.keys(): + msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) + if interactive and question( msg ): + self.authorities[signing_address] = auth_name + else: + target = None + + if target: + self.aliases[alias] = (signing_address, target) + + return target + + + def parse_url(self, url, show_message, question): + o = url[8:].split('?') + address = o[0] + if len(o)>1: + params = o[1].split('&') + else: + params = [] + + amount = label = message = signature = identity = '' + for p in params: + k,v = p.split('=') + uv = urldecode(v) + if k == 'amount': amount = uv + elif k == 'message': message = uv + elif k == 'label': label = uv + elif k == 'signature': + identity, signature = uv.split(':') + url = url.replace('&%s=%s'%(k,v),'') + else: + print k,v + + if signature: + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): + signing_address = self.get_alias(identity, True, show_message, question) + elif self.is_valid(identity): + signing_address = identity + else: + signing_address = None + if not signing_address: + return + try: + self.verify_message(signing_address, signature, url ) + self.receipt = (signing_address, signature, url) + except: + show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') + address = amount = label = identity = message = '' + + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): + payto_address = self.get_alias(address, True, show_message, question) + if payto_address: + address = address + ' <' + payto_address + '>' + + return address, amount, label, message, signature, identity, url + + + def update(self): + self.interface.poke() + self.up_to_date_event.wait() + + + diff --git a/watch_address b/watch_address @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +import interface, sys +try: + addr = sys.argv[1] +except: + print "usage: watch_address <bitcoin_address>" + +i = interface.TcpStratumInterface('ecdsa.org', 50001) +i.start() +i.send([('blockchain.address.subscribe',[addr])]) + +while True: + r = i.responses.get(True, 100000000000) + method = r.get('method') + if method == 'blockchain.address.subscribe': + i.send([('blockchain.address.get_history',[addr])]) + elif method == 'blockchain.address.get_history': + confirmed = unconfirmed = 0 + h = r.get('result') + if h is None: + continue + for item in h: + v = item['value'] + if item['height']: + confirmed += v + else: + unconfirmed += v + print (confirmed+unconfirmed)/1.e8 + +