electrum

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

commit dfd48319a38dd4d8a8afd874eb55175cd8aad590
parent 1cda13a407eaeff901f7bed289395573b0e5c3d8
Author: Maran <maran.hidskes@gmail.com>
Date:   Sat, 16 Mar 2013 21:38:12 +0100

Fix merge conflict

Diffstat:
M.gitignore | 4++--
MMANIFEST.in | 2++
MRELEASE-NOTES | 3+--
Mcontrib/build-wine/build-electrum-git.sh | 2+-
Mcontrib/build-wine/build-electrum.sh | 2+-
Mcontrib/build-wine/deterministic.spec | 60++++++++++++++++++++++++++++++++++++++++++++++++++----------
Acontrib/build-wine/electrum.nsi | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/build-wine/electrum.nsis | 104-------------------------------------------------------------------------------
Mcontrib/build-wine/prepare-wine.sh | 2+-
Melectrum | 31++++++++++++++++++-------------
Mgui/__init__.py | 1+
Mgui/exchange_rate.py | 5++++-
Mgui/gui_classic.py | 320+++++++++++++++++++++++++++++++++++--------------------------------------------
Mgui/gui_gtk.py | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Agui/plugins.py | 32++++++++++++++++++++++++++++++++
Aicons/electrum.ico | 0
Mlib/bitcoin.py | 93+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mlib/commands.py | 3++-
Mlib/deserialize.py | 4+++-
Mlib/interface.py | 4++--
Mlib/util.py | 35++++++++++++++++++++++++++++++++++-
Mlib/verifier.py | 34+++++++++++++++++++++++-----------
Mlib/wallet.py | 271++++++++++++-------------------------------------------------------------------
Mmake_packages | 2+-
Aplugins/aliases.py | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplugins/pointofsale.py | 192+++++++++++++++++++++++++++++++++----------------------------------------------
Mplugins/qrscanner.py | 103++++++++++++++++++++++++++++++++++++-------------------------------------------
Aplugins/virtualkeyboard.py | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msetup-release.py | 2++
29 files changed, 971 insertions(+), 789 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -4,4 +4,5 @@ lib/icons_rc.py build/ dist/ *.egg/ -/electrum.py- \ No newline at end of file +/electrum.py +contrib/pyinstaller/ diff --git a/MANIFEST.in b/MANIFEST.in @@ -22,3 +22,5 @@ include scripts/servers include scripts/validate_tx include scripts/watch_address recursive-include data * +recursive-include locale *.mo +recursive-include docs * diff --git a/RELEASE-NOTES b/RELEASE-NOTES @@ -35,8 +35,7 @@ For an example, see Gavin's tutorial: https://gist.github.com/gavinandresen/3966 1. user creates an unsigned transaction using the online (watching-only) wallet. 2. unsigned transaction is copied to the offline computer, and signed by the offline wallet. 3. signed transaction is copied to the online computer, broadcasted by the online client. - -* Raw transactions can also be loaded/signed/broadcasted via the GUI. + 4. All these steps can be done via the command line interface or the classic GUI. * Many command line commands have been renamed in order to make the syntax consistent with bitcoind. diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh @@ -58,7 +58,7 @@ $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w --onefile "C:/ele $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w deterministic.spec # For building NSIS installer, run: -wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsis +wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsi #wine $WINEPREFIX/drive_c/Program\ Files\ \(x86\)/NSIS/makensis.exe electrum.nsis DATE=`date +"%Y%m%d"` diff --git a/contrib/build-wine/build-electrum.sh b/contrib/build-wine/build-electrum.sh @@ -40,7 +40,7 @@ $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w --onefile "C:/ele $PYTHON "C:/pyinstaller/pyinstaller.py" --noconfirm --ascii -w deterministic.spec # For building NSIS installer, run: -wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsis +wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" electrum.nsi #wine $WINEPREFIX/drive_c/Program\ Files\ \(x86\)/NSIS/makensis.exe electrum.nsis cd dist diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec @@ -1,24 +1,64 @@ # -*- mode: python -*- -a = Analysis(['C:/electrum/electrum'], - pathex=['Z:\\electrum-wine'], - hiddenimports=[], - excludes=['Tkinter'], + +# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports +a = Analysis(['electrum', 'gui/gui_classic.py', 'gui/gui_lite.py', 'gui/gui_text.py', + 'lib/util.py', 'lib/wallet.py', 'lib/simple_config.py', + 'lib/bitcoin.py', 'lib/deserialize.py' + ], + hiddenimports=["lib","gui"], + pathex=['lib:gui:plugins'], hookspath=None) -pyz = PYZ(a.pure, level=0) + +##### include mydir in distribution ####### +def extra_datas(mydir): + def rec_glob(p, files): + import os + import glob + for d in glob.glob(p): + if os.path.isfile(d): + files.append(d) + rec_glob("%s/*" % d, files) + files = [] + rec_glob("%s/*" % mydir, files) + extra_datas = [] + for f in files: + extra_datas.append((f, f, 'DATA')) + + return extra_datas +########################################### + +# append dirs + +# Theme data +a.datas += extra_datas('data') + +# Localization +a.datas += extra_datas('locale') + +# Py folders that are needed because of the magic import finding +a.datas += extra_datas('gui') +a.datas += extra_datas('lib') +a.datas += extra_datas('plugins') + +pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, exclude_binaries=1, name=os.path.join('build\\pyi.win32\\electrum', 'electrum.exe'), - debug=False, + debug=True, strip=None, - upx=True, - console=False ) + upx=False, + icon='icons/electrum.ico', + console=True) + # The console True makes an annoying black box pop up, but it does make Electrum accept command line options. + coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=None, upx=True, + debug=False, + icon='icons/electrum.ico', + console=True, name=os.path.join('dist', 'electrum')) -app = BUNDLE(coll, - name=os.path.join('dist', 'electrum.app')) diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi @@ -0,0 +1,104 @@ +;-------------------------------- +;Include Modern UI + + !include "MUI2.nsh" + +;-------------------------------- +;General + + ;Name and file + Name "Electrum" + OutFile "dist/electrum-setup.exe" + + ;Default installation folder + InstallDir "$PROGRAMFILES\Electrum" + + ;Get installation folder from registry if available + InstallDirRegKey HKCU "Software\Electrum" "" + + ;Request application privileges for Windows Vista + RequestExecutionLevel admin + +;-------------------------------- +;Variables + +;-------------------------------- +;Interface Settings + + !define MUI_ABORTWARNING + +;-------------------------------- +;Pages + + ;!insertmacro MUI_PAGE_LICENSE "tmp/LICENCE" + ;!insertmacro MUI_PAGE_COMPONENTS + !insertmacro MUI_PAGE_DIRECTORY + + ;Start Menu Folder Page Configuration + !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" + !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Electrum" + !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + + ;!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder + + !insertmacro MUI_PAGE_INSTFILES + + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +;Installer Sections + +Section + + SetOutPath "$INSTDIR" + + ;ADD YOUR OWN FILES HERE... + file /r dist\electrum\*.* + + ;Store installation folder + WriteRegStr HKCU "Software\Electrum" "" $INSTDIR + + ;Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + + CreateShortCut "$DESKTOP\Electrum.lnk" "$INSTDIR\electrum.exe" "" + + ;create start-menu items + CreateDirectory "$SMPROGRAMS\Electrum" + CreateShortCut "$SMPROGRAMS\Electrum\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\Electrum\Electrum.lnk" "$INSTDIR\electrum.exe" "" "$INSTDIR\electrum.exe" 0 + +SectionEnd + +;-------------------------------- +;Descriptions + + ;Assign language strings to sections + ;!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + ; !insertmacro MUI_DESCRIPTION_TEXT ${SecDummy} $(DESC_SecDummy) + ;!insertmacro MUI_FUNCTION_DESCRIPTION_END + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + + ;ADD YOUR OWN FILES HERE... + RMDir /r "$INSTDIR\*.*" + + RMDir "$INSTDIR" + + Delete "$DESKTOP\Electrum.lnk" + Delete "$SMPROGRAMS\Electrum\*.*" + RmDir "$SMPROGRAMS\Electrum" + + DeleteRegKey /ifempty HKCU "Software\Electrum" + +SectionEnd diff --git a/contrib/build-wine/electrum.nsis b/contrib/build-wine/electrum.nsis @@ -1,104 +0,0 @@ -;-------------------------------- -;Include Modern UI - - !include "MUI2.nsh" - -;-------------------------------- -;General - - ;Name and file - Name "Electrum" - OutFile "dist/electrum-setup.exe" - - ;Default installation folder - InstallDir "$PROGRAMFILES\Electrum" - - ;Get installation folder from registry if available - InstallDirRegKey HKCU "Software\Electrum" "" - - ;Request application privileges for Windows Vista - RequestExecutionLevel admin - -;-------------------------------- -;Variables - -;-------------------------------- -;Interface Settings - - !define MUI_ABORTWARNING - -;-------------------------------- -;Pages - - !insertmacro MUI_PAGE_LICENSE "tmp/LICENCE" - ;!insertmacro MUI_PAGE_COMPONENTS - !insertmacro MUI_PAGE_DIRECTORY - - ;Start Menu Folder Page Configuration - !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" - !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Electrum" - !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" - - ;!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder - - !insertmacro MUI_PAGE_INSTFILES - - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - -;-------------------------------- -;Languages - - !insertmacro MUI_LANGUAGE "English" - -;-------------------------------- -;Installer Sections - -Section - - SetOutPath "$INSTDIR" - - ;ADD YOUR OWN FILES HERE... - file /r dist\electrum\*.* - - ;Store installation folder - WriteRegStr HKCU "Software\Electrum" "" $INSTDIR - - ;Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" - - - CreateShortCut "$DESKTOP\Electrum.lnk" "$INSTDIR\electrum.exe" "" - - ;create start-menu items - CreateDirectory "$SMPROGRAMS\Electrum" - CreateShortCut "$SMPROGRAMS\Electrum\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 - CreateShortCut "$SMPROGRAMS\Electrum\Electrum.lnk" "$INSTDIR\electrum.exe" "" "$INSTDIR\electrum.exe" 0 - -SectionEnd - -;-------------------------------- -;Descriptions - - ;Assign language strings to sections - ;!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - ; !insertmacro MUI_DESCRIPTION_TEXT ${SecDummy} $(DESC_SecDummy) - ;!insertmacro MUI_FUNCTION_DESCRIPTION_END - -;-------------------------------- -;Uninstaller Section - -Section "Uninstall" - - ;ADD YOUR OWN FILES HERE... - RMDir /r "$INSTDIR\*.*" - - RMDir "$INSTDIR" - - Delete "$DESKTOP\Electrum.lnk" - Delete "$SMPROGRAMS\Electrum\*.*" - RmDir "$SMPROGRAMS\Electrum" - - DeleteRegKey /ifempty HKCU "Software\Electrum" - -SectionEnd diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh @@ -4,7 +4,7 @@ PYTHON_URL=http://www.python.org/ftp/python/2.6.6/python-2.6.6.msi PYQT4_URL=http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.9.5/PyQt-Py2.6-x86-gpl-4.9.5-1.exe PYWIN32_URL=http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win32-py2.6.exe/download -PYINSTALLER_URL=https://github.com/downloads/pyinstaller/pyinstaller/pyinstaller-2.0.zip +PYINSTALLER_URL=http://downloads.sourceforge.net/project/pyinstaller/2.0/pyinstaller-2.0.zip NSIS_URL=http://prdownloads.sourceforge.net/nsis/nsis-2.46-setup.exe?download #ZBAR_URL=http://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download diff --git a/electrum b/electrum @@ -34,20 +34,20 @@ except ImportError: sys.exit("Error: AES does not seem to be installed. Try 'sudo pip install slowaes'") +is_local = os.path.dirname(os.path.realpath(__file__)) == os.getcwd() is_android = 'ANDROID_DATA' in os.environ +import __builtin__ +__builtin__.use_local_modules = is_local or is_android + # load local module as electrum -if os.path.exists("lib") or is_android: +if __builtin__.use_local_modules: import imp - fp, pathname, description = imp.find_module('lib') - imp.load_module('electrum', fp, pathname, description) - fp, pathname, description = imp.find_module('gui') - imp.load_module('electrum_gui', fp, pathname, description) - + imp.load_module('electrum', *imp.find_module('lib')) + imp.load_module('electrum_gui', *imp.find_module('gui')) from electrum import * - # get password routine def prompt_password(prompt, confirm=True): import getpass @@ -162,6 +162,7 @@ if __name__ == '__main__': gui.show_seed() verifier = WalletVerifier(interface, config) + verifier.start() wallet.set_verifier(verifier) synchronizer = WalletSynchronizer(wallet, config) synchronizer.start() @@ -181,7 +182,6 @@ if __name__ == '__main__': gui.password_dialog() wallet.save() - verifier.start() gui.main(url) wallet.save() @@ -236,14 +236,16 @@ if __name__ == '__main__': wallet.seed = None wallet.init_sequence(str(seed)) else: - wallet.seed = str(seed) - wallet.init_mpk( wallet.seed ) + wallet.init_seed( str(seed) ) if not options.offline: interface = Interface(config) - interface.start(wait=True) + if not interface.start(wait=True): + print_msg("Not connected, aborting.") + sys.exit(1) wallet.interface = interface verifier = WalletVerifier(interface, config) + verifier.start() wallet.set_verifier(verifier) print_msg("Recovering wallet...") @@ -345,7 +347,7 @@ if __name__ == '__main__': sys.exit(1) if max_args < 0: - if len(args) > min_args: + if len(args) > min_args + 1: message = ' '.join(args[min_args:]) print_msg("Warning: Final argument was reconstructed from several arguments:", repr(message)) args = args[0:min_args] + [ message ] @@ -358,9 +360,12 @@ if __name__ == '__main__': if cmd not in offline_commands and not options.offline: interface = Interface(config) interface.register_callback('connected', lambda: sys.stderr.write("Connected to " + interface.connection_msg + "\n")) - interface.start() + if not interface.start(wait=True): + print_msg("Not connected, aborting.") + sys.exit(1) wallet.interface = interface verifier = WalletVerifier(interface, config) + verifier.start() wallet.set_verifier(verifier) synchronizer = WalletSynchronizer(wallet, config) synchronizer.start() diff --git a/gui/__init__.py b/gui/__init__.py @@ -1 +1,2 @@ # do not remove this file +from plugins import BasePlugin diff --git a/gui/exchange_rate.py b/gui/exchange_rate.py @@ -36,7 +36,10 @@ class Exchanger(threading.Thread): response = connection.getresponse() if response.reason == httplib.responses[httplib.NOT_FOUND]: return - response = json.loads(response.read()) + try: + response = json.loads(response.read()) + except: + return quote_currencies = {} try: for r in response: diff --git a/gui/gui_classic.py b/gui/gui_classic.py @@ -61,8 +61,6 @@ elif platform.system() == 'Darwin': else: MONOSPACE_FONT = 'monospace' -ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$' - from electrum import ELECTRUM_VERSION import re @@ -226,7 +224,6 @@ class StatusBarButton(QPushButton): - def waiting_dialog(f): s = Timer() @@ -248,19 +245,35 @@ def waiting_dialog(f): w.destroy() -def ok_cancel_buttons(dialog): +def ok_cancel_buttons(dialog, ok_label=_("OK") ): hbox = QHBoxLayout() hbox.addStretch(1) - b = QPushButton("Cancel") + b = QPushButton(_("Cancel")) hbox.addWidget(b) b.clicked.connect(dialog.reject) - b = QPushButton("OK") + b = QPushButton(ok_label) hbox.addWidget(b) b.clicked.connect(dialog.accept) b.setDefault(True) return hbox +def text_dialog(parent, title, label, ok_label): + dialog = QDialog(parent) + dialog.setMinimumWidth(500) + dialog.setWindowTitle(title) + dialog.setModal(1) + l = QVBoxLayout() + dialog.setLayout(l) + l.addWidget(QLabel(label)) + txt = QTextEdit() + l.addWidget(txt) + l.addLayout(ok_cancel_buttons(dialog, ok_label)) + if dialog.exec_(): + return unicode(txt.toPlainText()) + + + default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370],[370,200,130]] } @@ -327,46 +340,44 @@ class ElectrumWindow(QMainWindow): self.console.showMessage(self.wallet.banner) # plugins that need to change the GUI do it here - self.run_hook('init') + self.run_hook('init_gui') # plugins def init_plugins(self): - import imp, pkgutil - if os.path.exists("plugins"): + import imp, pkgutil, __builtin__ + if __builtin__.use_local_modules: fp, pathname, description = imp.find_module('plugins') + plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])] + plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names) imp.load_module('electrum_plugins', fp, pathname, description) - plugin_names = [name for a, name, b in pkgutil.iter_modules(['plugins'])] - self.plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names) + plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names) else: import electrum_plugins plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)] - self.plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names] + plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names] - self.plugin_hooks = {} - for p in self.plugins: + self.plugins = [] + for p in plugins: try: - p.init(self) + self.plugins.append( p.Plugin(self) ) except: print_msg("Error:cannot initialize plugin",p) traceback.print_exc(file=sys.stdout) - def set_hook(self, name, callback): - h = self.plugin_hooks.get(name, []) - h.append(callback) - self.plugin_hooks[name] = h - - def unset_hook(self, name, callback): - h = self.plugin_hooks.get(name,[]) - if callback in h: h.remove(callback) - self.plugin_hooks[name] = h - - def run_hook(self, name, args = ()): - args = (self,) + args - for cb in self.plugin_hooks.get(name,[]): - apply(cb, args) + def run_hook(self, name, *args): + for p in self.plugins: + if not p.is_enabled(): + continue + try: + f = eval('p.'+name) + except: + continue + apply(f, args) + return + def set_label(self, name, text = None): changed = False old_text = self.wallet.labels.get(name) @@ -378,8 +389,7 @@ class ElectrumWindow(QMainWindow): if old_text: self.wallet.labels.pop(name) changed = True - self.run_hook('set_label', (name, text, changed)) - + self.run_hook('set_label', name, text, changed) return changed @@ -412,23 +422,6 @@ class ElectrumWindow(QMainWindow): def timer_actions(self): self.run_hook('timer_actions') - if self.payto_e.hasFocus(): - return - r = unicode( self.payto_e.text() ) - if r != self.previous_payto_e: - self.previous_payto_e = r - r = r.strip() - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): - try: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - except: - return - if to_address: - s = r + ' <' + to_address + '>' - self.payto_e.setText(s) - - - def update_status(self): if self.wallet.interface and self.wallet.interface.is_connected: if not self.wallet.up_to_date: @@ -520,7 +513,7 @@ class ElectrumWindow(QMainWindow): vbox.addWidget(QLabel("Date: %s"%time_str)) vbox.addWidget(QLabel("Status: %d confirmations"%conf)) if is_mine: - if fee: + if fee is not None: vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v-fee, False))) vbox.addWidget(QLabel("Transaction fee: %s"% format_satoshis(fee, False))) else: @@ -577,10 +570,11 @@ class ElectrumWindow(QMainWindow): def address_label_clicked(self, item, column, l, column_addr, column_label): if column == column_label and item.isSelected(): + is_editable = item.data(0, 32).toBool() + if not is_editable: + return addr = unicode( item.text(column_addr) ) label = unicode( item.text(column_label) ) - if label in self.wallet.aliases.keys(): - return item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) l.editItem( item, column ) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) @@ -590,26 +584,22 @@ class ElectrumWindow(QMainWindow): if column == column_label: addr = unicode( item.text(column_addr) ) text = unicode( item.text(column_label) ) - changed = False - - if text in self.wallet.aliases.keys(): - print_error("Error: This is one of your aliases") - label = self.wallet.labels.get(addr,'') - item.setText(column_label, QString(label)) + is_editable = item.data(0, 32).toBool() + if not is_editable: + return - else: - changed = self.set_label(addr, text) - if changed: - self.update_history_tab() - self.update_completions() + changed = self.set_label(addr, text) + if changed: + self.update_history_tab() + self.update_completions() self.current_item_changed(item) - self.run_hook('item_changed', (item, column)) + self.run_hook('item_changed', item, column) def current_item_changed(self, a): - self.run_hook('current_item_changed', (a,)) + self.run_hook('current_item_changed', a) @@ -755,7 +745,7 @@ class ElectrumWindow(QMainWindow): self.amount_e.textChanged.connect(lambda: entry_changed(False) ) self.fee_e.textChanged.connect(lambda: entry_changed(True) ) - self.run_hook('create_send_tab', (grid,)) + self.run_hook('create_send_tab', grid) return w2 @@ -764,8 +754,8 @@ class ElectrumWindow(QMainWindow): for addr,label in self.wallet.labels.items(): if addr in self.wallet.addressbook: l.append( label + ' <' + addr + '>') - l = l + self.wallet.aliases.keys() + self.run_hook('update_completions', l) self.completions.setStringList(l) @@ -780,19 +770,9 @@ class ElectrumWindow(QMainWindow): r = unicode( self.payto_e.text() ) r = r.strip() - # alias - m1 = re.match(ALIAS_REGEXP, r) # label or alias, with address in brackets - m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) - - if m1: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - if not to_address: - return - elif m2: - to_address = m2.group(2) - else: - to_address = r + m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + to_address = m.group(2) if m else r if not is_valid(to_address): QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK')) @@ -815,7 +795,7 @@ class ElectrumWindow(QMainWindow): self.show_message(str(e)) return - self.run_hook('send_tx', (tx,)) + self.run_hook('send_tx', tx) if label: self.set_label(tx.hash(), label) @@ -844,10 +824,19 @@ class ElectrumWindow(QMainWindow): def set_url(self, url): - payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) + address, amount, label, message, signature, identity, url = util.parse_url(url) + + if label and self.wallet.labels.get(address) != label: + if self.question('Give label "%s" to address %s ?'%(label,address)): + if address not in self.wallet.addressbook and not self.wallet.is_mine(address): + self.wallet.addressbook.append(address) + self.set_label(address, label) + + self.run_hook('set_url', url, self.show_message, self.question) + self.tabs.setCurrentIndex(1) - label = self.wallet.labels.get(payto) - m_addr = label + ' <'+ payto+'>' if label else payto + label = self.wallet.labels.get(address) + m_addr = label + ' <'+ address +'>' if label else address self.payto_e.setText(m_addr) self.message_e.setText(message) @@ -1011,54 +1000,45 @@ class ElectrumWindow(QMainWindow): t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize") menu.addAction(t, lambda: self.toggle_priority(addr)) - self.run_hook('receive_menu', (menu,)) + self.run_hook('receive_menu', menu) menu.exec_(self.receive_list.viewport().mapToGlobal(position)) - def payto(self, x, is_alias): - if not x: return - if is_alias: - label = x - m_addr = label - else: - addr = x - label = self.wallet.labels.get(addr) - m_addr = label + ' <' + addr + '>' if label else addr + def payto(self, addr): + if not addr: return + label = self.wallet.labels.get(addr) + m_addr = label + ' <' + addr + '>' if label else addr self.tabs.setCurrentIndex(1) self.payto_e.setText(m_addr) self.amount_e.setFocus() - def delete_contact(self, x, is_alias): + + def delete_contact(self, x): if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")): - if not is_alias and x in self.wallet.addressbook: + if x in self.wallet.addressbook: self.wallet.addressbook.remove(x) self.set_label(x, None) - elif is_alias and x in self.wallet.aliases: - self.wallet.aliases.pop(x) - self.update_history_tab() - self.update_contacts_tab() - self.update_completions() + self.update_history_tab() + self.update_contacts_tab() + self.update_completions() - def create_contact_menu(self, position): - # fixme: this function apparently has a side effect. - # if it is not called the menu pops up several times - #self.contacts_list.selectedIndexes() + def create_contact_menu(self, position): item = self.contacts_list.itemAt(position) if not item: return addr = unicode(item.text(0)) label = unicode(item.text(1)) - is_alias = label in self.wallet.aliases.keys() - x = label if is_alias else addr + is_editable = item.data(0,32).toBool() + payto_addr = item.data(0,33).toString() menu = QMenu() menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr)) - menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias)) + menu.addAction(_("Pay to"), lambda: self.payto(payto_addr)) menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address"))) - if not is_alias: + if is_editable: menu.addAction(_("Edit label"), lambda: self.edit_label(False)) - else: - menu.addAction(_("View alias details"), lambda: self.show_contact_details(label)) - menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias)) + menu.addAction(_("Delete"), lambda: self.delete_contact(addr)) + + self.run_hook('create_contact_menu', menu, item) menu.exec_(self.contacts_list.viewport().mapToGlobal(position)) @@ -1068,7 +1048,7 @@ class ElectrumWindow(QMainWindow): label = self.wallet.labels.get(address,'') item.setData(1,0,label) - self.run_hook('update_receive_item', (address, item)) + self.run_hook('update_receive_item', address, item) c, u = self.wallet.get_addr_balance(address) balance = format_satoshis( c + u, False, self.wallet.num_zeros ) @@ -1114,13 +1094,12 @@ class ElectrumWindow(QMainWindow): for address in account[is_change]: h = self.wallet.history.get(address,[]) - if not is_change: - if h == []: - gap += 1 - if gap > self.wallet.gap_limit: - is_red = True - else: - gap = 0 + if h == []: + gap += 1 + if gap > self.wallet.gap_limit: + is_red = True + else: + gap = 0 num_tx = '*' if h == ['*'] else "%d"%len(h) item = QTreeWidgetItem( [ address, '', '', num_tx] ) @@ -1143,43 +1122,28 @@ class ElectrumWindow(QMainWindow): # we use column 1 because column 0 may be hidden l.setCurrentItem(l.topLevelItem(0),1) - def show_contact_details(self, m): - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0] - QMessageBox.information(self, 'Alias', msg, 'OK') def update_contacts_tab(self): l = self.contacts_list l.clear() - alias_targets = [] - for alias, v in self.wallet.aliases.items(): - s, target = v - alias_targets.append(target) - item = QTreeWidgetItem( [ target, alias, '-'] ) - item.setBackgroundColor(0, QColor('lightgray')) - l.addTopLevelItem(item) - for address in self.wallet.addressbook: - if address in alias_targets: continue label = self.wallet.labels.get(address,'') - n = 0 - for tx in self.wallet.transactions.values(): - if address in map(lambda x: x[0], tx.outputs): n += 1 - + n = self.wallet.get_num_tx(address) item = QTreeWidgetItem( [ address, label, "%d"%n] ) item.setFont(0, QFont(MONOSPACE_FONT)) + # 32 = label can be edited (bool) + item.setData(0,32, True) + # 33 = payto string + item.setData(0,33, address) l.addTopLevelItem(item) + self.run_hook('update_contacts_tab', l) l.setCurrentItem(l.topLevelItem(0)) + def create_console_tab(self): from qt_console import Console self.console = console = Console() @@ -1288,7 +1252,7 @@ class ElectrumWindow(QMainWindow): try: seed = self.wallet.decode_seed(password) except: - QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK')) + QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK')) return self.show_seed(seed, self) @@ -1518,8 +1482,9 @@ class ElectrumWindow(QMainWindow): vbox.addLayout(grid) vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) + d.setLayout(vbox) + self.run_hook('password_dialog', pw, grid, 1) if not d.exec_(): return return unicode(pw.text()) @@ -1595,20 +1560,22 @@ class ElectrumWindow(QMainWindow): d.setModal(1) vbox = QVBoxLayout() - msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.") + msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + '\n') vbox.addWidget(QLabel(msg)) grid = QGridLayout() grid.setSpacing(8) seed_e = QLineEdit() - grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0) + grid.addWidget(QLabel(_('Seed or master public key')), 1, 0) grid.addWidget(seed_e, 1, 1) + grid.addWidget(HelpButton(_("Your seed can be entered as a mnemonic (sequence of words), or as a hexadecimal string.")), 1, 3) gap_e = QLineEdit() gap_e.setText("5") grid.addWidget(QLabel(_('Gap limit')), 2, 0) grid.addWidget(gap_e, 2, 1) + grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3) gap_e.textChanged.connect(lambda: numbify(gap_e,True)) vbox.addLayout(grid) @@ -1733,23 +1700,10 @@ class ElectrumWindow(QMainWindow): self.show_message("There was a problem sending your transaction:\n %s" % (result_message)) def do_process_from_text(self): - dialog = QDialog(self) - dialog.setMinimumWidth(500) - dialog.setWindowTitle(_('Input raw transaction')) - dialog.setModal(1) - l = QVBoxLayout() - dialog.setLayout(l) - l.addWidget(QLabel(_("Transaction:"))) - txt = QTextEdit() - l.addWidget(txt) - - ok_button = QPushButton(_("Load transaction")) - ok_button.setDefault(True) - ok_button.clicked.connect(dialog.accept) - l.addWidget(ok_button) - - dialog.exec_() - tx_dict = self.tx_dict_from_text(unicode(txt.toPlainText())) + text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction")) + if not text: + return + tx_dict = self.tx_dict_from_text(text) if tx_dict: self.create_process_transaction_window(tx_dict) @@ -1832,7 +1786,7 @@ class ElectrumWindow(QMainWindow): for key, value in json.loads(data).items(): self.wallet.labels[key] = value self.wallet.save() - QMessageBox.information(None, _("Labels imported"), _("Your labels where imported from")+" '%s'" % str(labelsFile)) + QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile)) except (IOError, os.error), reason: QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason)) @@ -1857,24 +1811,34 @@ class ElectrumWindow(QMainWindow): @protected def do_import_privkey(self, password): if not self.wallet.imported_keys: - r = QMessageBox.question(None, _('Warning'), _('Warning: Imported keys are not recoverable from seed.') + ' ' \ - + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '\n\n' \ + r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \ + + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \ + + _('In addition, when you send bitcoins from one of your imported addresses, the "change" will be sent to an address derived from your seed, unless you disabled this option.') + '<p>' \ + _('Are you sure you understand what you are doing?'), 3, 4) if r == 4: return - text, ok = QInputDialog.getText(self, _('Import private key'), _('Private Key') + ':') - if not ok: return - sec = str(text).strip() - try: - addr = self.wallet.import_key(sec, password) - if not addr: - QMessageBox.critical(None, _("Unable to import key"), "error") + text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import")) + if not text: return + + text = str(text).split() + badkeys = [] + addrlist = [] + for key in text: + try: + addr = self.wallet.import_key(key, password) + except BaseException as e: + badkeys.append(key) + continue + if not addr: + badkeys.append(key) else: - QMessageBox.information(None, _("Key imported"), addr) - self.update_receive_tab() - self.update_history_tab() - except BaseException as e: - QMessageBox.critical(None, _("Unable to import key"), str(e)) + addrlist.append(addr) + if addrlist: + QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist)) + if badkeys: + QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys)) + self.update_receive_tab() + self.update_history_tab() def settings_dialog(self): @@ -2036,7 +2000,7 @@ class ElectrumWindow(QMainWindow): grid_plugins.setColumnStretch(0,1) tabs.addTab(tab5, _('Plugins') ) def mk_toggle(cb, p): - return lambda: cb.setChecked(p.toggle(self)) + return lambda: cb.setChecked(p.toggle()) for i, p in enumerate(self.plugins): try: name, description = p.get_info() @@ -2222,7 +2186,7 @@ class ElectrumWindow(QMainWindow): for p in protocol_letters: i = protocol_letters.index(p) j = server_protocol.model().index(i,0) - if p not in pp.keys(): + if p not in pp.keys() and interface.is_connected: server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1) else: server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1) diff --git a/gui/gui_gtk.py b/gui/gui_gtk.py @@ -868,14 +868,14 @@ class ElectrumWindow: self.show_message(tx_details) elif treeview == self.contacts_treeview: m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) + #a = self.wallet.aliases.get(m) + #if a: + # if a[0] in self.wallet.authorities.keys(): + # s = self.wallet.authorities.get(a[0]) + # else: + # s = "self-signed" + # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] + # self.show_message(msg) def treeview_key_press(self, treeview, event): @@ -890,14 +890,14 @@ class ElectrumWindow: self.show_message(tx_details) elif treeview == self.contacts_treeview: m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self" - msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) + #a = self.wallet.aliases.get(m) + #if a: + # if a[0] in self.wallet.authorities.keys(): + # s = self.wallet.authorities.get(a[0]) + # else: + # s = "self" + # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] + # self.show_message(msg) return False @@ -1145,17 +1145,14 @@ class ElectrumWindow: def update_sending_tab(self): # detect addresses that are not mine in history, add them here... self.addressbook_list.clear() - for alias, v in self.wallet.aliases.items(): - s, target = v - label = self.wallet.labels.get(alias) - self.addressbook_list.append((alias, label, '-')) + #for alias, v in self.wallet.aliases.items(): + # s, target = v + # label = self.wallet.labels.get(alias) + # self.addressbook_list.append((alias, label, '-')) for address in self.wallet.addressbook: label = self.wallet.labels.get(address) - n = 0 - for tx in self.wallet.transactions.values(): - if address in map(lambda x:x[0], tx.outputs): n += 1 - + n = self.wallet.get_num_tx(address) self.addressbook_list.append((address, label, "%d"%n)) def update_history_tab(self): @@ -1176,7 +1173,7 @@ class ElectrumWindow: label, is_default_label = self.wallet.get_label(tx_hash) tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else '' - details = self.wallet.get_tx_details(tx_hash) + details = self.get_tx_details(tx_hash) self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, format_satoshis(value,True,self.wallet.num_zeros), @@ -1184,6 +1181,40 @@ class ElectrumWindow: if cursor: self.history_treeview.set_cursor( cursor ) + def get_tx_details(self, tx_hash): + import datetime + if not tx_hash: return '' + tx = self.wallet.transactions.get(tx_hash) + is_mine, v, fee = self.wallet.get_tx_value(tx) + conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash) + + if timestamp: + time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] + else: + time_str = 'pending' + + inputs = map(lambda x: x.get('address'), tx.inputs) + outputs = map(lambda x: x.get('address'), tx.d['outputs']) + tx_details = "Transaction Details" +"\n\n" \ + + "Transaction ID:\n" + tx_hash + "\n\n" \ + + "Status: %d confirmations\n"%conf + if is_mine: + if fee: + tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \ + + "Transaction fee: %s\n"% format_satoshis(fee, False) + else: + tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \ + + "Transaction fee: unknown\n" + else: + tx_details += "Amount received: %s\n"% format_satoshis(v, False) \ + + tx_details += "Date: %s\n\n"%time_str \ + + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \ + + "Outputs:\n-"+ '\n-'.join(outputs) + + return tx_details + + def newaddress_dialog(self, w): diff --git a/gui/plugins.py b/gui/plugins.py @@ -0,0 +1,32 @@ + + +class BasePlugin: + + def get_info(self): + return self.fullname, self.description + + def __init__(self, gui, name, fullname, description): + self.name = name + self.fullname = fullname + self.description = description + self.gui = gui + self.config = gui.config + + def toggle(self): + enabled = not self.is_enabled() + self.set_enabled(enabled) + self.init_gui() + return enabled + + def init_gui(self): + pass + + def is_enabled(self): + return self.is_available() and self.config.get('use_'+self.name) is True + + def is_available(self): + return True + + def set_enabled(self, enabled): + self.config.set_key('use_'+self.name, enabled, True) + diff --git a/icons/electrum.ico b/icons/electrum.ico Binary files differ. diff --git a/lib/bitcoin.py b/lib/bitcoin.py @@ -364,10 +364,9 @@ random_seed = lambda n: "%032x"%ecdsa.util.randrange( pow(2,n) ) def bip32_init(seed): import hmac - + seed = seed.decode('hex') I = hmac.new("Bitcoin seed", seed, hashlib.sha512).digest() - print "seed", seed.encode('hex') master_secret = I[0:32] master_chain = I[32:] @@ -415,8 +414,8 @@ def CKD_prime(K, c, n): class ElectrumSequence: """ Privatekey(type,n) = Master_private_key + H(n|S|type) """ - def __init__(self, master_public_key, mpk2 = None, mpk3 = None): - self.master_public_key = master_public_key + def __init__(self, mpk, mpk2 = None, mpk3 = None): + self.mpk = mpk self.mpk2 = mpk2 self.mpk3 = mpk3 @@ -445,7 +444,7 @@ class ElectrumSequence: address = public_key_to_bc_address( pubkey.decode('hex') ) elif not self.mpk3: pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence, use_mpk2=True) + pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"] else: pubkey1 = self.get_pubkey(sequence) @@ -456,7 +455,7 @@ class ElectrumSequence: def get_pubkey(self, sequence, mpk=None): curve = SECP256k1 - if mpk is None: mpk = self.master_public_key + if mpk is None: mpk = self.mpk z = self.get_sequence(sequence, mpk) master_public_key = ecdsa.VerifyingKey.from_string( mpk.decode('hex'), curve = SECP256k1 ) pubkey_point = master_public_key.pubkey.point + z*curve.generator @@ -465,7 +464,7 @@ class ElectrumSequence: def get_private_key_from_stretched_exponent(self, sequence, secexp): order = generator_secp256k1.order() - secexp = ( secexp + self.get_sequence(sequence, self.master_public_key) ) % order + secexp = ( secexp + self.get_sequence(sequence, self.mpk) ) % order pk = number_to_string( secexp, generator_secp256k1.order() ) compressed = False return SecretToASecret( pk, compressed ) @@ -483,7 +482,7 @@ class ElectrumSequence: secexp = self.stretch_key(seed) master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) master_public_key = master_private_key.get_verifying_key().to_string().encode('hex') - if master_public_key != self.master_public_key: + if master_public_key != self.mpk: print_error('invalid password (mpk)') raise BaseException('Invalid password') return True @@ -499,8 +498,8 @@ class ElectrumSequence: redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript'] else: pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence,mpk=self.mpk2) - pubkey3 = self.get_pubkey(sequence,mpk=self.mpk3) + pubkey2 = self.get_pubkey(sequence, mpk=self.mpk2) + pubkey3 = self.get_pubkey(sequence, mpk=self.mpk3) pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key redeemScript = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)['redeemScript'] return pk_addr, redeemScript @@ -510,43 +509,71 @@ class ElectrumSequence: class BIP32Sequence: - def __init__(self, mpkc, mpkc2 = None): - self.master_public_key, self.master_chain = mpkc - if mpkc2: - self.master_public_key2, self.master_chain2 = mpkc2 - self.is_p2sh = True - else: - self.is_p2sh = False + def __init__(self, mpk, mpk2 = None, mpk3 = None): + self.mpk = mpk + self.mpk2 = mpk2 + self.mpk3 = mpk3 @classmethod def mpk_from_seed(klass, seed): master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) return master_public_key.encode('hex'), master_chain.encode('hex') - def get_pubkey(self, sequence, use_mpk2=False): - if not use_mpl2: - K = self.master_public_key.decode('hex') - chain = self.master_chain.decode('hex') - else: - K = self.master_public_key_2.decode('hex') - chain = self.master_chain_2.decode('hex') + def get_pubkey(self, sequence, mpk = None): + if not mpk: mpk = self.mpk + master_public_key, master_chain = self.mpk + K = master_public_key.decode('hex') + chain = master_chain.decode('hex') for i in sequence: K, K_compressed, chain = CKD_prime(K, chain, i) - return K_compressed + return K_compressed.encode('hex') def get_address(self, sequence): - return hash_160_to_bc_address(hash_160(self.get_pubkey(sequence))) + if not self.mpk2: + pubkey = self.get_pubkey(sequence) + address = public_key_to_bc_address( pubkey.decode('hex') ) + elif not self.mpk3: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) + address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"] + else: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) + pubkey3 = self.get_pubkey(sequence, mpk = self.mpk3) + address = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)["address"] + return address - def get_private_key(self, seed, sequence): - k = self.master_secret - chain = self.master_chain + def get_private_key(self, sequence, seed): + master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) + chain = master_chain + k = master_secret for i in sequence: - k, k_compressed, chain = CKD(k, chain, i) - return SecretToASecret(k0, True) + k, chain = CKD(k, chain, i) + return SecretToASecret(k, True) + + def get_private_keys(self, sequence_list, seed): + return [ self.get_private_key( sequence, seed) for sequence in sequence_list] def check_seed(self, seed): master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) - assert self.master_public_key == master_public_key + assert self.mpk == (master_public_key.encode('hex'), master_chain.encode('hex')) + + def get_input_info(self, sequence): + if not self.mpk2: + pk_addr = self.get_address(sequence) + redeemScript = None + elif not self.mpk3: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk=self.mpk2) + pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key + redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript'] + else: + pubkey1 = self.get_pubkey(sequence) + pubkey2 = self.get_pubkey(sequence, mpk=self.mpk2) + pubkey3 = self.get_pubkey(sequence, mpk=self.mpk3) + pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key + redeemScript = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)['redeemScript'] + return pk_addr, redeemScript ################################## transactions @@ -842,7 +869,7 @@ class Transaction: def test_bip32(): - seed = "ff000000000000000000000000000000".decode('hex') + seed = "ff000000000000000000000000000000" master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed) print "secret key", master_secret.encode('hex') diff --git a/lib/commands.py b/lib/commands.py @@ -82,9 +82,10 @@ class Commands: self.wallet = wallet self.interface = interface self._callback = callback + self.password = None def _run(self, method, args, password_getter): - if method in protected_commands: + if method in protected_commands and self.wallet.use_encryption: self.password = apply(password_getter,()) f = eval('self.'+method) result = apply(f,args) diff --git a/lib/deserialize.py b/lib/deserialize.py @@ -3,6 +3,7 @@ # from bitcoin import public_key_to_bc_address, hash_160_to_bc_address, hash_encode, hash_160 +from util import print_error #import socket import time import struct @@ -356,7 +357,8 @@ def get_address_from_input_script(bytes): pubkeys = [ dec2[1][1].encode('hex'), dec2[2][1].encode('hex'), dec2[3][1].encode('hex') ] return pubkeys, signatures, hash_160_to_bc_address(hash_160(redeemScript), 5) - raise BaseException("no match for scriptsig") + print_error("cannot find address in input script", bytes.encode('hex')) + return [], [], "(None)" diff --git a/lib/interface.py b/lib/interface.py @@ -595,8 +595,8 @@ class Interface(threading.Thread): # wait until connection is established self.connect_event.wait() if not self.is_connected: - print_msg("Not connected, aborting.") - sys.exit(1) + return False + return True def run(self): while True: diff --git a/lib/util.py b/lib/util.py @@ -1,4 +1,4 @@ -import os, sys +import os, sys, re import platform import shutil from datetime import datetime @@ -147,3 +147,36 @@ def age(from_date, since_date = None, target_tz=None, include_seconds=False): return "about 1 year ago" else: return "over %d years ago" % (round(distance_in_minutes / 525600)) + + + + +# URL decode +_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) +urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) + +def parse_url(url): + o = url[8:].split('?') + address = o[0] + if len(o)>1: + params = o[1].split('&') + else: + params = [] + + amount = label = message = signature = identity = '' + for p in params: + k,v = p.split('=') + uv = urldecode(v) + if k == 'amount': amount = uv + elif k == 'message': message = uv + elif k == 'label': label = uv + elif k == 'signature': + identity, signature = uv.split(':') + url = url.replace('&%s=%s'%(k,v),'') + else: + print k,v + + return address, amount, label, message, signature, identity, url + + + diff --git a/lib/verifier.py b/lib/verifier.py @@ -49,14 +49,10 @@ class WalletVerifier(threading.Thread): def get_confirmations(self, tx): """ return the number of confirmations of a monitored transaction. """ with self.lock: - if tx in self.transactions.keys(): - if tx in self.verified_tx: - height, timestamp = self.verified_tx[tx] - conf = (self.local_height - height + 1) - else: - conf = -1 + if tx in self.verified_tx: + height, timestamp = self.verified_tx[tx] + conf = (self.local_height - height + 1) else: - #print "verifier: tx not in list", tx conf = 0 if conf <= 0: @@ -65,6 +61,13 @@ class WalletVerifier(threading.Thread): return conf, timestamp + def get_height(self, tx_hash): + with self.lock: + v = self.verified_tx.get(tx_hash) + height = v[0] if v else None + return height + + def add(self, tx_hash, tx_height): """ add a transaction to the list of monitored transactions. """ assert tx_height > 0 @@ -145,6 +148,10 @@ class WalletVerifier(threading.Thread): continue if not r: continue + if r.get('error'): + print_error('Verifier received an error:', r) + continue + # 3. handle response method = r['method'] params = r['params'] @@ -183,7 +190,8 @@ class WalletVerifier(threading.Thread): # we passed all the tests header = self.read_header(tx_height) timestamp = header.get('timestamp') - self.verified_tx[tx_hash] = (tx_height, timestamp) + with self.lock: + self.verified_tx[tx_hash] = (tx_height, timestamp) print_error("verified %s"%tx_hash) self.config.set_key('verified_tx2', self.verified_tx, True) self.interface.trigger_callback('updated') @@ -241,12 +249,16 @@ class WalletVerifier(threading.Thread): # this can be caused by a reorg. print_error("verify header failed"+ repr(header)) # undo verifications - for tx_hash, item in self.verified_tx.items(): + with self.lock: + items = self.verified_tx.items()[:] + for tx_hash, item in items: tx_height, timestamp = item if tx_height >= height: print_error("redoing", tx_hash) - self.verified_tx.pop(tx_hash) - if tx_hash in self.merkle_roots: self.merkle_roots.pop(tx_hash) + with self.lock: + self.verified_tx.pop(tx_hash) + if tx_hash in self.merkle_roots: + self.merkle_roots.pop(tx_hash) # return False to request previous header. return False diff --git a/lib/wallet.py b/lib/wallet.py @@ -33,9 +33,6 @@ import time from util import print_msg, print_error, user_dir, format_satoshis from bitcoin import * -# URL decode -_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) -urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) # AES encryption EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s)) @@ -82,19 +79,16 @@ class Wallet: self.use_encryption = config.get('use_encryption', False) self.seed = config.get('seed', '') # encrypted self.labels = config.get('labels', {}) - self.aliases = config.get('aliases', {}) # aliases for addresses - self.authorities = config.get('authorities', {}) # trusted addresses self.frozen_addresses = config.get('frozen_addresses',[]) self.prioritized_addresses = config.get('prioritized_addresses',[]) - self.receipts = config.get('receipts',{}) # signed URIs self.addressbook = config.get('contacts', []) self.imported_keys = config.get('imported_keys',{}) self.history = config.get('addr_history',{}) # address -> list(txid, height) - self.tx_height = config.get('tx_height',{}) self.accounts = config.get('accounts', {}) # this should not include public keys + self.SequenceClass = ElectrumSequence self.sequences = {} - self.sequences[0] = ElectrumSequence(self.config.get('master_public_key')) + self.sequences[0] = self.SequenceClass(self.config.get('master_public_key')) if self.accounts.get(0) is None: self.accounts[0] = { 0:[], 1:[], 'name':'Main account' } @@ -161,13 +155,13 @@ class Wallet: self.seed = seed self.config.set_key('seed', self.seed, True) self.config.set_key('seed_version', self.seed_version, True) - mpk = ElectrumSequence.mpk_from_seed(self.seed) + mpk = self.SequenceClass.mpk_from_seed(self.seed) self.init_sequence(mpk) def init_sequence(self, mpk): self.config.set_key('master_public_key', mpk, True) - self.sequences[0] = ElectrumSequence(mpk) + self.sequences[0] = self.SequenceClass(mpk) self.accounts[0] = { 0:[], 1:[], 'name':'Main account' } self.config.set_key('accounts', self.accounts, True) @@ -184,8 +178,8 @@ class Wallet: return address in self.addresses(True) def is_change(self, address): - #return address in self.change_addresses - return False + acct, s = self.get_address_index(address) + return s[0] == 1 def get_master_public_key(self): return self.sequences[0].master_public_key @@ -302,6 +296,7 @@ class Wallet: address = self.get_new_address( account, for_change, n) self.accounts[account][for_change].append(address) self.history[address] = [] + print_msg(address) return address @@ -412,6 +407,12 @@ class Wallet: # redo labels # self.update_tx_labels() + def get_num_tx(self, address): + n = 0 + for tx in self.transactions.values(): + if address in map(lambda x:x[0], tx.outputs): n += 1 + return n + def get_address_flags(self, addr): flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-" @@ -424,46 +425,6 @@ class Wallet: return tx.get_value(addresses, self.prevout_values) - def get_tx_details(self, tx_hash): - import datetime - if not tx_hash: return '' - tx = self.transactions.get(tx_hash) - is_mine, v, fee = self.get_tx_value(tx) - conf, timestamp = self.verifier.get_confirmations(tx_hash) - - if timestamp: - time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] - else: - time_str = 'pending' - - inputs = map(lambda x: x.get('address'), tx.inputs) - outputs = map(lambda x: x.get('address'), tx.d['outputs']) - tx_details = "Transaction Details" +"\n\n" \ - + "Transaction ID:\n" + tx_hash + "\n\n" \ - + "Status: %d confirmations\n"%conf - if is_mine: - if fee: - tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \ - + "Transaction fee: %s\n"% format_satoshis(fee, False) - else: - tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \ - + "Transaction fee: unknown\n" - else: - tx_details += "Amount received: %s\n"% format_satoshis(v, False) \ - - tx_details += "Date: %s\n\n"%time_str \ - + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \ - + "Outputs:\n-"+ '\n-'.join(outputs) - - r = self.receipts.get(tx_hash) - if r: - tx_details += "\n_______________________________________" \ - + '\n\nSigned URI: ' + r[2] \ - + "\n\nSigned by: " + r[0] \ - + '\n\nSignature: ' + r[1] - - return tx_details - def update_tx_outputs(self, tx_hash): tx = self.transactions.get(tx_hash) @@ -566,6 +527,7 @@ class Wallet: if h == ['*']: continue for tx_hash, tx_height in h: tx = self.transactions.get(tx_hash) + if tx is None: raise BaseException("Wallet not synchronized") for output in tx.d.get('outputs'): if output.get('address') != addr: continue key = tx_hash + ":%d" % output.get('index') @@ -640,11 +602,12 @@ class Wallet: def receive_tx_callback(self, tx_hash, tx, tx_height): if not self.check_new_tx(tx_hash, tx): - raise BaseException("error: received transaction is not consistent with history", tx_hash) + # may happen due to pruning + print_error("received transaction that is no longer referenced in history", tx_hash) + return with self.lock: self.transactions[tx_hash] = tx - self.tx_height[tx_hash] = tx_height #tx_height = tx.get('height') if self.verifier and tx_height>0: @@ -669,17 +632,12 @@ class Wallet: if tx_height>0: # add it in case it was previously unconfirmed if self.verifier: self.verifier.add(tx_hash, tx_height) - # set the height in case it changed - txh = self.tx_height.get(tx_hash) - if txh is not None and txh != tx_height: - print_error( "changing height for tx", tx_hash ) - self.tx_height[tx_hash] = tx_height def get_tx_history(self): with self.lock: history = self.transactions.items() - history.sort(key = lambda x: self.tx_height.get(x[0]) if self.tx_height.get(x[0]) else 1e12) + history.sort(key = lambda x: self.verifier.get_height(x[0]) if self.verifier.get_height(x[0]) else 1e12) result = [] balance = 0 @@ -724,6 +682,9 @@ class Wallet: default_label = self.labels[o_addr] except KeyError: default_label = o_addr + break + else: + default_label = '(internal)' else: for o in tx.outputs: o_addr, _ = o @@ -812,48 +773,6 @@ class Wallet: return True, out - def read_alias(self, alias): - # this might not be the right place for this function. - import urllib - - m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) - m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) - if m1: - url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) - elif m2: - url = 'https://' + alias + '/bitcoin.id' - else: - return '' - try: - lines = urllib.urlopen(url).readlines() - except: - return '' - - # line 0 - line = lines[0].strip().split(':') - if len(line) == 1: - auth_name = None - target = signing_addr = line[0] - else: - target, auth_name, signing_addr, signature = line - msg = "alias:%s:%s:%s"%(alias,target,auth_name) - print msg, signature - EC_KEY.verify_message(signing_addr, signature, msg) - - # other lines are signed updates - for line in lines[1:]: - line = line.strip() - if not line: continue - line = line.split(':') - previous = target - print repr(line) - target, signature = line - EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) - - if not is_valid(target): - raise ValueError("Invalid bitcoin address") - - return target, signing_addr, auth_name def update_password(self, seed, old_password, new_password): if new_password == '': new_password = None @@ -867,96 +786,6 @@ class Wallet: self.imported_keys[k] = c self.save() - def get_alias(self, alias, interactive = False, show_message=None, question = None): - try: - target, signing_address, auth_name = self.read_alias(alias) - except BaseException, e: - # raise exception if verify fails (verify the chain) - if interactive: - show_message("Alias error: " + str(e)) - return - - print target, signing_address, auth_name - - if auth_name is None: - a = self.aliases.get(alias) - if not a: - msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) - if interactive and question( msg ): - self.aliases[alias] = (signing_address, target) - else: - target = None - else: - if signing_address != a[0]: - msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias - if interactive and question( msg ): - self.aliases[alias] = (signing_address, target) - else: - target = None - else: - if signing_address not in self.authorities.keys(): - msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) - if interactive and question( msg ): - self.authorities[signing_address] = auth_name - else: - target = None - - if target: - self.aliases[alias] = (signing_address, target) - - return target - - - def parse_url(self, url, show_message, question): - o = url[8:].split('?') - address = o[0] - if len(o)>1: - params = o[1].split('&') - else: - params = [] - - amount = label = message = signature = identity = '' - for p in params: - k,v = p.split('=') - uv = urldecode(v) - if k == 'amount': amount = uv - elif k == 'message': message = uv - elif k == 'label': label = uv - elif k == 'signature': - identity, signature = uv.split(':') - url = url.replace('&%s=%s'%(k,v),'') - else: - print k,v - - if label and self.labels.get(address) != label: - if question('Give label "%s" to address %s ?'%(label,address)): - if address not in self.addressbook and not self.is_mine(address): - self.addressbook.append(address) - self.labels[address] = label - - if signature: - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): - signing_address = self.get_alias(identity, True, show_message, question) - elif is_valid(identity): - signing_address = identity - else: - signing_address = None - if not signing_address: - return - try: - EC_KEY.verify_message(signing_address, signature, url ) - self.receipt = (signing_address, signature, url) - except: - show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') - address = amount = label = identity = message = '' - - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): - payto_address = self.get_alias(address, True, show_message, question) - if payto_address: - address = address + ' <' + payto_address + '>' - - return address, amount, label, message, signature, identity, url - def freeze(self,addr): @@ -1007,15 +836,11 @@ class Wallet: 'labels': self.labels, 'contacts': self.addressbook, 'imported_keys': self.imported_keys, - 'aliases': self.aliases, - 'authorities': self.authorities, - 'receipts': self.receipts, 'num_zeros': self.num_zeros, 'frozen_addresses': self.frozen_addresses, 'prioritized_addresses': self.prioritized_addresses, 'gap_limit': self.gap_limit, 'transactions': tx, - 'tx_height': self.tx_height, } for k, v in s.items(): self.config.set_key(k,v) @@ -1024,17 +849,6 @@ class Wallet: def set_verifier(self, verifier): self.verifier = verifier - # review stored transactions and send them to the verifier - # (they are not necessarily in the history, because history items might have have been pruned) - for tx_hash, tx in self.transactions.items(): - tx_height = self.tx_height[tx_hash] - if tx_height <1: - print_error( "skipping", tx_hash, tx_height ) - continue - - if tx_height>0: - self.verifier.add(tx_hash, tx_height) - # review transactions that are in the history for addr, hist in self.history.items(): if hist == ['*']: continue @@ -1042,11 +856,6 @@ class Wallet: if tx_height>0: # add it in case it was previously unconfirmed self.verifier.add(tx_hash, tx_height) - # set the height in case it changed - txh = self.tx_height.get(tx_hash) - if txh is not None and txh != tx_height: - print_error( "changing height for tx", tx_hash ) - self.tx_height[tx_hash] = tx_height @@ -1082,7 +891,7 @@ class Wallet: if not tx: continue # already verified? - if self.tx_height.get(tx_hash): + if self.verifier.get_height(tx_hash): continue # unconfirmed tx print_error("new history is orphaning transaction:", tx_hash) @@ -1099,7 +908,6 @@ class Wallet: for item in h: if item.get('tx_hash') == tx_hash: height = item.get('height') - self.tx_height[tx_hash] = height if height: print_error("found height for", tx_hash, height) self.verifier.add(tx_hash, height) @@ -1155,22 +963,6 @@ class WalletSynchronizer(threading.Thread): def is_running(self): with self.lock: return self.running - def synchronize_wallet(self): - new_addresses = self.wallet.synchronize() - if new_addresses: - self.subscribe_to_addresses(new_addresses) - self.wallet.up_to_date = False - return - - if not self.interface.is_up_to_date('synchronizer'): - if self.wallet.is_up_to_date(): - self.wallet.set_up_to_date(False) - self.was_updated = True - return - - self.wallet.set_up_to_date(True) - self.was_updated = True - def subscribe_to_addresses(self, addresses): messages = [] @@ -1202,15 +994,30 @@ class WalletSynchronizer(threading.Thread): self.subscribe_to_addresses(self.wallet.addresses(True)) while self.is_running(): - # 1. send new requests - self.synchronize_wallet() + # 1. create new addresses + new_addresses = self.wallet.synchronize() + # request missing addresses + if new_addresses: + self.subscribe_to_addresses(new_addresses) + + # request missing transactions for tx_hash, tx_height in missing_tx: if (tx_hash, tx_height) not in requested_tx: self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer') requested_tx.append( (tx_hash, tx_height) ) missing_tx = [] + # detect if situation has changed + if not self.interface.is_up_to_date('synchronizer'): + if self.wallet.is_up_to_date(): + self.wallet.set_up_to_date(False) + self.was_updated = True + else: + if not self.wallet.is_up_to_date(): + self.wallet.set_up_to_date(True) + self.was_updated = True + if self.was_updated: self.interface.trigger_callback('updated') self.was_updated = False diff --git a/make_packages b/make_packages @@ -10,7 +10,7 @@ if __name__ == '__main__': sys.exit() os.system("python mki18n.py") - os.system("pyrcc4 icons.qrc -o lib/icons_rc.py") + os.system("pyrcc4 icons.qrc -o gui/icons_rc.py") os.system("python setup.py sdist --format=zip,gztar") _tgz="Electrum-%s.tar.gz"%version diff --git a/plugins/aliases.py b/plugins/aliases.py @@ -0,0 +1,199 @@ +import re +import platform +from decimal import Decimal + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui + +from electrum_gui.qrcodewidget import QRCodeWidget +from electrum_gui import bmp, pyqrnative +from electrum_gui.i18n import _ + + +ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$' + + + +from electrum_gui import BasePlugin +class Plugin(BasePlugin): + + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'aliases', 'Aliases', _('Retrieve aliases using http.')) + self.aliases = self.config.get('aliases', {}) # aliases for addresses + self.authorities = self.config.get('authorities', {}) # trusted addresses + self.receipts = self.config.get('receipts',{}) # signed URIs + + + def timer_actions(self): + if self.gui.payto_e.hasFocus(): + return + r = unicode( self.gui.payto_e.text() ) + if r != self.gui.previous_payto_e: + self.gui.previous_payto_e = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = self.get_alias(r, True, self.gui.show_message, self.gui.question) + except: + return + if to_address: + s = r + ' <' + to_address + '>' + self.gui.payto_e.setText(s) + + + def get_alias(self, alias, interactive = False, show_message=None, question = None): + try: + target, signing_address, auth_name = read_alias(self, alias) + except BaseException, e: + # raise exception if verify fails (verify the chain) + if interactive: + show_message("Alias error: " + str(e)) + return + + print target, signing_address, auth_name + + if auth_name is None: + a = self.aliases.get(alias) + if not a: + msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address) + if interactive and question( msg ): + self.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address != a[0]: + msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias + if interactive and question( msg ): + self.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address not in self.authorities.keys(): + msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address) + if interactive and question( msg ): + self.authorities[signing_address] = auth_name + else: + target = None + + if target: + self.aliases[alias] = (signing_address, target) + + return target + + + + def read_alias(self, alias): + import urllib + + m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) + m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) + if m1: + url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) + elif m2: + url = 'https://' + alias + '/bitcoin.id' + else: + return '' + try: + lines = urllib.urlopen(url).readlines() + except: + return '' + + # line 0 + line = lines[0].strip().split(':') + if len(line) == 1: + auth_name = None + target = signing_addr = line[0] + else: + target, auth_name, signing_addr, signature = line + msg = "alias:%s:%s:%s"%(alias,target,auth_name) + print msg, signature + EC_KEY.verify_message(signing_addr, signature, msg) + + # other lines are signed updates + for line in lines[1:]: + line = line.strip() + if not line: continue + line = line.split(':') + previous = target + print repr(line) + target, signature = line + EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) + + if not is_valid(target): + raise ValueError("Invalid bitcoin address") + + return target, signing_addr, auth_name + + + def set_url(self, url, show_message, question): + payto, amount, label, message, signature, identity, url = util.parse_url(url) + if signature: + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): + signing_address = get_alias(identity, True, show_message, question) + elif is_valid(identity): + signing_address = identity + else: + signing_address = None + if not signing_address: + return + try: + EC_KEY.verify_message(signing_address, signature, url ) + self.receipt = (signing_address, signature, url) + except: + show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') + address = amount = label = identity = message = '' + + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address): + payto_address = get_alias(address, True, show_message, question) + if payto_address: + address = address + ' <' + payto_address + '>' + + return address, amount, label, message, signature, identity, url + + + + def update_contacts_tab(self, l): + alias_targets = [] + for alias, v in self.aliases.items(): + s, target = v + alias_targets.append(target) + item = QTreeWidgetItem( [ target, alias, '-'] ) + item.setBackgroundColor(0, QColor('lightgray')) + item.setData(0,32,False) + item.setData(0,33,alias + ' <' + target + '>') + l.insertTopLevelItem(0,item) + + + def update_completions(self, l): + l[:] = l + self.aliases.keys() + + + def create_contact_menu(self, menu, item): + label = unicode(item.text(1)) + if label in self.aliases.keys(): + addr = unicode(item.text(0)) + label = unicode(item.text(1)) + menu.addAction(_("View alias details"), lambda: self.show_contact_details(label)) + menu.addAction(_("Delete alias"), lambda: delete_alias(self, label)) + + + def show_contact_details(self, m): + a = self.aliases.get(m) + if a: + if a[0] in self.authorities.keys(): + s = self.authorities.get(a[0]) + else: + s = "self-signed" + msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0] + QMessageBox.information(self.gui, 'Alias', msg, 'OK') + + + def delete_alias(self, x): + if self.gui.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")): + if x in self.aliases: + self.aliases.pop(x) + self.update_history_tab() + self.update_contacts_tab() + self.update_completions() diff --git a/plugins/pointofsale.py b/plugins/pointofsale.py @@ -8,8 +8,7 @@ import PyQt4.QtCore as QtCore import PyQt4.QtGui as QtGui from electrum_gui.qrcodewidget import QRCodeWidget -from electrum_gui import bmp, pyqrnative - +from electrum_gui import bmp, pyqrnative, BasePlugin from electrum_gui.i18n import _ @@ -89,98 +88,95 @@ class QR_Window(QWidget): self.qrw.set_addr( msg ) - - -config = {} -def get_info(): - return 'Point of Sale', _('Show QR code window and amounts requested for each address. Add menu item to request amount.') -def init(gui): - global config - config = gui.config - gui.requested_amounts = config.get('requested_amounts',{}) - gui.merchant_name = config.get('merchant_name', 'Invoice') - gui.qr_window = None - do_enable(gui, is_enabled()) +class Plugin(BasePlugin): -def is_enabled(): - return config.get('pointofsale') is True + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'pointofsale', 'Point of Sale', + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') ) + self.qr_window = None + self.requested_amounts = self.config.get('requested_amounts',{}) + self.merchant_name = self.config.get('merchant_name', 'Invoice') -def is_available(): - return True + def init_gui(self): + enabled = self.is_enabled() + if enabled: + self.gui.expert_mode = True + self.gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Request')]) + else: + self.gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Tx')]) -def toggle(gui): - enabled = not is_enabled() - config.set_key('pointofsale', enabled, True) - do_enable(gui, enabled) - update_gui(gui) - return enabled + self.toggle_QR_window(enabled) + + def close_main_window(self): + if self.qr_window: + self.qr_window.close() + self.qr_window = None -def do_enable(gui, enabled): - if enabled: - gui.expert_mode = True - gui.set_hook('item_changed', item_changed) - gui.set_hook('current_item_changed', recv_changed) - gui.set_hook('receive_menu', receive_menu) - gui.set_hook('update_receive_item', update_receive_item) - gui.set_hook('timer_actions', timer_actions) - gui.set_hook('close_main_window', close_main_window) - gui.set_hook('init', update_gui) - else: - gui.unset_hook('item_changed', item_changed) - gui.unset_hook('current_item_changed', recv_changed) - gui.unset_hook('receive_menu', receive_menu) - gui.unset_hook('update_receive_item', update_receive_item) - gui.unset_hook('timer_actions', timer_actions) - gui.unset_hook('close_main_window', close_main_window) - gui.unset_hook('init', update_gui) + + def timer_actions(self): + if self.qr_window: + self.qr_window.qrw.update_qr() + + + def toggle_QR_window(self, show): + if show and not self.qr_window: + self.qr_window = QR_Window(self.gui.exchanger) + self.qr_window.setVisible(True) + self.qr_window_geometry = self.qr_window.geometry() + item = self.gui.receive_list.currentItem() + if item: + address = str(item.text(1)) + label = self.gui.wallet.labels.get(address) + amount, currency = self.requested_amounts.get(address, (None, None)) + self.qr_window.set_content( address, label, amount, currency ) + elif show and self.qr_window and not self.qr_window.isVisible(): + self.qr_window.setVisible(True) + self.qr_window.setGeometry(self.qr_window_geometry) + elif not show and self.qr_window and self.qr_window.isVisible(): + self.qr_window_geometry = self.qr_window.geometry() + self.qr_window.setVisible(False) -def update_gui(gui): - enabled = is_enabled() - if enabled: - gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Request')]) - else: - gui.receive_list.setHeaderLabels([ _('Address'), _('Label'), _('Balance'), _('Tx')]) - toggle_QR_window(gui, enabled) - - -def toggle_QR_window(self, show): - if show and not self.qr_window: - self.qr_window = QR_Window(self.exchanger) - self.qr_window.setVisible(True) - self.qr_window_geometry = self.qr_window.geometry() - item = self.receive_list.currentItem() - if item: - address = str(item.text(1)) - label = self.wallet.labels.get(address) + def update_receive_item(self, address, item): + try: amount, currency = self.requested_amounts.get(address, (None, None)) - self.qr_window.set_content( address, label, amount, currency ) - - elif show and self.qr_window and not self.qr_window.isVisible(): - self.qr_window.setVisible(True) - self.qr_window.setGeometry(self.qr_window_geometry) + except: + print "cannot get requested amount", address, self.requested_amounts.get(address) + amount, currency = None, None + self.requested_amounts.pop(address) - elif not show and self.qr_window and self.qr_window.isVisible(): - self.qr_window_geometry = self.qr_window.geometry() - self.qr_window.setVisible(False) + amount_str = amount + (' ' + currency if currency else '') if amount is not None else '' + item.setData(column_index,0,amount_str) + + def current_item_changed(self, a): + if a is not None and self.qr_window and self.qr_window.isVisible(): + address = str(a.text(0)) + label = self.gui.wallet.labels.get(address) + try: + amount, currency = self.requested_amounts.get(address, (None, None)) + except: + amount, currency = None, None + self.qr_window.set_content( address, label, amount, currency ) -def item_changed(self, item, column): - if column == column_index: + + def item_changed(self, item, column): + if column != column_index: + return address = str( item.text(0) ) text = str( item.text(column) ) try: - seq = self.wallet.get_address_index(address) + seq = self.gui.wallet.get_address_index(address) index = seq[-1] except: print "cannot get index" @@ -198,9 +194,9 @@ def item_changed(self, item, column): currency = currency.upper() self.requested_amounts[address] = (amount, currency) - self.wallet.config.set_key('requested_amounts', self.requested_amounts, True) + self.gui.wallet.config.set_key('requested_amounts', self.requested_amounts, True) - label = self.wallet.labels.get(address) + label = self.gui.wallet.labels.get(address) if label is None: label = self.merchant_name + ' - %04d'%(index+1) self.wallet.labels[address] = label @@ -213,50 +209,20 @@ def item_changed(self, item, column): if address in self.requested_amounts: self.requested_amounts.pop(address) - self.update_receive_item(self.receive_list.currentItem()) - + self.gui.update_receive_item(self.gui.receive_list.currentItem()) -def recv_changed(self, a): - if a is not None and self.qr_window and self.qr_window.isVisible(): - address = str(a.text(0)) - label = self.wallet.labels.get(address) - try: - amount, currency = self.requested_amounts.get(address, (None, None)) - except: - amount, currency = None, None - self.qr_window.set_content( address, label, amount, currency ) -def edit_amount(self): - l = self.receive_list - item = l.currentItem() - item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - l.editItem( item, column_index ) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + def edit_amount(self): + l = self.gui.receive_list + item = l.currentItem() + item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + l.editItem( item, column_index ) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) -def receive_menu(self, menu): - menu.addAction(_("Request amount"), lambda: edit_amount(self)) - - -def update_receive_item(self, address, item): - try: - amount, currency = self.requested_amounts.get(address, (None, None)) - except: - print "cannot get requested amount", address, self.requested_amounts.get(address) - amount, currency = None, None - self.requested_amounts.pop(address) - - amount_str = amount + (' ' + currency if currency else '') if amount is not None else '' - item.setData(column_index,0,amount_str) - - -def close_main_window(self): - if self.qr_window: - self.qr_window.close() - self.qr_window = None + + def receive_menu(self, menu): + menu.addAction(_("Request amount"), self.edit_amount) -def timer_actions(self): - if self.qr_window: - self.qr_window.qrw.update_qr() diff --git a/plugins/qrscanner.py b/plugins/qrscanner.py @@ -1,60 +1,69 @@ from electrum.util import print_error from urlparse import urlparse, parse_qs +from PyQt4.QtGui import QPushButton +from electrum_gui.i18n import _ try: import zbar except ImportError: zbar = None +from electrum_gui import BasePlugin +class Plugin(BasePlugin): + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'qrscans', 'QR scans', "QR Scans.\nInstall the zbar package to enable this plugin") + + def is_available(self): + if not zbar: + return False + try: + proc = zbar.Processor() + proc.init() + except zbar.SystemError: + # Cannot open video device + return False + return True -def init(gui): - if is_enabled(): - gui.set_hook('create_send_tab', create_send_tab) - else: - gui.unset_hook('create_send_tab', create_send_tab) - -def get_info(): - return 'QR scans', "QR Scans.\nInstall the zbar package to enable this plugin" - -def is_enabled(): - return is_available() - -def toggle(gui): - return is_enabled() + def create_send_tab(self, grid): + b = QPushButton(_("Scan QR code")) + b.clicked.connect(self.fill_from_qr) + grid.addWidget(b, 1, 5) -def is_available(): - if not zbar: - return False - try: + def scan_qr(self): proc = zbar.Processor() proc.init() - except zbar.SystemError: - # Cannot open video device - return False - - return True + proc.visible = True + + while True: + try: + proc.process_one() + except: + # User closed the preview window + return {} + + for r in proc.results: + if str(r.type) != 'QRCODE': + continue + return parse_uri(r.data) + -def scan_qr(): - proc = zbar.Processor() - proc.init() - proc.visible = True + def fill_from_qr(self): + qrcode = self.scan_qr() + if 'address' in qrcode: + self.gui.payto_e.setText(qrcode['address']) + if 'amount' in qrcode: + self.gui.amount_e.setText(str(qrcode['amount'])) + if 'label' in qrcode: + self.gui.message_e.setText(qrcode['label']) + if 'message' in qrcode: + self.gui.message_e.setText("%s (%s)" % (self.gui.message_e.text(), qrcode['message'])) + - while True: - try: - proc.process_one() - except: - # User closed the preview window - return {} - for r in proc.results: - if str(r.type) != 'QRCODE': - continue - return parse_uri(r.data) - def parse_uri(uri): if ':' not in uri: # It's just an address (not BIP21) @@ -80,24 +89,6 @@ def parse_uri(uri): -def fill_from_qr(self): - qrcode = qrscanner.scan_qr() - if 'address' in qrcode: - self.payto_e.setText(qrcode['address']) - if 'amount' in qrcode: - self.amount_e.setText(str(qrcode['amount'])) - if 'label' in qrcode: - self.message_e.setText(qrcode['label']) - if 'message' in qrcode: - self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message'])) - - -def create_send_tab(gui, grid): - if qrscanner.is_available(): - b = QPushButton(_("Scan QR code")) - b.clicked.connect(lambda: fill_from_qr(gui)) - grid.addWidget(b, 1, 5) - if __name__ == '__main__': diff --git a/plugins/virtualkeyboard.py b/plugins/virtualkeyboard.py @@ -0,0 +1,65 @@ +from PyQt4.QtGui import * +from electrum_gui import BasePlugin +from electrum_gui.i18n import _ + +class Plugin(BasePlugin): + + + def __init__(self, gui): + BasePlugin.__init__(self, gui, 'virtualkeyboard', 'Virtual Keyboard', + _("Add an optional, mouse keyboard to the password dialog.\nWarning: do not use this if it makes you pick a weaker password.")) + self.vkb = None + self.vkb_index = 0 + + + def password_dialog(self, pw, grid, pos): + vkb_button = QPushButton(_("+")) + vkb_button.setFixedWidth(20) + vkb_button.clicked.connect(lambda: self.toggle_vkb(grid, pw)) + grid.addWidget(vkb_button, pos, 2) + self.kb_pos = 2 + + + def toggle_vkb(self, grid, pw): + if self.vkb: grid.removeItem(self.vkb) + self.vkb = self.virtual_keyboard(self.vkb_index, pw) + grid.addLayout(self.vkb, self.kb_pos, 0, 1, 3) + self.vkb_index += 1 + + + def virtual_keyboard(self, i, pw): + import random + i = i%3 + if i == 0: + chars = 'abcdefghijklmnopqrstuvwxyz ' + elif i == 1: + chars = 'ABCDEFGHIJKLMNOPQRTSUVWXYZ ' + elif i == 2: + chars = '1234567890!?.,;:/%&()[]{}+-' + + n = len(chars) + s = [] + for i in xrange(n): + while True: + k = random.randint(0,n-1) + if k not in s: + s.append(k) + break + + def add_target(t): + return lambda: pw.setText(str( pw.text() ) + t) + + vbox = QVBoxLayout() + grid = QGridLayout() + grid.setSpacing(2) + for i in range(n): + l_button = QPushButton(chars[s[i]]) + l_button.setFixedWidth(25) + l_button.setFixedHeight(25) + l_button.clicked.connect(add_target(chars[s[i]]) ) + grid.addWidget(l_button, i/6, i%6) + + vbox.addLayout(grid) + + return vbox + diff --git a/setup-release.py b/setup-release.py @@ -28,6 +28,8 @@ if sys.platform == 'darwin': setup_requires=['py2app'], app=[mainscript], options=dict(py2app=dict(argv_emulation=True, + includes = ['PyQt4.QtCore','PyQt4.QtGui', 'sip'], + packages = ['lib', 'gui', 'plugins'], iconfile='electrum.icns', resources=["data", "icons"])), )